441 lines
19 KiB
Python
441 lines
19 KiB
Python
import datetime
|
|
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, HTTPRequest
|
|
from mandaye.server import get_response
|
|
|
|
"""
|
|
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
|
|
* saml2_sp_logout_method: GET or POST
|
|
* saml2_authnresp_binding: only post is supported for now
|
|
* saml2_authnreq_http_method: only http_redirect at the moment
|
|
* saml2_name_identifier_format: only persistent at the moment
|
|
"""
|
|
|
|
END_POINTS_PATH = {
|
|
'metadata': '/mandaye/metadata',
|
|
'single_sign_on_post': '/mandaye/singleSignOnPost',
|
|
'single_logout': '/mandaye/singleLogout',
|
|
'single_logout_return': '/mandaye/singleLogoutReturn',
|
|
}
|
|
|
|
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
|
|
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']
|
|
)
|
|
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_sp_logout_url': None,
|
|
'saml2_sp_logout_method': 'GET',
|
|
'saml2_authnresp_binding': lasso.SAML2_METADATA_BINDING_POST,
|
|
'saml2_authnreq_http_method': lasso.HTTP_METHOD_REDIRECT,
|
|
'saml2_name_identifier_format': lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT
|
|
}
|
|
|
|
self.metadata_map = (
|
|
('AssertionConsumerService',
|
|
lasso.SAML2_METADATA_BINDING_POST ,
|
|
END_POINTS_PATH['single_sign_on_post']
|
|
),
|
|
('SingleLogoutService',
|
|
lasso.SAML2_METADATA_BINDING_REDIRECT,
|
|
END_POINTS_PATH['single_logout'],
|
|
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):
|
|
return [
|
|
{
|
|
'path': r'%s$' % END_POINTS_PATH['metadata'],
|
|
'method': 'GET',
|
|
'response': [{
|
|
'filter': self.metadata,
|
|
}]
|
|
},
|
|
{
|
|
'path': r'%s$' % END_POINTS_PATH['single_sign_on_post'],
|
|
'method': 'POST',
|
|
'response': [{'filter': self.single_sign_on_post}]
|
|
},
|
|
{
|
|
'path': r'%s$' % END_POINTS_PATH['single_logout'],
|
|
'method': 'GET',
|
|
'response': [{
|
|
'filter': self.single_logout,
|
|
}]
|
|
},
|
|
{
|
|
'path': r'%s$' % END_POINTS_PATH['single_logout_return'],
|
|
'method': 'GET',
|
|
'response': [{
|
|
'filter': self.single_logout_return,
|
|
}]
|
|
},
|
|
]
|
|
|
|
def local_logout(self, env, values, request, response):
|
|
logger.info('SP logout initiated by Mandaye')
|
|
# Mandaye logout
|
|
self.logout(env, values, request, response)
|
|
|
|
next_url = None
|
|
qs = parse_qs(env['QUERY_STRING'])
|
|
if qs.has_key('RelayState'):
|
|
next_url = qs['RelayState'][0]
|
|
elif qs.has_key('next_url'):
|
|
next_url = qs['next_url'][0]
|
|
elif values.has_key('next_url'):
|
|
next_url = values['next_url']
|
|
|
|
req_cookies = request.cookies
|
|
if not self.config['saml2_sp_logout_url']:
|
|
logger.warning('SP_LOGOUT_URL not set into saml2 configuration only removing cookies')
|
|
for cookie in req_cookies.values():
|
|
cookie['expires'] = 'Thu, 01 Jan 1970 00:00:01 GMT'
|
|
cookie['path'] = '/'
|
|
if next_url:
|
|
return _302(next_url, req_cookies)
|
|
else:
|
|
return _302('/', req_cookies)
|
|
if self.config['saml2_sp_logout_method'] == 'POST':
|
|
headers = HTTPHeader({'Content-Type': ['application/x-www-form-urlencoded']})
|
|
else:
|
|
headers = HTTPHeader()
|
|
request = HTTPRequest(req_cookies, headers, self.config['saml2_sp_logout_method'])
|
|
response = get_response(env, request, self.config['saml2_sp_logout_url'])
|
|
if next_url:
|
|
return _302(next_url, response.cookies)
|
|
else:
|
|
return response
|
|
|
|
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 %s: %s" % \
|
|
(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 = END_POINTS_PATH['metadata']
|
|
single_sign_on_post_path = \
|
|
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):
|
|
if env['beaker.session'].has_key('unique_id'):
|
|
return _302('/')
|
|
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 %s' % target_idp)
|
|
logger.debug('sso: metadata url is %s' % self.config['saml2_idp_metadata'])
|
|
logger.debug('sso: mandaye metadata are %s' % 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):
|
|
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('sso: message posted %s' % message)
|
|
login.processAuthnResponseMsg(message)
|
|
|
|
subject_confirmation = utils.get_absolute_uri(env)
|
|
if not env['beaker.session'].has_key('request_id'):
|
|
raise MandayeSamlException("singleSignOnPost: Unable to find request_id in session")
|
|
saml_request_id = env['beaker.session']['request_id']
|
|
check = saml2utils.authnresponse_checking(login, subject_confirmation,
|
|
logger, saml_request_id=saml_request_id)
|
|
if not check:
|
|
raise MandayeSamlException("singleSignOnPost: error checking authn response")
|
|
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)
|
|
env['beaker.session']['attributes'] = 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))
|
|
|