This repository has been archived on 2023-02-21. You can view files and clone it, but cannot push or open issues or pull requests.
mandaye/mandaye/auth/saml2.py

462 lines
21 KiB
Python

import os
import urllib2
import lasso
from urlparse import parse_qs
from mandaye import config, utils
from mandaye.saml import saml2utils
from mandaye.auth.authform import AuthForm
from mandaye.exceptions import MandayeSamlException, ImproperlyConfigured
from mandaye.response import _302, _401
from mandaye.log import logger
from mandaye.http import HTTPResponse, HTTPHeader
"""
Mandaye saml2 authentification support
To use it you must set the following options into your
virtual host :
* saml2_idp_metadata: a link to your idp metadata
* saml2_signature_public_key: a path to your public key
* saml2_signature_private_key: a path to your private key
Optional options :
* saml2_sp_logout_url: the url to logout the service provider (deprecated: use sp_logout_url instead)
* saml2_authnresp_binding (default: post): artifact, post, redirect or soap
* saml2_authnreq_http_method: only http_redirect at the moment
* saml2_name_identifier_format (default: persistant): email, transient, persistent, unspecified (username like gapps),
encrypted, entity, windows, kerberos or x509
* saml2_metadata_url: saml end point of the metadata
* saml2_single_sign_on_post_url: saml end point of single sign on post
* saml2_single_logout_url: saml end point of logout
* saml2_single_logout_return_url: saml end point of the single logout return
"""
# XXX: remove this for the 1.0. Keep it only for compability reasons.
END_POINTS_PATH = {
'metadata': '/mandaye/metadata',
'single_sign_on_post': '/mandaye/singleSignOnPost',
'single_logout': '/mandaye/singleLogout',
'single_logout_return': '/mandaye/singleLogoutReturn',
}
NAME_IDENTIFIERS_FORMAT = {
'email': lasso.SAML2_NAME_IDENTIFIER_FORMAT_EMAIL,
'transient': lasso.SAML2_NAME_IDENTIFIER_FORMAT_TRANSIENT,
'persistent': lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT,
'unspecified': lasso.SAML2_NAME_IDENTIFIER_FORMAT_UNSPECIFIED,
'username': lasso.SAML2_NAME_IDENTIFIER_FORMAT_UNSPECIFIED,
'encrypted': lasso.SAML2_NAME_IDENTIFIER_FORMAT_ENCRYPTED,
'entity': lasso.SAML2_NAME_IDENTIFIER_FORMAT_ENTITY,
'windows': lasso.SAML2_NAME_IDENTIFIER_FORMAT_WINDOWS,
'kerberos': lasso.SAML2_NAME_IDENTIFIER_FORMAT_KERBEROS,
'x509': lasso.SAML2_NAME_IDENTIFIER_FORMAT_X509,
}
METADATA_BINDING = {
'artifact': lasso.SAML2_METADATA_BINDING_ARTIFACT,
'post': lasso.SAML2_METADATA_BINDING_POST,
'redirect': lasso.SAML2_METADATA_BINDING_REDIRECT,
'soap': lasso.SAML2_METADATA_BINDING_SOAP
}
class SAML2Auth(AuthForm):
""" SAML 2 authentification
"""
def __init__(self, env, mapper):
""" saml2_config: saml 2 config module
env: WSGI environment
mapper: mapper's module like mandaye.mappers.linuxfr
"""
self.env = env
self.END_POINTS_PATH = {
'metadata': str(self.env['mandaye.config'].\
get('saml2_metadata_url', '/mandaye/metadata')),
'single_sign_on_post': str(self.env['mandaye.config'].\
get('saml2_single_sign_on_post_url', '/mandaye/singleSignOnPost')),
'single_logout': str(self.env['mandaye.config'].\
get('saml2_single_logout_url', '/mandaye/singleLogout')),
'single_logout_return': str(self.env['mandaye.config'].\
get('saml2_single_logout_return_url', '/mandaye/singleLogoutReturn')),
}
for param in ('saml2_idp_metadata',
'saml2_signature_public_key',
'saml2_signature_private_key'):
if not self.env['mandaye.config'].has_key(param):
err = 'you must set %s option in vhost : %s' % \
(param, self.env['mandaye.vhost'])
logger.error(err)
raise ImproperlyConfigured, err
public_key = self._get_file_content(
self.env['mandaye.config']['saml2_signature_public_key']
)
private_key = self._get_file_content(
self.env['mandaye.config']['saml2_signature_private_key']
)
authnresp_binding = self.env['mandaye.config'].get('saml2_authnresp_binding', 'post')
name_identifier_format = self.env['mandaye.config'].get('saml2_name_identifier_format', 'persistent')
if authnresp_binding not in METADATA_BINDING.keys():
err = "saml2_authnresp_binding: '%s' invalid value (must be artifact, post, redirect or soap)"
raise ImproperlyConfigured, err
if name_identifier_format not in NAME_IDENTIFIERS_FORMAT.keys():
err = "saml2_authnresp_binding: '%s' invalid value (must be email, transient, persistent," +\
" unspecified (username like gapps), encrypted, entity, windows, kerberos or x509)"
raise ImproperlyConfigured, err
self.config = {
'saml2_idp_metadata': self.env['mandaye.config']['saml2_idp_metadata'],
'saml2_signature_public_key': public_key,
'saml2_signature_private_key': private_key,
'saml2_authnresp_binding': METADATA_BINDING[authnresp_binding],
'saml2_authnreq_http_method': lasso.HTTP_METHOD_REDIRECT,
'saml2_name_identifier_format': NAME_IDENTIFIERS_FORMAT[name_identifier_format]
}
self.metadata_map = (
('AssertionConsumerService',
lasso.SAML2_METADATA_BINDING_POST ,
self.END_POINTS_PATH['single_sign_on_post']
),
('SingleLogoutService',
lasso.SAML2_METADATA_BINDING_REDIRECT,
self.END_POINTS_PATH['single_logout'],
self.END_POINTS_PATH['single_logout_return']),
)
self.metadata_options = { 'key': public_key }
super(SAML2Auth, self).__init__(env, mapper)
def _get_file_content(self, path):
if not os.path.isabs(path):
path = os.path.join(config.config_root,
path)
if not os.path.exists(path):
err = "%s: file %s doesn't exist" % \
(path, self.env['mandaye.vhost'])
logger.error(err)
raise ImproperlyConfigured, err
with open(path, 'r') as f:
content = f.read()
return content
def get_default_mapping(self):
default_mapping = super(SAML2Auth, self).get_default_mapping()
default_mapping.extend([
{
'path': r'%s$' % self.urls.get('connection_url', '/mandaye/sso'),
'method': 'GET',
'response': {'filter': self.sso,}
},
{
'path': r'%s$' % self.END_POINTS_PATH['metadata'],
'method': 'GET',
'response': {'filter': self.metadata,}
},
{
'path': r'%s$' % self.END_POINTS_PATH['single_sign_on_post'],
'method': 'POST',
'response': {'auth': 'single_sign_on_post'}
},
{
'path': r'%s$' % self.END_POINTS_PATH['single_logout'],
'method': 'GET',
'response': {'auth': 'single_logout',}
},
{
'path': r'%s$' % self.END_POINTS_PATH['single_logout_return'],
'method': 'GET',
'response': {'auth': 'single_logout_return',}
},
])
return default_mapping
def _get_idp_metadata_file_path(self):
metadata_file_path = None
if self.config['saml2_idp_metadata']:
metadata_file_path = os.path.join(config.data_dir,
self.config['saml2_idp_metadata'].\
replace('://', '_').\
replace('/', '_')
)
if not os.path.isfile(metadata_file_path):
try:
response = urllib2.urlopen(self.config['saml2_idp_metadata'])
metadata = response.read()
response.close()
except Exception, e:
logger.error("Unable to fetch metadata %r: %r",
self.config['saml2_idp_metadata'], str(e))
raise MandayeSamlException("Unable to find metadata: %s" % str(e))
metadata_file = open(metadata_file_path, 'w')
metadata_file.write(metadata)
metadata_file.close()
return metadata_file_path
def _get_metadata(self, env):
url_prefix = env['mandaye.scheme'] + '://' + env['HTTP_HOST']
metadata_path = self.END_POINTS_PATH['metadata']
single_sign_on_post_path = \
self.END_POINTS_PATH['single_sign_on_post']
metagen = saml2utils.Saml2Metadata(url_prefix + metadata_path,
url_prefix = url_prefix)
metagen.add_sp_descriptor(self.metadata_map, self.metadata_options)
return str(metagen)
def sso(self, env, values, request, response):
# always use a new beaker session
env['beaker.session'].regenerate_id()
qs = parse_qs(env['QUERY_STRING'])
target_idp = self.config['saml2_idp_metadata']
metadata_file_path = self._get_idp_metadata_file_path()
if not metadata_file_path:
raise MandayeSamlException("sso: unable to load provider")
logger.debug('sso: target_idp is %r', target_idp)
logger.debug('sso: metadata url is %r', self.config['saml2_idp_metadata'])
logger.debug('sso: mandaye metadata are %r', self._get_metadata(env))
server = lasso.Server.newFromBuffers(self._get_metadata(env),
self.config['saml2_signature_private_key'])
if not server:
raise MandayeSamlException("sso: error creating server object.")
logger.debug('sso: mandaye server object created')
server.addProvider(lasso.PROVIDER_ROLE_IDP, metadata_file_path)
login = lasso.Login(server)
if not login:
raise MandayeSamlException("sso: error creating login object.")
http_method = self.config['saml2_authnreq_http_method']
try:
login.initAuthnRequest(target_idp, http_method)
except lasso.Error, error:
raise MandayeSamlException("sso: Error initiating request %s" % lasso.strError(error[0]))
login.request.nameIDPolicy.format = self.config['saml2_name_identifier_format']
login.request.nameIDPolicy.allowCreate = True
login.request.nameIDPolicy.spNameQualifier = None
login.request.protocolBinding = self.config['saml2_authnresp_binding']
if qs.has_key('next_url'):
login.msgRelayState = qs['next_url'][0]
try:
login.buildAuthnRequestMsg()
except lasso.Error, error:
raise MandayeSamlException("sso: Error initiating request %s" % lasso.strError(error[0]))
logger.debug('sso: set request id in session %s' % login.request.iD)
env['beaker.session']['request_id'] = login.request.iD
env['beaker.session'].save()
if not login.msgUrl:
raise MandayeSamlException("sso: Unable to perform sso by redirection")
return _302(login.msgUrl)
def single_sign_on_post(self, env, values, request, response):
if self.urls.get('connection_failed_url'):
failed_url = self.urls['connection_failed_url']
else:
failed_url = '/'
metadata_file_path = self._get_idp_metadata_file_path()
if not metadata_file_path:
raise MandayeSamlException("single_sign_on_post: Unable to load provider")
server = lasso.Server.newFromBuffers(self._get_metadata(env),
self.config['saml2_signature_private_key'])
if not server:
raise MandayeSamlException("singleSignOnPost: error creating server object")
server.addProvider(lasso.PROVIDER_ROLE_IDP, metadata_file_path)
login = lasso.Login(server)
if not login:
raise MandayeSamlException("singleSignOnPost: Error creating login object")
if env['REQUEST_METHOD'] != 'POST':
raise MandayeSamlException("singleSignOnPost: Not a POST request")
msg = env['wsgi.input']
params = parse_qs(msg.read())
if not params or not lasso.SAML2_FIELD_RESPONSE in params.keys():
raise MandayeSamlException("singleSignOnPost: Missing response")
message = params[lasso.SAML2_FIELD_RESPONSE][0]
logger.debug('singleSignOnPost message: %r', message)
if not env['beaker.session'].has_key('request_id'):
logger.warning("singleSignOnPost: Unable to find request_id in session")
return _302(failed_url)
saml_request_id = env['beaker.session']['request_id']
try:
login.processAuthnResponseMsg(message)
except lasso.ProfileRequestDeniedError, e:
logger.warning('singleSignOnPost: request denied for nameid %r', saml_request_id)
logger.warning('singleSignOnPost: request denied error: %r', e)
return _302(failed_url)
subject_confirmation = utils.get_absolute_uri(env)
check = saml2utils.authnresponse_checking(login, subject_confirmation,
logger, saml_request_id=saml_request_id)
if not check:
logger.warning("singleSignOnPost: error checking authn response for %r",
saml_request_id)
return _302(failed_url)
logger.debug('sso: response successfully checked')
try:
login.acceptSso()
except lasso.Error, error:
raise MandayeSamlException("singleSignOnPost: Error validating\
sso (%s)" % lasso.strError(error[0]))
logger.debug('sso: sso accepted, session validation')
env['beaker.session']['validated'] = True
attributes = saml2utils.get_attributes_from_assertion(login.assertion,
logger)
clean_attributes = dict()
for k, v in attributes.iteritems():
if len(k) > 1:
clean_attributes[k[0]] = v
else:
clean_attributes[k] = v
env['beaker.session']['attributes'] = clean_attributes
env['beaker.session']['unique_id'] = login.nameIdentifier.content
env['beaker.session']['liberty_session'] = login.session.dump()
env['beaker.session'].save()
next_url = None
if params.has_key('RelayState'):
next_url = params['RelayState'][0]
elif values.has_key('next_url'):
next_url = values['next_url']
if next_url:
return _302("%s?next_url=%s" % (self.urls.get('login_url'), next_url))
else:
return _302(self.urls.get('login_url'))
def slo(self, env, values, request, response):
"""
Single Logout SP initiated by redirected
"""
logger.debug('slo: new slo request')
target_idp = self.config['saml2_idp_metadata']
metadata_file_path = self._get_idp_metadata_file_path()
if not metadata_file_path:
raise MandayeSamlException("slo: Unable to load provider.")
logger.debug('slo: target idp %s' % target_idp)
logger.debug('slo: metadata file path %s' % metadata_file_path)
server = lasso.Server.newFromBuffers(self._get_metadata(env),
self.config['saml2_signature_private_key'])
if not server:
raise MandayeSamlException("slo: Error creating server object")
logger.debug('slo: mandaye server object created')
server.addProvider(lasso.PROVIDER_ROLE_IDP, metadata_file_path)
logout = lasso.Logout(server)
# Load liberty session
if not env['beaker.session'].has_key('liberty_session'):
logger.warning('slo: no liberty session in the session')
return self.local_logout(env, values, request, response)
logout.setSessionFromDump(env['beaker.session']['liberty_session'])
if not logout:
logger.warning('slo: error creating logout object')
return self.local_logout(env, values, request, response)
try:
logout.initRequest(None, lasso.HTTP_METHOD_REDIRECT)
except Exception, e:
logger.warning('sp_slo: init request error')
logger.warning('sp_slo error : %s' % str(e))
return self.local_logout(env, values, request, response)
# Manage RelayState
qs = parse_qs(env['QUERY_STRING'])
if qs.has_key('next_url'):
logout.msgRelayState = qs['next_url'][0]
try:
logout.buildRequestMsg()
except Exception, e:
logger.warning('slo: build request error')
logger.warning('slo error : %s' % e)
return self.local_logout(env, values, request, response)
logger.info('slo: sp_slo by redirect')
return _302(logout.msgUrl)
def slo_return_response(self, logout, cookies=None):
try:
logout.buildResponseMsg()
except lasso.Error, error:
logger.warning('saml2_slo_return_response: %s' % lasso.strError(error[0]))
return _401('saml2_slo_return_response: %s' % lasso.strError(error[0]))
else:
logger.info('saml2_slo_return_response: redirect to %s' % logout.msgUrl)
return _302(logout.msgUrl, cookies)
def single_logout(self, env, values, request, response):
"""
Single Logout IdP initiated by Redirect
"""
qs = env['QUERY_STRING']
if not env['QUERY_STRING']:
raise MandayeSamlException("single_logout: Single Logout by \
Redirect without query string")
server = lasso.Server.newFromBuffers(self._get_metadata(env),
self.config['saml2_signature_private_key'])
if not server:
logger.warning('single_logout: Service provider not configured')
return _401('single_logout: Service provider not configured')
metadata_file_path = self._get_idp_metadata_file_path()
server.addProvider(lasso.PROVIDER_ROLE_IDP, metadata_file_path)
logout = lasso.Logout(server)
if not logout:
logger.error('single_logout: Unable to create Logout object')
raise MandayeSamlException("single_logout: Unable to create Logout object")
try:
logout.processRequestMsg(qs)
except lasso.Error, error:
logger.error('saml2_slo: %s' % lasso.strError(error[0]))
return self.slo_return_response(logout)
logger.info('single_logout: slo from %s' % logout.remoteProviderId)
# Load liberty session
if not env['beaker.session'].has_key('liberty_session'):
logger.error('single_logout: no liberty session in the session')
raise MandayeSamlException("single_logout: no liberty session in the session")
logout.setSessionFromDump(env['beaker.session']['liberty_session'])
try:
logout.validateRequest()
except lasso.Error, error:
logger.error('single_logout: %s' % lasso.strError(error[0]))
return self.slo_return_response(logout)
# SP logout
response = self.local_logout(env, values, request, response)
return self.slo_return_response(logout, response.cookies)
def single_logout_return(self, env, values, request, response):
"""
Return point of the slo
"""
metadata_file_path = self._get_idp_metadata_file_path()
server = lasso.Server.newFromBuffers(self._get_metadata(env),
self.config['saml2_signature_private_key'])
server.addProvider(lasso.PROVIDER_ROLE_IDP, metadata_file_path)
if not server:
logger.error('single_logout_return: Error creating server object.')
raise MandayeSamlException("single_logout_return: Error creating server object")
qs = env['QUERY_STRING']
if not env['QUERY_STRING']:
logger.error('single_logout_return: Single Logout \
by Redirect without query string')
raise MandayeSamlException("single_logout_return: Single Logout \
by Redirect without query string")
logout = lasso.Logout(server)
if not logout:
logger.error('single_logout_return: Unable to create Logout object')
raise MandayeSamlException("single_logout_return: Unable to create Logout object")
# Load liberty session
if not env['beaker.session'].has_key('liberty_session'):
logger.warning('single_logout_return: no liberty session found.')
try:
logout.processResponseMsg(qs)
except lasso.Error, e:
logger.warning("single_logout_return: %s" % lasso.strError(e[0]))
# Local logout
return self.local_logout(env, values, request, response)
def metadata(self, env, values, request, response):
headers = HTTPHeader({'Content-Type': ['text/xml']})
return HTTPResponse(200, 'Found', headers,
self._get_metadata(env))