From 40f87edb92544f36ce11e796111dc4ccf28eeb91 Mon Sep 17 00:00:00 2001 From: Art Lukyanchyk <artiom.lukyanchyk@hs-hannover.de> Date: Tue, 22 Aug 2017 11:06:42 +0200 Subject: [PATCH] Make it possible to run SSO on localhost and without SSL (development setup). --- .dev-config/nginx-https-config.sh | 84 ------------------------------- README.md | 31 ++++++++---- ssoauth/app_settings/__init__.py | 3 +- ssoauth/app_settings/defaults.py | 15 ++++-- ssoauth/apps.py | 16 +++--- ssoauth/checks.py | 12 +++++ ssoauth/sso_utils.py | 14 ++++-- ssoauth/views.py | 17 ++++--- 8 files changed, 75 insertions(+), 117 deletions(-) delete mode 100755 .dev-config/nginx-https-config.sh diff --git a/.dev-config/nginx-https-config.sh b/.dev-config/nginx-https-config.sh deleted file mode 100755 index 9da3b24..0000000 --- a/.dev-config/nginx-https-config.sh +++ /dev/null @@ -1,84 +0,0 @@ -#!/bin/bash - -# Django usually listens on port 8000 without SSL. -# Some of our projects however require SSL. -# Run this script to set up nginx SSL proxy for Django. - - -if [[ "$EUID" != 0 ]]; then - echo "Need to be run as root." - exit 1 -fi - -SITES_AVAILABLE="/etc/nginx/sites-available" -SITES_ENABLED="/etc/nginx/sites-enabled" -CONFIG_FILENAME="443-ssl-to-8000" - -CERT_PUB="/etc/ssl/certs/ssl-cert-snakeoil.pem" -CERT_KEY="/etc/ssl/private/ssl-cert-snakeoil.key" - -set -e # die on error -set -u # die if some var not set - -if [[ ! -d "$SITES_AVAILABLE" ]] || [[ ! -d "$SITES_AVAILABLE" ]]; then - echo "Is nginx installed?" - exit 1 -fi - -if [[ ! -e "$CERT_PUB" ]] || [[ ! -e "$CERT_KEY" ]]; then - echo "No snakeoil certs?" - exit 1 -fi - -echo "Seting up site config..." - -sudo cat > "$SITES_AVAILABLE/$CONFIG_FILENAME" << EOF - -upstream localhost8000 { - server localhost:8000; -} - -server { - listen 443; - ssl on; - - allow 127.0.0.1; - allow 141.71.113.0/24; - deny all; - - ssl_certificate $CERT_PUB; - ssl_certificate_key $CERT_KEY; - ssl_protocols TLSv1 TLSv1.1 TLSv1.2; - ssl_ciphers "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4"; - - location / { - proxy_pass http://localhost8000; - proxy_set_header Host \$host; - proxy_set_header X-Forwarded-For \$remote_addr; - proxy_set_header X-Forwarded-Proto "https"; - proxy_set_header REMOTE-USER \$remote_user; - } -} - -EOF - -set +e # stop dying on errors now - -sudo ln -f -s "$SITES_AVAILABLE/$CONFIG_FILENAME" "$SITES_ENABLED/" - -echo "Restarting nginx..." - -sudo systemctl restart nginx - -if [[ "`systemctl is-active nginx`" == "active" ]]; then - echo "http://localhost:8000 with SSL is here: https://localhost" -else - echo "Oops. Seems like nginx is dead now." - echo - systemctl status nginx | cat - echo -fi - -exit - - diff --git a/README.md b/README.md index 246d027..274bf80 100644 --- a/README.md +++ b/README.md @@ -14,10 +14,26 @@ - Add the app into `INSTALLED_APPS` - Include the app's `urls.py` into the project `urls.py` `urlpatterns` - Add to the project settings - - ```python + - Development + ```python + # imports + import socket + import os + # SP SP_HOST = "141.71.foo.bar" # your SP host or IP address + SP_PORT = 8000 + SP_SSL = False + SP_FORCE_ENTITY_ID = "auto-{0}-{1}".format(socket.gethostname(), os.path.dirname(os.path.dirname(__file__))) + # IDP IDP_META_URL = "https://idp-test.it.hs-hannover.de/idp/shibboleth" # development - # IDP_META_URL = "https://idp.hs-hannover.de/idp/shibboleth" # production + # IDP_IGNORE = True # set to True if you experience problems with the IDP (SSO will NOT work) + ``` + - Production + ```python + # SP + SP_HOST = "141.71.foo.bar" # your SP host or IP address + # IDP + IDP_META_URL = "https://idp.hs-hannover.de/idp/shibboleth" # production ``` - generate a new key pair: - create `cert` directory: @@ -27,15 +43,10 @@ - `./cert/sp.key` put the private key here - `./cert/sp.pem` put the certificate here, signing is optional -#### Development setup - - `sudo apt-get install nginx` - - run `.dev-config/nginx-https-config.sh` - - in case you already have port 443 busy, use your force to solve the issue :) - - #### Integrating your project into the existing SSO infrastructure -- **Ask somebody who knows more. Seriously.** -- **Try on the test IdP before you brick the production!** +- **Do this only if you want to use SSO. For development it's usually enough to use the dev view instead.** +- Ask somebody who knows more. Seriously. +- Try on the test IdP before you brick the production! - Grab your meta - Run your project. - Find meta of your SP (relative path `/saml2/meta` or view name `sso-saml2-meta`) diff --git a/ssoauth/app_settings/__init__.py b/ssoauth/app_settings/__init__.py index b00d50e..420e5a5 100644 --- a/ssoauth/app_settings/__init__.py +++ b/ssoauth/app_settings/__init__.py @@ -18,8 +18,7 @@ for setting_name in [k for k in globals().keys() if k.isupper()]: # checks assert SP_HOST and SP_PORT, "Need SP_HOST and SP_PORT configured in settings." -assert not SP_HOST.startswith(("127.", "::1", "localhost",)), "Too many localhosts around. Need a real hostname or IP address." -assert not SP_HOST.lower().startswith(("http:", "https:",)), "Need host name without a protocol." +assert not SP_HOST.lower().startswith(("http:", "https:",)), "Need host name without protocol and port." # helpers diff --git a/ssoauth/app_settings/defaults.py b/ssoauth/app_settings/defaults.py index b62f97d..3af2425 100644 --- a/ssoauth/app_settings/defaults.py +++ b/ssoauth/app_settings/defaults.py @@ -13,19 +13,28 @@ Settings you may want to change: """ # host and port, not what Django thinks, but what nginx serves -SP_HOST = None # e.g. "141.71.113.1" +SP_HOST = None # e.g. "141.71.foo.bar", for development can use "localhost" and set FORCE_ENTITY_ID SP_PORT = 443 +SP_SSL = True IDP_META_URL = "https://idp.hs-hannover.de/idp/shibboleth" # test is "https://idp-test.it.hs-hannover.de/idp/shibboleth" SP_KEY = "{project_settings}/cert/sp.key" SP_CERT = "{project_settings}/cert/sp.pem" + """ -Settings you DON'T want to change (in fact, you want to avoid even thinking about them): +Settings you might want to change on development (don't change them for production): """ -IDP_REQUIRED = True # die on start if cannot find IDP or parse its meta +# development helpers +SP_FORCE_ENTITY_ID = None # do NOT set for production, set to some unique string on development +IDP_IGNORE = False # ignore IDP entirely, SSO will not function + + +""" +Settings you DON'T want to change (in fact, you want to avoid even thinking about them): +""" SP_METADATA_LIFETIME_DAYS = 365 * 20 diff --git a/ssoauth/apps.py b/ssoauth/apps.py index bad7353..f9056e1 100644 --- a/ssoauth/apps.py +++ b/ssoauth/apps.py @@ -1,6 +1,7 @@ from django.apps import AppConfig from django.core import management from django.db.models.signals import post_migrate +from django import conf from . import logger from . import sso_utils from . import app_settings @@ -12,13 +13,14 @@ class SSOAuthConfig(AppConfig): def ready(self, *args, **kwargs): super().ready(*args, **kwargs) # OneLogin settings stuff - try: - app_settings.ONELOGIN_SETTINGS = sso_utils.create_onelogin_settings(app_settings.ONELOGIN_SETTINGS_TEMPLATE) - except Exception as e: - if app_settings.IDP_REQUIRED: - raise e - else: - logger.error("SSO will not work. {ec}: {e}".format(ec=e.__class__.__name__, e=str(e),)) + if app_settings.IDP_IGNORE: + assert conf.settings.DEBUG, "And how should SSO work on production if you ignore the IDP?" + logger.info("SSO will not work.") + else: + try: + app_settings.ONELOGIN_SETTINGS = sso_utils.create_onelogin_settings(app_settings.ONELOGIN_SETTINGS_TEMPLATE) + except Exception as e: + raise RuntimeError("SSO failed to start. {ec}: {e}".format(ec=e.__class__.__name__, e=str(e),)) # default groups post_migrate.connect(self.post_migrate_callback, sender=self) diff --git a/ssoauth/checks.py b/ssoauth/checks.py index 11798c3..d7155de 100644 --- a/ssoauth/checks.py +++ b/ssoauth/checks.py @@ -3,6 +3,7 @@ from django.contrib.auth import get_user_model from django.db.utils import OperationalError, ProgrammingError from django import conf from django import urls +from . import app_settings def _get_abstract_user(): @@ -90,3 +91,14 @@ def session_lifetime(app_configs, **kwargs): )) return errors + +@register(Tags.compatibility) +def production_entity_id(app_configs, **kwargs): + errors = list() + if not conf.settings.DEBUG and app_settings.SP_FORCE_ENTITY_ID: + errors.append(Warning( + "You set SP_FORCE_ENTITY_ID. Should better avoid doing so on production.", + obj=conf.settings, + )) + return errors + diff --git a/ssoauth/sso_utils.py b/ssoauth/sso_utils.py index 449936e..7679477 100644 --- a/ssoauth/sso_utils.py +++ b/ssoauth/sso_utils.py @@ -55,18 +55,22 @@ def get_idp_runtime_info(meta_url): def create_onelogin_settings(template): """ This function is intended to be run only once, on app startup. Raises exceptions. """ + # get the template template = copy(template) + # prepare some values + protocol = "https" if app_settings.SP_SSL else "http" + port_suffix = "" if app_settings.SP_PORT in [80, 443] else ":{0}".format(app_settings.SP_PORT) + host = app_settings.SP_HOST + host_full = "{protocol}://{host}{port_suffix}".format(**locals()) # SP settings - this_host = "https://{host}".format(host=app_settings.SP_HOST) - if app_settings.SP_PORT != 443: - this_host += ":{}".format(app_settings.SP_PORT) - template["sp"]["entityId"] = this_host + urls.reverse("sso-saml2-meta") - template["sp"]["assertionConsumerService"]["url"] = this_host + urls.reverse("sso-saml2-acs") + template["sp"]["entityId"] = app_settings.SP_FORCE_ENTITY_ID or (host_full + urls.reverse("sso-saml2-meta")) + template["sp"]["assertionConsumerService"]["url"] = host_full + urls.reverse("sso-saml2-acs") # IDP settings idp_info = get_idp_runtime_info(app_settings.IDP_META_URL) template["idp"]["x509certMulti"]["signing"] = idp_info["certificates"]["signing"] template["idp"]["x509certMulti"]["encryption"] = idp_info["certificates"]["encryption"] template["idp"]["singleSignOnService"]["url"] = idp_info["bindings"]["sso_redirect"] template["idp"]["singleLogoutService"]["url"] = idp_info["bindings"]["slo_redirect"] + # done return OneLogin_Saml2_Settings(settings=template, sp_validation_only=True) diff --git a/ssoauth/views.py b/ssoauth/views.py index 5f8e1c5..c100631 100644 --- a/ssoauth/views.py +++ b/ssoauth/views.py @@ -31,16 +31,21 @@ ATTRIBUTE_MAPPING = dict( class SAMLMixin: """ Merges Django runtime info and settings for OneLogin toolkit """ + def __init__(self, *args, **kwargs): + if not app_settings.ONELOGIN_SETTINGS: + raise exceptions.ImproperlyConfigured("SSO is not configured.") + super().__init__(*args, **kwargs) + @staticmethod def get_onelogin_request_data(request): return { # since nginx is in the way, the settings cannot be fetched from the request - 'https': 'on', # nginx should do it and django will never have a clue - 'http_host': app_settings.SP_HOST, # nginx might change it - 'server_port': app_settings.SP_PORT, # nginx will change it - 'script_name': request.META['PATH_INFO'], - 'get_data': request.GET.copy(), - 'post_data': request.POST.copy(), + "https": "on" if app_settings.SP_SSL else "off", + "http_host": app_settings.SP_HOST, # nginx might change it + "server_port": app_settings.SP_PORT, # nginx will change it + "script_name": request.META["PATH_INFO"], + "get_data": request.GET.copy(), + "post_data": request.POST.copy(), } @classmethod -- GitLab