Commit 4ee31f4d authored by Tanner Prestegard's avatar Tanner Prestegard Committed by GraceDB

Add API backend for full cert verify and auth

New API backend which gets a full X509 certificate, verifies it,
and extracts the subject.  To be used in the cloud deployment
with Traefik.
parent c29061a2
......@@ -262,6 +262,7 @@ SHIB_ATTRIBUTE_MAP = {
# Headers to use for X509 authentication
# List of authentication backends to use when attempting to authenticate
# a user. Will be used in this order. Authentication for the API is
......@@ -49,6 +49,13 @@ DATABASES = {
# Main server "hostname" - a little hacky but OK
# Use full client certificate to authenticate
# Update allowed hosts from environment variables -----------------------------
hosts_from_env = os.environ.get('DJANGO_ALLOWED_HOSTS', None)
if hosts_from_env is not None:
import base64
import logging
import OpenSSL.crypto
import OpenSSL.SSL
import re
import urlparse
from django.contrib.auth import get_user_model, authenticate
from django.conf import settings
......@@ -54,6 +58,10 @@ class GraceDbBasicAuthentication(authentication.BasicAuthentication):
class GraceDbX509Authentication(authentication.BaseAuthentication):
Authentication based on X509 certificate subject.
Certificate should be verified by Apache already.
api_only = True
www_authenticate_realm = 'api'
subject_dn_header = getattr(settings, 'X509_SUBJECT_DN_HEADER',
......@@ -122,6 +130,90 @@ class GraceDbX509Authentication(authentication.BaseAuthentication):
return (user, None)
class GraceDbX509FullCertAuthentication(GraceDbX509Authentication):
Authentication based on a full X509 certificate. We verify the
certificate here.
api_only = True
www_authenticate_realm = 'api'
cert_header = getattr(settings, 'X509_CERT_HEADER',
def authenticate(self, request):
# Make sure this request is directed to the API
if self.api_only and not is_api_request(request.path):
return None
# Try to get certificate from request headers
cert_data = self.get_certificate_data_from_request(request)
# If no certificate is found, abort
if not cert_data:
return None
# Verify certificate
certificate = self.verify_certificate_chain(cert_data)
except exceptions.AuthenticationFailed as e:
except Exception as e:
raise exceptions.AuthenticationFailed(_('Certificate could not be '
return self.authenticate_credentials(certificate)
def get_certificate_data_from_request(cls, request):
"""Get certificate data from request"""
cert_quoted = request.META.get(cls.cert_header, None)
if cert_quoted is None:
return None
# Process the certificate a bit
cert_b64 = urlparse.unquote(cert_quoted)
cert_der = base64.b64decode(cert_b64)
return cert_der
def verify_certificate_chain(self, cert_data,
# Load certificate data
certificate = OpenSSL.crypto.load_certificate(
OpenSSL.crypto.FILETYPE_ASN1, cert_data)
# Set up context and get certificate store
ctx = OpenSSL.SSL.Context(OpenSSL.SSL.TLSv1_METHOD)
ctx.load_verify_locations(None, capath=trusted_certs)
store = ctx.get_cert_store()
# Verify certificate
store_ctx = OpenSSL.crypto.X509StoreContext(store, certificate)
# Check if expired
if certificate.has_expired():
raise exceptions.AuthenticationFailed(_('Certificate has expired'))
return certificate
def authenticate_credentials(self, certificate):
# Convert certificate to subject
subject = self.get_certificate_subject_string(certificate)
return super(GraceDbX509FullCertAuthentication, self) \
def get_certificate_subject_string(certificate):
subject = certificate.get_subject()
subject_string = '/' + "/".join(["=".join(c) for c in
return subject_string
class GraceDbAuthenticatedAuthentication(authentication.BaseAuthentication):
If user is already authenticated by the main Django middleware,
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment