209 lines
7.9 KiB
Python
209 lines
7.9 KiB
Python
#!/usr/bin/env python
|
|
# -*- coding: utf-8 -*-
|
|
|
|
"""
|
|
This file is part of web2py Web Framework (Copyrighted, 2007-2014).
|
|
Developed by Massimo Di Pierro <mdipierro@cs.depaul.edu>.
|
|
License: LGPL v3
|
|
|
|
Login will be done via Web2py's CAS application, instead of web2py's
|
|
login form.
|
|
|
|
Include in your model (eg db.py)::
|
|
|
|
auth.define_tables(username=True)
|
|
from gluon.contrib.login_methods.saml2_auth import Saml2Auth
|
|
import os
|
|
auth.settings.login_form=Saml2Auth(
|
|
config_file = os.path.join(request.folder,'private','sp_conf'),
|
|
maps=dict(
|
|
username=lambda v: v['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn'][0],
|
|
email=lambda v: v['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn'][0],
|
|
user_id=lambda v: v['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn'][0]))
|
|
|
|
you must have private/sp_conf.py, the pysaml2 sp configuration file. For example:
|
|
|
|
|
|
#!/usr/bin/env python
|
|
# -*- coding: utf-8 -*-
|
|
|
|
from saml2 import BINDING_HTTP_POST, BINDING_HTTP_REDIRECT
|
|
import os.path
|
|
import requests
|
|
import tempfile
|
|
|
|
BASEDIR = os.path.abspath(os.path.dirname(__file__))
|
|
|
|
# Web2py SP url and application name
|
|
HOST = 'http://127.0.0.1:8000'
|
|
APP = 'sp'
|
|
|
|
# To load the IDP metadata...
|
|
IDP_METADATA = 'http://127.0.0.1:8088/metadata'
|
|
|
|
def full_path(local_file):
|
|
return os.path.join(BASEDIR, local_file)
|
|
|
|
CONFIG = {
|
|
# your entity id, usually your subdomain plus the url to the metadata view.
|
|
'entityid': '%s/%s/default/metadata' % (HOST, APP),
|
|
'service': {
|
|
'sp' : {
|
|
'name': 'MYSP',
|
|
'endpoints': {
|
|
'assertion_consumer_service': [
|
|
('%s/%s/default/user/login' % (HOST, APP), BINDING_HTTP_REDIRECT),
|
|
('%s/%s/default/user/login' % (HOST, APP), BINDING_HTTP_POST),
|
|
],
|
|
},
|
|
},
|
|
},
|
|
# Your private and public key.
|
|
'key_file': full_path('pki/mykey.pem'),
|
|
'cert_file': full_path('pki/mycert.pem'),
|
|
|
|
# where the remote metadata is stored
|
|
'metadata': {
|
|
"remote": [{
|
|
"url": IDP_METADATA,
|
|
"cert":full_path('pki/mycert.pem')
|
|
}]
|
|
},
|
|
}
|
|
|
|
"""
|
|
|
|
from saml2 import BINDING_HTTP_REDIRECT, BINDING_HTTP_POST
|
|
from saml2.client import Saml2Client
|
|
from gluon.utils import web2py_uuid
|
|
from gluon import current, redirect, URL
|
|
import os, types
|
|
|
|
def obj2dict(obj, processed=None):
|
|
"""
|
|
converts any object into a dict, recursively
|
|
"""
|
|
processed = processed if not processed is None else set()
|
|
if obj is None:
|
|
return None
|
|
if isinstance(obj,(int,long,str,unicode,float,bool)):
|
|
return obj
|
|
if id(obj) in processed:
|
|
return '<reference>'
|
|
processed.add(id(obj))
|
|
if isinstance(obj,(list,tuple)):
|
|
return [obj2dict(item,processed) for item in obj]
|
|
if not isinstance(obj, dict) and hasattr(obj,'__dict__'):
|
|
obj = obj.__dict__
|
|
else:
|
|
return repr(obj)
|
|
return dict((key,obj2dict(value,processed)) for key,value in obj.items()
|
|
if not key.startswith('_') and
|
|
not type(value) in (types.FunctionType,
|
|
types.LambdaType,
|
|
types.BuiltinFunctionType,
|
|
types.BuiltinMethodType))
|
|
|
|
def saml2_handler(session, request, config_filename = None, entityid = None):
|
|
config_filename = config_filename or os.path.join(request.folder,'private','sp_conf')
|
|
client = Saml2Client(config_file = config_filename)
|
|
if not entityid:
|
|
idps = client.metadata.with_descriptor("idpsso")
|
|
entityid = idps.keys()[0]
|
|
bindings = [BINDING_HTTP_REDIRECT, BINDING_HTTP_POST]
|
|
binding, destination = client.pick_binding(
|
|
"single_sign_on_service", bindings, "idpsso", entity_id=entityid)
|
|
if request.env.request_method == 'GET':
|
|
binding = BINDING_HTTP_REDIRECT
|
|
elif request.env.request_method == 'POST':
|
|
binding = BINDING_HTTP_POST
|
|
if not request.vars.SAMLResponse:
|
|
req_id, req = client.create_authn_request(destination, binding=BINDING_HTTP_POST)
|
|
relay_state = web2py_uuid().replace('-','')
|
|
session.saml_outstanding_queries = {req_id: request.url}
|
|
session.saml_req_id = req_id
|
|
http_args = client.apply_binding(binding, str(req), destination,
|
|
relay_state=relay_state)
|
|
return {'url':dict(http_args["headers"])['Location']}
|
|
else:
|
|
relay_state = request.vars.RelayState
|
|
req_id = session.saml_req_id
|
|
unquoted_response = request.vars.SAMLResponse
|
|
res = {}
|
|
try:
|
|
data = client.parse_authn_request_response(
|
|
unquoted_response, binding, session.saml_outstanding_queries)
|
|
res['response'] = data if data else {}
|
|
except Exception as e:
|
|
import traceback
|
|
res['error'] = traceback.format_exc()
|
|
return res
|
|
|
|
|
|
class Saml2Auth(object):
|
|
|
|
def __init__(self, config_file=None, maps=dict(
|
|
username=lambda v:v['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn'][0],
|
|
email=lambda v:v['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn'][0],
|
|
user_id=lambda v:v['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn'][0],
|
|
), logout_url=None, change_password_url=None, entityid=None):
|
|
self.config_file = config_file
|
|
self.maps = maps
|
|
|
|
# URL for redirecting users to when they sign out
|
|
self.saml_logout_url = logout_url
|
|
|
|
# URL to let users change their password in the IDP system
|
|
self.saml_change_password_url = change_password_url
|
|
|
|
# URL to specify an IDP if using federation metadata or an MDQ
|
|
self.entityid = entityid
|
|
|
|
def login_url(self, next="/"):
|
|
d = saml2_handler(current.session, current.request, entityid=self.entityid)
|
|
if 'url' in d:
|
|
redirect(d['url'])
|
|
elif 'error' in d:
|
|
current.session.flash = d['error']
|
|
redirect(URL('default','index'))
|
|
elif 'response' in d:
|
|
# a['assertions'][0]['attribute_statement'][0]['attribute']
|
|
# is list of
|
|
# {'name': 'http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname', 'name_format': None, 'text': None, 'friendly_name': None, 'attribute_value': [{'text': 'CAA\\dev-mdp', 'extension_attributes': "{'{http://www.w3.org/2001/XMLSchema-instance}type': 'xs:string'}", 'extension_elements': []}], 'extension_elements': [], 'extension_attributes': '{}'}
|
|
try:
|
|
attributes = d['response'].assertions[0].attribute_statement[0].attribute
|
|
except:
|
|
attributes = d['response'].assertion.attribute_statement[0].attribute
|
|
current.session.saml2_info = dict(
|
|
(a.name, [i.text for i in a.attribute_value]) for a in attributes)
|
|
return next
|
|
|
|
def logout_url(self, next="/"):
|
|
current.session.saml2_info = None
|
|
current.session.auth = None
|
|
self._SAML_logout()
|
|
return next
|
|
|
|
def change_password_url(self, next="/"):
|
|
self._SAML_change_password()
|
|
return next
|
|
|
|
def get_user(self):
|
|
user = current.session.saml2_info
|
|
if user:
|
|
d = {'source': 'web2py saml2'}
|
|
for key in self.maps:
|
|
d[key] = self.maps[key](user)
|
|
return d
|
|
return None
|
|
|
|
def _SAML_logout(self):
|
|
"""
|
|
exposed SAML.logout()
|
|
redirects to the SAML logout page
|
|
"""
|
|
redirect(self.saml_logout_url)
|
|
|
|
def _SAML_change_password(self):
|
|
redirect(self.saml_change_password_url)
|