-
Notifications
You must be signed in to change notification settings - Fork 96
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit f123a78
Showing
44 changed files
with
1,652 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
*.sw[a-z] | ||
*.py[oc] | ||
*.log | ||
|
||
*.egg-info/ | ||
*.tox/ | ||
|
||
*.sqlite3 | ||
*.db | ||
|
||
.env | ||
|
||
.cache | ||
.coverage | ||
|
||
# Python release artifacts | ||
build/ | ||
dist/ | ||
_build/ | ||
|
||
tags |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
language: python | ||
python: | ||
- "2.7" | ||
- "3.4" | ||
- "3.5" | ||
- "3.6" | ||
env: | ||
- DJANGO_VERSION=1.8 | ||
- DJANGO_VERSION=1.9 | ||
- DJANGO_VERSION=1.10 | ||
- DJANGO_VERSION=1.11b1 | ||
install: | ||
- pip install -q Django==$DJANGO_VERSION | ||
- pip install -q -r requirements-dev.txt | ||
- python setup.py install | ||
script: | ||
- python -Wall runtests.py |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
Copyright (c) 2015 Mobify Research & Development Inc. | ||
|
||
Permission is hereby granted, free of charge, to any person obtaining a copy | ||
of this software and associated documentation files (the "Software"), to deal | ||
in the Software without restriction, including without limitation the rights | ||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
copies of the Software, and to permit persons to whom the Software is | ||
furnished to do so, subject to the following conditions: | ||
|
||
The above copyright notice and this permission notice shall be included in all | ||
copies or substantial portions of the Software. | ||
|
||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
SOFTWARE. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
include README.md | ||
include LICENSE.txt | ||
recursive-include djangosaml2idp/templates docs * | ||
global-exclude *.pyc *.sqlite *.db |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
djangosaml2idp | ||
=============== | ||
|
||
.. image:: https://img.shields.io/pypi/v/djangosaml2idp.svg | ||
:target: https://pypi.python.org/pypi/djangosaml2idp | ||
:alt: PyPi | ||
|
||
.. image:: https://travis-ci.org/mhindery/djangosaml2idp.svg?branch=master | ||
:target: https://travis-ci.org/mhindery/djangosaml2idp | ||
:alt: Travis CI | ||
|
||
.. image:: https://landscape.io/github/mhindery/djangosaml2idp/master/landscape.svg?style=flat | ||
:target: https://landscape.io/github/mhindery/djangosaml2idp/master | ||
:alt: Code Health | ||
|
||
.. image:: https://lima.codeclimate.com/github/mhindery/djangosaml2idp/badges/gpa.svg | ||
:target: https://lima.codeclimate.com/github/mhindery/djangosaml2idp | ||
:alt: Code Climate | ||
|
||
.. image:: https://requires.io/github/mhindery/djangosaml2idp/requirements.svg?branch=master | ||
:target: https://requires.io/github/mhindery/djangosaml2idp/requirements/?branch=master | ||
:alt: Requirements Status | ||
|
||
|
||
djangosaml2idp implements the Identity Provider side of the SAML2 protocol with Django. | ||
It builds on top of PySAML2_, is compatible with Python 2/3 and all current supported Django versions. | ||
|
||
.. _PySAML2: https://github.com/rohe/pysaml2/ | ||
|
||
Installation | ||
------------ | ||
|
||
PySAML2 uses xmlsec1_ binary to sign SAML assertions so you need to install | ||
it either through your operating system package or by compiling the source | ||
code. It doesn't matter where the final executable is installed because | ||
you will need to set the full path to it in the configuration stage. | ||
``xmlsec`` is available (at least) for Debian, OSX and Alpine Linux. | ||
|
||
.. _xmlsec1: http://www.aleksey.com/xmlsec/ | ||
|
||
Now you can install the djangosaml2idp package using pip. This | ||
will also install PySAML2 and its dependencies automatically:: | ||
|
||
pip install djangosaml2idp | ||
|
||
|
||
Configuration & Usage | ||
--------------------- | ||
The first thing you need to do is add ``djangosaml2idp`` to the list of installed apps:: | ||
|
||
INSTALLED_APPS = ( | ||
'django.contrib.admin', | ||
'djangosaml2idp', | ||
... | ||
) | ||
|
||
Now include ``djangosaml2idp`` in your project by adding it in the url config:: | ||
|
||
from django.conf.urls import url, include | ||
from django.contrib import admin | ||
|
||
urlpatterns = [ | ||
url(r'^idp/', include('djangosaml2idp.urls')), | ||
url(r'^admin/', admin.site.urls), | ||
... | ||
] | ||
|
||
In your Django settings, configure your IdP. Configuration follows the pysaml2_configuration_. The IdP from the example project looks like this:: | ||
|
||
... | ||
import saml2 | ||
from saml2.saml import NAMEID_FORMAT_EMAILADDRESS, NAMEID_FORMAT_UNSPECIFIED | ||
from saml2.sigver import get_xmlsec_binary | ||
|
||
BASE_URL = "http://localhost:9000/idp" | ||
|
||
SAML_IDP_CONFIG = { | ||
"entityid": "%s/metadata" % BASE_URL, | ||
"description": "Example IdP setup", | ||
"valid_for": 168, | ||
"service": { | ||
"idp": { | ||
"name": "Django localhost IdP", | ||
"endpoints": { | ||
"single_sign_on_service": [ | ||
("%s/sso/post" % BASE_URL, saml2.BINDING_HTTP_POST), | ||
("%s/sso/redirect" % BASE_URL, saml2.BINDING_HTTP_REDIRECT), | ||
], | ||
}, | ||
"name_id_format": [NAMEID_FORMAT_EMAILADDRESS, NAMEID_FORMAT_UNSPECIFIED], | ||
'sign_response': True, | ||
'sign_assertion': True, | ||
}, | ||
}, | ||
'metadata': { | ||
'local': [os.path.join(os.path.join(os.path.join(BASE_DIR, 'idp'), 'saml2_config'), 'sp_metadata.xml')], | ||
}, | ||
# Signing | ||
'key_file': BASE_DIR + '/certificates/private_key.pem', # private part | ||
'cert_file': BASE_DIR + '/certificates/public_key.pem', # public part | ||
# Encryption | ||
'encryption_keypairs': [{ | ||
'key_file': BASE_DIR + '/certificates/private_key.pem', # private part | ||
'cert_file': BASE_DIR + '/certificates/public_key.pem', # public part | ||
}], | ||
"xmlsec_binary": get_xmlsec_binary(["/opt/local/bin", "/usr/bin/xmlsec1"]), | ||
'debug' : DEBUG, | ||
} | ||
|
||
You also have to define a mapping for each SP you talk to:: | ||
|
||
SAML_IDP_ACS_ATTRIBUTE_MAPPING = { | ||
"http://localhost:8000/saml2/metadata/": { | ||
# Map Django user attributes to output SAML attributes | ||
'email': 'email', | ||
'first_name': 'first_name', | ||
'last_name': 'last_name', | ||
'is_staff': 'is_staff', | ||
'is_superuser': 'is_superuser', | ||
} | ||
} | ||
|
||
That's all for the IdP configuration. Assuming you run the Django development server on localhost:8000, you can get its metadata by visiting http://localhost:8000/idp/metadata/. | ||
Use this metadata xml to configure your SP. Place the metadata xml from that SP in the location specified in the config dict (sp_metadata.xml in the example above). | ||
|
||
.. _pysaml2_configuration: https://github.com/rohe/pysaml2/blob/master/doc/howto/config.rst | ||
|
||
Example project | ||
--------------- | ||
``example_project`` contains a barebone demo setup. | ||
It consists of a Service Provider implemented with ``djangosaml2`` and an Identity Provider using ``djangosaml2idp``. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
# -*- coding: utf-8 -*- | ||
from __future__ import absolute_import, division, print_function, unicode_literals | ||
|
||
__version__ = '0.2.0' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
<head> | ||
<title>SAML 2.0 POST</title> | ||
</head> | ||
<body> | ||
<form method="post" action="{{ acs_url }}"> | ||
<input type="hidden" name="RelayState" value="{{ relay_state }}" /> | ||
<input type="hidden" name="SAMLResponse" value="{{ saml_response }}" /> | ||
<input type="submit" value="Submit" /> | ||
</form> | ||
{% if autosubmit %} | ||
<script type="text/javascript"> window.onload = function () { document.forms[0].submit(); }</script> | ||
{% endif %} | ||
</body> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
from __future__ import absolute_import, division, print_function, unicode_literals | ||
|
||
from django.conf.urls import url | ||
from . import views | ||
|
||
urlpatterns = [ | ||
url(r'^sso/post', views.sso_entry, name="saml_login_post"), | ||
url(r'^sso/redirect', views.sso_entry, name="saml_login_redirect"), | ||
url(r'^login/process/$', views.login_process, name='saml_login_process'), | ||
url(r'^metadata/$', views.metadata, name='saml2_idp_metadata'), | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
# -*- coding: utf-8 -*- | ||
from __future__ import (absolute_import, division, print_function, | ||
unicode_literals) | ||
|
||
import copy | ||
import logging | ||
|
||
from django.conf import settings | ||
from django.contrib.auth.decorators import login_required | ||
from django.core.urlresolvers import reverse | ||
from django.http import (HttpResponse, HttpResponseBadRequest, | ||
HttpResponseRedirect, HttpResponseServerError) | ||
from django.utils.datastructures import MultiValueDictKeyError | ||
from django.views.decorators.csrf import csrf_exempt | ||
from saml2 import BINDING_HTTP_POST, BINDING_HTTP_REDIRECT | ||
from saml2.authn_context import PASSWORD, AuthnBroker, authn_context_class_ref | ||
from saml2.config import IdPConfig | ||
from saml2.ident import NameID | ||
from saml2.metadata import entity_descriptor | ||
from saml2.s_utils import UnknownPrincipal, UnsupportedBinding | ||
from saml2.server import Server | ||
from six import text_type | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
@csrf_exempt | ||
def sso_entry(request): | ||
passed_data = request.POST if request.method == 'POST' else request.GET | ||
try: | ||
request.session['SAMLRequest'] = passed_data['SAMLRequest'] | ||
except (KeyError, MultiValueDictKeyError) as e: | ||
return HttpResponseBadRequest(e) | ||
request.session['RelayState'] = passed_data.get('RelayState', '') | ||
# For HTTP-REDIRECT endpoint | ||
if "SigAlg" in passed_data and "Signature" in passed_data: | ||
request.session['SigAlg'] = passed_data['SigAlg'] | ||
request.session['Signature'] = passed_data['Signature'] | ||
return HttpResponseRedirect(reverse('saml_login_process')) | ||
|
||
|
||
def create_identity(user, sp_mapping): | ||
identity = {} | ||
for user_attr, out_attr in sp_mapping.items(): | ||
if hasattr(user, user_attr): | ||
identity[out_attr] = getattr(user, user_attr) | ||
return identity | ||
|
||
|
||
# TODO Add http redirect logic based on https://github.com/rohe/pysaml2/blob/master/example/idp2_repoze/idp.py#L327 | ||
@login_required | ||
def login_process(request): | ||
""" | ||
Processor-based login continuation. | ||
Presents a SAML 2.0 Assertion for POSTing back to the Service Provider. | ||
""" | ||
# Construct server with config from settings dict | ||
conf = IdPConfig() | ||
conf.load(copy.deepcopy(settings.SAML_IDP_CONFIG)) | ||
IDP = Server(config=conf) | ||
# Parse incoming request | ||
try: | ||
req_info = IDP.parse_authn_request(request.session['SAMLRequest'], BINDING_HTTP_POST) | ||
_authn_req = req_info.message | ||
except Exception as excp: | ||
return HttpResponseBadRequest(excp) | ||
|
||
# Signed request for HTTP-REDIRECT | ||
if "SigAlg" in request.session and "Signature" in request.session: | ||
_certs = IDP.metadata.certs(_authn_req.issuer.text, "any", "signing") | ||
verified_ok = False | ||
for cert in _certs: | ||
# TODO implement | ||
#if verify_redirect_signature(_info, IDP.sec.sec_backend, cert): | ||
# verified_ok = True | ||
# break | ||
pass | ||
if not verified_ok: | ||
return HttpResponseBadRequest("Message signature verification failure") | ||
|
||
binding_out, destination = IDP.pick_binding( | ||
service="assertion_consumer_service", | ||
entity_id=_authn_req.issuer.text) | ||
|
||
# Gather response arguments | ||
try: | ||
resp_args = IDP.response_args(_authn_req) | ||
except (UnknownPrincipal, UnsupportedBinding) as excp: | ||
return HttpResponseServerError(excp) | ||
|
||
# Create Identity dict (SP-specific) | ||
try: | ||
sp_mapping = settings.SAML_IDP_ACS_ATTRIBUTE_MAPPING.get(resp_args['sp_entity_id'], {'username': 'username'}) | ||
except AttributeError: | ||
identity = {'username': 'username'} | ||
identity = create_identity(request.user, sp_mapping) | ||
|
||
# TODO investigate how this works, because I don't get it | ||
req_authn_context = _authn_req.requested_authn_context or PASSWORD | ||
AUTHN_BROKER = AuthnBroker() | ||
AUTHN_BROKER.add(authn_context_class_ref(req_authn_context), "") | ||
|
||
# Construct SamlResponse message | ||
try: | ||
authn_resp = IDP.create_authn_response( | ||
identity=identity, userid=request.user.username, | ||
name_id=NameID(format=resp_args['name_id_policy'].format, sp_name_qualifier=destination, text=request.user.username), | ||
authn=AUTHN_BROKER.get_authn_by_accr(req_authn_context), | ||
sign_response=IDP.config.getattr("sign_response", "idp") or False, | ||
sign_assertion=IDP.config.getattr("sign_assertion", "idp") or False, | ||
**resp_args) | ||
except Exception as excp: | ||
return HttpResponseServerError(excp) | ||
|
||
# Return as html with self-submitting form. | ||
http_args = IDP.apply_binding( | ||
binding=binding_out, | ||
msg_str="%s" % authn_resp, | ||
destination=destination, | ||
relay_state=request.session['RelayState'], | ||
response=True) | ||
return HttpResponse(http_args['data']) | ||
|
||
|
||
def metadata(request): | ||
"""Returns an XML with the SAML 2.0 metadata for this Idp as configured in the settings.py file. | ||
""" | ||
conf = IdPConfig() | ||
conf.load(copy.deepcopy(settings.SAML_IDP_CONFIG)) | ||
metadata = entity_descriptor(conf) | ||
return HttpResponse(content=text_type(metadata).encode('utf-8'), content_type="text/xml; charset=utf8") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
# Minimal makefile for Sphinx documentation | ||
# | ||
|
||
# You can set these variables from the command line. | ||
SPHINXOPTS = | ||
SPHINXBUILD = sphinx-build | ||
SPHINXPROJ = djangosaml2idp | ||
SOURCEDIR = . | ||
BUILDDIR = _build | ||
|
||
# Put it first so that "make" without argument is like "make help". | ||
help: | ||
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) | ||
|
||
.PHONY: help Makefile | ||
|
||
# Catch-all target: route all unknown targets to Sphinx using the new | ||
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). | ||
%: Makefile | ||
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) |
Oops, something went wrong.