1 # Copyright (C) 2014 Ipsilon project Contributors, for license see COPYING
3 from ipsilon.providers.common import ProviderPageBase, ProviderException
4 from ipsilon.providers.common import AuthenticationError, InvalidRequest
5 from ipsilon.providers.saml2.provider import ServiceProvider
6 from ipsilon.providers.saml2.provider import InvalidProviderId
7 from ipsilon.providers.saml2.provider import NameIdNotAllowed
8 from ipsilon.tools import saml2metadata as metadata
9 from ipsilon.util.policy import Policy
10 from ipsilon.util.user import UserSession
11 from ipsilon.util.trans import Transaction
19 class UnknownProvider(ProviderException):
21 def __init__(self, message):
22 super(UnknownProvider, self).__init__(message)
26 class AuthenticateRequest(ProviderPageBase):
28 def __init__(self, site, provider, *args, **kwargs):
29 super(AuthenticateRequest, self).__init__(site, provider)
34 def _preop(self, *args, **kwargs):
36 # generate a new id or get current one
37 self.trans = Transaction('saml2', **kwargs)
39 self.debug('self.binding=%s, transdata=%s' %
40 (self.binding, self.trans.retrieve()))
41 if self.binding is None:
42 # SAML binding is unknown, try to get it from transaction
43 transdata = self.trans.retrieve()
44 self.binding = transdata.get('saml2_binding')
46 # SAML binding known, store in transaction
47 data = {'saml2_binding': self.binding}
48 self.trans.store(data)
50 # Only check for cookie for those bindings which use one
51 if self.binding not in (metadata.SAML2_SERVICE_MAP['sso-soap'][1]):
52 if self.trans.cookie.value != self.trans.provider:
53 self.debug('Invalid transaction, %s != %s' % (
54 self.trans.cookie.value, self.trans.provider))
55 except Exception, e: # pylint: disable=broad-except
56 self.debug('Transaction initialization failed: %s' % repr(e))
57 raise cherrypy.HTTPError(400, 'Invalid transaction id')
59 def pre_GET(self, *args, **kwargs):
60 self._preop(*args, **kwargs)
62 def pre_POST(self, *args, **kwargs):
63 self._preop(*args, **kwargs)
65 def auth(self, login):
67 self.saml2checks(login)
68 except AuthenticationError, e:
69 self.saml2error(login, e.code, e.message)
70 return self.reply(login)
72 def _parse_request(self, message):
74 login = self.cfg.idp.get_login_handler()
77 login.processAuthnRequestMsg(message)
78 except (lasso.ProfileInvalidMsgError,
79 lasso.ProfileMissingIssuerError), e:
81 msg = 'Malformed Request %r [%r]' % (e, message)
82 raise InvalidRequest(msg)
84 except (lasso.ProfileInvalidProtocolprofileError,
87 msg = 'Invalid SAML Request: %r (%r [%r])' % (login.request,
89 raise InvalidRequest(msg)
91 except (lasso.ServerProviderNotFoundError,
92 lasso.ProfileUnknownProviderError), e:
94 msg = 'Invalid SP [%s] (%r [%r])' % (login.remoteProviderId,
96 raise UnknownProvider(msg)
98 self.debug('SP %s requested authentication' % login.remoteProviderId)
102 def _idp_initiated_login(self, spidentifier, relaystate):
104 Perform an Idp-initiated login
106 Exceptions are handled by the caller
108 login = self.cfg.idp.get_login_handler()
110 login.initIdpInitiatedAuthnRequest(spidentifier)
112 # Hardcode for now, handle Artifact later
113 login.request.protocolBinding = lasso.SAML2_METADATA_BINDING_POST
115 login.processAuthnRequestMsg()
117 if relaystate is not None:
118 login.msgRelayState = relaystate
120 provider = ServiceProvider(self.cfg, login.remoteProviderId)
121 if provider.splink is not None:
122 login.msgRelayState = provider.splink
124 login.msgRelayState = login.remoteProviderId
128 def saml2login(self, request, spidentifier=None, relaystate=None):
130 request: the SAML request
131 spidentifier: the provider ID for IdP-initiated login
132 relaystate: optional string to direct user to particular place on
133 the SP after sending POST. If one is not provided then
134 the protected site from the SP is used, otherwise it
135 is set to the remote provider ID.
137 if not request and not spidentifier:
138 raise cherrypy.HTTPError(400,
139 'SAML request token missing or empty')
143 login = self._idp_initiated_login(spidentifier, relaystate)
144 except lasso.ServerProviderNotFoundError:
145 raise cherrypy.HTTPError(400, 'Unknown Service Provider')
146 except Exception, e: # pylint: disable=broad-except
148 raise cherrypy.HTTPError(500)
151 login = self._parse_request(request)
152 except InvalidRequest, e:
154 raise cherrypy.HTTPError(400, 'Invalid SAML request token')
155 except UnknownProvider, e:
157 raise cherrypy.HTTPError(400, 'Unknown Service Provider')
158 except Exception, e: # pylint: disable=broad-except
160 raise cherrypy.HTTPError(500)
164 def saml2checks(self, login):
168 if user.is_anonymous:
169 if self.stage == 'init':
170 returl = '%s/saml2/SSO/Continue?%s' % (
171 self.basepath, self.trans.get_GET_arg())
172 data = {'saml2_stage': 'auth',
173 'saml2_request': login.dump(),
174 'login_return': returl,
175 'login_target': login.remoteProviderId}
176 self.trans.store(data)
177 redirect = '%s/login?%s' % (self.basepath,
178 self.trans.get_GET_arg())
179 raise cherrypy.HTTPRedirect(redirect)
181 raise AuthenticationError(
182 "Unknown user", lasso.SAML2_STATUS_CODE_AUTHN_FAILED)
184 self._audit("Logged in user: %s [%s]" % (user.name, user.fullname))
186 # We can wipe the transaction now, as this is the last step
189 # TODO: check if this is the first time this user access this SP
190 # If required by user prefs, ask user for consent once and then
194 # TODO: check destination
197 provider = ServiceProvider(self.cfg, login.remoteProviderId)
198 nameidfmt = provider.get_valid_nameid(login.request.nameIdPolicy)
199 except NameIdNotAllowed, e:
200 raise AuthenticationError(
201 str(e), lasso.SAML2_STATUS_CODE_INVALID_NAME_ID_POLICY)
202 except InvalidProviderId, e:
203 raise AuthenticationError(
204 str(e), lasso.SAML2_STATUS_CODE_AUTHN_FAILED)
206 # TODO: check login.request.forceAuthn
208 login.validateRequestMsg(not user.is_anonymous, consent)
210 authtime = datetime.datetime.utcnow()
211 skew = datetime.timedelta(0, 60)
212 authtime_notbefore = authtime - skew
213 authtime_notafter = authtime + skew
215 # TODO: get authentication type fnd name format from session
216 # need to save which login manager authenticated and map it to a
217 # saml2 authentication context
218 authn_context = lasso.SAML2_AUTHN_CONTEXT_UNSPECIFIED
220 timeformat = '%Y-%m-%dT%H:%M:%SZ'
221 login.buildAssertion(authn_context,
222 authtime.strftime(timeformat),
224 authtime_notbefore.strftime(timeformat),
225 authtime_notafter.strftime(timeformat))
228 if nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT:
229 idpsalt = self.cfg.idp_nameid_salt
231 raise AuthenticationError(
232 "idp nameid salt is not set in configuration"
234 value = hashlib.sha512()
235 value.update(idpsalt)
236 value.update(login.remoteProviderId)
237 value.update(user.name)
238 nameid = '_' + value.hexdigest()
239 elif nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_TRANSIENT:
240 nameid = '_' + uuid.uuid4().hex
241 elif nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_KERBEROS:
242 userattrs = us.get_user_attrs()
243 nameid = userattrs.get('gssapi_principal_name')
244 elif nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_EMAIL:
245 nameid = us.get_user().email
247 nameid = '%s@%s' % (user.name, self.cfg.default_email_domain)
248 elif nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_UNSPECIFIED:
249 nameid = provider.normalize_username(user.name)
252 login.assertion.subject.nameId.format = nameidfmt
253 login.assertion.subject.nameId.content = nameid
256 self.error('Authentication succeeded but it was not ' +
257 'provided by NameID %s' % nameidfmt)
258 raise AuthenticationError("Unavailable Name ID type",
259 lasso.SAML2_STATUS_CODE_AUTHN_FAILED)
261 # Check attribute policy and perform mapping and filtering.
262 # If the SP has its own mapping or filtering policy use that
263 # instead of the global policy.
264 if (provider.attribute_mappings is not None and
265 len(provider.attribute_mappings) > 0):
266 attribute_mappings = provider.attribute_mappings
268 attribute_mappings = self.cfg.default_attribute_mapping
269 if (provider.allowed_attributes is not None and
270 len(provider.allowed_attributes) > 0):
271 allowed_attributes = provider.allowed_attributes
273 allowed_attributes = self.cfg.default_allowed_attributes
274 self.debug("Allowed attrs: %s" % allowed_attributes)
275 self.debug("Mapping: %s" % attribute_mappings)
276 policy = Policy(attribute_mappings, allowed_attributes)
277 userattrs = us.get_user_attrs()
278 mappedattrs, _ = policy.map_attributes(userattrs)
279 attributes = policy.filter_attributes(mappedattrs)
281 if '_groups' in attributes and 'groups' not in attributes:
282 attributes['groups'] = attributes['_groups']
284 self.debug("%s's attributes: %s" % (user.name, attributes))
286 # The saml-core-2.0-os specification section 2.7.3 requires
287 # the AttributeStatement element to be non-empty.
289 if not login.assertion.attributeStatement:
290 attrstat = lasso.Saml2AttributeStatement()
291 login.assertion.attributeStatement = [attrstat]
293 attrstat = login.assertion.attributeStatement[0]
294 if not attrstat.attribute:
295 attrstat.attribute = ()
297 for key in attributes:
301 values = attributes[key]
302 if isinstance(values, dict):
304 if not isinstance(values, list):
307 attr = lasso.Saml2Attribute()
309 attr.nameFormat = lasso.SAML2_ATTRIBUTE_NAME_FORMAT_BASIC
310 value = str(value).encode('utf-8')
311 self.debug('value %s' % value)
312 node = lasso.MiscTextNode.newWithString(value)
313 node.textChild = True
314 attrvalue = lasso.Saml2AttributeValue()
315 attrvalue.any = [node]
316 attr.attributeValue = [attrvalue]
317 attrstat.attribute = attrstat.attribute + (attr,)
319 self.debug('Assertion: %s' % login.assertion.dump())
321 saml_sessions = self.cfg.idp.sessionfactory
323 lasso_session = lasso.Session()
324 lasso_session.addAssertion(login.remoteProviderId, login.assertion)
325 provider = ServiceProvider(self.cfg, login.remoteProviderId)
326 saml_sessions.add_session(login.assertion.id,
327 login.remoteProviderId,
329 lasso_session.dump(),
331 provider.logout_mechs)
333 def saml2error(self, login, code, message):
334 status = lasso.Samlp2Status()
335 status.statusCode = lasso.Samlp2StatusCode()
336 status.statusCode.value = lasso.SAML2_STATUS_CODE_RESPONDER
337 status.statusCode.statusCode = lasso.Samlp2StatusCode()
338 status.statusCode.statusCode.value = code
339 login.response.status = status
341 def reply(self, login):
342 if login.protocolProfile == lasso.LOGIN_PROTOCOL_PROFILE_BRWS_ART:
344 raise cherrypy.HTTPError(501)
345 elif login.protocolProfile == lasso.LOGIN_PROTOCOL_PROFILE_BRWS_POST:
346 login.buildAuthnResponseMsg()
347 self.debug('POSTing back to SP [%s]' % (login.msgUrl))
349 "title": 'Redirecting back to the web application',
350 "action": login.msgUrl,
352 [lasso.SAML2_FIELD_RESPONSE, login.msgBody],
353 [lasso.SAML2_FIELD_RELAYSTATE, login.msgRelayState],
355 "submit": 'Return to application',
357 return self._template('saml2/post_response.html', **context)
359 elif login.protocolProfile == lasso.LOGIN_PROTOCOL_PROFILE_BRWS_LECP:
360 login.buildResponseMsg()
361 self.debug("Returning ECP: %s" % login.msgBody)
365 raise cherrypy.HTTPError(500)