first commit

This commit is contained in:
Benjamin Dauvergne 2014-03-20 17:36:35 +01:00
commit 37728a07bc
23 changed files with 1961 additions and 0 deletions

13
.gitignore vendored Normal file
View File

@ -0,0 +1,13 @@
MANIFEST
*.pyc
*.pyo
*.db
.*.swp
cache/
dist/
static/
doc/_build
authentic.egg-info
local_settings.py
log.log
authentic2/locale/fr/LC_MESSAGES/django.mo

10
README.txt Normal file
View File

@ -0,0 +1,10 @@
Install
=======
You just have to install the package in your virtualenv and relaunch, it will
be automatically loaded by the plugin framework.
Settings
========

View File

@ -0,0 +1,38 @@
__version__ = '1.0'
class Plugin(object):
def __init__(self):
from authentic2.decorators import TRANSIENT_USER_TYPES
from . import transient
TRANSIENT_USER_TYPES.append(transient.SAML2TransientUser)
def get_before_urls(self):
from . import urls
return urls.urlpatterns
def get_apps(self):
return [__name__, 'authentic2.saml']
def get_authentication_backends(self):
return (
'authentic2_auth_saml2.backends.AuthSAML2PersistentBackend',
'authentic2_auth_saml2.backends.AuthSAML2TransientBackend',
)
def get_auth_frontends(self):
return ('authentic2_auth_saml2.frontend.AuthSAML2Frontend',)
def get_idp_backends(self):
return ('authentic2_auth_saml2.backends.AuthSAML2Backend',)
def logout_list(self, request):
return []
def get_saml2_authn_context(self, backend_cls):
import lasso
if backend_cls.startswith('authentic2_auth_saml2.'):
return lasso.SAML2_AUTHN_CONTEXT_UNSPECIFIED
return None

View File

@ -0,0 +1,24 @@
class AppSettings(object):
def __init__(self, prefix):
self.prefix = prefix
@property
def AUTOMATIC_GRANT(self):
return self._setting('AUTOMATIC_GRANT', ())
def _setting(self, name, dflt):
from django.conf import settings
getter = getattr(settings,
'ALLAUTH_SETTING_GETTER',
lambda name, dflt: getattr(settings, name, dflt))
return getter(self.prefix + name, dflt)
# Ugly? Guido recommends this himself ...
# http://mail.python.org/pipermail/python-ideas/2012-May/014969.html
import sys
app_settings = AppSettings('A2_OAUTH2_')
app_settings.__name__ = __name__
sys.modules[__name__] = app_settings

View File

@ -0,0 +1,151 @@
import string
import random
import logging
import lasso
import urllib
from django.db import transaction
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _
from authentic2.compat import get_user_model
from authentic2.saml.common import \
lookup_federation_by_name_id_and_provider_id, add_federation, \
get_idp_options_policy
from authentic2.saml.models import LIBERTY_SESSION_DUMP_KIND_SP, \
LibertySessionDump, LibertyProvider
from .transient import SAML2TransientUser
logger = logging.getLogger(__name__)
class AuthSAML2Backend:
def logout_list(self, request):
q = LibertySessionDump. \
objects.filter(django_session_key=request.session.session_key,
kind=LIBERTY_SESSION_DUMP_KIND_SP)
if not q:
logger.debug('logout_list: no LibertySessionDump found')
return []
'''
We deal with a single IdP session
'''
try:
provider_id = lasso.Session(). \
newFromDump(q[0].session_dump.encode('utf-8')). \
get_assertions().keys()[0]
except:
return []
if not provider_id:
return []
logger.debug('logout_list: Found session for %s' % provider_id)
name = provider_id
provider = None
try:
provider = LibertyProvider.objects.get(entity_id=provider_id)
name = provider.name
except LibertyProvider.DoesNotExist:
logger.error('logout_list: session found for unknown provider %s' \
% provider_id)
return []
policy = get_idp_options_policy(provider)
if not policy:
logger.error('logout_list: No policy found for %s' % provider_id)
return []
elif not policy.forward_slo:
logger.info('logout_list: %s configured to not reveive slo' \
% provider_id)
return []
else:
code = '<div>'
code += _('Sending logout to %(pid)s....') % { 'pid': name or provider_id }
query = urllib.urlencode({
'entity_id': provider_id
})
url = '%s?%s' % (reverse('a2-auth-saml2-slo'), query)
code += '''<iframe src="%s" marginwidth="0" marginheight="0" \
scrolling="no" style="border: none" width="16" height="16" onload="window.iframe_count -= 1; console.log(window.location.href + ' decrement iframe_count');"></iframe></div>''' \
% url
return [ code ]
class AuthSAML2PersistentBackend:
supports_object_permissions = False
supports_anonymous_user = False
def authenticate(self, name_id=None, provider_id=None, create=False):
'''Authenticate persistent NameID'''
if not name_id or not provider_id:# or not name_id.nameQualifier:
return None
#fed = lookup_federation_by_name_identifier(name_id=name_id)
fed = lookup_federation_by_name_id_and_provider_id(name_id, provider_id)
if fed is None:
if create == True:
return self.create_user(name_id=name_id,
provider_id=provider_id)
else:
return None
else:
return fed.user
def get_user(self, user_id):
User = get_user_model()
try:
return User.objects.get(id=user_id)
except User.DoesNotExist:
return None
@transaction.commit_on_success
def create_user(self, username=None, name_id=None, provider_id=None):
'''Create a new user mapping to the given NameID'''
if not name_id or \
name_id.format != \
lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT or \
not name_id.nameQualifier:
raise ValueError('Invalid NameID')
if not username:
# FIXME: maybe keep more information in the forged username
username = 'saml2-%s' % ''. \
join([random.SystemRandom().choice(string.letters) for x in range(10)])
User = get_user_model()
user = User()
user.username = username
if hasattr(User, 'set_unusable_password'):
user.set_unusable_password()
user.is_active = True
user.save()
logger.info('automatic creation of user %r', user)
add_federation(user, name_id=name_id, provider_id=provider_id)
return user
@classmethod
def get_saml2_authn_context(cls):
return lasso.SAML2_AUTHN_CONTEXT_UNSPECIFIED
class AuthSAML2TransientBackend:
supports_object_permissions = False
supports_anonymous_user = False
def authenticate(self, name_id=None):
'''Create temporary user for transient NameID'''
if not name_id or \
name_id.format != \
lasso.SAML2_NAME_IDENTIFIER_FORMAT_TRANSIENT or \
not name_id.content:
return None
user = SAML2TransientUser(id=name_id.content)
return user
def get_user(self, user_id):
'''Create temporary user for transient NameID'''
return SAML2TransientUser(id=user_id)
@classmethod
def get_saml2_authn_context(cls):
return lasso.SAML2_AUTHN_CONTEXT_UNSPECIFIED

View File

@ -0,0 +1,20 @@
from django.conf import settings
from django.utils.http import urlencode
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.http import HttpResponseRedirect
from functools import wraps
def anonymous_only(func):
'''Logout before entering this view'''
@wraps(func)
def f(request, *args, **kwargs):
if request.user.is_authenticated():
current_url = request.build_absolute_uri()
query = urlencode({REDIRECT_FIELD_NAME: current_url})
logout_url = '{0}?{1}'.format(settings.LOGOUT_URL, query)
return HttpResponseRedirect(logout_url)
return func(request, *args, **kwargs)
return f

View File

@ -0,0 +1,19 @@
from django import forms
from django.utils.translation import ugettext as _
from authentic2.utils import IterableFactory
from authentic2.saml import models
def provider_list_to_choices(qs):
for idp in qs:
yield idp.entity_id, idp.name
def get_idp_list():
qs = models.LibertyProvider.objects.idp_enabled().order_by('name')
return provider_list_to_choices(qs)
class AuthSAML2Form(forms.Form):
entity_id = forms.ChoiceField(label=_('Choose your identity provider'),
choices=IterableFactory(get_idp_list))

View File

@ -0,0 +1,42 @@
import urllib
from django.utils.translation import gettext_noop
from django.http import HttpResponseRedirect
from django.contrib.auth import REDIRECT_FIELD_NAME
from authentic2.saml.models import LibertyProvider
from . import forms, views_profile
class AuthSAML2Frontend(object):
def enabled(self):
return LibertyProvider.objects.idp_enabled().exists()
def id(self):
return 'saml2'
def name(self):
return gettext_noop('SAML 2.0')
def form(self):
return forms.AuthSAML2Form
def post(self, request, form, nonce, next):
entity_id = form.cleaned_data['entity_id']
query = urllib.urlencode({
'entity_id': entity_id,
REDIRECT_FIELD_NAME: next
})
return HttpResponseRedirect('/authsaml2/sso?%s' % query)
def get_context(self):
'''Specific context variable used by the specific template'''
qs = LibertyProvider.objects.idp_enabled().order_by('name')
choices = forms.provider_list_to_choices(qs)
return { 'idp_providers': choices }
def template(self):
return 'authsaml2/login_form.html'
def profile(self, request):
return views_profile.profile(request)

View File

@ -0,0 +1,339 @@
# French translation of Authentic
# Copyright (C) 2010, 2011 Entr'ouvert
# This file is distributed under the same license as the Authentic package.
# Frederic Peters <fpeters@entrouvert.com>, 2010.
#
msgid ""
msgstr ""
"Project-Id-Version: Authentic\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2014-02-27 16:42+0100\n"
"PO-Revision-Date: 2014-02-27 16:42+0100\n"
"Last-Translator: Mikaël Ates <mates@entrouvert.com>\n"
"Language-Team: None\n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n>1;\n"
#: backends.py:64
#, python-format
msgid "Sending logout to %(pid)s...."
msgstr "Envoi de la deconnesion a %(pid)s...."
#: forms.py:16
msgid "Choose your identity provider"
msgstr "Choisissez votre fournisseur d'identité"
#: frontend.py:19
msgid "SAML 2.0"
msgstr "SAML 2.0"
#: models.py:91
msgid "Anonymous"
msgstr "Anonyme"
#: saml2_endpoints.py:119 saml2_endpoints.py:125
msgid "sso: Service provider not configured"
msgstr ""
#: saml2_endpoints.py:131
msgid "sso: No SAML2 identity provider selected"
msgstr ""
#: saml2_endpoints.py:139
msgid "sso: The provider does not exist"
msgstr ""
#: saml2_endpoints.py:144
msgid "sso: Unable to create Login object"
msgstr ""
#: saml2_endpoints.py:153
#, python-format
msgid "sso: %s does not have any supported SingleSignOn endpoint"
msgstr ""
#: saml2_endpoints.py:159
#, python-format
msgid "sso: initAuthnRequest %s"
msgstr ""
#: saml2_endpoints.py:165
msgid "sso: No IdP policy defined"
msgstr ""
#: saml2_endpoints.py:171
#, python-format
msgid "SSO: buildAuthnRequestMsg %s"
msgstr ""
#: saml2_endpoints.py:198
msgid "singleSignOnArtifact: Service provider not configured"
msgstr ""
#: saml2_endpoints.py:216
msgid "singleSignOnArtifact: Unable to create Login object"
msgstr ""
#: saml2_endpoints.py:222
msgid "singleSignOnArtifact: No message given."
msgstr ""
#: saml2_endpoints.py:243
#, python-format
msgid "singleSignOnArtifact: provider %r unknown"
msgstr ""
#: saml2_endpoints.py:252
#, python-format
msgid "singleSignOnArtifact: initRequest %s"
msgstr ""
#: saml2_endpoints.py:260
#, python-format
msgid "singleSignOnArtifact: buildRequestMsg %s"
msgstr ""
#: saml2_endpoints.py:272
#, python-format
msgid ""
"singleSignOnArtifact: Failure to communicate with artifact "
"resolver %r"
msgstr ""
#: saml2_endpoints.py:277
#, python-format
msgid ""
"singleSignOnArtifact: Artifact resolver at %r returned an empty "
"response"
msgstr ""
#: saml2_endpoints.py:293
#, python-format
msgid "singleSignOnArtifact: processResponseMsg raised %s"
msgstr ""
#: saml2_endpoints.py:308
msgid "singleSignOnPost: Service provider not configured"
msgstr ""
#: saml2_endpoints.py:314
msgid "singleSignOnPost: Unable to create Login object"
msgstr ""
#: saml2_endpoints.py:323
msgid "singleSignOnPost: No message given."
msgstr ""
#: saml2_endpoints.py:350
#, python-format
msgid "singleSignOnPost: provider %r unknown"
msgstr ""
#: saml2_endpoints.py:361
#, python-format
msgid "singleSignOnPost: %s"
msgstr ""
#: saml2_endpoints.py:384
msgid "sso_after_response: error checking authn response"
msgstr ""
#: saml2_endpoints.py:390
#, python-format
msgid "sso_after_response: acceptSso raised %s"
msgstr ""
#: saml2_endpoints.py:526
msgid "sso_after_response: No IdP policy defined"
msgstr ""
#: saml2_endpoints.py:575
msgid ""
"sso_after_response: No backend for temporary federation "
"is configured"
msgstr ""
#: saml2_endpoints.py:596
msgid ""
"sso_after_response: Transient access policy: Configuration error"
msgstr ""
#: saml2_endpoints.py:672
msgid ""
"sso_after_response: You were not asked your consent for "
"account linking"
msgstr ""
#: saml2_endpoints.py:689
msgid ""
"sso_after_response: Persistent Account policy: Configuration "
"error"
msgstr ""
#: saml2_endpoints.py:693
msgid ""
"sso_after_response: Transient access policy: NameId format not "
"supported"
msgstr ""
#: saml2_endpoints.py:716
msgid "finish_federation: Service provider not configured"
msgstr ""
#: saml2_endpoints.py:722
msgid "finish_federation: Unable to create Login object"
msgstr ""
#: saml2_endpoints.py:729
msgid "finish_federation: Error loading session."
msgstr ""
#: saml2_endpoints.py:745
msgid ""
"SSO/finish_federation: Error adding new federation for "
"this user"
msgstr ""
#: saml2_endpoints.py:784
msgid "finish_federation: Unable to perform federation"
msgstr ""
#: saml2_endpoints.py:1250
msgid "fedTerm/SP UI: No provider for defederation"
msgstr ""
#: saml2_endpoints.py:1255
msgid "fedTerm/SP UI: Unable to defederate a not logged user!"
msgstr ""
#: saml2_endpoints.py:1261
msgid "fedTerm/SP UI: Service provider not configured"
msgstr ""
#: saml2_endpoints.py:1268
msgid "fedTerm/SP UI: No such identity provider."
msgstr ""
#: saml2_endpoints.py:1278
msgid "fedTerm/SP UI: Not a valid federation"
msgstr ""
#: saml2_endpoints.py:1294
#, python-format
msgid "fedTerm/SP UI: %s"
msgstr ""
#: saml2_endpoints.py:1302 saml2_endpoints.py:1333
#, python-format
msgid "fedTerm/SP SOAP: %s"
msgstr ""
#: saml2_endpoints.py:1311
msgid ""
"fedTerm/SP SOAP: Unable to perform SOAP defederation "
"request"
msgstr ""
#: saml2_endpoints.py:1320 saml2_endpoints.py:1354
#, python-format
msgid "fedTerm/SP Redirect: %s"
msgstr ""
#: saml2_endpoints.py:1342
msgid ""
"fedTerm/SP SOAP: Unable to perform SOAP defederation request"
msgstr ""
#: saml2_endpoints.py:1359
msgid "Unknown HTTP method."
msgstr ""
#: saml2_endpoints.py:1372
msgid "fedTerm/SP Redirect: Service provider not configured"
msgstr ""
#: saml2_endpoints.py:1380
msgid "fedTerm/SP Redirect: Error managing manage dump"
msgstr ""
#: saml2_endpoints.py:1395
msgid "fedTerm/SP Redirect: Defederation failed"
msgstr ""
#: saml2_endpoints.py:1421
#, python-format
msgid "fedTerm/Return: provider %r unknown"
msgstr ""
#: saml2_endpoints.py:1428
#, python-format
msgid "fedTerm/manage_name_id_return: %s"
msgstr ""
#: saml2_endpoints.py:1476
#, python-format
msgid "fedTerm/SOAP: provider %r unknown"
msgstr ""
#: utils.py:42
#, python-format
msgid "An error happened. Report this %s to the administrator."
msgstr ""
#: views_disco.py:53
msgid "redirect_to_disco: unable to build disco request"
msgstr ""
#: views_disco.py:63
msgid "HTTP request not supported"
msgstr ""
#: views_profile.py:52
msgid "Successful federation deletion."
msgstr ""
#: templates/authsaml2/account_linking.html:5
msgid "Log in to link your account"
msgstr "Connectez-vous pour lier vos comptes"
#: templates/authsaml2/account_linking.html:9
msgid "Log in to link with your existing account"
msgstr "Connectez-vous pour lier avec un compte existant"
#: templates/authsaml2/account_linking.html:17
#: templates/authsaml2/account_linking.html:24
msgid "Username:"
msgstr "Nom d'utilisateur :"
#: templates/authsaml2/account_linking.html:20
#: templates/authsaml2/account_linking.html:28
msgid "Password:"
msgstr "Mot de passe :"
#: templates/authsaml2/account_linking.html:32
#: templates/authsaml2/login_form.html:6 templates/authsaml2/profile.html:25
msgid "Log in"
msgstr "S'identifier"
#: templates/authsaml2/error_authsaml2.html:8
msgid "Back"
msgstr "Retour"
#: templates/authsaml2/profile.html:3
msgid "SAML2 Federations"
msgstr "Fédérations SAML2"
#: templates/authsaml2/profile.html:9
msgid "Delete a federation?"
msgstr "Supprimer une fédération ?"
#: templates/authsaml2/profile.html:13
msgid "Delete"
msgstr "Supprimer"
#: templates/authsaml2/profile.html:21
msgid "Add a federation?"
msgstr "Ajouter une fédération ?"

View File

@ -0,0 +1,17 @@
from django.dispatch import Signal
#authz_decision
authz_decision = Signal(providing_args = ["request","attributes","provider"])
#user login
auth_login = Signal(providing_args = ["request","attributes"])
#user logout
auth_logout = Signal(providing_args = ["user"])
from authentic2.saml.common import authz_decision_cb
authz_decision.connect(authz_decision_cb,
dispatch_uid='authz_decision_on_attributes')

View File

@ -0,0 +1,40 @@
{% extends "base.html" %}
{% load i18n %}
{% block title %}
{% trans "Log in to link your account" %}
{% endblock %}
{% block content %}
<p>* {% trans "Log in to link with your existing account" %}</p>
<div id="login-actions">
<form id="login-form" method="post" action="{% url "a2-auth-saml2-account-linking" %}?request_id={{ request_id}}">
{% csrf_token %}
<ul class="errorlist">
{% for error in form.non_field_errors %}
<li>{{ error|escape }}</li>
{% endfor %}
{% for error in form.username.errors %}
<li>{% trans "Username:" %} {{ error|escape }}</li>
{% endfor %}
{% for error in form.password.errors %}
<li>{% trans "Password:" %} {{ error|escape }}</li>
{% endfor %}
</ul>
<p>
<label for="id_username">{% trans "Username:" %}</label>
<input id="id_username" type="text" name="username" maxlength="30" />
</p>
<p>
<label for="id_password">{% trans "Password:" %}</label>
<input type="password" name="password" id="id_password" />
</p>
<input type="submit" value="{% trans 'Log in' %}" class="submit" />
<input type="hidden" name="next" value="{{ next }}" />
</form>
</div>
<script type="text/javascript">
document.getElementById('id_username').focus();
</script>
{% endblock %}

View File

@ -0,0 +1,9 @@
{% extends "base.html" %}
{% load i18n %}
{% block bodyargs %}onload="setTimeout(function () { window.location='{{ next_page }}' }, {{ redir_timeout }})"{% endblock %}
{% block content %}
<h1>{{ title }}</h1>
<p><a href="{{ back }}">{% trans "Back" %}<a/></p>
{% endblock %}

View File

@ -0,0 +1,8 @@
{% load i18n %}
<div>
<form method="post" action="">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" name="{{ submit_name }}" value="{% trans "Log in" %}"/>
</form>
</div>

View File

@ -0,0 +1,30 @@
{% load i18n %}
{% if form or federations %}
<h4>{% trans "SAML2 Federations" %}</h4>
<div>
{% if linked_providers %}
<p>
<h5>{% trans "Delete a federation?" %}</h5>
{% for provider_id, name in linked_providers %}
<form action="{% url 'a2-auth-saml2-delete-federation' %}/{{ provider_id }}/" method="post">
<label for="id_del_fed">{{ name }}</label>
<input type="submit" class="submit-link" value="{% trans "Delete" %}">
</form>
{% endfor %}
</p>
{% endif %}
{% if form %}
<p>
<h5>{% trans "Add a federation?" %}</h5>
<form method="get" action="{% url "a2-auth-saml2-sso" %}">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" name="submit-authsaml2" value="{% trans "Log in" %}"/>
</form>
</p>
{% endif %}
</div>
{% endif %}

View File

@ -0,0 +1 @@
{% extends "rest_framework/base.html" %}

View File

@ -0,0 +1,92 @@
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.db.models.manager import EmptyManager
from django.contrib.auth.models import _user_get_all_permissions, _user_has_perm, _user_has_module_perms
class FakePk:
name = 'pk'
class FakeMeta:
pk = FakePk()
class SAML2TransientUser(object):
'''Class compatible with django.contrib.auth.models.User
which represent an user authenticated using a Transient
federation'''
id = None
pk = None
is_staff = False
is_active = False
is_superuser = False
_groups = EmptyManager()
_user_permissions = EmptyManager()
_meta = FakeMeta()
def __init__(self, id):
self.id = id
self.pk = id
def __unicode__(self):
return 'AnonymousUser'
def __str__(self):
return unicode(self).encode('utf-8')
def __eq__(self, other):
return isinstance(other, self.__class__)
def __ne__(self, other):
return not self.__eq__(other)
def __hash__(self):
return 1 # instances always return the same hash value
def save(self, **kwargs):
pass
def delete(self):
raise NotImplementedError
def set_password(self, raw_password):
raise NotImplementedError
def check_password(self, raw_password):
raise NotImplementedError
def _get_groups(self):
return self._groups
groups = property(_get_groups)
def _get_user_permissions(self):
return self._user_permissions
user_permissions = property(_get_user_permissions)
def get_group_permissions(self, obj=None):
return set()
def get_all_permissions(self, obj=None):
return _user_get_all_permissions(self, obj=obj)
def has_perm(self, perm, obj=None):
return _user_has_perm(self, perm, obj=obj)
def has_perms(self, perm_list, obj=None):
for perm in perm_list:
if not self.has_perm(perm, obj):
return False
return True
def has_module_perms(self, module):
return _user_has_module_perms(self, module)
def is_anonymous(self):
#XXX: Should return True
return False
def is_authenticated(self):
return True
def get_username(self):
return _('Anonymous')
username = property(get_username)

View File

@ -0,0 +1,45 @@
from django.conf.urls import patterns, url
urlpatterns = patterns('authentic2.authsaml2.views',
url(r'^metadata$', 'metadata'),
# Receive request from user interface
url(r'^sso/$', 'sso', name='a2-auth-saml2-sso'),
url(r'^account-linking/(?P<response_id>.*)/$', 'account_linking',
name='a2-auth-saml2-account-linking'),
url(r'^singleSignOnArtifact$', 'assertion_consumer_artifact'),
url(r'^singleSignOnPost$', 'assertion_consumer_post'),
# Receive request from functions
url(r'^sp_slo/$', 'sp_slo', name='a2-auth-saml2-slo'),
# Receive response from Redirect SP initiated
url(r'^singleLogoutReturn$', 'slo_sp_response'),
# Receive request from SOAP IdP initiated
url(r'^singleLogoutSOAP$', 'slo_soap'),
# Receive request from Redirect IdP initiated
url(r'^singleLogout$', 'singleLogout'),
# Back of SLO treatment by the IdP Side
url(r'^finish_slo$', 'finish_slo',
name='a2-auth-saml2-finish-slo'),
# Receive request from user interface
url(r'^federationTermination$', 'federationTermination'),
# Receive response from Redirect SP initiated
url(r'^manageNameIdReturn$', 'manageNameIdReturn'),
# Receive request from SOAP IdP initiated
url(r'^manageNameIdSOAP$', 'manageNameIdSOAP'),
# Receive request from Redirect IdP initiated
url(r'^manageNameId$', 'manageNameId'),
# Receive request from Redirect IdP initiated
#Send idp discovery request
)
urlpatterns += patterns('authentic2.authsaml2.views_disco',
url(r'^redirect_to_disco/$', 'redirect_to_disco',
name='a2-auth-saml2-redirect-to-disco'),
#receive idp discovery response
url(r'^discoveryReturn/$', 'disco_response',
name='a2-auth-saml2-disco-response'),
)
urlpatterns += patterns('authentic2.authsaml2.views_profile',
url(r'^delete_federation/(?P<provider_id>\d+)/$', 'delete_federation',
name='a2-auth-saml2-delete-federation'),
)

View File

@ -0,0 +1,148 @@
import re
import time
import logging
from django.template import RequestContext
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.utils.translation import ugettext as _
from django.shortcuts import render_to_response
from django.contrib import messages
from django.conf import settings
from authentic2.saml.models import (nameid2kwargs, LibertySession,
LibertyFederation)
__redirection_timeout = 1600
__root_refererer_re = re.compile('^(https?://[^/]*/?)')
logger = logging.getLogger(__name__)
def error_page(request, message=None, back=None, logger=None,
default_message=True, timer=False):
'''View that show a simple error page to the user with a back link.
back - url for the back link, if None, return to root of the referer
or the local root.
'''
if logger:
logger.debug('Showing message %r on an error page' % message)
else:
logging.debug('Showing message %r on an error page' % message)
if back is None:
referer = request.META.get('HTTP_REFERER')
if referer:
root_referer = __root_refererer_re.match(referer)
if root_referer:
back = root_referer.group(1)
if back is None:
back = '/'
global __redirection_timeout
context = RequestContext(request)
if timer:
context['redir_timeout'] = __redirection_timeout
context['next_page'] = back
display_message = getattr(settings, 'DISPLAY_MESSAGE_ERROR_PAGE', ())
if default_message and not display_message:
messages.add_message(request, messages.ERROR,
_('An error happened. Report this %s to the administrator.') % \
time.strftime("[%Y-%m-%d %a %H:%M:%S]", time.localtime()))
elif message:
messages.add_message(request, messages.ERROR, message)
return render_to_response('authsaml2/error_authsaml2.html', {'back': back},
context_instance=context)
# Used to register requested url during SAML redirections
def register_next_target(request, url=None):
if url:
next = url
else:
next = request.GET.get(REDIRECT_FIELD_NAME)
if not next:
next = '/'
request.session['next'] = next
logger.debug('saving next target %r', next)
def get_registered_url(request):
if 'next' in request.session:
return request.session['next']
return None
def register_request_id(request, request_id):
request.session['saml_request_id'] = request_id
# Used for account linking
def save_federation_temp(request, login, attributes):
if login and login.identity:
request.session['identity_dump'] = login.identity.dump()
request.session['remoteProviderId'] = login.remoteProviderId
request.session['nameId'] = login.nameIdentifier
request.session['attributes'] = attributes
def load_federation_temp(request, login):
if 'identity_dump' in request.session:
login.setIdentityFromDump(request.session['identity_dump'])
def save_login_session(request, login):
try:
session_index = login.response.assertion[0].authnStatement[0].sessionIndex
except (AttributeError, IndexError):
return
LibertySession.objects.get_or_create(
django_session_key=request.session.session_key,
session_index=session_index,
provider_id=login.remoteProviderId,
**nameid2kwargs(login.nameIdentifier))
def logout_session(session_key, entity_id):
qs = LibertySession.objects.filter(
django_session_key=session_key)
qs = qs.filter(idp__entity_id=entity_id)
return qs
def kill_logout_session(session_key, entity_id):
qs = logout_session(session_key, entity_id)
qs.delete()
def load_logout_session(logout, session_key, entity_id):
qs = LibertySession.objects.filter(
django_session_key=session_key)
qs = qs.filter(idp__entity_id=entity_id)
assert qs.exists(), 'no session found for session_key %r and entity_id %r' % \
(session_key, entity_id)
logout.setSessionDump(qs.to_session_dump())
def has_federation(user, entity_id):
return LibertyFederation.objects.filter(user=user,
sp__entity_id=entity_id).exists()
def kill_federations(user, entity_id):
LibertyFederation.objects.filter(user=user,
sp__entity_id=entity_id).update(user=None)
def get_sessions(name_id, session_indexes=None):
qs = LibertySession.objects.filter(
**nameid2kwargs(name_id))
if session_indexes:
qs.filter(session_index__in=session_indexes)
return qs
def has_sessions(name_id, session_indexes=None):
return get_sessions(name_id, session_indexes).exists()
def get_session_keys(sessions):
return sessions.values_list('django_session_key', flat=True)
def get_django_session_key_for_session_index(session_index)
ls = LibertySession.objects.get(session_index=session_index)
return ls.django_session_key
def can_do_synchronous_logout(sessions):
django_session_keys = get_session_keys(sessions)
# FIXME: we should check every auth and idp for synchronous logout
if LibertySession.objects.filter(
django_session__in=django_session_keys,
idp__isnull=False).exists():
return False
return True

View File

@ -0,0 +1,742 @@
"""SAML 2.0 SP implementation"""
import logging, operator
import lasso
import authentic2.idp.views as idp_views
from django.conf import settings
from django.shortcuts import render, redirect
from django.core.urlresolvers import reverse
from django.http import HttpResponse, HttpResponseRedirect, \
HttpResponseBadRequest, HttpResponseForbidden
from django.views.decorators.csrf import csrf_exempt
from django.contrib.auth import (login as auth_login,
REDIRECT_FIELD_NAME, authenticate)
from django.contrib import messages
from django.utils.translation import ugettext as _
from django.utils.http import urlencode
from django.core.cache import cache
from django.contrib.auth.decorators import login_required
from authentic2.saml.common import (load_provider,
return_saml2_response, return_saml2_request,
get_saml2_query_request, get_saml2_post_response, soap_call,
get_authorization_policy, get_idp_options_policy,
add_federation, send_soap_request, get_soap_message,
get_saml2_metadata, create_saml2_server,
maintain_liberty_session_on_service_provider,
get_session_not_on_or_after,
AUTHENTIC_STATUS_CODE_UNKNOWN_PROVIDER,
AUTHENTIC_STATUS_CODE_INTERNAL_SERVER_ERROR,
AUTHENTIC_STATUS_CODE_UNAUTHORIZED)
from authentic2.saml.models import (nameid2kwargs, LibertyProvider,
save_key_values, NAME_ID_FORMATS, get_and_delete_key_values)
from authentic2.saml.saml2utils import (authnresponse_checking,
get_attributes_from_assertion)
from authentic2.idp.saml.saml2_endpoints import return_logout_error
from authentic2.authsaml2.utils import error_page
from authentic2.authsaml2 import signals
from authentic2.utils import cache_and_validate, flush_django_session
from . import utils
from .decorators import anonymous_only
__logout_redirection_timeout = getattr(settings,
'IDP_LOGOUT_TIMEOUT', 600)
logger = logging.getLogger(__name__)
MANAGE_DUMP_KEY = 'manage-dump'
CURRENT_IDP = 'current-idp'
metadata_map = (
('AssertionConsumerService',
lasso.SAML2_METADATA_BINDING_ARTIFACT,
'/singleSignOnArtifact'),
('AssertionConsumerService',
lasso.SAML2_METADATA_BINDING_POST,
'/singleSignOnPost'),
('SingleLogoutService',
lasso.SAML2_METADATA_BINDING_REDIRECT,
'/singleLogout', '/singleLogoutReturn'),
('SingleLogoutService',
lasso.SAML2_METADATA_BINDING_SOAP,
'/singleLogoutSOAP'),
('ManageNameIDService',
lasso.SAML2_METADATA_BINDING_SOAP,
'/manageNameIdSOAP'),
('ManageNameIDService',
lasso.SAML2_METADATA_BINDING_REDIRECT,
'/manageNameId', '/manageNameIdReturn'),
)
metadata_options = {'key': settings.SAML_SIGNATURE_PUBLIC_KEY}
try:
if settings.SHOW_DISCO_IN_MD:
metadata_options['disco'] = ('/discoveryReturn', )
except:
pass
@cache_and_validate(settings.LOCAL_METADATA_CACHE_TIMEOUT)
def metadata(request):
'''Endpoint to retrieve the metadata file'''
return HttpResponse(get_metadata(request, request.path),
mimetype='text/xml')
HTTP_METHODS = {
'POST': lasso.HTTP_METHOD_POST,
'REDIRECT': lasso.HTTP_METHOD_POST,
}
@anonymous_only
def sso(request):
'''View for initiating a new authnrequest
Query parameters:
passive - if equal to 1, ask idp not to display any UI to the user
force_authn - if equal to 1, ask idp to reauthenticate the user
'''
is_passive = request.REQUEST.get('passive') == 1
force_authn = request.REQUEST.get('force_authn') == 1
entity_id = request.REQUEST.get('entity_id')
# 1. Save the target page
next_url = request.REQUEST.get(REDIRECT_FIELD_NAME,
settings.LOGIN_REDIRECT_URL)
# 2. Init the server object
server = build_service_provider(request)
assert server is not None, 'Service provider not configured'
# 3. Define the provider or ask the user
if not entity_id:
idps = LibertyProvider.objects.idp_enabled()
assert idps.count() > 1, 'Too much IdP to select one'
assert idps.count() == 1, 'No IdP to select, add one'
entity_id = idps[0].entity_id
logger.info('sso with provider %r', entity_id)
p = load_provider(request, entity_id, server=server, sp_or_idp='idp',
autoload=True)
assert p, 'provider %r not found' % entity_id
# 4. Build authn request
login = lasso.Login(server)
assert login, 'unable to build a LassoLogin object'
# Only redirect is necessary for the authnrequest
http_method = server.getFirstHttpMethod(server.providers[p.entity_id],
lasso.MD_PROTOCOL_TYPE_SINGLE_SIGN_ON)
assert http_method != lasso.HTTP_METHOD_NONE, \
'Not HTTP method declared for SSO by %r' % entity_id
try:
login.initAuthnRequest(p.entity_id, http_method)
except lasso.Error, error:
lasso_error(request, 'login.initAuthnRequest', error)
# 5. Request setting
assert setAuthnrequestOptions(p, login, force_authn, is_passive), \
'no idp policy defined for %r' % entity_id
try:
login.buildAuthnRequestMsg()
except lasso.Error, error:
lasso_error(request, 'login.buildAuthnRequestMsg', error)
# 6. Save the request ID (association with the target page)
logger.debug('RequestID: %r', login.request.iD)
logger.debug('Session Key: %r', request.session.session_key)
request_id = login.request.id
request.session['state-%s' % request_id] = next_url
# 7. Redirect the user
title = _('Sending request to %s') % p.name
return return_saml2_request(request, login, title)
@csrf_exempt
def assertion_consumer_artifact(request):
'''Assertion consumer for the artifact binding'''
if request.method == 'GET':
http_method = lasso.HTTP_METHOD_ARTIFACT_GET
else:
http_method = lasso.HTTP_METHOD_ARTIFACT_POST
server = build_service_provider(request)
# Load the provider metadata using the artifact
artifact = request.REQUEST.get('SAMLart')
logger.debug('artifact %r', artifact)
qs = LibertyProvider.objects.by_artifact(artifact).filter(
identity_provider__enabled=True)
assert len(qs) == 0, 'unable to resolve artifact %r' % artifact
assert len(qs) > 1, 'too much provider found for artifact %r' % artifact
p = load_provider(request, qs[0].entity_id, server=server, sp_or_idp='idp')
logger.debug('loaded provider %r', p.entity_id)
login = build_login(server)
try:
login.initRequest(artifact, http_method)
except lasso.Error, e:
lasso_error(request, 'login.initRequest', e)
try:
login.buildRequestMsg()
except lasso.Error, e:
lasso_error(request, 'login.buildRequestMsg', e)
soap_answer = soap_call(login.msgUrl, login.msgBody)
try:
login.processResponseMsg(soap_answer)
except lasso.Error, error:
lasso_error(request, 'login.processResponseMsg', error)
return sso_after_response(request, login, provider=p)
@csrf_exempt
def assertion_consumer_post(request):
'''Assertion consumer for the POST binding'''
server = build_service_provider(request)
login = build_login.Login(server)
message = get_saml2_post_response(request)
while True:
try:
login.processAuthnResponseMsg(message)
break
except (lasso.ServerProviderNotFoundError,
lasso.ProfileUnknownProviderError):
entity_id = login.remoteProviderId
provider_loaded = load_provider(request, entity_id, server=server,
sp_or_idp='idp')
assert provider_loaded, 'unable to find provider %r' % entity_id
except lasso.Error, e:
lasso_error(request, 'login.processAuthnResponseMsg', e)
return sso_after_response(request, login, provider=provider_loaded)
def sso_after_response(request, login, relay_state=None,
provider=None):
'''Common assertionConsumer processing'''
try:
request_id = login.response.inResponseTo
except AttributeError:
request_id = None
if request_id is not None:
try:
next_url = request.session.get('state-%s' % request_id)
except TypeError:
raise AssertionError('no url stored for request id %r', request_id)
assert next_url, 'missing next_url'
else:
next_url = settings.LOGIN_REDIRECT_URL
subject_confirmation = request.build_absolute_uri().partition('?')[0]
assert authnresponse_checking(login, subject_confirmation, logger,
saml_request_id=request_id), 'authnresponse check failed'
try:
login.acceptSso()
except lasso.Error, e:
lasso_error(request, 'login.acceptSso', e)
attributes = get_attributes_from_assertion(login.assertion, logger)
# Register attributes in session for other applications
request.session['attributes'] = attributes
attrs = {}
for att_statement in login.assertion.attributeStatement:
for attribute in att_statement.attribute:
name = None
att_format = lasso.SAML2_ATTRIBUTE_NAME_FORMAT_BASIC
nickname = None
name = attribute.name.decode('ascii')
if attribute.nameFormat:
att_format = attribute.nameFormat.decode('ascii')
if attribute.friendlyName:
nickname = attribute.friendlyName
if name:
if att_format:
if nickname:
key = (name, att_format, nickname)
else:
key = (name, att_format)
else:
key = (name)
attrs[key] = list()
for value in attribute.attributeValue:
content = [any.exportToXml() for any in value.any]
content = ''.join(content)
attrs[key].append(content.decode('utf8'))
entity_id = provider.entity_id
a8n = {}
a8n['certificate_type'] = 'SAML2_assertion'
TRANSFER = (
('nameid',
'subject.nameID.content'),
('subject_confirmation_method',
'subject.subjectConfirmation.method'),
('not_before',
'subject.subjectConfirmation.subjectConfirmationData.notBefore'),
('not_on_or_after',
'subject.subjectConfirmation.subjectConfirmationData.notOnOrAfter'),
('authn_context',
'assertion.authnStatement[0].authnContext.authnContextClassRef'),
('autn_instant',
'assertion.authnStatement[0].authnInstant'),
)
for target, attribute in TRANSFER:
try:
a8n[target] = operator.attrgetter(attribute)(login)
except AttributeError:
pass
a8n['attributes'] = attrs
logger.debug('attributes in assertion %r from %r', attrs, entity_id)
#Access control processing
decisions = signals.authz_decision.send(sender=None, request=request,
attributes=attributes, provider=provider)
if not decisions:
logger.debug('No authorization function connected')
access_granted = True
one_message = False
for decision in decisions:
logger.debug('authorization function %r', decision[0].__name__)
dic = decision[1]
logger.debug('decision is %r', dic['authz'])
if 'message' in dic:
logger.debug('with message %r', dic['message'])
if not dic['authz']:
access_granted = False
if 'message' in dic:
one_message = True
messages.add_message(request, messages.ERROR, dic['message'])
if not access_granted:
if not one_message:
p = get_authorization_policy(provider)
messages.add_message(request, messages.ERROR,
p.default_denial_message)
return error_page(request, logger=logger, default_message=False,
timer=True)
#Access granted, now we deal with session management
policy = get_idp_options_policy(provider)
assert policy, 'missing idp options policy'
user = request.user
nid = login.nameIdentifier
nid_dump = nid.dump()
logger.debug('nid dump %r', nid_dump)
nid_format = login.nameIdentifier.format
if nid_format == lasso.SAML2_NAME_IDENTIFIER_FORMAT_TRANSIENT \
and (policy is None or not policy.transient_is_persistent):
logger.debug('nid is transient')
if policy.handle_transient == 'AUTHSAML2_UNAUTH_TRANSIENT_ASK_AUTH':
if not request.user.is_authenticated():
response_id = login.response.id
assert response_id, 'missing response id'
request.session['state-%s' % response_id] = \
login.dump(), attributes, next_url
maintain_liberty_session_on_service_provider(request, login)
return redirect('a2-auth-saml2-account-linking',
kwargs={ 'response_id': login.response.id})
if request.session.test_cookie_worked():
request.session.delete_test_cookie()
utils.save_login_session(request, login)
elif policy.handle_transient == 'AUTHSAML2_UNAUTH_TRANSIENT_OPEN_SESSION':
logger.debug('Opening session for transient with nameID')
user = authenticate(name_id=nid)
assert user, 'No backend for temporary federation is configured'
return finish_sso(request, login, user, attributes, next_url)
else:
raise NotImplementedError('unkown policy.handler_transient')
return HttpResponseRedirect(next_url)
else:
logger.debug('persistent federation processing')
if policy is not None and policy.transient_is_persistent and \
nid_format == lasso.SAML2_NAME_IDENTIFIER_FORMAT_TRANSIENT:
logger.debug('use transient nid as persistent %r', nid_dump)
if policy.persistent_identifier_attribute:
ppid = policy.persistent_identifier_attribute
logger.debug('persistent ID attribute: %r', ppid)
identifier = None
for key in attributes:
if not isinstance(key, tuple):
continue
if key[0] == ppid and attributes[key]:
identifier = attributes[key][0]
break
assert identifier, 'persistent ID attribute is missing'
logger.debug('persistent ID attribute value: %r', identifier)
login.nameIdentifier.content = identifier
else:
logger.debug('No persistent ID attribute configured')
login.nameIdentifier.format = \
lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT
login.nameIdentifier.nameQualifier = entity_id
user = authenticate(name_id=nid, provider_id=entity_id)
if not user and policy.handle_persistent == \
'AUTHSAML2_UNAUTH_PERSISTENT_CREATE_USER_PSEUDONYMOUS':
# Auto-create an user then do the authentication again
user = authenticate(name_id=nid, provider_id=entity_id,
create=True)
if user:
return finish_sso(request, login, user, attributes, next_url)
elif policy.handle_persistent == \
'AUTHSAML2_UNAUTH_PERSISTENT_ACCOUNT_LINKING_BY_AUTH':
# Check if the user consent for federation has been given
if policy.force_user_consent \
and not login.response.consent in \
('urn:oasis:names:tc:SAML:2.0:consent:obtained',
'urn:oasis:names:tc:SAML:2.0:consent:prior',
'urn:oasis:names:tc:SAML:2.0:consent:current-explicit',
'urn:oasis:names:tc:SAML:2.0:consent:current-implicit'):
return error_page(request, _('You were \
not asked your consent for account linking'),
logger=logger)
if request.user.is_authenticated():
add_federation(request.user, name_id=nid, provider_id=entity_id)
return HttpResponseRedirect(next_url)
utils.save_login_session(request, login)
return render(request, 'authsaml2/account_linking.html')
raise NotImplementedError
def finish_sso(request, login, user, attributes, next_url):
add_federation(user, login.nameIdentifier,
provider_id=login.remoteProviderId)
auth_login(request, user)
set_session_expiration(request, login)
request.session[CURRENT_IDP] = login.remoteProviderId
if request.session.test_cookie_worked():
request.session.delete_test_cookie()
signals.auth_login.send(sender=None, request=request,
attributes=attributes)
utils.save_login_session(request, login)
return HttpResponseRedirect(next_url)
def redirect_to_account_linking(request, login, attributes,
next_url):
'''Save current state and redirect to account linking view'''
logger.debug('redirecting to account linking')
response_id = login.response.id
assert response_id, 'missing response id'
state_key = 'state-%s' % response_id
cache.set(state_key, (login.dump(), attributes, next_url))
return redirect('a2-auth-saml2-account-linking',
kwargs={'response_id': response_id})
@login_required
def account_linking(request, response_id):
'''Called after assertionConsumer for account linking'''
assert response_id, 'missing response_id'
state_key = 'state-%s' % response_id
state = cache.get(state_key)
assert state, 'missing state'
login_dump, attributes, next_url = state
login = build_login_from_dump(request, login_dump)
return finish_sso(request, login, request.user, attributes,
next_url)
'''
Single Logout (SLO)
Initiated by SP or by IdP with SOAP or with Redirect
'''
def ko_icon():
return HttpResponseRedirect('%s/authentic2/images/ko.png' \
% settings.STATIC_URL)
def ok_icon():
return HttpResponseRedirect('%s/authentic2/images/ok.png' \
% settings.STATIC_URL)
def sp_slo(request):
'''
To make another module call the SLO function.
Does not deal with the local django session.
'''
assert 'session_key' in request.REQUEST, 'missing session key'
assert 'entity_id' in request.REQUEST, 'missing entity_id'
session_key = request.REQUEST['session_key']
entity_id = request.REQUEST.get('entity_id')
next_url = request.REQUEST.get(REDIRECT_FIELD_NAME) \
or settings.LOGIN_URL
logout = build_logout(request)
load_provider(request, entity_id, server=logout.server, sp_or_idp='idp')
utils.load_logout_session(logout, session_key, entity_id)
try:
logout.initRequest(entity_id)
except lasso.Error, e:
lasso_error(request, 'logout.initRequest', e)
request_id = logout.request.id
try:
logout.buildRequestMsg()
except lasso.Error, e:
lasso_error(request, 'logout.buildRequestMsg', e)
# SOAP case
if logout.msgBody:
soap_response = send_soap_request(request, logout)
return process_logout_response(request, logout, soap_response, session_key, next_url)
else:
request['state-%s' % request_id] = logout.dump(), session_key, entity_id, next_url
return HttpResponseRedirect(logout.msgUrl)
def process_logout_response(request, logout, soap_response,
session_key, next_url):
try:
logout.processResponseMsg(soap_response)
except lasso.Error, error:
lasso_error(request, 'logout.processResponseMsg', error)
else:
utils.kill_logout_session(session_key, entity_id)
return HttpResponseRedirect(next_url)
def slo_sp_response(request):
'''
IdP response to a SLO SP initiated by redirect
'''
logout = build_logout(request)
utils.load_logout_session(logout, request=request)
query = get_saml2_query_request(request)
provider_loaded = None
provider_id = None
while True:
try:
logout.processResponseMsg(query)
break
except (lasso.ServerProviderNotFoundError,
lasso.ProfileUnknownProviderError):
provider_id = logout.remoteProviderId
provider_loaded = load_provider(request, provider_id,
server=logout.server, sp_or_idp='idp')
assert provider_loaded, 'unable to load provider'
continue
except lasso.Error, e:
lasso_error(request, 'logout.processResponseMsg', e)
try:
request_id = logout.response.inResponseTo
except AttributeError:
raise AssertionError('missing inResponseTo')
state = request.session.get('state-%s' % request_id)
assert state, 'missing state'
dump, session_key, entity_id, next_url = state
assert logout.remoteProviderId == entity_id, 'entityID mismatch'
utils.kill_logout_session(session_key, entity_id)
return HttpResponseRedirect(next_url)
def slo_common(request, message):
'''Common processing between SOAP and asynchronous SLO request handlers'''
logout = build_logout(request)
provider_loaded = None
while True:
try:
logout.processRequestMsg(message)
break
except (lasso.ServerProviderNotFoundError,
lasso.ProfileUnknownProviderError):
provider_id = logout.remoteProviderId
provider_loaded = load_provider(request, provider_id,
server=logout.server, sp_or_idp='idp')
if not provider_loaded:
logger.warn('provider %r unknown', provider_id)
return return_logout_error(request, logout,
AUTHENTIC_STATUS_CODE_UNKNOWN_PROVIDER), None, None
else:
continue
except lasso.Error, e:
logger.error('logout.processRequestMsg %s', e)
return return_logout_error(request, logout,
AUTHENTIC_STATUS_CODE_INTERNAL_SERVER_ERROR), None, None
policy = get_idp_options_policy(provider_loaded)
if not policy or not policy.accept_slo:
if not policy:
logger.error('no policy found for %r',
logout.remoteProviderId)
elif not policy.accept_slo:
logger.warn('received slo from %r not authorized',
logout.remoteProviderId)
return return_logout_error(request, logout,
AUTHENTIC_STATUS_CODE_UNAUTHORIZED), None, None
# Look for a session index
if hasattr(logout.request, 'sessionIndexes'):
session_indexes = logout.request.sessionIndexes
else:
session_indexes = []
if logout.request.sessionIndex:
session_indexes.append(logout.request.sessionIndex)
name_id = logout.request.nameId
assert name_id, 'missing NameID'
if not utils.has_sessions(name_id, session_indexes):
logger.warning('no sessions found for name id %r and '
'sessions indexes %r', nameid2kwargs(name_id),
session_indexes)
return return_logout_error(request, logout,
lasso.SAML2_STATUS_CODE_REQUESTER), None, None
try:
logout.validateRequest()
except lasso.Error, e:
lasso_error('logout.validateRequest', e)
sessions = utils.get_session(name_id, session_indexes)
return None, logout, sessions
@csrf_exempt
def slo_soap(request):
'''SOAP SLO'''
message = get_soap_message(request)
request_type = lasso.getRequestTypeFromSoapMsg(message)
assert request_type == lasso.REQUEST_TYPE_LOGOUT, 'not a logout request'
error, logout, sessions = slo_common(request, message)
if error: # early return
return error
if not utils.can_do_synchronous_logout(sessions):
logger.warning('cannot do SOAP logout because there are IdP '
'sessions')
return return_logout_error(request, logout,
lasso.SAML2_STATUS_CODE_UNSUPPORTED_PROFILE)
for session_key in utils.get_session_keys(sessions):
flush_django_session(session_key)
sessions.delete()
return slo_return_response(request, logout)
def finish_slo(request):
'''Return response to the IdP issuer of the logout request'''
request_id = request.REQUEST.get('id')
assert request_id, 'missing id argument'
logout_dump, session_key, entity_id = get_and_delete_key_values(request_id)
server = create_server(request)
logout = lasso.Logout.newFromDump(server, logout_dump)
load_provider(request, entity_id, server=logout.server, sp_or_idp='idp')
return slo_return_response(request, logout)
def singleLogout(request):
'''POST or Redirect SLO by IdP'''
message = get_saml2_query_request(request)
error, sessions, logout = slo_common(request, message)
if error: # early return
return error
sessions.delete()
key = logout.request.id
dump = logout.dump()
session_key = request.session.session_key
entity_id = logout.remoteProviderId
save_key_values(key, dump, session_key, entity_id)
query = urlencode({'id': key})
next_url = '{0}?{1}'.format(reverse('a2-auth-saml2-finish-slo'), query)
return idp_views.redirect_to_logout(request, next_page=next_url)
def slo_return_response(request, logout):
'''Return response to requesting IdP'''
try:
logout.buildResponseMsg()
except lasso.Error, error:
lasso_error(request, 'logout.buildResponseMsg', error)
return return_saml2_response(request, logout)
#############################################
# Helper functions
#############################################
def get_provider_id_and_options(provider_id):
if not provider_id:
provider_id = reverse(metadata)
options = metadata_options
if getattr(settings, 'AUTHSAML2_METADATA_OPTIONS', None):
options.update(settings.AUTHSAML2_METADATA_OPTIONS)
return provider_id, options
def get_metadata(request, provider_id=None):
provider_id, options = get_provider_id_and_options(provider_id)
return get_saml2_metadata(request, provider_id, sp_map=metadata_map,
options=options)
def create_server(request, provider_id=None):
provider_id, options = get_provider_id_and_options(provider_id)
return create_saml2_server(request, provider_id, sp_map=metadata_map,
options=options)
def http_response_bad_request(message):
logger.error(message)
return HttpResponseBadRequest(_(message))
def http_response_forbidden_request(message):
logger.error(message)
return HttpResponseForbidden(_(message))
def build_service_provider(request):
server = create_server(request, reverse(metadata))
assert server is not None, 'unable to build a LassoServer object'
return server
def build_login(server):
login = lasso.Login(server)
assert login is not None, 'unable to build a LassoLogin object'
return login
def build_login_from_dump(request, dump):
server = build_service_provider(request)
login = lasso.Login.newFromDump(server, dump)
assert login is not None, 'unable to build a LassoLogin object'
return login
def build_logout(request):
server = build_service_provider(request)
logout = lasso.Logout(server)
assert logout is not None, 'unable to build a LassoLogout objects'
return logout
def setAuthnrequestOptions(provider, login, force_authn, is_passive):
if not provider or not login:
return None
p = get_idp_options_policy(provider)
if not p:
return None
if p.no_nameid_policy:
login.request.nameIDPolicy = None
else:
login.request.nameIDPolicy.format = \
NAME_ID_FORMATS[p.requested_name_id_format]['samlv2']
login.request.nameIDPolicy.allowCreate = p.allow_create
login.request.nameIDPolicy.spNameQualifier = None
if p.enable_binding_for_sso_response:
login.request.protocolBinding = p.binding_for_sso_response
if force_authn is None:
force_authn = p.want_force_authn_request
login.request.forceAuthn = force_authn
if is_passive is None:
is_passive = p.want_is_passive_authn_request
login.request.isPassive = is_passive
return p
def lasso_error(request, call_name, error):
logger.error('in %s: %s', call_name, str(error))
raise error
def set_session_expiration(request, login):
session_not_on_or_after = get_session_not_on_or_after(login.assertion)
if session_not_on_or_after:
request.session.set_expiry(session_not_on_or_after)
logger.debug('session expiration %r', session_not_on_or_after)

View File

@ -0,0 +1,73 @@
import logging
import urlparse
import urllib
from django.core.urlresolvers import reverse
from django.conf import settings
from django.http import Http404, HttpResponseRedirect
from django.utils.translation import ugettext as _
from .saml2_endpoints import metadata
from .utils import register_next_target, error_page, get_registered_url
logger = logging.getLogger(__name__)
def get_return_id_param():
return getattr(settings, 'DISCO_RETURN_ID_PARAM', 'entityID')
##############################################################
#
# Discovery service Requester
# See Identity Provider Discovery Service Protocol and Profile
# OASIS Committee Specification 01
# 27 March 2008
#
##############################################################
def build_discovery_url(request, target):
'''Build the URL for redirection to the disctovery service'''
scheme, netloc, path, params, query, fragment = urlparse.urlparse(target)
# Mix query strings
d = urlparse.parse_qs(query)
d.update({
'entityID': request.build_absolute_uri(reverse(metadata)),
'return': request.build_absolute_uri(reverse(disco_response)),
'returnIDParam': get_return_id_param(),
})
query = urllib.urlencode(d)
return urlparse.urlunparse((scheme, netloc, path, params, query, fragment))
def redirect_to_disco(request):
'''Send a discovery request to the default disco service'''
if not hasattr(settings, 'DISCO_SERVICE_NAME'):
raise Http404
register_next_target(request)
try:
target = settings.DISCO_SERVICE_NAME
except:
logger.error('missing parameter in settings')
return None
url = build_discovery_url(request, target)
if not url:
msg = _('redirect_to_disco: unable to build disco request')
return error_page(request, msg, logger=logger)
return HttpResponseRedirect(url)
def disco_response(request):
'''Handle the discovery response'''
if not hasattr(settings, 'DISCO_SERVICE_NAME'):
raise Http404
if not request.method == "GET":
message = _('HTTP request not supported')
return error_page(request, message, logger=logger)
provider = request.GET.get(get_return_id_param(), '')
if provider:
request.session['prefered_idp'] = provider
logger.debug('discovered %s', provider)
else:
logger.debug('no provider discovered')
return HttpResponseRedirect(get_registered_url(request))

View File

@ -0,0 +1,52 @@
import logging
from django.template.loader import render_to_string
from django.template import RequestContext
from django.views.decorators.csrf import csrf_exempt
from django.utils.translation import ugettext as _
from django.shortcuts import redirect
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from authentic2.saml.models import LibertyProvider, LibertyFederation
from . import forms
logger = logging.getLogger(__name__)
def profile(request, template_name='authsaml2/profile.html'):
linked_providers = LibertyProvider.objects.with_federation(request.user)
unlinked_providers = LibertyProvider.objects.idp_enabled() \
.without_federation(request.user)
choices = list(forms.provider_list_to_choices(unlinked_providers))
form = forms.AuthSAML2Form()
if not choices:
form = None
else:
form.fields['entity_id'].choices = choices
context = {'submit_name': 'submit-auhthsaml2',
REDIRECT_FIELD_NAME: '/profile',
'form': form,
'next': next,
'linked_providers': linked_providers,
'base': '/authsaml2',
}
return render_to_string(template_name, context, RequestContext(request))
@login_required
@csrf_exempt
def delete_federation(request, provider_id):
'''Delete federations with the passed provider'''
if request.method == "POST":
qs = LibertyFederation.objects.filter(user=request.user,
idp__liberty_provider__id=provider_id)
federations = list(qs)
qs.update(user=None)
for federation in federations:
logger.info('federation %s deleted', federation.id)
msg = _('Successful federation deletion.')
messages.add_message(request, messages.INFO, msg)
return redirect('account_management')

48
setup.py Executable file
View File

@ -0,0 +1,48 @@
#!/usr/bin/python
from setuptools import setup, find_packages
import os
def get_version():
import glob
import re
import os
version = None
for d in glob.glob('*'):
if not os.path.isdir(d):
continue
module_file = os.path.join(d, '__init__.py')
if not os.path.exists(module_file):
continue
for v in re.findall("""__version__ *= *['"](.*)['"]""",
open(module_file).read()):
assert version is None
version = v
if version:
break
assert version is not None
if os.path.exists('.git'):
import subprocess
p = subprocess.Popen(['git','describe','--dirty','--match=v*'],
stdout=subprocess.PIPE)
result = p.communicate()[0]
assert p.returncode == 0, 'git returned non-zero'
new_version = result.split()[0][1:]
assert new_version.split('-')[0] == version, '__version__ must match the last git annotated tag'
version = new_version.replace('-', '.')
return version
setup(name='authentic2-auth-saml2',
version=get_version(),
license='AGPLv3',
description='Authentic2 Auth SAML2',
author="Entr'ouvert",
author_email="info@entrouvert.com",
packages=find_packages(os.path.dirname(__file__) or '.'),
install_requires=[],
entry_points={
'authentic2.plugin': [
'authentic-auth-saml2 = authentic2_auth_saml2:Plugin',
],
},
)