462 lines
21 KiB
Python
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))
|
|
|