├── .gitignore ├── .travis.yml ├── LICENSE.txt ├── README ├── README.md ├── django_ssl_auth ├── __init__.py ├── base.py └── fineid.py ├── doc ├── fineid │ ├── FINEID.md │ └── Makefile └── samples │ └── nginx.conf ├── setup.py └── testapp ├── manage.py ├── requirements.txt └── ssltest ├── __init__.py ├── settings.py ├── tests.py ├── urls.py ├── views.py └── wsgi.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | .coverage 26 | .tox 27 | nosetests.xml 28 | 29 | # Translations 30 | *.mo 31 | 32 | # Mr Developer 33 | .mr.developer.cfg 34 | .project 35 | .pydevproject 36 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: python 3 | python: 4 | - "2.7" 5 | - "3.4" 6 | - "3.5" 7 | env: 8 | - DJANGO="django>=1.8,<1.9" 9 | - DJANGO="django>=1.9,<1.10" 10 | install: 11 | - pip install "$DJANGO" 12 | - pip install . 13 | - pip install -r testapp/requirements.txt 14 | script: python -Wall testapp/manage.py test 15 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright © 2013 SSH Communications Security Corp. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | README.md -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # The authoritative repository for this project is maintained by [Joey Wilhelm](https://github.com/tarkatronic/django-ssl-auth) 3 | 4 | << Kimvais, 2017-10-14 5 | 6 | 7 | django-ssl-auth 8 | ====================== 9 | 10 | SSL authentication backend & middleware for Django for authenticating users with SSL client certificates 11 | 12 | ## License 13 | 14 | MIT license, see LICENSE.txt for full text. 15 | 16 | ## Setup 17 | 18 | ### SSL 19 | 20 | Set up nginx and create SSL certificates for your server and set up the paths 21 | to server private key, server certificate and CA certificate used to sign 22 | the client certificates. Example configuration file is in samples/nginx.conf 23 | 24 | If you are on OS X, I suggest OS X KeyChain access for doing this for 25 | testing, as it will automatically make your client certificates available in 26 | all both Google chrome & Safari. Instructions can be found e.g. 27 | http://www.dummies.com/how-to/content/how-to-become-a-certificate-authority-using-lion-s.html 28 | 29 | On other platforms, there are many tutorials on how to do this with OpenSSL 30 | e.g. http://pages.cs.wisc.edu/~zmiller/ca-howto/ 31 | 32 | Restart your ngninx (sudo nginx -s restart), make sure your green unicorn is 33 | running and check that your https:// url loads your application and the 34 | _server certificate is valid_. 35 | 36 | ### This module 37 | 38 | 1. run setup.py (sudo python setup.py install) or install the latest release usning `pip install django_ssl_auth ` 39 | 2. edit your `settings.py` 40 | 1. add `"django_ssl_auth.SSLClientAuthMiddleware"` to your `MIDDLEWARE_CLASSES` 41 | 2. add `"django_ssl_auth.SSLClientAuthBackend"` to your `AUTHENTICATION_BACKENDS` 42 | 43 | #### Configuration 44 | There are two things you need to do in `settings.py` 45 | 46 | 1. Define a function that can return a dictionary with fields that 47 | are required by your user model, e.g. `USER_DATA_FN = 'django_ssl_auth.fineid.user_dict_from_dn` is a sample implementation that takes the required fields from the DN of a Finnish government issued ID smart card for the `contrib.auth.models.User`. 48 | 2. To automatically create `User`s for all valid certificate holders, set `AUTOCREATE_VALID_SSL_USERS = True`. Auto-created users will be set to inactive by default, consider using the [`User.is_active`](https://docs.djangoproject.com/en/1.9/ref/contrib/auth/#django.contrib.auth.models.User.is_active) field in your [`LOGIN_DIRECT_URL`](https://docs.djangoproject.com/en/1.9/ref/settings/#login-redirect-url) view to notifying the user of their status. 49 | 50 | For details, see `testapp/ssltest/settings.py` 51 | 52 | #### Smart Card support 53 | 54 | For (Finnish) instructions see `doc/fineid/FINEID.md` 55 | 56 | 57 | ## TODO 58 | 59 | * Active directory integration. 60 | 61 | ## How to get help 62 | 63 | Please do ask your questions on http://stackoverflow.com/ 64 | I am active there, and more likely to answer you publicly. 65 | Also, you can try catching Kimvais on #django@freenode 66 | 67 | -------------------------------------------------------------------------------- /django_ssl_auth/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright © 2013 SSH Communication Security Corporation. 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | # THE SOFTWARE. 23 | # 24 | from .base import SSLClientAuthBackend, SSLClientAuthMiddleware 25 | from . import fineid 26 | -------------------------------------------------------------------------------- /django_ssl_auth/base.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright © 2013 SSH Communication Security Corporation. 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | # THE SOFTWARE. 23 | # 24 | 25 | import logging 26 | from django.conf import settings 27 | from django.contrib.auth import login, authenticate 28 | from django.core.exceptions import ImproperlyConfigured 29 | 30 | try: 31 | from django.utils.deprecation import MiddlewareMixin # Django 1.10+ 32 | except ImportError: 33 | MiddlewareMixin = object # Django < 1.10 34 | from django.http import HttpResponseRedirect 35 | from django.shortcuts import resolve_url 36 | from importlib import import_module 37 | 38 | try: 39 | from django.contrib.auth import get_user_model 40 | 41 | User = get_user_model() 42 | except ImportError: 43 | from django.contrib.auth.models import User 44 | 45 | logging.basicConfig() 46 | logger = logging.getLogger(__name__) 47 | 48 | class SSLClientAuthBackend(object): 49 | @staticmethod 50 | def authenticate(request=None): 51 | _module_name, _function_name = settings.USER_DATA_FN.rsplit('.', 1) 52 | _module = import_module(_module_name) # We need a non-empty fromlist 53 | USER_DATA_FN = getattr(_module, _function_name) 54 | 55 | if not request.is_secure(): 56 | logger.debug("insecure request") 57 | return None 58 | authentication_status = request.META.get('HTTP_X_SSL_AUTHENTICATED', 59 | None) 60 | if (authentication_status != "SUCCESS" or 61 | 'HTTP_X_SSL_USER_DN' not in request.META): 62 | logger.warn( 63 | "HTTP_X_SSL_AUTHENTICATED marked failed or " 64 | "HTTP_X_SSL_USER_DN " 65 | "header missing") 66 | return None 67 | dn = request.META.get('HTTP_X_SSL_USER_DN') 68 | user_data = USER_DATA_FN(dn) 69 | username = user_data['username'] 70 | try: 71 | user = User.objects.get(username=username) 72 | except User.DoesNotExist: 73 | logger.info("user {0} not found".format(username)) 74 | if settings.AUTOCREATE_VALID_SSL_USERS: 75 | user = User(**user_data) 76 | user.save() 77 | else: 78 | return None 79 | logger.info("user {0} authenticated using a certificate issued to " 80 | "{1}".format(username, dn)) 81 | return user 82 | 83 | def get_user(self, user_id): 84 | try: 85 | return User.objects.get(pk=user_id) 86 | except User.DoesNotExist: 87 | return None 88 | 89 | 90 | class SSLClientAuthMiddleware(MiddlewareMixin): 91 | def process_request(self, request): 92 | if not hasattr(request, 'user'): 93 | raise ImproperlyConfigured() 94 | if request.user.is_authenticated(): 95 | return 96 | if int(request.META.get('HTTP_X_REST_API', 0)): 97 | user = authenticate(request=request) 98 | if user is None or not user.is_authenticated(): 99 | return 100 | logger.debug("REST API call, not logging user in") 101 | request.user = user 102 | elif request.path_info == settings.LOGIN_URL: 103 | user = authenticate(request=request) 104 | if user is None or not user.is_authenticated(): 105 | return 106 | logger.info("Logging user in") 107 | login(request, user) 108 | return HttpResponseRedirect(resolve_url(settings.LOGIN_REDIRECT_URL)) 109 | 110 | -------------------------------------------------------------------------------- /django_ssl_auth/fineid.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright 2013 SSH Communication Security Corporation. 5 | # All rights reserved. 6 | # This software is protected by international copyright laws. 7 | # 8 | 9 | """ 10 | Utility functions for handling certificates on Finnish ID cards a.k.a. 11 | FINeID (www.fineid.fi) 12 | """ 13 | 14 | def _dictify_dn(dn): 15 | return dict(x.split('=') for x in dn.split('/') if '=' in x) 16 | 17 | def user_dict_from_dn(dn): 18 | d = _dictify_dn(dn) 19 | ret = dict() 20 | ret['username'] = d['serialNumber'] 21 | ret['last_name'] = d['SN'].title() 22 | ret['first_name'] = d['GN'].title() 23 | ret['email'] = '' 24 | return ret 25 | -------------------------------------------------------------------------------- /doc/fineid/FINEID.md: -------------------------------------------------------------------------------- 1 | # How to use the Finnish e-ID (FINeID) cards in Django on OS X 2 | 3 | ## Client side: 4 | 5 | Instructions in Finnish (Ohjeita suomeksi): 6 | * http://hopeinenomena.net/viewtopic.php?f=44&t=71540 7 | 8 | 1. Install the official card reader software http://www.fineid.fi/ 9 | 1. Install OpenSC http://dl.dropbox.com/u/7089298/OpenSC/OpenSC-0.12.2-10.8.dmg 10 | 1. Install libtool (sudo port install libtool) 11 | 1. Create a critical symlink: sudo ln -s /opt/local/lib/libltdl.7.dylib /usr/lib 12 | 1. `pkcs15-tool -D` should now show something like: 13 | Using reader with a card: ACS ACR38U-CCID 00 00 14 | PKCS#15 Card [HENKILOKORTTI]: 15 | Version : 1 16 | Serial number : 12345678901234567890 17 | Manufacturer ID: VRK-FINEID 18 | Language : fi 19 | Flags : Login required 20 | ... 21 | 1. Reboot (Fastest way to get KeyChain access to pick up the new keychain 22 | "HENKILOKORTTI") 23 | 1. Going to URL https://vrk.fineid.fi/ should now prompt for Certificate and 24 | PIN code in both Safari and Google Chrome 25 | 26 | ## Server side: 27 | 28 | 1. set up your django app with `django_ssl_auth` as specified in the top-level 29 | README.md 30 | 1. Download the CA certificates from the Finnish Population Register Centre 31 | (`make` in this directory, or manually): 32 | 1. wget http://vrk.fineid.fi/certs/vrkrootc.crt 33 | 1. wget http://vrk.fineid.fi/certs/vrkcqc.crt 34 | 1. openssl x509 -inform der -outform pem -in vrkcqc.crt >> vrkca.crt 35 | 1. openssl x509 -inform der -outform pem -in vrkrootc.crt >> vrkca.crt 36 | 1. Set-up your nginx (see `nginx.conf`) 37 | 1. Run the `testapp` 38 | 1. `export DJANGO_SETTINGS_MODULE=ssltest.settings` 39 | 1. `gunicorn ssltest.wsgi` 40 | 1. navigate to `https://localhost/fi` and you should see your name and SATU 41 | in a not-at-all-formatted view. 42 | 43 | Further information about the Finnish Gov.t CAs 44 | 45 | http://fineid.fi/default.aspx?docid=2237&site=9&id=332 46 | -------------------------------------------------------------------------------- /doc/fineid/Makefile: -------------------------------------------------------------------------------- 1 | all: clean ca 2 | 3 | ca: 4 | wget http://vrk.fineid.fi/certs/vrkrootc.crt 5 | wget http://vrk.fineid.fi/certs/vrkcqc.crt 6 | openssl x509 -inform der -outform pem -in vrkcqc.crt >> vrkca.crt 7 | openssl x509 -inform der -outform pem -in vrkrootc.crt >> vrkca.crt 8 | 9 | clean: 10 | rm *.crt || true 11 | -------------------------------------------------------------------------------- /doc/samples/nginx.conf: -------------------------------------------------------------------------------- 1 | worker_processes 5; 2 | 3 | events { 4 | worker_connections 1024; 5 | } 6 | 7 | 8 | http { 9 | include mime.types; 10 | default_type application/octet-stream; 11 | sendfile on; 12 | keepalive_timeout 65; 13 | server { 14 | listen 443; 15 | server_name _; 16 | add_header Strict-Transport-Security max-age=31536000; 17 | 18 | location /static/ { # STATIC_URL 19 | alias /path/to/your/static/; # STATIC_ROOT 20 | expires 30d; 21 | } 22 | 23 | location / { 24 | # Change this based on what your 25 | # manage.py run_gunicorn parameters are 26 | proxy_pass http://localhost:8000/; 27 | proxy_pass_header Server; 28 | proxy_set_header Host $http_host; 29 | proxy_redirect off; 30 | proxy_connect_timeout 60; 31 | proxy_read_timeout 90; 32 | # SSL settings for django-ssl-client-auth 33 | proxy_set_header X-SSL-User-DN $ssl_client_s_dn; 34 | proxy_set_header X-SSL-Authenticated $ssl_client_verify; 35 | proxy_set_header X-Real-IP $remote_addr; 36 | proxy_set_header X-Scheme $scheme; 37 | proxy_set_header X-Forwarded-Proto $scheme; 38 | proxy_set_header X-Rest-API 0; # Change to 1 to bypass 39 | # call to 'django.contrib.auth.login' 40 | } 41 | ssl on; 42 | # Certificate locations 43 | ssl_certificate /opt/local/etc/pki/server.crt; 44 | ssl_certificate_key /opt/local/etc/pki/server.key; 45 | ssl_client_certificate /opt/local/etc/pki/ca.crt; 46 | # Change to 'required' if only SSL Client authenticated 47 | # request should be passed to Django 48 | ssl_verify_client on; 49 | # Increase verify_depth if there are intermediate CAs, 50 | # such as with FinEID 51 | ssl_verify_depth 2; 52 | 53 | ssl_session_timeout 5m; 54 | 55 | # Disallow poor algorithms for SSL 56 | ssl_protocols SSLv2 SSLv3 TLSv1 TLSv1.1 TLSv1.2; 57 | ssl_ciphers ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RS A-AES128-GCM-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-RC4-SHA:ECDHE-RSA-AES256-S HA:RC4-SHA; 58 | ssl_prefer_server_ciphers on; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from distutils.core import setup 4 | 5 | setup(name='django-ssl-auth', 6 | version='1.0.1', 7 | description='Django SSL Client Authentication', 8 | author='Kimmo Parviainen-Jalanko', 9 | author_email='kimvais@ssh.com', 10 | url='https://github.com/kimvais/django-ssl-client-auth/', 11 | packages=['django_ssl_auth'], 12 | requires=['Django (>=1.8)'] 13 | ) 14 | -------------------------------------------------------------------------------- /testapp/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ssltest.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /testapp/requirements.txt: -------------------------------------------------------------------------------- 1 | Django>=1.8.0 2 | django-ssl-auth>=1.0.0 3 | gunicorn 4 | -------------------------------------------------------------------------------- /testapp/ssltest/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kimvais/django-ssl-auth/4b549f56974f49e8c5fd9d65ea154a0fb4860858/testapp/ssltest/__init__.py -------------------------------------------------------------------------------- /testapp/ssltest/settings.py: -------------------------------------------------------------------------------- 1 | # Django settings for ssltest project. 2 | 3 | DEBUG = True 4 | TEMPLATE_DEBUG = DEBUG 5 | 6 | ADMINS = ( 7 | # ('Your Name', 'your_email@example.com'), 8 | ) 9 | 10 | MANAGERS = ADMINS 11 | 12 | DATABASES = { 13 | 'default': { 14 | 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'. 15 | 'NAME': 'database.sqlite3', # Or path to database file if using sqlite3. 16 | # The following settings are not used with sqlite3: 17 | 'USER': '', 18 | 'PASSWORD': '', 19 | 'HOST': '', # Empty for localhost through domain sockets or '127.0.0.1' for localhost through TCP. 20 | 'PORT': '', # Set to empty string for default. 21 | } 22 | } 23 | 24 | # Hosts/domain names that are valid for this site; required if DEBUG is False 25 | # See https://docs.djangoproject.com/en/1.5/ref/settings/#allowed-hosts 26 | ALLOWED_HOSTS = [] 27 | 28 | # Local time zone for this installation. Choices can be found here: 29 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name 30 | # although not all choices may be available on all operating systems. 31 | # In a Windows environment this must be set to your system time zone. 32 | TIME_ZONE = 'America/Chicago' 33 | 34 | # Language code for this installation. All choices can be found here: 35 | # http://www.i18nguy.com/unicode/language-identifiers.html 36 | LANGUAGE_CODE = 'en-us' 37 | 38 | SITE_ID = 1 39 | 40 | # If you set this to False, Django will make some optimizations so as not 41 | # to load the internationalization machinery. 42 | USE_I18N = True 43 | 44 | # If you set this to False, Django will not format dates, numbers and 45 | # calendars according to the current locale. 46 | USE_L10N = True 47 | 48 | # If you set this to False, Django will not use timezone-aware datetimes. 49 | USE_TZ = True 50 | 51 | # Absolute filesystem path to the directory that will hold user-uploaded files. 52 | # Example: "/var/www/example.com/media/" 53 | MEDIA_ROOT = '' 54 | 55 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 56 | # trailing slash. 57 | # Examples: "http://example.com/media/", "http://media.example.com/" 58 | MEDIA_URL = '' 59 | 60 | # Absolute path to the directory static files should be collected to. 61 | # Don't put anything in this directory yourself; store your static files 62 | # in apps' "static/" subdirectories and in STATICFILES_DIRS. 63 | # Example: "/var/www/example.com/static/" 64 | STATIC_ROOT = '' 65 | 66 | # URL prefix for static files. 67 | # Example: "http://example.com/static/", "http://static.example.com/" 68 | STATIC_URL = '/static/' 69 | 70 | # Additional locations of static files 71 | STATICFILES_DIRS = ( 72 | # Put strings here, like "/home/html/static" or "C:/www/django/static". 73 | # Always use forward slashes, even on Windows. 74 | # Don't forget to use absolute paths, not relative paths. 75 | ) 76 | 77 | # List of finder classes that know how to find static files in 78 | # various locations. 79 | STATICFILES_FINDERS = ( 80 | 'django.contrib.staticfiles.finders.FileSystemFinder', 81 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 82 | # 'django.contrib.staticfiles.finders.DefaultStorageFinder', 83 | ) 84 | 85 | # Make this unique, and don't share it with anybody. 86 | SECRET_KEY = '6jfz5j*pffufpb%o$_bnk1ej!4=4r98)!a(n(j=u_g6qy#8!2k' 87 | 88 | # List of callables that know how to import templates from various sources. 89 | TEMPLATE_LOADERS = ( 90 | 'django.template.loaders.filesystem.Loader', 91 | 'django.template.loaders.app_directories.Loader', 92 | # 'django.template.loaders.eggs.Loader', 93 | ) 94 | 95 | MIDDLEWARE_CLASSES = ( 96 | 'django.middleware.common.CommonMiddleware', 97 | 'django.contrib.sessions.middleware.SessionMiddleware', 98 | 'django.middleware.csrf.CsrfViewMiddleware', 99 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 100 | 'django.contrib.messages.middleware.MessageMiddleware', 101 | 'django_ssl_auth.SSLClientAuthMiddleware', 102 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 103 | 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 104 | ) 105 | 106 | ROOT_URLCONF = 'ssltest.urls' 107 | 108 | # Python dotted path to the WSGI application used by Django's runserver. 109 | WSGI_APPLICATION = 'ssltest.wsgi.application' 110 | 111 | TEMPLATE_DIRS = ( 112 | # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". 113 | # Always use forward slashes, even on Windows. 114 | # Don't forget to use absolute paths, not relative paths. 115 | ) 116 | 117 | INSTALLED_APPS = ( 118 | 'django.contrib.auth', 119 | 'django.contrib.contenttypes', 120 | 'django.contrib.sessions', 121 | 'django.contrib.sites', 122 | 'django.contrib.messages', 123 | 'django.contrib.staticfiles', 124 | # Uncomment the next line to enable the admin: 125 | # 'django.contrib.admin', 126 | # Uncomment the next line to enable admin documentation: 127 | # 'django.contrib.admindocs', 128 | ) 129 | 130 | SESSION_SERIALIZER = 'django.contrib.sessions.serializers.JSONSerializer' 131 | 132 | # A sample logging configuration. The only tangible logging 133 | # performed by this configuration is to send an email to 134 | # the site admins on every HTTP 500 error when DEBUG=False. 135 | # See http://docs.djangoproject.com/en/dev/topics/logging for 136 | # more details on how to customize your logging configuration. 137 | LOGGING = { 138 | 'version': 1, 139 | 'disable_existing_loggers': False, 140 | 'filters': { 141 | 'require_debug_false': { 142 | '()': 'django.utils.log.RequireDebugFalse' 143 | } 144 | }, 145 | 'handlers': { 146 | 'mail_admins': { 147 | 'level': 'ERROR', 148 | 'filters': ['require_debug_false'], 149 | 'class': 'django.utils.log.AdminEmailHandler' 150 | } 151 | }, 152 | 'loggers': { 153 | 'django.request': { 154 | 'handlers': ['mail_admins'], 155 | 'level': 'ERROR', 156 | 'propagate': True, 157 | }, 158 | } 159 | } 160 | 161 | LOGIN_REDIRECT_URL = 'home' 162 | LOGIN_URL = ('/login') 163 | 164 | AUTHENTICATION_BACKENDS = ('django_ssl_auth.SSLClientAuthBackend', ) 165 | USER_DATA_FN = 'django_ssl_auth.fineid.user_dict_from_dn' 166 | AUTOCREATE_VALID_SSL_USERS = True 167 | 168 | # This setting is used for testing so that the test cases can simulate 169 | # an https connection to Django. Please see the Django documentation 170 | # and understand this setting before considering using it in your own site. 171 | # https://docs.djangoproject.com/en/1.9/ref/settings/#secure-proxy-ssl-header 172 | SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTOCOL', 'https') 173 | -------------------------------------------------------------------------------- /testapp/ssltest/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from django.contrib.auth.models import AnonymousUser, User 3 | from django.core.urlresolvers import reverse 4 | 5 | class Tests(TestCase): 6 | """Automated tests to ensure everything is working as designed.""" 7 | 8 | def test_login_new_user(self): 9 | """Ensure users are automatically created.""" 10 | 11 | # Make sure the user doesn't already exist. 12 | with self.assertRaises(User.DoesNotExist): 13 | User.objects.get(username='1') 14 | 15 | # Simulate an SSL connection (of a new user) 16 | response = self.client.get('/login', 17 | HTTP_X_SSL_AUTHENTICATED='SUCCESS', 18 | HTTP_X_SSL_USER_DN='C=FI/serialNumber=1/GN=John/SN=Smith/CN=John Smith', 19 | HTTP_X_FORWARDED_PROTOCOL='https') 20 | 21 | # Ensure the new user was created 22 | try: 23 | user = User.objects.get(username='1') 24 | except: 25 | self.fail("New user wasn't created.") 26 | 27 | self.assertEqual(user.first_name, 'John', str('First name was incorrect.')) 28 | self.assertEqual(user.last_name, 'Smith', str('Last name was incorrect.')) 29 | -------------------------------------------------------------------------------- /testapp/ssltest/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | from . import views 3 | 4 | urlpatterns = [ 5 | url(r'^fi/?$', views.Fineid.as_view(), name='fi'), 6 | url(r'^$', views.Test.as_view(), name='home'), 7 | ] 8 | -------------------------------------------------------------------------------- /testapp/ssltest/views.py: -------------------------------------------------------------------------------- 1 | from pprint import pformat 2 | from django.http import HttpResponse 3 | from django.views.generic import View 4 | from django_ssl_auth.fineid import user_dict_from_dn 5 | 6 | 7 | class Fineid(View): 8 | def get(self, request, **kwargs): 9 | ctx = dict( 10 | user_data=user_dict_from_dn(request.META[ 11 | 'HTTP_X_SSL_USER_DN']), 12 | authentication_status=request.META['HTTP_X_SSL_AUTHENTICATED'], 13 | user=str(request.user)) 14 | return HttpResponse(pformat(ctx), content_type="text/plain") 15 | 16 | class Test(View): 17 | def get(self, request, **kwargs): 18 | ctx = dict( 19 | user_dn=request.META[ 20 | 'HTTP_X_SSL_USER_DN'], 21 | authentication_status=request.META['HTTP_X_SSL_AUTHENTICATED'], 22 | user=str(request.user)) 23 | return HttpResponse(pformat(ctx), content_type="text/plain") 24 | -------------------------------------------------------------------------------- /testapp/ssltest/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for ssltest project. 3 | 4 | This module contains the WSGI application used by Django's development server 5 | and any production WSGI deployments. It should expose a module-level variable 6 | named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover 7 | this application via the ``WSGI_APPLICATION`` setting. 8 | 9 | Usually you will have the standard Django WSGI application here, but it also 10 | might make sense to replace the whole Django WSGI application with a custom one 11 | that later delegates to the Django one. For example, you could introduce WSGI 12 | middleware here, or combine a Django application with an application of another 13 | framework. 14 | 15 | """ 16 | import os 17 | 18 | # We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks 19 | # if running multiple sites in the same mod_wsgi process. To fix this, use 20 | # mod_wsgi daemon mode with each site in its own daemon process, or use 21 | # os.environ["DJANGO_SETTINGS_MODULE"] = "ssltest.settings" 22 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ssltest.settings") 23 | 24 | # This application object is used by any WSGI server configured to use this 25 | # file. This includes Django's development server, if the WSGI_APPLICATION 26 | # setting points here. 27 | from django.core.wsgi import get_wsgi_application 28 | application = get_wsgi_application() 29 | 30 | # Apply WSGI middleware here. 31 | # from helloworld.wsgi import HelloWorldApplication 32 | # application = HelloWorldApplication(application) 33 | --------------------------------------------------------------------------------