Skip to content

Commit 9c446ce

Browse files
author
fang.li
committed
initial add project
1 parent 6121c21 commit 9c446ce

File tree

11 files changed

+442
-0
lines changed

11 files changed

+442
-0
lines changed

AUTHORS.rst

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
This project is written and maintained by Fang Li and
2+
various contributors:
3+
4+
5+
Django Saml2 Auth
6+
-----------------
7+
8+
- Fang Li
9+
10+
11+
12+
Pysaml2
13+
-------
14+
15+
- Roland Hedberg and it's contributors
16+
17+
18+
19+
Contributors
20+
------------
21+

LICENSE

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
Copyright 2016 Fang Li
2+
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
7+
http://www.apache.org/licenses/LICENSE-2.0
8+
9+
Unless required by applicable law or agreed to in writing, software
10+
distributed under the License is distributed on an "AS IS" BASIS,
11+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
See the License for the specific language governing permissions and
13+
limitations under the License.

MANIFEST.in

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
include LICENSE README.rst AUTHORS.rst
2+
recursive-include django_saml2_auth/templates/django_saml2_auth *.html
3+
global-exclude *.pyc[co]

README.rst

+133
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
=====================================
2+
Django SAML2 Authentication Made Easy
3+
=====================================
4+
5+
:Author: Fang Li
6+
:Version: 1.0.1b1
7+
8+
.. image:: https://api.travis-ci.org/fangli/django_saml2_auth.png?branch=master
9+
:target: https://travis-ci.org/fangli/django_saml2_auth
10+
11+
.. image:: https://img.shields.io/pypi/v/django_saml2_auth.svg
12+
:target: https://pypi.python.org/pypi/django_saml2_auth
13+
14+
.. image:: https://img.shields.io/pypi/dm/django_saml2_auth.svg
15+
:target: https://pypi.python.org/pypi/django_saml2_auth
16+
17+
This project aim to provide a dead simple way to integrate your Django powered app with SAML2 Authentication.
18+
Try it now, and get rid of the complicated configuration of saml.
19+
20+
Any SAML2 based SSO(Single-Sign-On) with dynamic metadata configuration was supported by this django plugin, Such as okta.
21+
22+
23+
24+
Install
25+
=======
26+
27+
You can install this plugin via `pip`:
28+
29+
.. code-block:: bash
30+
31+
# pip install django_saml2_auth
32+
33+
or from source:
34+
35+
.. code-block:: bash
36+
37+
# git clone https://github.com/fangli/django-saml2-auth
38+
# cd django-saml2-auth
39+
# python setup.py install
40+
41+
42+
43+
What does this plugin do?
44+
=========================
45+
46+
This plugin takes over django's login page and redirect user to SAML2 SSO authentication service. While a user
47+
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,
48+
otherwise redirect the user to the last visited page.
49+
50+
51+
52+
How to use?
53+
===========
54+
55+
1. Override the default login page in root urls.py, by adding these lines **BEFORE** any `urlpatterns`:
56+
57+
.. code-block:: python
58+
59+
# This is the SAML2 related URLs, you can change "^saml2_auth/" to any path you want, like "^sso_auth/", "^sso_login/", etc. (required)
60+
url(r'^saml2_auth/', include('django_saml2_auth.urls')),
61+
62+
# If you want to replace the default user login with SAML2, just use the following line (optional)
63+
url(r'^accounts/login/$', 'django_saml2_auth.views.signin'),
64+
65+
# If you want to replace the admin login with SAML2, use the following line (optional)
66+
url(r'^admin/login/$', 'django_saml2_auth.views.signin'),
67+
68+
2. In settings.py, add SAML2 related configuration.
69+
70+
Please note only METADATA_AUTO_CONF_URL is required. The following block just shows the full featured configuration and their default values.
71+
72+
.. code-block:: python
73+
74+
SAML2_AUTH = {
75+
'METADATA_AUTO_CONF_URL': '[The auto(dynamic) metadata configuration URL of SAML2]',
76+
'NEW_USER_PROFILE': {
77+
'USER_GROUPS': [], # The default group name when a new user logged in
78+
'ACTIVE_STATUS': True, # The default active status of new user
79+
'STAFF_STATUS': True, # The staff status of new user
80+
'SUPERUSER_STATUS': False, # The superuser status of new user
81+
},
82+
'ATTRIBUTES_MAP': { # Change Email/UserName/FirstName/LastName to corresponding SAML2 userprofile attributes.
83+
'email': 'Email',
84+
'username': 'UserName',
85+
'first_name': 'FirstName',
86+
'last_name': 'LastName',
87+
}
88+
}
89+
90+
3. Well done.
91+
92+
93+
94+
Customize
95+
=========
96+
97+
You are allowed to override the default permission `denied` page and new user `welcome` page.
98+
99+
Just put a template named 'django_saml2_auth/welcome.html' or 'django_saml2_auth/denied.html' under your project's template folder.
100+
101+
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
102+
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
103+
using `user` context.
104+
105+
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
106+
`urlpatterns`:
107+
108+
.. code-block:: python
109+
110+
# If you want to replace the default user logout with plugin built-in page, just use the following line (optional)
111+
url(r'^accounts/logout/$', 'django_saml2_auth.views.signout'),
112+
113+
# If you want to replace the admin logout with SAML2, use the following line (optional)
114+
url(r'^admin/logout/$', 'django_saml2_auth.views.signout'),
115+
116+
In a similar way, you can customize this logout template by added a template 'django_saml2_auth/signout.html'.
117+
118+
119+
By default, we assume your SAML2 service provided user attribute Email/UserName/FirstName/LastName. Please change it to the correct
120+
user attributes mapping.
121+
122+
123+
124+
How to Contribute
125+
=================
126+
127+
#. Check for open issues or open a fresh issue to start a discussion around a feature idea or a bug.
128+
#. Fork `the repository`_ on GitHub to start making your changes to the **master** branch (or branch off of it).
129+
#. Write a test which shows that the bug was fixed or that the feature works as expected.
130+
#. Send a pull request and bug the maintainer until it gets merged and published. :) Make sure to add yourself to AUTHORS_.
131+
132+
.. _`the repository`: http://github.com/fangli/django-saml2-auth
133+
.. _AUTHORS: https://github.com/fangli/django-saml2-auth/blob/master/AUTHORS.rst

django_saml2_auth/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
__version__ = '1.0.1b1'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{% load i18n %}
2+
<!DOCTYPE html>
3+
<html>
4+
<head>
5+
<meta charset="utf-8">
6+
<title>{% trans "Permission Denied" %}</title>
7+
</head>
8+
<body>
9+
<h2>{% trans "Sorry, you are not allowed to access this app" %}</h2>
10+
<hr>
11+
<p>{% trans "If you think it's an incorrect configuration, write email to your system administrator" %}</p>
12+
</body>
13+
</html>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{% load i18n %}
2+
<!DOCTYPE html>
3+
<html>
4+
<head>
5+
<meta charset="utf-8">
6+
<title>{% trans "Signed out" %}</title>
7+
</head>
8+
<body>
9+
<h2>{% trans "You have signed out successfully." %}</h2>
10+
<hr>
11+
<p>{% trans "If you want to login again or switch to another account, please do it in SSO." %}</p>
12+
</body>
13+
</html>

django_saml2_auth/urls.py

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from django.conf.urls import url, patterns
2+
3+
4+
app_name = 'django_saml2_auth'
5+
6+
urlpatterns = patterns(
7+
'django_saml2_auth.views',
8+
url(r'^acs/$', "acs", name="acs"),
9+
url(r'^welcome/$', "welcome", name="welcome"),
10+
url(r'^denied/$', "denied", name="denied"),
11+
)

django_saml2_auth/views.py

+169
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
#!/usr/bin/env python
2+
# -*- coding:utf-8 -*-
3+
4+
5+
import urllib2
6+
from saml2 import (
7+
BINDING_HTTP_POST,
8+
BINDING_HTTP_REDIRECT,
9+
entity,
10+
)
11+
from saml2.client import Saml2Client
12+
from saml2.config import Config as Saml2Config
13+
14+
from django.conf import settings
15+
from django.core.urlresolvers import reverse
16+
from django.contrib.auth.models import (User, Group)
17+
from django.contrib.auth.decorators import login_required
18+
from django.contrib.auth import login, logout
19+
from django.shortcuts import render
20+
from django.views.decorators.csrf import csrf_exempt
21+
from django.template import TemplateDoesNotExist
22+
from django.http import HttpResponseRedirect
23+
24+
25+
def get_current_domain(r):
26+
return '{scheme}://{host}'.format(
27+
scheme=r.scheme,
28+
host=r.get_host(),
29+
)
30+
31+
32+
def _get_saml_client(domain):
33+
acs_url = domain + reverse('acs')
34+
import tempfile
35+
tmp = tempfile.NamedTemporaryFile()
36+
f = open(tmp.name, 'w')
37+
f.write(urllib2.urlopen(settings.SAML2_AUTH['METADATA_AUTO_CONF_URL']).read())
38+
f.close()
39+
saml_settings = {
40+
'metadata': {
41+
"local": [tmp.name],
42+
},
43+
'service': {
44+
'sp': {
45+
'endpoints': {
46+
'assertion_consumer_service': [
47+
(acs_url, BINDING_HTTP_REDIRECT),
48+
(acs_url, BINDING_HTTP_POST)
49+
],
50+
},
51+
'allow_unsolicited': True,
52+
'authn_requests_signed': False,
53+
'logout_requests_signed': True,
54+
'want_assertions_signed': True,
55+
'want_response_signed': False,
56+
},
57+
},
58+
}
59+
60+
spConfig = Saml2Config()
61+
spConfig.load(saml_settings)
62+
spConfig.allow_unknown_attributes = True
63+
saml_client = Saml2Client(config=spConfig)
64+
tmp.close()
65+
return saml_client
66+
67+
68+
@login_required
69+
def welcome(r):
70+
try:
71+
return render(r, 'django_saml2_auth/welcome.html', context={'user': r.user})
72+
except TemplateDoesNotExist:
73+
return HttpResponseRedirect(reverse('admin:index'))
74+
75+
76+
def denied(r):
77+
return render(r, 'django_saml2_auth/denied.html')
78+
79+
80+
def _create_new_user(username, email, firstname, lastname):
81+
user = User.objects.create_user(username, email)
82+
user.first_name = firstname
83+
user.last_name = lastname
84+
user.groups = [Group.objects.get(name=x) for x in settings.SAML2_AUTH.get('NEW_USER_PROFILE', {}).get('USER_GROUPS', [])]
85+
user.is_active = settings.SAML2_AUTH.get('NEW_USER_PROFILE', {}).get('ACTIVE_STATUS', True)
86+
user.is_staff = settings.SAML2_AUTH.get('NEW_USER_PROFILE', {}).get('STAFF_STATUS', True)
87+
user.is_superuser = settings.SAML2_AUTH.get('NEW_USER_PROFILE', {}).get('SUPERUSER_STATUS', False)
88+
user.save()
89+
return user
90+
91+
92+
@csrf_exempt
93+
def acs(r):
94+
saml_client = _get_saml_client(get_current_domain(r))
95+
resp = r.POST.get('SAMLResponse', None)
96+
next_url = r.session.get('login_next_url', reverse('admin:index'))
97+
98+
if not resp:
99+
return HttpResponseRedirect(reverse('denied'))
100+
101+
authn_response = saml_client.parse_authn_request_response(
102+
resp, entity.BINDING_HTTP_POST)
103+
if authn_response is None:
104+
return HttpResponseRedirect(reverse('denied'))
105+
106+
user_identity = authn_response.get_identity()
107+
if user_identity is None:
108+
return HttpResponseRedirect(reverse('denied'))
109+
110+
user_email = user_identity[settings.SAML2_AUTH.get('ATTRIBUTES_MAP', {}).get('email', 'Email')][0]
111+
user_name = user_identity[settings.SAML2_AUTH.get('ATTRIBUTES_MAP', {}).get('username', 'UserName')][0]
112+
user_first_name = user_identity[settings.SAML2_AUTH.get('ATTRIBUTES_MAP', {}).get('first_name', 'FirstName')][0]
113+
user_last_name = user_identity[settings.SAML2_AUTH.get('ATTRIBUTES_MAP', {}).get('last_name', 'LastName')][0]
114+
115+
target_user = None
116+
is_new_user = False
117+
118+
try:
119+
target_user = User.objects.get(username=user_name)
120+
except User.DoesNotExist:
121+
target_user = _create_new_user(user_name, user_email, user_first_name, user_last_name)
122+
is_new_user = True
123+
124+
r.session.flush()
125+
126+
if target_user.is_active:
127+
target_user.backend = 'django.contrib.auth.backends.ModelBackend'
128+
login(r, target_user)
129+
else:
130+
return HttpResponseRedirect(reverse('denied'))
131+
132+
if is_new_user:
133+
try:
134+
return render(r, 'django_saml2_auth/welcome.html', context={'user': r.user})
135+
except TemplateDoesNotExist:
136+
return HttpResponseRedirect(next_url)
137+
else:
138+
return HttpResponseRedirect(next_url)
139+
140+
141+
def signin(r):
142+
import urlparse
143+
from urllib import unquote
144+
next_url = r.GET.get('next', reverse('admin:index'))
145+
146+
try:
147+
if "next=" in unquote(next_url):
148+
next_url = urlparse.parse_qs(urlparse.urlparse(unquote(next_url)).query)['next'][0]
149+
except:
150+
next_url = r.GET.get('next', reverse('admin:index'))
151+
152+
r.session['login_next_url'] = next_url
153+
154+
saml_client = _get_saml_client(get_current_domain(r))
155+
_, info = saml_client.prepare_for_authenticate()
156+
157+
redirect_url = None
158+
159+
for key, value in info['headers']:
160+
if key == 'Location':
161+
redirect_url = value
162+
break
163+
164+
return HttpResponseRedirect(redirect_url)
165+
166+
167+
def signout(r):
168+
logout(r)
169+
return render(r, 'django_saml2_auth/signout.html')

setup.cfg

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[bdist_wheel]
2+
universal=1

0 commit comments

Comments
 (0)