1394 lines
58 KiB
Plaintext
1394 lines
58 KiB
Plaintext
import os
|
|
import datetime
|
|
import urllib
|
|
|
|
import lasso
|
|
import common
|
|
|
|
from quixote import get_request, get_response, redirect, get_field, get_publisher
|
|
from quixote.http_request import parse_header
|
|
from quixote.directory import Directory, AccessControlled
|
|
from quixote import get_user, get_session, get_session_manager
|
|
|
|
from qommon import get_cfg, get_logger
|
|
from qommon import template
|
|
from qommon import errors
|
|
from qommon.storage import StorableObject
|
|
from qommon.tokens import Token
|
|
from qommon.publisher import sitecharset2utf8
|
|
|
|
import authentic
|
|
from authentic import identities
|
|
from authentic.misc import get_lasso_server, get_provider_key, redirect_with_return_url
|
|
from authentic.form import *
|
|
import authentic.login_token as login_token
|
|
import idwsf2
|
|
import qommon.soap as soap
|
|
import authentic.misc as misc
|
|
import qommon.misc as qmisc
|
|
|
|
authn_methods = {
|
|
'password': lasso.SAML2_AUTHN_CONTEXT_PASSWORD,
|
|
'password-on-https': lasso.SAML2_AUTHN_CONTEXT_PASSWORD_PROTECTED_TRANSPORT,
|
|
'password-and-ip': lasso.SAML2_AUTHN_CONTEXT_INTERNET_PROTOCOL_PASSWORD,
|
|
'client-certificate': lasso.SAML2_AUTHN_CONTEXT_TLS_CLIENT,
|
|
}
|
|
|
|
def get_saml2_authn_method(session):
|
|
authn_method = 'password'
|
|
if session and session.authentication_method:
|
|
authn_method = session.authentication_method
|
|
return authn_methods.get(authn_method)
|
|
|
|
class Saml2Artifact(StorableObject):
|
|
_names = 'saml2_artifacts'
|
|
artifact = None
|
|
session_id = None
|
|
provider_id = None
|
|
message = None
|
|
|
|
def get_by_artifact(cls, artifact):
|
|
return cls.get(artifact)
|
|
get_by_artifact = classmethod(get_by_artifact)
|
|
|
|
|
|
class SpDir(Directory):
|
|
def _q_lookup(self, component):
|
|
return SpUI(component)
|
|
|
|
def error_then_home(error):
|
|
return_url = get_field('returnURL')
|
|
if return_url:
|
|
c_to = (return_url, _('Continue'))
|
|
else:
|
|
c_to = (get_publisher().get_root_url(), _('Home'))
|
|
return template.error_page(error, continue_to = c_to)
|
|
|
|
def get_identity():
|
|
authentic.identities.get_store().load_identities()
|
|
session = get_session()
|
|
if not session:
|
|
return None
|
|
user = session.user
|
|
try:
|
|
return authentic.identities.get_store().get_identity(user)
|
|
except:
|
|
return None
|
|
|
|
def save_session(profile, session = None):
|
|
if not session:
|
|
session = get_session()
|
|
if profile.session:
|
|
session.lasso_session_dump = profile.session.dump()
|
|
else:
|
|
session.lasso_session_dump = None
|
|
|
|
def load_session(profile, session):
|
|
if not session:
|
|
session = get_session()
|
|
if session and session.lasso_session_dump:
|
|
profile.setSessionFromDump(session.lasso_session_dump)
|
|
|
|
def load_identity(profile, identity = None, session = None):
|
|
if not identity:
|
|
identity = get_identity()
|
|
if not identity:
|
|
return
|
|
if identity and identity.lasso_dump:
|
|
profile.setIdentityFromDump(identity.lasso_dump)
|
|
|
|
def save_identity(profile, identity = None):
|
|
if not profile.isIdentityDirty:
|
|
return
|
|
if not identity:
|
|
identity = get_identity()
|
|
if not identity:
|
|
return
|
|
if profile.identity:
|
|
identity.lasso_dump = profile.identity.dump()
|
|
else:
|
|
identity.lasso_dump = None
|
|
identity.name_identifiers = []
|
|
authentic.identities.get_store().save(identity)
|
|
|
|
class RootDirectory(common.LassoDirectory):
|
|
_q_exports = ['singleSignOn', 'failSingleSignOn', 'singleSignOnSOAP', 'artifact', 'consent',
|
|
'singleLogoutSOAP', 'singleLogoutPOST', 'singleLogoutFinish',
|
|
'singleLogout', 'singleLogoutReturn',
|
|
'manageNameIdSOAP', 'manageNameId', 'manageNameIdReturn',
|
|
'metadata', ('metadata.xml', 'metadata'), 'public_key',
|
|
'sp', 'intro', 'disco',
|
|
'proxySingleSignOnArtifact', 'proxySingleSignOnPost',
|
|
'proxySingleSignOnRedirect','proxySingleLogoutSOAP','proxySingleLogout','proxySingleLogoutReturn',
|
|
'proxyAssertionConsumerArtifact', 'proxyAssertionConsumerPost',
|
|
'proxyAssertionConsumerRedirect', 'continueSSO', 'failSSO']
|
|
|
|
sp = SpDir()
|
|
|
|
def _q_index(self):
|
|
raise errors.AccessError()
|
|
|
|
def check_on(self):
|
|
return redirect(get_publisher().get_root_url() + 'images/check_on.png')
|
|
|
|
def check_off(self):
|
|
return redirect(get_publisher().get_root_url() + 'images/check_off.png')
|
|
|
|
def postRequestTo(self, url, body, relayState='', title=''):
|
|
return self.postTo(url, body, "SAMLRequest", relay_state=relay_state,
|
|
title=title)
|
|
|
|
def postResponseTo(self, url, body, relay_state='', title=''):
|
|
return self.postTo(url, body, "SAMLResponse", relay_state=relay_state,
|
|
title=title)
|
|
|
|
def singleSignOn(self):
|
|
'''Handle an AuthnRequest, if not logged and
|
|
interaction is authorized, ask for login'''
|
|
return self.single_sign_on()
|
|
|
|
def get_query(self):
|
|
'''Retrieve the query from the given login token'''
|
|
id = get_field('id')
|
|
if not id:
|
|
return None
|
|
token = login_token.LoginToken.get(id, ignore_errors=True)
|
|
if token:
|
|
return getattr(token, 'query', None)
|
|
return None
|
|
|
|
def invoke_login(self, login, query):
|
|
request_id = login.request.iD
|
|
login_url = get_request().environ['SCRIPT_NAME'] + '/login'
|
|
token = login_token.LoginToken(request_id)
|
|
token.query = query
|
|
token.store()
|
|
request_id = urllib.quote(request_id)
|
|
return redirect_with_return_url(login_url,
|
|
(('okURL', '/saml/continueSSO?id=%s' % request_id),
|
|
('cancelURL', '/saml/failSSO?id=%s' % request_id),
|
|
('LoginToken', request_id),
|
|
('service',qmisc.get_provider_key(login.remoteProviderId))))
|
|
|
|
def continueSSO(self):
|
|
query = self.get_query()
|
|
return self.single_sign_on(False, query)
|
|
|
|
def failSSO(self):
|
|
query = self.get_query()
|
|
return self.single_sign_on(True, query)
|
|
|
|
def single_sign_on(self, request_denied = False, query = None):
|
|
server = get_lasso_server(protocol = 'saml2')
|
|
if not server:
|
|
return template.error_page(_('SAML 2.0 support not yet configured.'))
|
|
|
|
login = lasso.Login(server)
|
|
request = get_request()
|
|
if query is None:
|
|
if request.get_method() == 'GET':
|
|
query = request.get_query()
|
|
elif request.get_method() == 'POST':
|
|
query = request.form.get('SAMLRequest')
|
|
else:
|
|
return template.error_page(_('Invalid request message'))
|
|
try:
|
|
login.processAuthnRequestMsg(query)
|
|
except lasso.DsError, e:
|
|
return template.error_page('Cryptographic error: %s' % lasso.strError(e.code))
|
|
except lasso.Error, error:
|
|
self.logProfileError(login, error, 'singleSignOn login.processAuthnRequestMsg')
|
|
get_logger().debug('singleSignOn query: %r' % query)
|
|
if error[0] == lasso.SERVER_ERROR_PROVIDER_NOT_FOUND or \
|
|
error[0] == lasso.PROFILE_ERROR_UNKNOWN_PROVIDER:
|
|
return template.error_page(_('Authentication request initiated by an unaffiliated provider.'))
|
|
elif error[0] == lasso.PROFILE_ERROR_INVALID_MSG or \
|
|
error[0] == lasso.PROFILE_ERROR_MISSING_REMOTE_PROVIDERID:
|
|
return template.error_page(_('Invalid request message'))
|
|
else:
|
|
return error_then_home(_('Internal Server Error'))
|
|
# Loggin informations on the request
|
|
issuer = login.remoteProviderId
|
|
authn_request = login.request
|
|
nameIDPolicy = login.request.nameIdPolicy
|
|
if nameIDPolicy:
|
|
if nameIDPolicy.allowCreate:
|
|
allow_create = 'allowing'
|
|
else:
|
|
allow_create = 'disallowing'
|
|
policy = 'with format "%(format)s", SP name qualifier "%(name_qualifier)s" and %(allow_create)s creation' % {
|
|
'format': nameIDPolicy.format or '',
|
|
'name_qualifier': nameIDPolicy.spNameQualifier or '',
|
|
'allow_create': allow_create }
|
|
else:
|
|
policy = 'with default name id policy'
|
|
assertion_consumer_flags = []
|
|
if authn_request.protocolBinding:
|
|
assertion_consumer_flags.append('with binding "%s"' % \
|
|
login.request.protocolBinding)
|
|
if authn_request.assertionConsumerServiceUrl:
|
|
assertion_consumer_flags.append('with URL "%s"' % \
|
|
authn_request.assertionConsumerServiceUrl)
|
|
if authn_request.assertionConsumerServiceIndex != -1:
|
|
assertion_consumer_flags.append('with idx "%s"' %
|
|
authn_request.assertionConsumerServiceIndex)
|
|
default = 'default '
|
|
if assertion_consumer_flags:
|
|
default = ''
|
|
assertion_consumer_flags = ','.join(assertion_consumer_flags)
|
|
request_flags = []
|
|
if login.request.forceAuthn:
|
|
request_flags.append('forcing authentication')
|
|
if login.request.isPassive:
|
|
request_flags.append('without interaction')
|
|
get_logger().info('Received AuthnRequest from %(remote_provider)s \
|
|
%(policy)s, %(request_flags)s targetting %(default)sassertion consumer service\
|
|
%(assertion_consumer_flags)s'
|
|
% { 'remote_provider' : issuer,
|
|
'policy': policy,
|
|
'default': default,
|
|
'assertion_consumer_flags': assertion_consumer_flags,
|
|
'protocol_binding': login.request.protocolBinding,
|
|
'request_flags': ', '.join(request_flags)})
|
|
|
|
session = get_session()
|
|
session.saml2 = True
|
|
request_id = login.request.iD
|
|
|
|
if session and session.lasso_session_dump:
|
|
login.setSessionFromDump(session.lasso_session_dump)
|
|
user_authenticated = False
|
|
if not request_denied:
|
|
if session and session.user:
|
|
user_authenticated = True
|
|
auth_ok = login_token.LoginToken.has_good_authentication(request_id)
|
|
if authn_request.forceAuthn and not auth_ok:
|
|
user_authenticated = False
|
|
if not user_authenticated and not authn_request.isPassive:
|
|
return self.invoke_login(login, query)
|
|
return self.sso_after_authentication(login, user_authenticated)
|
|
|
|
def logLoginValidateRequestError(self, login, error):
|
|
self.logProfileError(login, error, 'login.validateRequestMsg')
|
|
|
|
def logLoginProcessResponseMsgError(self, login, error):
|
|
self.logProfileError(login, error, 'login.processResponseMsg')
|
|
|
|
def logProfileError(self, login, error, what):
|
|
if isinstance(error, lasso.Error):
|
|
message = error[1]
|
|
else:
|
|
message = str(error)
|
|
get_logger().warn('%(what)s from %(provider)s failed: %(message)s' %
|
|
{ 'provider': login.remoteProviderId, 'message': message, \
|
|
'what': what })
|
|
|
|
def check_basic_authentication(self, request):
|
|
authorization=request.get_header('Authorization')
|
|
if authorization and authorization.startswith('Basic '):
|
|
account = identities.PasswordAccount()
|
|
account.login, account.password = \
|
|
base64.b64decode.authorization[6:].split(':', 1)
|
|
return identities.get_store().get_identity_for_account(account)
|
|
return None
|
|
|
|
@common.soap_endpoint
|
|
def singleSignOnSOAP(self):
|
|
server = get_lasso_server(protocol = 'saml2')
|
|
if not server:
|
|
return template.error_page(_('SAML 2.0 support not yet configured.'))
|
|
login = lasso.Login(server)
|
|
request = get_request()
|
|
soap_request_msg = self.get_soap_message()
|
|
try:
|
|
login.processAuthnRequestMsg(soap_request_msg)
|
|
except lasso.Error, error:
|
|
self.logProfileError(logint, error, 'singleSignOnSOAP login.processAuthnRequestMsg')
|
|
if error[0] == lasso.PROFILE_ERROR_MISSING_REMOTE_PROVIDERID:
|
|
return template.error_page(_('Request initiated by an unaffiliated provider.'))
|
|
if error[0] == lasso.DS_ERROR_INVALID_SIGNATURE:
|
|
return template.error_page(_('Failed to check authentication request signature.'))
|
|
if error[0] == lasso.DS_ERROR_SIGNATURE_NOT_FOUND:
|
|
return template.error_page(_('Authentication request is not signed.'))
|
|
return error_then_home(_('Internal Server Error'))
|
|
|
|
user_authenticated = False
|
|
user = self.check_basic_authentication(request)
|
|
if user:
|
|
user_authenticated = True
|
|
|
|
# Consent must be obtained from service provider because
|
|
# identity provider is unable to interact with user
|
|
consent_obtained = False
|
|
if user_authenticated:
|
|
if not login.mustAskForConsent():
|
|
consent_obtained = True
|
|
|
|
try:
|
|
login.validateRequestMsg(user_authenticated, consent_obtained)
|
|
except lasso.Error, error:
|
|
self.logLoginValidateRequestError(login, error)
|
|
return self.finishSingleSignOnSOAP(login)
|
|
else:
|
|
now = datetime.datetime.utcnow()
|
|
notBefore = now-datetime.timedelta(0,60)
|
|
notOnOrAfter = now+datetime.timedelta(0,60)
|
|
login.buildAssertion(get_saml2_authn_method(session),
|
|
now.isoformat()+'Z',
|
|
'unused', # reauthenticateOnOrAfter is only for ID-FF 1.2
|
|
notBefore.isoformat()+'Z',
|
|
notOnOrAfter.isoformat()+'Z')
|
|
login.assertion.authnStatement[0].sessionIndex = login.assertion.id
|
|
common.SessionIndex(login.assertion.id).store()
|
|
save_identity(login, user)
|
|
|
|
if login.protocolProfile == lasso.LOGIN_PROTOCOL_PROFILE_BRWS_LECP:
|
|
login.buildResponseMsg(None)
|
|
else:
|
|
return template.error_page(_('Unknown SAML2 protocol binding'))
|
|
|
|
response = get_response()
|
|
response.set_content_type('application/vnd.paos+xml', 'UTF-8')
|
|
response.set_header('PAOS', 'ver=urn:liberty:paos:2003-08;urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp')
|
|
return login.msgBody
|
|
|
|
@common.soap_endpoint
|
|
def finishSingleSignOnSOAP(self, login):
|
|
login.buildResponseMsg(None)
|
|
response = get_response()
|
|
response.set_content_type('application/vnd.paos+xml', 'UTF-8')
|
|
response.set_header('PAOS', 'ver=urn:liberty:paos:2003-08;urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp')
|
|
return login.msgBody
|
|
|
|
def check_access_authorizations(cls, login):
|
|
return True
|
|
check_access_authorizations = classmethod(check_access_authorizations)
|
|
|
|
def sso_after_authentication(self, login, user_authenticated, proxied = False):
|
|
# part 2 of SSO: user has been authenticated (or not)
|
|
session, identity = self.restore_user_details(login)
|
|
|
|
if user_authenticated:
|
|
if not self.check_access_authorizations(login):
|
|
# XXX: notify the user here and now?
|
|
get_logger().warn('user refused because of authorizations')
|
|
user_authenticated = False
|
|
|
|
consent_obtained = False
|
|
if user_authenticated:
|
|
if login.mustAskForConsent():
|
|
session.lasso_login_dump = login.dump()
|
|
return redirect(get_request().environ['SCRIPT_NAME'] + '/saml/consent')
|
|
return self.sso_after_consent(login, user_authenticated, True)
|
|
|
|
def restore_user_details(self, login):
|
|
authentic.identities.get_store().load_identities()
|
|
session = get_session()
|
|
try:
|
|
identity = authentic.identities.get_store().get_identity(session.user)
|
|
except KeyError:
|
|
identity = None
|
|
if identity and identity.lasso_dump:
|
|
login.setIdentityFromDump(identity.lasso_dump)
|
|
if session and session.lasso_session_dump:
|
|
login.setSessionFromDump(session.lasso_session_dump)
|
|
return (session, identity)
|
|
|
|
def disco_service_url(self):
|
|
request = get_request()
|
|
url = '%s://%s/disco/soapEndpoint' % (request.get_scheme(), request.get_server())
|
|
return url
|
|
|
|
def saml2_add_attribute_values(self, assertion, name, values):
|
|
if not assertion.attributeStatement:
|
|
assertion.attributeStatement = [ lasso.Saml2AttributeStatement() ]
|
|
attribute_statement = assertion.attributeStatement[0]
|
|
for attribute in attribute_statement.attribute:
|
|
if attribute.name == name and attribute.nameFormat == lasso.SAML2_ATTRIBUTE_NAME_FORMAT_BASIC:
|
|
break
|
|
else:
|
|
attribute = lasso.Saml2Attribute()
|
|
attribute.name = name
|
|
attribute.nameFormat = lasso.SAML2_ATTRIBUTE_NAME_FORMAT_BASIC
|
|
attribute_statement.attribute = list(attribute_statement.attribute) + [ attribute ]
|
|
attribute_value_list = list(attribute.attributeValue)
|
|
for value in values:
|
|
if value is True:
|
|
value = 'true'
|
|
elif value is False:
|
|
value = 'false'
|
|
else:
|
|
value = str(value)
|
|
if type(value) is unicode:
|
|
value = value.encode('utf-8')
|
|
else:
|
|
value = sitecharset2utf8(value)
|
|
text_node = lasso.MiscTextNode.newWithString(value)
|
|
text_node.textChild = True
|
|
attribute_value = lasso.Saml2AttributeValue()
|
|
attribute_value.any = [ text_node ]
|
|
attribute_value_list.append(attribute_value)
|
|
attribute.attributeValue = attribute_value_list
|
|
|
|
def build_assertion(self, login, session, identity):
|
|
now = datetime.datetime.utcnow()
|
|
notBefore = now-datetime.timedelta(0,60)
|
|
notOnOrAfter = now+datetime.timedelta(0,60)
|
|
login.buildAssertion(get_saml2_authn_method(session),
|
|
session.authentication_instant.isoformat()+'Z',
|
|
'unused', # reauthenticateOnOrAfter is only for ID-FF 1.2
|
|
notBefore.isoformat()+'Z',
|
|
notOnOrAfter.isoformat()+'Z')
|
|
if lasso.WSF_ENABLED:
|
|
idwsf2.addDiscoveryBootStrapToLogin(login,
|
|
self.disco_service_url(),
|
|
login.server.providerId, (lasso.SECURITY_MECH_BEARER,))
|
|
login.assertion.authnStatement[0].sessionIndex = login.assertion.id
|
|
common.SessionIndex(login.assertion.id).store()
|
|
|
|
name_id_format = None
|
|
if login.request.nameIDPolicy:
|
|
name_id_format = login.request.nameIDPolicy.format
|
|
|
|
if name_id_format == lasso.SAML2_NAME_IDENTIFIER_FORMAT_EMAIL:
|
|
if not identity.email:
|
|
return template.error_page(_('The service asked for NameIDFormat email, \
|
|
but the current does not have one.'))
|
|
login.response.assertion[0].subject.nameID.content = identity.email
|
|
elif name_id_format == lasso.SAML2_NAME_IDENTIFIER_FORMAT_UNSPECIFIED:
|
|
if not session.name_identifiers or not session.name_identifiers[-1]:
|
|
# Final IdP : must create the nameId if it doesn't exist
|
|
# XXX: This is mainly for testing. If it should be use in real world, the NameID
|
|
# generation should be configurable somehow
|
|
nameID = 'test'
|
|
else:
|
|
# Proxying IdP : a name identifier is already set in the session, copy it to the
|
|
# new assertion
|
|
nameID = session.name_identifiers[-1]
|
|
login.nameIdentifier.content = nameID
|
|
login.response.assertion[0].subject.nameID.content = nameID
|
|
provider_key = misc.get_provider_key(login.remoteProviderId)
|
|
self.add_attributes(provider_key, login.response.assertion[0],
|
|
identity, self.saml2_add_attribute_values)
|
|
|
|
def set_default_name_id(self, login):
|
|
'''If no NameID format is asked by the service provider or if it asked
|
|
for the format 'unspecified', use the default from the settings.
|
|
'''
|
|
try:
|
|
lp = get_cfg('providers')[misc.get_provider_key(
|
|
login.remoteProviderId)]
|
|
except KeyError:
|
|
raise errors.TraversalError()
|
|
if not login.request.nameIDPolicy:
|
|
login.request.nameIDPolicy = lasso.Samlp2NameIDPolicy()
|
|
if not login.request.nameIDPolicy.format or \
|
|
login.request.nameIDPolicy.format == \
|
|
lasso.SAML2_NAME_IDENTIFIER_FORMAT_UNSPECIFIED:
|
|
nid_format = lp.get('default_name_id_format', 'persistent')
|
|
if nid_format == 'persistent':
|
|
login.request.nameIDPolicy.format = lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT
|
|
elif nid_format == 'transient':
|
|
login.request.nameIDPolicy.format = lasso.SAML2_NAME_IDENTIFIER_FORMAT_TRANSIENT
|
|
elif nid_format == 'encrypted':
|
|
login.request.nameIDPolicy.format = lasso.SAML2_NAME_IDENTIFIER_FORMAT_ENCRYPTED
|
|
elif nid_format == 'email':
|
|
login.request.nameIDPolicy.format = lasso.SAML2_NAME_IDENTIFIER_FORMAT_EMAIL
|
|
elif nid_format == 'none':
|
|
login.request.nameIDPolicy.format = lasso.SAML2_NAME_IDENTIFIER_FORMAT_UNSPECIFIED
|
|
|
|
def sso_after_consent(self, login, user_authenticated, consent_obtained, intro_cookie = True):
|
|
# part 3 of SSO: if necessary, user has given its consent
|
|
session, identity = self.restore_user_details(login)
|
|
get_logger().debug('sso_after_consent user_authenticated:%s \
|
|
consent_obtained: %s intro_cookie: %s' % (user_authenticated, consent_obtained,
|
|
intro_cookie))
|
|
|
|
self.set_default_name_id(login)
|
|
|
|
try:
|
|
login.validateRequestMsg(user_authenticated, consent_obtained)
|
|
except lasso.Error, error:
|
|
self.logLoginValidateRequestError(login, error)
|
|
do_federation = False
|
|
else:
|
|
do_federation = True
|
|
self.build_assertion(login, session, identity)
|
|
|
|
if login.protocolProfile == lasso.LOGIN_PROTOCOL_PROFILE_BRWS_ART:
|
|
login.buildArtifactMsg(lasso.HTTP_METHOD_ARTIFACT_GET)
|
|
elif login.protocolProfile in (lasso.LOGIN_PROTOCOL_PROFILE_BRWS_POST,
|
|
lasso.LOGIN_PROTOCOL_PROFILE_REDIRECT):
|
|
login.buildAuthnResponseMsg()
|
|
elif login.protocolProfile == lasso.LOGIN_PROTOCOL_PROFILE_BRWS_LECP:
|
|
login.buildResponseMsg(None)
|
|
else:
|
|
return template.error_page(_('Unknown SAML2 protocol binding'))
|
|
|
|
if do_federation and identity:
|
|
if login.isIdentityDirty:
|
|
identity.lasso_dump = login.identity.dump()
|
|
authentic.identities.get_store().save(identity)
|
|
|
|
if login.nameIdentifier:
|
|
# not there if not authenticated or consent not granted
|
|
if not session.name_identifiers:
|
|
session.name_identifiers = []
|
|
if not login.nameIdentifier.content in session.name_identifiers:
|
|
session.name_identifiers.append(login.nameIdentifier.content)
|
|
|
|
if login.isSessionDirty:
|
|
save_session(login)
|
|
get_session_manager().maintain_session(session)
|
|
|
|
common_domain = get_cfg('idp').get('common_domain')
|
|
common_domain_setter_url = get_cfg('idp').get('common_domain_setter_url')
|
|
if common_domain_setter_url and common_domain and intro_cookie and False:
|
|
token = Token(expiration_delay = 600) # ten minutes
|
|
token.session_id = session.id
|
|
token.protocol = 'saml2'
|
|
token.provider_id = login.server.providerId
|
|
token.common_domain = common_domain
|
|
token.next_url = get_request().get_url(1) + '/intro'
|
|
token.store()
|
|
session.lasso_login_dump = login.dump()
|
|
return redirect(common_domain_setter_url + '?tok=%s' % token.id)
|
|
|
|
return self.sso_after_intro(login, session)
|
|
|
|
|
|
def sso_after_intro(self, login, session):
|
|
if login.protocolProfile == lasso.LOGIN_PROTOCOL_PROFILE_BRWS_ART:
|
|
artifact = Saml2Artifact(id = login.artifact)
|
|
artifact.artifact = login.artifact
|
|
artifact.message = login.artifactMessage
|
|
artifact.session_id = session.id
|
|
artifact.provider_id = login.remoteProviderId
|
|
artifact.store()
|
|
return redirect(login.msgUrl)
|
|
elif login.protocolProfile == lasso.LOGIN_PROTOCOL_PROFILE_BRWS_LECP:
|
|
response = get_response()
|
|
response.set_content_type('text/xml')
|
|
return login.msgBody
|
|
else:
|
|
if login.msgBody:
|
|
return self.postResponseTo(login.msgUrl, login.msgBody,
|
|
relay_state = login.msgRelayState,
|
|
title = N_('Authentication Response'))
|
|
else:
|
|
return redirect(login.msgUrl)
|
|
|
|
|
|
def intro(self):
|
|
session = get_session()
|
|
login = lasso.Login.newFromDump(
|
|
get_lasso_server(protocol = 'saml2'), session.lasso_login_dump)
|
|
return self.sso_after_intro(login, get_session())
|
|
|
|
|
|
def consent [html] (self):
|
|
form = Form(enctype="multipart/form-data", id = "login")
|
|
form.add_submit("consent", _("Consent to Federation"))
|
|
form.add_submit("refuse", _("Refuse Federation"))
|
|
if not form.is_submitted() or form.has_errors():
|
|
template.html_top()
|
|
"""<div id="login-top"><h1>%s</h1></div>
|
|
<div id="login-form">""" % _('Login')
|
|
# XXX: get site name from provider/metadata/organization to have
|
|
# an appropriate question.
|
|
"<p>%s</p>" % _('Do you consent to federate your account with the other site?')
|
|
form.render()
|
|
'</div>'
|
|
else:
|
|
if form.get_widget('consent').parse():
|
|
consent = True
|
|
else:
|
|
consent = False
|
|
session = get_session()
|
|
login = lasso.Login.newFromDump(
|
|
get_lasso_server(protocol = 'saml2'), session.lasso_login_dump)
|
|
session.lasso_login_dump = None
|
|
return self.sso_after_consent(login, True, consent)
|
|
|
|
|
|
@common.soap_endpoint
|
|
def artifact(self):
|
|
#try:
|
|
soap_message = self.get_soap_message()
|
|
#except:
|
|
# return
|
|
|
|
response = get_response()
|
|
response.set_content_type('text/xml')
|
|
|
|
authentic.identities.get_store().load_identities()
|
|
login = lasso.Login(get_lasso_server(protocol = 'saml2'))
|
|
try:
|
|
login.processRequestMsg(soap_message)
|
|
except Exception, e:
|
|
get_logger().error('artifact resolve error %s' % e)
|
|
raise # leverage common.soap_endpoint
|
|
try:
|
|
artifact = Saml2Artifact.get_by_artifact(login.artifact)
|
|
except KeyError:
|
|
login.buildResponseMsg(None)
|
|
return login.msgBody
|
|
login.artifactMessage = artifact.message
|
|
artifact.remove_self()
|
|
session = get_session_manager().get(artifact.session_id)
|
|
if session and session.lasso_session_dump:
|
|
login.setSessionFromDump(session.lasso_session_dump)
|
|
login.buildResponseMsg(None)
|
|
|
|
if session and login.isSessionDirty:
|
|
save_session(login)
|
|
get_session_manager().commit_changes(session)
|
|
|
|
return login.msgBody
|
|
|
|
def setup_logout_for_validation(self):
|
|
pass
|
|
|
|
def check_logout_request(self, logout_profile):
|
|
'''Make any needed check on the logout request,
|
|
must return True if the request if accepted, False otherwise
|
|
'''
|
|
return True
|
|
|
|
# SLO support
|
|
def singleLogout(self):
|
|
'Single Logout initiated by service provider with Redirect binding'
|
|
return self.single_logout_sp(get_request().get_query(),
|
|
lasso.HTTP_METHOD_REDIRECT)
|
|
|
|
@common.soap_endpoint
|
|
def singleLogoutSOAP(self):
|
|
'Single Logout initiated by service provider with SOAP binding'
|
|
try:
|
|
soap_message = self.get_soap_message()
|
|
except:
|
|
return
|
|
return self.single_logout_sp(soap_message, lasso.HTTP_METHOD_SOAP)
|
|
|
|
def singleLogoutPOST(self):
|
|
'Single Logout initiated by service provider with POST binding'
|
|
return self.single_logout_sp(get_field('SAMLRequest'),
|
|
lasso.HTTP_METHOD_POST, relay_state = get_field('RelayState'))
|
|
|
|
def singleLogoutFinish(self):
|
|
'Single Logout Finish synchronous sequence'
|
|
session = get_session()
|
|
logout = None
|
|
try:
|
|
logout = lasso.Logout.newFromDump(
|
|
get_lasso_server(protocol = 'saml2'),
|
|
session.lasso_logout_dump)
|
|
return self.slo_idp_finish(logout, session, method=lasso.HTTP_METHOD_REDIRECT)
|
|
except:
|
|
pass
|
|
# Catch All case
|
|
if get_session():
|
|
get_session_manager().expire_session()
|
|
return redirect(session.after_url or get_publisher().get_root_url())
|
|
|
|
def single_logout_sp(self, message, method, relay_state = None):
|
|
assert(method is not None)
|
|
logout = lasso.Logout(get_lasso_server(protocol = 'saml2'))
|
|
try:
|
|
logout.processRequestMsg(message)
|
|
except lasso.Error, error:
|
|
return self.slo_sp_finish(logout, method, error = error)
|
|
|
|
if relay_state:
|
|
logout.relayState = relay_state
|
|
|
|
get_logger().info('received SLO request from %s' % logout.remoteProviderId)
|
|
get_session().service = get_provider_key(logout.remoteProviderId)
|
|
# Look for a session index
|
|
try:
|
|
session_index = common.SessionIndex.get(logout.request.sessionIndex)
|
|
name_identifier = logout.nameIdentifier.content
|
|
identity = authentic.identities.get_store().get_identity_for_name_identifier(name_identifier)
|
|
session = get_session_manager().get(session_index.session_id)
|
|
if get_session() and get_session().id == session.id:
|
|
session = get_session()
|
|
load_session(logout, session)
|
|
except Exception, error:
|
|
# No session index: request is refused
|
|
return self.slo_sp_finish(logout, method, error = error)
|
|
|
|
# Tag current session as in_slo
|
|
session.in_slo_sp = True
|
|
# identity loading is useless since Lasso >= 2.2.90
|
|
load_identity(logout, identity = None, session = session)
|
|
|
|
self.setup_logout_for_validation()
|
|
|
|
try:
|
|
logout.validateRequest()
|
|
except lasso.Error, error:
|
|
return self.slo_sp_finish(logout, method, error = error)
|
|
|
|
if not self.check_logout_request(logout):
|
|
return self.slo_sp_finish(logout, method, error = error)
|
|
|
|
# logout user now!
|
|
session.user = None
|
|
if method == lasso.HTTP_METHOD_SOAP:
|
|
get_session_manager().expire_session()
|
|
session.remove_self()
|
|
|
|
# FIXME: support redirect to proxy in slo_idp
|
|
if session.proxied_idp and identity:
|
|
self.proxy_slo_soap(session, identity)
|
|
|
|
if method != lasso.HTTP_METHOD_SOAP:
|
|
save_session(logout)
|
|
# Eventually start a provider logout
|
|
return self.slo_idp(session, method = method, logout = logout)
|
|
|
|
def slo_sp_finish(self, logout, method, error = None):
|
|
assert(logout is not None)
|
|
if error:
|
|
self.logProfileError(logout, error, 'SLO Request')
|
|
else:
|
|
if method != lasso.HTTP_METHOD_SOAP:
|
|
get_session_manager().expire_session()
|
|
if not method:
|
|
method = logout.server.getFirstHttpMethod(
|
|
logout.server.getProvider(logout.remoteProviderId),
|
|
|
|
lasso.MD_PROTOCOL_TYPE_SINGLE_LOGOUT)
|
|
logout.http_request_method = method
|
|
# FIXME: use method if supported or find first supported HTTP
|
|
# method
|
|
|
|
# Handle partial logout
|
|
if getattr(get_session(), 'partial_logout', False):
|
|
# FIXME: set status code to PartialLogout
|
|
pass
|
|
try:
|
|
logout.buildResponseMsg()
|
|
except lasso.Error, error:
|
|
# Log and display an error
|
|
self.logProfileError(logout, error, 'slo_sp_finish')
|
|
if method == lasso.HTTP_METHOD_SOAP:
|
|
# FIXME: return SOAP Fault
|
|
pass
|
|
return error_then_home("%s: %s" % (_('Internal Error'),
|
|
_('Logout cannot be finished')))
|
|
# FIXME: if we support other binding than SOAP or Redirect, this must
|
|
# be extended
|
|
if method == lasso.HTTP_METHOD_SOAP:
|
|
response = get_response()
|
|
response.set_content_type('text/xml')
|
|
return logout.msgBody
|
|
else:
|
|
if method == lasso.HTTP_METHOD_POST:
|
|
return self.postResponseTo(logout.msgBody,
|
|
logout.msgUrl, relay_state = logout.relayState,
|
|
title = _('Logout Response'))
|
|
elif method == lasso.HTTP_METHOD_REDIRECT:
|
|
return redirect(logout.msgUrl)
|
|
|
|
def slo_idp(self, session = None, method = None, logout = None):
|
|
# Single Logout initiated by IdP
|
|
if not session:
|
|
session = get_session()
|
|
if not session:
|
|
raise errors.AccessError()
|
|
# shortcut
|
|
if not session.lasso_session_dump:
|
|
return self.slo_idp_finish(logout, session, method)
|
|
if not logout:
|
|
logout = lasso.Logout(get_lasso_server(protocol = 'saml2'))
|
|
load_session(logout, session)
|
|
# delog user
|
|
# Identity loading is useless since lasso >= 2.2.90
|
|
load_identity(logout, identity = None, session = session)
|
|
if logout.identity:
|
|
session.lasso_identity_dump = logout.identity.dump()
|
|
session.user = None
|
|
logout.resetProviderIdIndex()
|
|
if method == lasso.HTTP_METHOD_SOAP:
|
|
return self.slo_idp_soap(logout, session)
|
|
else:
|
|
return self.slo_idp_multi_binding(logout, session)
|
|
|
|
def slo_idp_multi_binding [html] (self, logout, session, url = None):
|
|
remote_provider_id = logout.getNextProviderId()
|
|
# Single Logout initiated by IdP; GET profile
|
|
if not remote_provider_id:
|
|
# Early exit
|
|
return self.slo_idp_finish(logout, session)
|
|
session.store()
|
|
url = get_publisher().get_root_url() + 'saml/singleLogoutFinish'
|
|
template.html_top(onload = "setTimeout(function () { window.location = '%s' }, 1000)" % url)
|
|
'<div id="logout-top"><h1>%s</h1></div>' % _('Logout')
|
|
"""<div id="logout-sps">
|
|
<p>%s</p>
|
|
<ul>""" % _('Logging out from service providers:')
|
|
|
|
while remote_provider_id is not None:
|
|
method = logout.server.getFirstHttpMethod(
|
|
logout.server.getProvider(remote_provider_id),
|
|
lasso.MD_PROTOCOL_TYPE_SINGLE_LOGOUT)
|
|
'<li>%s... ' % remote_provider_id
|
|
if method == lasso.HTTP_METHOD_REDIRECT:
|
|
try:
|
|
# without t=, the result would be printed in the HTML page
|
|
t = logout.initRequest(remote_provider_id, lasso.HTTP_METHOD_REDIRECT)
|
|
except lasso.Error, error:
|
|
# Cannot happen with lasso > 2.2.90
|
|
if error[0] == lasso.PROFILE_ERROR_IDENTITY_NOT_FOUND:
|
|
break # this is bad, abort.
|
|
# Cannot happen with lasso > 2.2.90
|
|
elif error[0] == lasso.PROFILE_ERROR_FEDERATION_NOT_FOUND:
|
|
message = _('missing federation')
|
|
elif error[0] == lasso.PROFILE_ERROR_UNSUPPORTED_PROFILE:
|
|
message = _('logout not supported')
|
|
else:
|
|
message = _('internal error')
|
|
get_logger().error('SLO/GET: logout.initRequest failed for %s: %s' % (remote_provider_id, error[1]))
|
|
'%s' % message
|
|
else:
|
|
try:
|
|
t = logout.buildRequestMsg()
|
|
except lasso.Error, error:
|
|
'%s' % _('internal error')
|
|
get_logger().error('SLO/GET: logout.initRequest failed for %s: %s' % (remote_provider_id, error[1]))
|
|
else:
|
|
# TODO: use organization name instead of provider_id
|
|
'''<iframe src="%(url)s" alt="" marginwidth="0" marginheight="0" scrolling="no" style="border: none" width="12" height="12">
|
|
<img src="%(url)s" width="12" height="12"></img>
|
|
</iframe>''' % { 'url': logout.msgUrl }
|
|
elif method == lasso.HTTP_METHOD_SOAP:
|
|
if self.slo_soap_one_provider(logout, remote_provider_id):
|
|
'<img src="%s" alt="OK"/>' % (get_publisher().get_root_url() + 'images/check_on.png')
|
|
else:
|
|
'<img src="%s" alt="KO"/>' % (get_publisher().get_root_url() + 'images/check_off.png')
|
|
else:
|
|
get_logger().error('SLO: unsupported logout profile for %s: %s' % (remote_provider_id, method))
|
|
'Cannot logout unsupported profile'
|
|
'</li>'
|
|
remote_provider_id = logout.getNextProviderId()
|
|
# FIXME: send proxy logout
|
|
'</ul></div>'
|
|
'<p><a href="%s">%s</a></p>' % (url, _('Finish logging out'))
|
|
session.lasso_logout_dump = logout.dump()
|
|
save_session(logout, session)
|
|
|
|
def slo_soap_one_provider(self, logout, remote_provider_id):
|
|
'''Could raise lasso.ProfileIdentityNotFoundError with lasso < 2.2.90'''
|
|
assert(remote_provider_id is not None)
|
|
try:
|
|
logout.initRequest(remote_provider_id, lasso.HTTP_METHOD_SOAP)
|
|
logout.buildRequestMsg()
|
|
soap_answer = self.soap_call(logout.msgUrl, logout.msgBody)
|
|
logout.processResponseMsg(soap_answer)
|
|
except soap.SOAPException, exception:
|
|
self.logProfileError(logout, exception, 'slo_idp_soap: HTTP transport failed')
|
|
except Exception, error:
|
|
self.logProfileError(logout, error, 'slo_idp_soap')
|
|
else:
|
|
return True
|
|
return False
|
|
|
|
def slo_idp_soap(self, logout, session):
|
|
'''SLO IDP Special case for SOAP only'''
|
|
remote_provider_id = logout.getNextProviderId()
|
|
if not remote_provider_id:
|
|
# FIXME: send proxy soap logout
|
|
return self.slo_idp_finish(logout, session, method = lasso.HTTP_METHOD_SOAP)
|
|
self.slo_soap_one_provider(logout, remote_provider_id)
|
|
return self.slo_idp_soap(logout, session)
|
|
|
|
def slo_idp_finish(self, logout, session, method = None):
|
|
'''End of the SLO IdP sequence'''
|
|
if getattr(session, 'in_slo_sp', False):
|
|
session.in_slo_sp = False
|
|
return self.slo_sp_finish(logout, method)
|
|
else:
|
|
get_session_manager().expire_session()
|
|
return misc.redirect_to_after_url() or misc.redirect_home()
|
|
|
|
def singleLogoutReturn(self):
|
|
session = get_session()
|
|
ok = True
|
|
# return must not need a logged user
|
|
if not session:
|
|
raise errors.AccessError()
|
|
logout = lasso.Logout(get_lasso_server(protocol = 'saml2'))
|
|
load_session(logout, session)
|
|
if getattr(session, "lasso_identity_dump", None):
|
|
# FIXME: in later version of lasso this should be useless
|
|
logout.setIdentityFromDump(session.lasso_identity_dump)
|
|
else:
|
|
load_identity(logout, identity = None, session = session)
|
|
request = get_request()
|
|
try:
|
|
logout.processResponseMsg(request.get_query())
|
|
except lasso.Error, error:
|
|
self.logProfileError(logout, error, 'singleLogoutReturn')
|
|
ok = False
|
|
|
|
if ok:
|
|
get_request().session = None
|
|
return self.check_on()
|
|
else:
|
|
session.partial_logout = True
|
|
session.store()
|
|
return self.check_off()
|
|
|
|
# Defederation
|
|
|
|
def manageNameId(self):
|
|
request = get_request()
|
|
if not lasso.isSamlQuery(request.get_query()):
|
|
return redirect('.')
|
|
|
|
manage = lasso.NameIdManagement(get_lasso_server(protocol = 'saml2'))
|
|
manage.processRequestMsg(request.get_query())
|
|
|
|
session = get_session()
|
|
authentic.identities.get_store().load_identities()
|
|
try:
|
|
identity = authentic.identities.get_store().get_identity(session.user)
|
|
except KeyError:
|
|
identity = None
|
|
|
|
self.manage_name_id(manage, identity)
|
|
return redirect(manage.msgUrl)
|
|
|
|
def manageNameIdReturn(self):
|
|
# TODO: Implement this function using Lasso
|
|
return redirect(get_publisher().get_root_url())
|
|
|
|
@common.soap_endpoint
|
|
def manageNameIdSOAP(self):
|
|
soap_message = self.get_soap_message()
|
|
|
|
response = get_response()
|
|
response.set_content_type('text/xml')
|
|
|
|
request_type = lasso.getRequestTypeFromSoapMsg(soap_message)
|
|
|
|
if request_type != lasso.REQUEST_TYPE_NAME_ID_MANAGEMENT:
|
|
get_logger().warn('SOAP message on name id management url of not appropriate type')
|
|
return
|
|
|
|
manage = lasso.NameIdManagement(get_lasso_server(protocol = 'saml2'))
|
|
manage.processRequestMsg(soap_message)
|
|
|
|
name_identifier = manage.nameIdentifier.content
|
|
identity = authentic.identities.get_store().get_identity_for_name_identifier(name_identifier)
|
|
self.manage_name_id(manage, identity)
|
|
return manage.msgBody
|
|
|
|
|
|
def manage_name_id(self, manage, identity):
|
|
load_identity(manage, identity = identity)
|
|
|
|
try:
|
|
manage.validateRequest()
|
|
except lasso.Error, error:
|
|
self.logProfileError(manage, error, 'manage.validateRequest')
|
|
else:
|
|
save_identity(manage, identity = identity)
|
|
manage.buildResponseMsg()
|
|
|
|
def metadata(self):
|
|
get_publisher().reload_cfg()
|
|
response = get_response()
|
|
response.set_content_type('text/xml', 'utf-8')
|
|
metadata = unicode(open(authentic.misc.get_abs_path(
|
|
get_cfg('idp')['saml2_metadata'])).read(), 'utf-8')
|
|
return metadata
|
|
|
|
def public_key(self):
|
|
get_publisher().reload_cfg()
|
|
response = get_response()
|
|
response.set_content_type('application/octet-stream')
|
|
publickey = file(authentic.misc.get_abs_path(get_cfg('idp')['publickey'])).read()
|
|
return publickey
|
|
|
|
def proxy_login_fill_authn_request(self, login, nameIdPolicy = None):
|
|
session = get_session()
|
|
login.request.nameIDPolicy.format = lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT
|
|
login.request.nameIDPolicy.allowCreate = True
|
|
login.request.forceAuthn = False
|
|
login.request.isPassive = False
|
|
login.request.consent = 'urn:oasis:names:tc:SAML:2.0:consent:current-implicit'
|
|
if session.lasso_login_dump:
|
|
login_proxy = lasso.Login.newFromDump(login.server, session.lasso_login_dump)
|
|
if (hasattr(lasso, 'SAML2_NAME_IDENTIFIER_FORMAT_EMAIL') and \
|
|
login_proxy.request.nameIDPolicy.format == lasso.SAML2_NAME_IDENTIFIER_FORMAT_EMAIL) or \
|
|
(hasattr(lasso, 'SAML2_NAME_IDENTIFIER_FORMAT_UNSPECIFIED') and \
|
|
login_proxy.request.nameIDPolicy.format == lasso.SAML2_NAME_IDENTIFIER_FORMAT_UNSPECIFIED):
|
|
login.request.nameIDPolicy.format = login_proxy.request.nameIDPolicy.format
|
|
login.request.protocolBinding = login_proxy.request.protocolBinding
|
|
# XXX: replicate other parameters from the request we proxy
|
|
if nameIdPolicy: # forced value
|
|
login.request.nameIDPolicy.format = lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT
|
|
|
|
def perform_proxy_login(self, idp = None, nameIdPolicy = None, extensions = None, relay_state = None):
|
|
server = get_lasso_server(lasso.PROVIDER_ROLE_SP, protocol = 'saml2')
|
|
login = lasso.Login(server)
|
|
login.initAuthnRequest(idp, lasso.HTTP_METHOD_REDIRECT)
|
|
self.proxy_login_fill_authn_request(login, nameIdPolicy)
|
|
|
|
if relay_state is not None:
|
|
get_session().remember(login.request.requestId, relay_state)
|
|
|
|
try:
|
|
try:
|
|
login.buildAuthnRequestMsg()
|
|
except lasso.Error, error:
|
|
self.logProfileError(login, error, 'perform_proxy_login HTTP-Redirect login.buildAuthnRequestMsg')
|
|
login.initAuthnRequest(idp, lasso.HTTP_METHOD_POST)
|
|
self.proxy_login_fill_authn_request(login, nameIdPolicy)
|
|
login.buildAuthnRequestMsg()
|
|
except lasso.Error, error:
|
|
self.logProfileError(login, error, 'perform_proxy_login HTTP-POST login.buildAuthnRequestMsg')
|
|
return error_then_home("%s: %s" % (_('Internal Error'), _('Proxy Login failed')))
|
|
|
|
if login.msgBody:
|
|
return self.postRequestTo(login.msgUrl, login.msgBody,
|
|
relay_state = login.relayState, title = N_('Authentication Request'))
|
|
else:
|
|
return redirect(login.msgUrl)
|
|
|
|
def proxy_auth_ok(self, login):
|
|
pass # to override
|
|
|
|
def proxy_auth_peer_cancelled(self, login):
|
|
pass
|
|
|
|
def proxy_auth_federation_not_found(self, login):
|
|
pass
|
|
|
|
def proxyAssertionConsumerArtifact(self):
|
|
server = get_lasso_server(lasso.PROVIDER_ROLE_SP, protocol = 'saml2')
|
|
if not server:
|
|
return template.error_page(_('SAML 2.0 support not yet configured.'))
|
|
login = lasso.Login(server)
|
|
request = get_request()
|
|
session = get_session()
|
|
|
|
if request.get_method() == 'GET':
|
|
artifact = request.get_query()
|
|
response_method = lasso.HTTP_METHOD_ARTIFACT_GET
|
|
else:
|
|
artifact = request.form.get('SAMLart')
|
|
response_method = lasso.HTTP_METHOD_ARTIFACT_POST
|
|
|
|
try:
|
|
login.initRequest(artifact, response_method)
|
|
except lasso.Error, error:
|
|
if error[0] == lasso.PROFILE_ERROR_MISSING_ARTIFACT:
|
|
get_logger().error(_('during proxy single sign on received a query without an artifact: %s') % request.get_query())
|
|
return template.error_page(_('Missing SAML Artifact'))
|
|
else:
|
|
self.logProfileError(login, error, 'proxySingleSignOnArtifact login.initRequest')
|
|
return error_then_home(_('Internal Server Error'))
|
|
|
|
login.buildRequestMsg()
|
|
|
|
try:
|
|
soap_answer = self.soap_call(login.msgUrl, login.msgBody)
|
|
except SOAPException:
|
|
return template.error_page(_('Failure to communicate with identity provider'))
|
|
|
|
try:
|
|
login.processResponseMsg(soap_answer)
|
|
except lasso.Error, error:
|
|
self.logLoginProcessResponseMsgError(login, error)
|
|
if error[0] == lasso.LOGIN_ERROR_FEDERATION_NOT_FOUND:
|
|
t = self.proxy_auth_federation_not_found(login)
|
|
if t:
|
|
return t
|
|
return template.error_page('there was no federation')
|
|
t = self.proxy_auth_peer_cancelled(login)
|
|
if t:
|
|
return t
|
|
login = lasso.Login.newFromDump(get_lasso_server(protocol = 'saml2'),
|
|
session.lasso_login_dump)
|
|
session.lasso_login_dump = None
|
|
return self.sso_after_authentication(login, False, proxied = True)
|
|
else:
|
|
self.proxy_auth_ok(login)
|
|
|
|
return self.proxy_sso_ok(login)
|
|
|
|
def proxyAssertionConsumerPost(self):
|
|
server = get_lasso_server(lasso.PROVIDER_ROLE_SP, protocol = 'saml2')
|
|
if not server:
|
|
return template.error_page(_('SAML 2.0 support not yet configured.'))
|
|
login = lasso.Login(server)
|
|
request = get_request()
|
|
session = get_session()
|
|
|
|
try:
|
|
login.processAuthnResponseMsg(get_field('SAMLResponse'))
|
|
except lasso.Error, error:
|
|
if error[0] == lasso.LOGIN_ERROR_FEDERATION_NOT_FOUND:
|
|
t = self.proxy_auth_federation_not_found(login)
|
|
if t:
|
|
return t
|
|
return template.error_page('there was no federation')
|
|
t = self.proxy_auth_peer_cancelled(login)
|
|
if t:
|
|
return t
|
|
login = lasso.Login.newFromDump(get_lasso_server(protocol = 'saml2'),
|
|
session.lasso_login_dump)
|
|
session.lasso_login_dump = None
|
|
return self.sso_after_authentication(login, False, proxied = True)
|
|
else:
|
|
self.proxy_auth_ok(login)
|
|
|
|
return self.proxy_sso_ok(login)
|
|
|
|
def proxyAssertionConsumerRedirect(self):
|
|
server = get_lasso_server(lasso.PROVIDER_ROLE_SP, protocol = 'saml2')
|
|
if not server:
|
|
return template.error_page(_('SAML 2.0 support not yet configured.'))
|
|
login = lasso.Login(server)
|
|
request = get_request()
|
|
session = get_session()
|
|
|
|
try:
|
|
login.processAuthnResponseMsg(request.get_query())
|
|
except lasso.Error, error:
|
|
if error[0] == lasso.LOGIN_ERROR_FEDERATION_NOT_FOUND:
|
|
t = self.proxy_auth_federation_not_found(login)
|
|
if t:
|
|
return t
|
|
return template.error_page('there was no federation')
|
|
t = self.proxy_auth_peer_cancelled(login)
|
|
if t:
|
|
return t
|
|
login = lasso.Login.newFromDump(get_lasso_server(protocol = 'saml2'),
|
|
session.lasso_login_dump)
|
|
session.lasso_login_dump = None
|
|
return self.sso_after_authentication(login, False, proxied = True)
|
|
else:
|
|
self.proxy_auth_ok(login)
|
|
|
|
return self.proxy_sso_ok(login)
|
|
|
|
def proxy_sso_ok(self, login):
|
|
session = get_session()
|
|
if session.lasso_proxy_session_dump:
|
|
login.setSessionFromDump(session.lasso_proxy_session_dump)
|
|
try:
|
|
common.SessionIndex(login.response.assertion[0].authnStatement[0].sessionIndex).store()
|
|
except:
|
|
pass
|
|
ni = login.nameIdentifier.content
|
|
authentic.identities.get_store().load_identities()
|
|
identity = authentic.identities.get_store().get_identity_for_name_identifier(ni)
|
|
|
|
if identity is None and get_session().user:
|
|
identity = authentic.identities.get_store().get_identity(get_session().user)
|
|
|
|
if identity:
|
|
if identity.lasso_proxy_dump:
|
|
login.setIdentityFromDump(identity.lasso_proxy_dump)
|
|
else:
|
|
# XXX: only create identity when "federated" ?
|
|
identity = authentic.identities.Identity()
|
|
identity_keys = authentic.identities.get_store().keys()
|
|
base_key = get_provider_key(login.remoteProviderId)
|
|
i = 1
|
|
while ('%s-%d' % (base_key, i)) in identity_keys:
|
|
i += 1
|
|
identity.id = '%s-%d' % (base_key, i)
|
|
identity.name = ''
|
|
identity.email = ''
|
|
identity.proxied_identity_origin = login.remoteProviderId
|
|
authentic.identities.get_store().add(identity)
|
|
session.set_user(identity.id)
|
|
session.authentication_instant = datetime.datetime.utcnow()
|
|
session.authentication_method = 'saml'
|
|
session.proxied_idp = login.remoteProviderId
|
|
if not session.name_identifiers:
|
|
session.name_identifiers = []
|
|
session.name_identifiers.append(login.nameIdentifier.content)
|
|
|
|
login.acceptSso()
|
|
if login.isIdentityDirty:
|
|
identity.lasso_proxy_dump = login.identity.dump()
|
|
if login.isSessionDirty:
|
|
session.lasso_proxy_session_dump = login.session.dump()
|
|
|
|
authentic.identities.get_store().save(identity)
|
|
|
|
if session.lasso_login_dump:
|
|
login = lasso.Login.newFromDump(get_lasso_server(protocol = 'saml2'),
|
|
session.lasso_login_dump)
|
|
session.lasso_login_dump = None
|
|
return self.sso_after_authentication(login, True, proxied = True)
|
|
|
|
if session.after_url:
|
|
after_url = session.after_url
|
|
session.after_url = None
|
|
return redirect(after_url)
|
|
return redirect(get_request().environ['SCRIPT_NAME'] + '/')
|
|
|
|
|
|
def proxy_slo_soap(self, session, identity):
|
|
# slo request from SP, continue with SOAP logout request to proxied-IdP
|
|
server = get_lasso_server(lasso.PROVIDER_ROLE_SP, protocol = 'saml2')
|
|
logout = lasso.Logout(server)
|
|
load_session(logout, session)
|
|
if identity and identity.lasso_proxy_dump:
|
|
logout.setIdentityFromDump(identity.lasso_proxy_dump)
|
|
logout.initRequest(session.proxied_idp, lasso.HTTP_METHOD_SOAP)
|
|
logout.buildRequestMsg()
|
|
try:
|
|
soap_answer = self.soap_call(logout.msgUrl, logout.msgBody)
|
|
except soap.SOAPException:
|
|
pass
|
|
else:
|
|
try:
|
|
logout.processResponseMsg(soap_answer)
|
|
except lasso.Error, error:
|
|
pass
|
|
else:
|
|
save_session(logout, session)
|
|
|
|
# Keep compatibility with old metadatas files
|
|
proxySingleSignOnArtifact = proxyAssertionConsumerArtifact
|
|
proxySingleSignOnPost = proxyAssertionConsumerPost
|
|
proxySingleSignOnRedirect = proxyAssertionConsumerRedirect
|
|
|
|
class SpUI(AccessControlled, common.LassoDirectory):
|
|
_q_exports = ['login', 'terminate']
|
|
|
|
def _q_access(self):
|
|
session = get_session()
|
|
if not session or session.user is None:
|
|
raise errors.AccessUnauthorizedError()
|
|
|
|
def __init__(self, component):
|
|
self.provider_key = component
|
|
try:
|
|
self.lp = get_cfg('providers')[component]
|
|
except KeyError:
|
|
raise errors.TraversalError()
|
|
|
|
def init_provider(self, role = lasso.PROVIDER_ROLE_SP, encryption_mode = None):
|
|
lp = self.lp
|
|
if lp['role'] != role and lp['role'] != lasso.PROVIDER_ROLE_NONE:
|
|
raise errors.TraversalError()
|
|
self.p = lasso.Provider(role,
|
|
authentic.misc.get_abs_path(lp['metadata']),
|
|
authentic.misc.get_abs_path(lp.get('publickey')), None)
|
|
if self.p is not None:
|
|
if encryption_mode is None:
|
|
# take config from settings
|
|
encryption_mode = lasso.ENCRYPTION_MODE_NONE
|
|
if get_cfg('providers')[self.provider_key].get('encrypt_nameid'):
|
|
encryption_mode |= lasso.ENCRYPTION_MODE_NAMEID
|
|
if get_cfg('providers')[self.provider_key].get('encrypt_assertion'):
|
|
encryption_mode |= lasso.ENCRYPTION_MODE_ASSERTION
|
|
|
|
if encryption_mode:
|
|
self.p.setEncryptionMode(encryption_mode);
|
|
|
|
|
|
def login(self, encryption_mode = None, method = None, nid_format = 'none',
|
|
relay_state = None):
|
|
self.init_provider(encryption_mode = encryption_mode)
|
|
login = lasso.Login(get_lasso_server(protocol = 'saml2'))
|
|
login.initIdpInitiatedAuthnRequest(self.p.providerId)
|
|
|
|
if method:
|
|
login.request.protocolBinding = method
|
|
|
|
if nid_format == 'persistent':
|
|
login.request.nameIDPolicy.format = lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT
|
|
elif nid_format == 'transient':
|
|
login.request.nameIDPolicy.format = lasso.SAML2_NAME_IDENTIFIER_FORMAT_TRANSIENT
|
|
elif nid_format == 'encrypted':
|
|
login.request.nameIDPolicy.format = lasso.SAML2_NAME_IDENTIFIER_FORMAT_ENCRYPTED
|
|
elif nid_format == 'none':
|
|
login.request.nameIDPolicy.format = lasso.SAML2_NAME_IDENTIFIER_FORMAT_UNSPECIFIED
|
|
|
|
if relay_state:
|
|
login.request.relayState = relay_state
|
|
|
|
login.request.nameIDPolicy.allowCreate = True
|
|
|
|
get_logger().info('SSO to %s' % self.p.providerId)
|
|
try:
|
|
login.processAuthnRequestMsg(None)
|
|
except lasso.Error, error:
|
|
if error[0] == lasso.LOGIN_ERROR_NO_DEFAULT_ENDPOINT:
|
|
return template.error_page(
|
|
_('No default endpoint for this service provider'))
|
|
self.logProfileError(login, error, "login.processAuthnRequestMsg")
|
|
return template.error_page(
|
|
_('No default endpoint for this service provider'))
|
|
|
|
session = get_session()
|
|
session.saml2 = True
|
|
|
|
rt = RootDirectory()
|
|
return rt.sso_after_consent(login, True, True, intro_cookie = False)
|
|
|
|
|
|
def terminate(self, method = lasso.HTTP_METHOD_SOAP):
|
|
self.init_provider()
|
|
manage = lasso.NameIdManagement(get_lasso_server(protocol = 'saml2'))
|
|
session = get_session()
|
|
# No more needed for Lasso > 2.2.90
|
|
load_session(manage, session)
|
|
load_identity(manage, identity = None, session = session)
|
|
|
|
get_logger().info('fedterm to %s' % self.p.providerId)
|
|
try:
|
|
manage.initRequest(self.p.providerId, None, method)
|
|
except lasso.Error, error:
|
|
if error[0] == lasso.PROFILE_ERROR_FEDERATION_NOT_FOUND:
|
|
raise errors.TraversalError() # no such federation
|
|
if error[0] == lasso.PROFILE_ERROR_IDENTITY_NOT_FOUND:
|
|
raise errors.TraversalError() # no such federation
|
|
elif error[0] == lasso.PROFILE_ERROR_UNSUPPORTED_PROFILE:
|
|
if method != lasso.HTTP_METHOD_REDIRECT:
|
|
self.terminate(lasso.HTTP_METHOD_REDIRECT)
|
|
else:
|
|
get_logger().error('FTN initRequest failed for %s: no supported profile' % self.p.providerId)
|
|
return error_then_home(_("This service doesn't support federation termination"))
|
|
get_logger().error('FTN initRequest failed for %s: %s' % (self.p.providerId, error[1]))
|
|
return error_then_home(_('Federation termination failed for %s') % self.p.providerId)
|
|
|
|
try:
|
|
manage.buildRequestMsg()
|
|
except lasso.Error, error:
|
|
get_logger().error('FTN failed for %s: %s' % (self.p.providerId, error[1]))
|
|
return error_then_home(_('Federation termination failed for %s') % self.p.providerId)
|
|
|
|
name_identifier = manage.nameIdentifier.content
|
|
save_identity(manage)
|
|
if name_identifier in (session.name_identifiers or []):
|
|
session.name_identifiers.remove(name_identifier)
|
|
|
|
if not manage.msgUrl:
|
|
return error_then_home(_("Service provider doesn't support federation termination profile."))
|
|
|
|
if manage.msgBody:
|
|
try:
|
|
response_msg = self.soap_call(manage.msgUrl, manage.msgBody)
|
|
except soap.SOAPException:
|
|
return error_then_home(_('Service provider failed to process request.'))
|
|
else:
|
|
return redirect(manage.msgUrl)
|
|
try:
|
|
manage.processResponseMsg(response_msg)
|
|
except lasso.Error, error:
|
|
return template.error_page(_('Service provider response could not be processed.'))
|
|
|
|
save_identity(manage)
|
|
return misc.redirect_to_return_url() or misc.redirect_home()
|