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 = { ...@@ -262,6 +262,7 @@ SHIB_ATTRIBUTE_MAP = {
# Headers to use for X509 authentication # Headers to use for X509 authentication
X509_SUBJECT_DN_HEADER = 'HTTP_SSL_CLIENT_S_DN' X509_SUBJECT_DN_HEADER = 'HTTP_SSL_CLIENT_S_DN'
X509_ISSUER_DN_HEADER = 'HTTP_SSL_CLIENT_I_DN' X509_ISSUER_DN_HEADER = 'HTTP_SSL_CLIENT_I_DN'
X509_CERT_HEADER = 'X_FORWARDED_TLS_CLIENT_CERT'
# List of authentication backends to use when attempting to authenticate # List of authentication backends to use when attempting to authenticate
# a user. Will be used in this order. Authentication for the API is # a user. Will be used in this order. Authentication for the API is
......
...@@ -49,6 +49,13 @@ DATABASES = { ...@@ -49,6 +49,13 @@ DATABASES = {
# Main server "hostname" - a little hacky but OK # Main server "hostname" - a little hacky but OK
SERVER_HOSTNAME = SERVER_FQDN.split('.')[0] SERVER_HOSTNAME = SERVER_FQDN.split('.')[0]
# Use full client certificate to authenticate
REST_FRAMEWORK['DEFAULT_AUTHENTICATION_CLASSES'] = (
'api.backends.GraceDbAuthenticatedAuthentication',
'api.backends.GraceDbX509FullCertAuthentication',
'api.backends.GraceDbBasicAuthentication',
)
# Update allowed hosts from environment variables ----------------------------- # Update allowed hosts from environment variables -----------------------------
hosts_from_env = os.environ.get('DJANGO_ALLOWED_HOSTS', None) hosts_from_env = os.environ.get('DJANGO_ALLOWED_HOSTS', None)
if hosts_from_env is not None: if hosts_from_env is not None:
......
import base64
import logging import logging
import OpenSSL.crypto
import OpenSSL.SSL
import re import re
import urlparse
from django.contrib.auth import get_user_model, authenticate from django.contrib.auth import get_user_model, authenticate
from django.conf import settings from django.conf import settings
...@@ -54,6 +58,10 @@ class GraceDbBasicAuthentication(authentication.BasicAuthentication): ...@@ -54,6 +58,10 @@ class GraceDbBasicAuthentication(authentication.BasicAuthentication):
class GraceDbX509Authentication(authentication.BaseAuthentication): class GraceDbX509Authentication(authentication.BaseAuthentication):
"""
Authentication based on X509 certificate subject.
Certificate should be verified by Apache already.
"""
api_only = True api_only = True
www_authenticate_realm = 'api' www_authenticate_realm = 'api'
subject_dn_header = getattr(settings, 'X509_SUBJECT_DN_HEADER', subject_dn_header = getattr(settings, 'X509_SUBJECT_DN_HEADER',
...@@ -122,6 +130,90 @@ class GraceDbX509Authentication(authentication.BaseAuthentication): ...@@ -122,6 +130,90 @@ class GraceDbX509Authentication(authentication.BaseAuthentication):
return (user, None) 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',
'X_FORWARDED_TLS_CLIENT_CERT')
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
try:
certificate = self.verify_certificate_chain(cert_data)
except exceptions.AuthenticationFailed as e:
raise
except Exception as e:
raise exceptions.AuthenticationFailed(_('Certificate could not be '
'verified'))
return self.authenticate_credentials(certificate)
@classmethod
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,
trusted_certs='/etc/grid-security/certificates'):
# 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)
store_ctx.verify_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) \
.authenticate_credentials(subject)
@staticmethod
def get_certificate_subject_string(certificate):
subject = certificate.get_subject()
subject_string = '/' + "/".join(["=".join(c) for c in
subject.get_components()])
return subject_string
class GraceDbAuthenticatedAuthentication(authentication.BaseAuthentication): class GraceDbAuthenticatedAuthentication(authentication.BaseAuthentication):
""" """
If user is already authenticated by the main Django middleware, 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