Skip to content

Commit

Permalink
Some cleanup and added comments. Improve docs.
Browse files Browse the repository at this point in the history
  • Loading branch information
mhindery committed Mar 14, 2017
1 parent f123a78 commit 5a8a8f5
Show file tree
Hide file tree
Showing 23 changed files with 1,116 additions and 376 deletions.
52 changes: 30 additions & 22 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ djangosaml2idp
:target: https://pypi.python.org/pypi/djangosaml2idp
:alt: PyPi

.. image:: https://readthedocs.org/projects/djangosaml2idp/badge/?version=latest
:alt: Documentation Status
:scale: 100%
:target: https://djangosaml2idp.readthedocs.io/en/latest/?badge=latest

.. image:: https://travis-ci.org/mhindery/djangosaml2idp.svg?branch=master
:target: https://travis-ci.org/mhindery/djangosaml2idp
:alt: Travis CI
Expand Down Expand Up @@ -34,7 +39,7 @@ 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.
xmlsec is available (at least) for Debian, OSX and Alpine Linux.

.. _xmlsec1: http://www.aleksey.com/xmlsec/

Expand Down Expand Up @@ -72,46 +77,49 @@ In your Django settings, configure your IdP. Configuration follows the pysaml2_c
from saml2.saml import NAMEID_FORMAT_EMAILADDRESS, NAMEID_FORMAT_UNSPECIFIED
from saml2.sigver import get_xmlsec_binary

BASE_URL = "http://localhost:9000/idp"
LOGIN_URL = '/login/'
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),
'debug' : DEBUG,
'xmlsec_binary': get_xmlsec_binary(['/opt/local/bin', '/usr/bin/xmlsec1']),
'entityid': '%s/metadata' % BASE_URL,
'description': 'Example IdP setup',

'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],
'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
'key_file': BASE_DIR + '/certificates/private_key.pem',
'cert_file': BASE_DIR + '/certificates/public_key.pem',
# Encryption
'encryption_keypairs': [{
'key_file': BASE_DIR + '/certificates/private_key.pem', # private part
'cert_file': BASE_DIR + '/certificates/public_key.pem', # public part
'key_file': BASE_DIR + '/certificates/private_key.pem',
'cert_file': BASE_DIR + '/certificates/public_key.pem',
}],
"xmlsec_binary": get_xmlsec_binary(["/opt/local/bin", "/usr/bin/xmlsec1"]),
'debug' : DEBUG,
'valid_for': 365,
}

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
SAML_IDP_ATTRIBUTE_MAPPING = {
'http://localhost:8000/saml2/metadata/': {
# DJANGO: SAML
'email': 'email',
'first_name': 'first_name',
'last_name': 'last_name',
Expand Down
2 changes: 1 addition & 1 deletion djangosaml2idp/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, print_function, unicode_literals

__version__ = '0.2.0'
__version__ = '0.2.1'
6 changes: 6 additions & 0 deletions djangosaml2idp/identity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
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
2 changes: 0 additions & 2 deletions djangosaml2idp/templates/djangosaml2idp/login.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,5 @@
<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>
42 changes: 20 additions & 22 deletions djangosaml2idp/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,38 +21,35 @@
from saml2.server import Server
from six import text_type

from .identity import create_identity

logger = logging.getLogger(__name__)



@csrf_exempt
def sso_entry(request):
""" Entrypoint view for SSO. Gathers the parameters from the HTTP request, stores them in the session
and redirects the requester to the login_process view.
"""
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
# TODO check how the redirect saml way works. Taken from example idp in pysaml2.
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.
""" View which processes the actual SAML request and returns a self-submitting form with the SAML response.
The login_required decorator ensures the user authenticates first on the IdP using 'normal' ways.
"""
# Construct server with config from settings dict
conf = IdPConfig()
Expand All @@ -61,13 +58,12 @@ def login_process(request):
# 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)

# TODO this is taken from example, but no idea how this works or whats it does. Check SAML2 specification?
# 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")
_certs = IDP.metadata.certs(req_info.message.issuer.text, "any", "signing")
verified_ok = False
for cert in _certs:
# TODO implement
Expand All @@ -80,23 +76,24 @@ def login_process(request):

binding_out, destination = IDP.pick_binding(
service="assertion_consumer_service",
entity_id=_authn_req.issuer.text)
entity_id=req_info.message.issuer.text)

# Gather response arguments
try:
resp_args = IDP.response_args(_authn_req)
resp_args = IDP.response_args(req_info.message)
except (UnknownPrincipal, UnsupportedBinding) as excp:
return HttpResponseServerError(excp)

# Create Identity dict (SP-specific)
# TODO abstract somewhat, make pluggable/configurable
try:
sp_mapping = settings.SAML_IDP_ACS_ATTRIBUTE_MAPPING.get(resp_args['sp_entity_id'], {'username': 'username'})
sp_mapping = settings.SAML_IDP_ATTRIBUTE_MAPPING.get(resp_args['sp_entity_id'], {'username': 'username'})
except AttributeError:
identity = {'username': 'username'}
sp_mapping = {'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
# TODO investigate how this works, because I don't get it. Specification?
req_authn_context = req_info.message.requested_authn_context or PASSWORD
AUTHN_BROKER = AuthnBroker()
AUTHN_BROKER.add(authn_context_class_ref(req_authn_context), "")

Expand All @@ -123,7 +120,8 @@ def login_process(request):


def metadata(request):
"""Returns an XML with the SAML 2.0 metadata for this Idp as configured in the settings.py file.
""" Returns an XML with the SAML 2.0 metadata for this Idp.
The metadata is constructed on-the-fly based on the config dict in the django settings.
"""
conf = IdPConfig()
conf.load(copy.deepcopy(settings.SAML_IDP_CONFIG))
Expand Down
36 changes: 36 additions & 0 deletions docs/example_setup/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
Example SP/IdP implementation
=============================

This is a barebone example implementation of a setup with a Service Provider and Identity Provider.
The SP is implemented using djangosaml2, the IdP using djangosaml2idp.
Both are default django projects with only the bare minimum of added code for a functional demo or start.
This to keep it clear and obvious how to implement the SP/IdP functionality without other clutter serving as distraction.

A docker-compose_ file is included, providing a minimum-entry-barrier setup to get up and running, without complicated & error-prone setup requirements.
The example will run equally on Mac/Windows/Linux using docker.

.. _docker-compose: https://docs.docker.com/compose/

How to run
----------

Go to this folder in a terminal and start the containers::

docker-compose up -d

Give it a minute the first time to download and build the required images. They'll be cached for the successive runs.
You now have a SP running at http://localhost:8000/ and a IdP at http://localhost:9000/ (you can check with :code:`docker-compose ps`), configured to talk with each other.
In order to do some login, you will need to create a user account on the IdP::

docker exec -it djangosaml2idp_idp python manage.py createsuperuser

Now go to the SP in your browser. The page shows you're not logged in; click on the link to login. You'll get redirected to the IdP which
shows a basic login form. Enter the credentials from the user you just created. You will then get redirected back to the SP, showing you are logged in with the user information.
And that is essentially what SAML2 does :)

Cleanup
-------

To stop the containers::

docker-compose stop
112 changes: 9 additions & 103 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ djangosaml2idp
:target: https://travis-ci.org/mhindery/djangosaml2idp
:alt: Travis CI

.. image:: https://readthedocs.org/projects/djangosaml2idp/badge/?version=latest
:alt: Documentation Status
:scale: 100%
:target: https://djangosaml2idp.readthedocs.io/en/latest/?badge=latest

.. image:: https://landscape.io/github/mhindery/djangosaml2idp/master/landscape.svg?style=flat
:target: https://landscape.io/github/mhindery/djangosaml2idp/master
:alt: Code Health
Expand All @@ -29,114 +34,15 @@ It builds on top of PySAML2_, is compatible with Python 2/3 and all current supp

.. _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``.

Table of contents
==================

.. toctree::
:maxdepth: 2
:caption: Contents:

installation
example_setup/README


Indices and tables
Expand Down
Loading

0 comments on commit 5a8a8f5

Please sign in to comment.