Skip to content
Snippets Groups Projects
Commit 40f87edb authored by Art's avatar Art :lizard:
Browse files

Make it possible to run SSO on localhost and without SSL (development setup).

parent 3388c4dc
Branches
Tags
No related merge requests found
#!/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
...@@ -14,10 +14,26 @@ ...@@ -14,10 +14,26 @@
- Add the app into `INSTALLED_APPS` - Add the app into `INSTALLED_APPS`
- Include the app's `urls.py` into the project `urls.py` `urlpatterns` - Include the app's `urls.py` into the project `urls.py` `urlpatterns`
- Add to the project settings - 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_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-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: - generate a new key pair:
- create `cert` directory: - create `cert` directory:
...@@ -27,15 +43,10 @@ ...@@ -27,15 +43,10 @@
- `./cert/sp.key` put the private key here - `./cert/sp.key` put the private key here
- `./cert/sp.pem` put the certificate here, signing is optional - `./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 #### Integrating your project into the existing SSO infrastructure
- **Ask somebody who knows more. Seriously.** - **Do this only if you want to use SSO. For development it's usually enough to use the dev view instead.**
- **Try on the test IdP before you brick the production!** - Ask somebody who knows more. Seriously.
- Try on the test IdP before you brick the production!
- Grab your meta - Grab your meta
- Run your project. - Run your project.
- Find meta of your SP (relative path `/saml2/meta` or view name `sso-saml2-meta`) - Find meta of your SP (relative path `/saml2/meta` or view name `sso-saml2-meta`)
......
...@@ -18,8 +18,7 @@ for setting_name in [k for k in globals().keys() if k.isupper()]: ...@@ -18,8 +18,7 @@ for setting_name in [k for k in globals().keys() if k.isupper()]:
# checks # checks
assert SP_HOST and SP_PORT, "Need SP_HOST and SP_PORT configured in settings." 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 protocol and port."
assert not SP_HOST.lower().startswith(("http:", "https:",)), "Need host name without a protocol."
# helpers # helpers
......
...@@ -13,19 +13,28 @@ Settings you may want to change: ...@@ -13,19 +13,28 @@ Settings you may want to change:
""" """
# host and port, not what Django thinks, but what nginx serves # 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_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" 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_KEY = "{project_settings}/cert/sp.key"
SP_CERT = "{project_settings}/cert/sp.pem" 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 SP_METADATA_LIFETIME_DAYS = 365 * 20
......
from django.apps import AppConfig from django.apps import AppConfig
from django.core import management from django.core import management
from django.db.models.signals import post_migrate from django.db.models.signals import post_migrate
from django import conf
from . import logger from . import logger
from . import sso_utils from . import sso_utils
from . import app_settings from . import app_settings
...@@ -12,13 +13,14 @@ class SSOAuthConfig(AppConfig): ...@@ -12,13 +13,14 @@ class SSOAuthConfig(AppConfig):
def ready(self, *args, **kwargs): def ready(self, *args, **kwargs):
super().ready(*args, **kwargs) super().ready(*args, **kwargs)
# OneLogin settings stuff # OneLogin settings stuff
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: try:
app_settings.ONELOGIN_SETTINGS = sso_utils.create_onelogin_settings(app_settings.ONELOGIN_SETTINGS_TEMPLATE) app_settings.ONELOGIN_SETTINGS = sso_utils.create_onelogin_settings(app_settings.ONELOGIN_SETTINGS_TEMPLATE)
except Exception as e: except Exception as e:
if app_settings.IDP_REQUIRED: raise RuntimeError("SSO failed to start. {ec}: {e}".format(ec=e.__class__.__name__, e=str(e),))
raise e
else:
logger.error("SSO will not work. {ec}: {e}".format(ec=e.__class__.__name__, e=str(e),))
# default groups # default groups
post_migrate.connect(self.post_migrate_callback, sender=self) post_migrate.connect(self.post_migrate_callback, sender=self)
......
...@@ -3,6 +3,7 @@ from django.contrib.auth import get_user_model ...@@ -3,6 +3,7 @@ from django.contrib.auth import get_user_model
from django.db.utils import OperationalError, ProgrammingError from django.db.utils import OperationalError, ProgrammingError
from django import conf from django import conf
from django import urls from django import urls
from . import app_settings
def _get_abstract_user(): def _get_abstract_user():
...@@ -90,3 +91,14 @@ def session_lifetime(app_configs, **kwargs): ...@@ -90,3 +91,14 @@ def session_lifetime(app_configs, **kwargs):
)) ))
return errors 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
...@@ -55,18 +55,22 @@ def get_idp_runtime_info(meta_url): ...@@ -55,18 +55,22 @@ def get_idp_runtime_info(meta_url):
def create_onelogin_settings(template): def create_onelogin_settings(template):
""" This function is intended to be run only once, on app startup. Raises exceptions. """ """ This function is intended to be run only once, on app startup. Raises exceptions. """
# get the template
template = copy(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 # SP settings
this_host = "https://{host}".format(host=app_settings.SP_HOST) template["sp"]["entityId"] = app_settings.SP_FORCE_ENTITY_ID or (host_full + urls.reverse("sso-saml2-meta"))
if app_settings.SP_PORT != 443: template["sp"]["assertionConsumerService"]["url"] = host_full + urls.reverse("sso-saml2-acs")
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")
# IDP settings # IDP settings
idp_info = get_idp_runtime_info(app_settings.IDP_META_URL) idp_info = get_idp_runtime_info(app_settings.IDP_META_URL)
template["idp"]["x509certMulti"]["signing"] = idp_info["certificates"]["signing"] template["idp"]["x509certMulti"]["signing"] = idp_info["certificates"]["signing"]
template["idp"]["x509certMulti"]["encryption"] = idp_info["certificates"]["encryption"] template["idp"]["x509certMulti"]["encryption"] = idp_info["certificates"]["encryption"]
template["idp"]["singleSignOnService"]["url"] = idp_info["bindings"]["sso_redirect"] template["idp"]["singleSignOnService"]["url"] = idp_info["bindings"]["sso_redirect"]
template["idp"]["singleLogoutService"]["url"] = idp_info["bindings"]["slo_redirect"] template["idp"]["singleLogoutService"]["url"] = idp_info["bindings"]["slo_redirect"]
# done
return OneLogin_Saml2_Settings(settings=template, sp_validation_only=True) return OneLogin_Saml2_Settings(settings=template, sp_validation_only=True)
...@@ -31,16 +31,21 @@ ATTRIBUTE_MAPPING = dict( ...@@ -31,16 +31,21 @@ ATTRIBUTE_MAPPING = dict(
class SAMLMixin: class SAMLMixin:
""" Merges Django runtime info and settings for OneLogin toolkit """ """ 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 @staticmethod
def get_onelogin_request_data(request): def get_onelogin_request_data(request):
return { return {
# since nginx is in the way, the settings cannot be fetched from the request # 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 "https": "on" if app_settings.SP_SSL else "off",
'http_host': app_settings.SP_HOST, # nginx might change it "http_host": app_settings.SP_HOST, # nginx might change it
'server_port': app_settings.SP_PORT, # nginx will change it "server_port": app_settings.SP_PORT, # nginx will change it
'script_name': request.META['PATH_INFO'], "script_name": request.META["PATH_INFO"],
'get_data': request.GET.copy(), "get_data": request.GET.copy(),
'post_data': request.POST.copy(), "post_data": request.POST.copy(),
} }
@classmethod @classmethod
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment