Skip to content

Commit

Permalink
initial add project
Browse files Browse the repository at this point in the history
  • Loading branch information
fang.li committed Apr 12, 2016
1 parent 6121c21 commit 9c446ce
Show file tree
Hide file tree
Showing 11 changed files with 442 additions and 0 deletions.
21 changes: 21 additions & 0 deletions AUTHORS.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
This project is written and maintained by Fang Li and
various contributors:


Django Saml2 Auth
-----------------

- Fang Li



Pysaml2
-------

- Roland Hedberg and it's contributors



Contributors
------------

13 changes: 13 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Copyright 2016 Fang Li

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
3 changes: 3 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
include LICENSE README.rst AUTHORS.rst
recursive-include django_saml2_auth/templates/django_saml2_auth *.html
global-exclude *.pyc[co]
133 changes: 133 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
=====================================
Django SAML2 Authentication Made Easy
=====================================

:Author: Fang Li
:Version: 1.0.1b1

.. image:: https://api.travis-ci.org/fangli/django_saml2_auth.png?branch=master
:target: https://travis-ci.org/fangli/django_saml2_auth

.. image:: https://img.shields.io/pypi/v/django_saml2_auth.svg
:target: https://pypi.python.org/pypi/django_saml2_auth

.. image:: https://img.shields.io/pypi/dm/django_saml2_auth.svg
:target: https://pypi.python.org/pypi/django_saml2_auth

This project aim to provide a dead simple way to integrate your Django powered app with SAML2 Authentication.
Try it now, and get rid of the complicated configuration of saml.

Any SAML2 based SSO(Single-Sign-On) with dynamic metadata configuration was supported by this django plugin, Such as okta.



Install
=======

You can install this plugin via `pip`:

.. code-block:: bash
# pip install django_saml2_auth
or from source:

.. code-block:: bash
# git clone https://github.com/fangli/django-saml2-auth
# cd django-saml2-auth
# python setup.py install
What does this plugin do?
=========================

This plugin takes over django's login page and redirect user to SAML2 SSO authentication service. While a user
logged in and redirected back, it will check if this user is already in system. If not, it will create the user using django's default UserModel,
otherwise redirect the user to the last visited page.



How to use?
===========

1. Override the default login page in root urls.py, by adding these lines **BEFORE** any `urlpatterns`:

.. code-block:: python
# This is the SAML2 related URLs, you can change "^saml2_auth/" to any path you want, like "^sso_auth/", "^sso_login/", etc. (required)
url(r'^saml2_auth/', include('django_saml2_auth.urls')),
# If you want to replace the default user login with SAML2, just use the following line (optional)
url(r'^accounts/login/$', 'django_saml2_auth.views.signin'),
# If you want to replace the admin login with SAML2, use the following line (optional)
url(r'^admin/login/$', 'django_saml2_auth.views.signin'),
2. In settings.py, add SAML2 related configuration.

Please note only METADATA_AUTO_CONF_URL is required. The following block just shows the full featured configuration and their default values.

.. code-block:: python
SAML2_AUTH = {
'METADATA_AUTO_CONF_URL': '[The auto(dynamic) metadata configuration URL of SAML2]',
'NEW_USER_PROFILE': {
'USER_GROUPS': [], # The default group name when a new user logged in
'ACTIVE_STATUS': True, # The default active status of new user
'STAFF_STATUS': True, # The staff status of new user
'SUPERUSER_STATUS': False, # The superuser status of new user
},
'ATTRIBUTES_MAP': { # Change Email/UserName/FirstName/LastName to corresponding SAML2 userprofile attributes.
'email': 'Email',
'username': 'UserName',
'first_name': 'FirstName',
'last_name': 'LastName',
}
}
3. Well done.



Customize
=========

You are allowed to override the default permission `denied` page and new user `welcome` page.

Just put a template named 'django_saml2_auth/welcome.html' or 'django_saml2_auth/denied.html' under your project's template folder.

In case of 'django_saml2_auth/welcome.html' existed, when a new user logged in, we'll show this template instead of redirecting user to the
previous visited page. So you can have some first-visit notes and welcome words in this page. You can get user context in the template by
using `user` context.

By the way, we have a built-in logout page as well, if you want to use it, just add the following lines into your urls.py, before any
`urlpatterns`:

.. code-block:: python
# If you want to replace the default user logout with plugin built-in page, just use the following line (optional)
url(r'^accounts/logout/$', 'django_saml2_auth.views.signout'),
# If you want to replace the admin logout with SAML2, use the following line (optional)
url(r'^admin/logout/$', 'django_saml2_auth.views.signout'),
In a similar way, you can customize this logout template by added a template 'django_saml2_auth/signout.html'.


By default, we assume your SAML2 service provided user attribute Email/UserName/FirstName/LastName. Please change it to the correct
user attributes mapping.



How to Contribute
=================

#. Check for open issues or open a fresh issue to start a discussion around a feature idea or a bug.
#. Fork `the repository`_ on GitHub to start making your changes to the **master** branch (or branch off of it).
#. Write a test which shows that the bug was fixed or that the feature works as expected.
#. Send a pull request and bug the maintainer until it gets merged and published. :) Make sure to add yourself to AUTHORS_.

.. _`the repository`: http://github.com/fangli/django-saml2-auth
.. _AUTHORS: https://github.com/fangli/django-saml2-auth/blob/master/AUTHORS.rst
1 change: 1 addition & 0 deletions django_saml2_auth/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__version__ = '1.0.1b1'
13 changes: 13 additions & 0 deletions django_saml2_auth/templates/django_saml2_auth/denied.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{% load i18n %}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>{% trans "Permission Denied" %}</title>
</head>
<body>
<h2>{% trans "Sorry, you are not allowed to access this app" %}</h2>
<hr>
<p>{% trans "If you think it's an incorrect configuration, write email to your system administrator" %}</p>
</body>
</html>
13 changes: 13 additions & 0 deletions django_saml2_auth/templates/django_saml2_auth/signout.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{% load i18n %}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>{% trans "Signed out" %}</title>
</head>
<body>
<h2>{% trans "You have signed out successfully." %}</h2>
<hr>
<p>{% trans "If you want to login again or switch to another account, please do it in SSO." %}</p>
</body>
</html>
11 changes: 11 additions & 0 deletions django_saml2_auth/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from django.conf.urls import url, patterns


app_name = 'django_saml2_auth'

urlpatterns = patterns(
'django_saml2_auth.views',
url(r'^acs/$', "acs", name="acs"),
url(r'^welcome/$', "welcome", name="welcome"),
url(r'^denied/$', "denied", name="denied"),
)
169 changes: 169 additions & 0 deletions django_saml2_auth/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
#!/usr/bin/env python
# -*- coding:utf-8 -*-


import urllib2
from saml2 import (
BINDING_HTTP_POST,
BINDING_HTTP_REDIRECT,
entity,
)
from saml2.client import Saml2Client
from saml2.config import Config as Saml2Config

from django.conf import settings
from django.core.urlresolvers import reverse
from django.contrib.auth.models import (User, Group)
from django.contrib.auth.decorators import login_required
from django.contrib.auth import login, logout
from django.shortcuts import render
from django.views.decorators.csrf import csrf_exempt
from django.template import TemplateDoesNotExist
from django.http import HttpResponseRedirect


def get_current_domain(r):
return '{scheme}://{host}'.format(
scheme=r.scheme,
host=r.get_host(),
)


def _get_saml_client(domain):
acs_url = domain + reverse('acs')
import tempfile
tmp = tempfile.NamedTemporaryFile()
f = open(tmp.name, 'w')
f.write(urllib2.urlopen(settings.SAML2_AUTH['METADATA_AUTO_CONF_URL']).read())
f.close()
saml_settings = {
'metadata': {
"local": [tmp.name],
},
'service': {
'sp': {
'endpoints': {
'assertion_consumer_service': [
(acs_url, BINDING_HTTP_REDIRECT),
(acs_url, BINDING_HTTP_POST)
],
},
'allow_unsolicited': True,
'authn_requests_signed': False,
'logout_requests_signed': True,
'want_assertions_signed': True,
'want_response_signed': False,
},
},
}

spConfig = Saml2Config()
spConfig.load(saml_settings)
spConfig.allow_unknown_attributes = True
saml_client = Saml2Client(config=spConfig)
tmp.close()
return saml_client


@login_required
def welcome(r):
try:
return render(r, 'django_saml2_auth/welcome.html', context={'user': r.user})
except TemplateDoesNotExist:
return HttpResponseRedirect(reverse('admin:index'))


def denied(r):
return render(r, 'django_saml2_auth/denied.html')


def _create_new_user(username, email, firstname, lastname):
user = User.objects.create_user(username, email)
user.first_name = firstname
user.last_name = lastname
user.groups = [Group.objects.get(name=x) for x in settings.SAML2_AUTH.get('NEW_USER_PROFILE', {}).get('USER_GROUPS', [])]
user.is_active = settings.SAML2_AUTH.get('NEW_USER_PROFILE', {}).get('ACTIVE_STATUS', True)
user.is_staff = settings.SAML2_AUTH.get('NEW_USER_PROFILE', {}).get('STAFF_STATUS', True)
user.is_superuser = settings.SAML2_AUTH.get('NEW_USER_PROFILE', {}).get('SUPERUSER_STATUS', False)
user.save()
return user


@csrf_exempt
def acs(r):
saml_client = _get_saml_client(get_current_domain(r))
resp = r.POST.get('SAMLResponse', None)
next_url = r.session.get('login_next_url', reverse('admin:index'))

if not resp:
return HttpResponseRedirect(reverse('denied'))

authn_response = saml_client.parse_authn_request_response(
resp, entity.BINDING_HTTP_POST)
if authn_response is None:
return HttpResponseRedirect(reverse('denied'))

user_identity = authn_response.get_identity()
if user_identity is None:
return HttpResponseRedirect(reverse('denied'))

user_email = user_identity[settings.SAML2_AUTH.get('ATTRIBUTES_MAP', {}).get('email', 'Email')][0]
user_name = user_identity[settings.SAML2_AUTH.get('ATTRIBUTES_MAP', {}).get('username', 'UserName')][0]
user_first_name = user_identity[settings.SAML2_AUTH.get('ATTRIBUTES_MAP', {}).get('first_name', 'FirstName')][0]
user_last_name = user_identity[settings.SAML2_AUTH.get('ATTRIBUTES_MAP', {}).get('last_name', 'LastName')][0]

target_user = None
is_new_user = False

try:
target_user = User.objects.get(username=user_name)
except User.DoesNotExist:
target_user = _create_new_user(user_name, user_email, user_first_name, user_last_name)
is_new_user = True

r.session.flush()

if target_user.is_active:
target_user.backend = 'django.contrib.auth.backends.ModelBackend'
login(r, target_user)
else:
return HttpResponseRedirect(reverse('denied'))

if is_new_user:
try:
return render(r, 'django_saml2_auth/welcome.html', context={'user': r.user})
except TemplateDoesNotExist:
return HttpResponseRedirect(next_url)
else:
return HttpResponseRedirect(next_url)


def signin(r):
import urlparse
from urllib import unquote
next_url = r.GET.get('next', reverse('admin:index'))

try:
if "next=" in unquote(next_url):
next_url = urlparse.parse_qs(urlparse.urlparse(unquote(next_url)).query)['next'][0]
except:
next_url = r.GET.get('next', reverse('admin:index'))

r.session['login_next_url'] = next_url

saml_client = _get_saml_client(get_current_domain(r))
_, info = saml_client.prepare_for_authenticate()

redirect_url = None

for key, value in info['headers']:
if key == 'Location':
redirect_url = value
break

return HttpResponseRedirect(redirect_url)


def signout(r):
logout(r)
return render(r, 'django_saml2_auth/signout.html')
2 changes: 2 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[bdist_wheel]
universal=1
Loading

0 comments on commit 9c446ce

Please sign in to comment.