Create a SAML2 session during login
[cascardo/ipsilon.git] / ipsilon / providers / saml2 / auth.py
1 # Copyright (C) 2014 Ipsilon project Contributors, for license see COPYING
2
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.providers.saml2.sessions import SAMLSessionFactory
9 from ipsilon.tools import saml2metadata as metadata
10 from ipsilon.util.policy import Policy
11 from ipsilon.util.user import UserSession
12 from ipsilon.util.trans import Transaction
13 import cherrypy
14 import datetime
15 import lasso
16 import uuid
17 import hashlib
18
19
20 class UnknownProvider(ProviderException):
21
22     def __init__(self, message):
23         super(UnknownProvider, self).__init__(message)
24         self.debug(message)
25
26
27 class AuthenticateRequest(ProviderPageBase):
28
29     def __init__(self, *args, **kwargs):
30         super(AuthenticateRequest, self).__init__(*args, **kwargs)
31         self.stage = 'init'
32         self.trans = None
33         self.binding = None
34
35     def _preop(self, *args, **kwargs):
36         try:
37             # generate a new id or get current one
38             self.trans = Transaction('saml2', **kwargs)
39
40             self.debug('self.binding=%s, transdata=%s' %
41                        (self.binding, self.trans.retrieve()))
42             if self.binding is None:
43                 # SAML binding is unknown, try to get it from transaction
44                 transdata = self.trans.retrieve()
45                 self.binding = transdata.get('saml2_binding')
46             else:
47                 # SAML binding known, store in transaction
48                 data = {'saml2_binding': self.binding}
49                 self.trans.store(data)
50
51             # Only check for cookie for those bindings which use one
52             if self.binding not in (metadata.SAML2_SERVICE_MAP['sso-soap'][1]):
53                 if self.trans.cookie.value != self.trans.provider:
54                     self.debug('Invalid transaction, %s != %s' % (
55                         self.trans.cookie.value, self.trans.provider))
56         except Exception, e:  # pylint: disable=broad-except
57             self.debug('Transaction initialization failed: %s' % repr(e))
58             raise cherrypy.HTTPError(400, 'Invalid transaction id')
59
60     def pre_GET(self, *args, **kwargs):
61         self._preop(*args, **kwargs)
62
63     def pre_POST(self, *args, **kwargs):
64         self._preop(*args, **kwargs)
65
66     def auth(self, login):
67         try:
68             self.saml2checks(login)
69         except AuthenticationError, e:
70             self.saml2error(login, e.code, e.message)
71         return self.reply(login)
72
73     def _parse_request(self, message):
74
75         login = self.cfg.idp.get_login_handler()
76
77         try:
78             login.processAuthnRequestMsg(message)
79         except (lasso.ProfileInvalidMsgError,
80                 lasso.ProfileMissingIssuerError), e:
81
82             msg = 'Malformed Request %r [%r]' % (e, message)
83             raise InvalidRequest(msg)
84
85         except (lasso.ProfileInvalidProtocolprofileError,
86                 lasso.DsError), e:
87
88             msg = 'Invalid SAML Request: %r (%r [%r])' % (login.request,
89                                                           e, message)
90             raise InvalidRequest(msg)
91
92         except (lasso.ServerProviderNotFoundError,
93                 lasso.ProfileUnknownProviderError), e:
94
95             msg = 'Invalid SP [%s] (%r [%r])' % (login.remoteProviderId,
96                                                  e, message)
97             raise UnknownProvider(msg)
98
99         self.debug('SP %s requested authentication' % login.remoteProviderId)
100
101         return login
102
103     def saml2login(self, request):
104
105         if not request:
106             raise cherrypy.HTTPError(400,
107                                      'SAML request token missing or empty')
108
109         try:
110             login = self._parse_request(request)
111         except InvalidRequest, e:
112             self.debug(str(e))
113             raise cherrypy.HTTPError(400, 'Invalid SAML request token')
114         except UnknownProvider, e:
115             self.debug(str(e))
116             raise cherrypy.HTTPError(400, 'Unknown Service Provider')
117         except Exception, e:  # pylint: disable=broad-except
118             self.debug(str(e))
119             raise cherrypy.HTTPError(500)
120
121         return login
122
123     def saml2checks(self, login):
124
125         us = UserSession()
126         user = us.get_user()
127         if user.is_anonymous:
128             if self.stage == 'init':
129                 returl = '%s/saml2/SSO/Continue?%s' % (
130                     self.basepath, self.trans.get_GET_arg())
131                 data = {'saml2_stage': 'auth',
132                         'saml2_request': login.dump(),
133                         'login_return': returl,
134                         'login_target': login.remoteProviderId}
135                 self.trans.store(data)
136                 redirect = '%s/login?%s' % (self.basepath,
137                                             self.trans.get_GET_arg())
138                 raise cherrypy.HTTPRedirect(redirect)
139             else:
140                 raise AuthenticationError(
141                     "Unknown user", lasso.SAML2_STATUS_CODE_AUTHN_FAILED)
142
143         self._audit("Logged in user: %s [%s]" % (user.name, user.fullname))
144
145         # We can wipe the transaction now, as this is the last step
146         self.trans.wipe()
147
148         # TODO: check if this is the first time this user access this SP
149         # If required by user prefs, ask user for consent once and then
150         # record it
151         consent = True
152
153         # TODO: check destination
154
155         try:
156             provider = ServiceProvider(self.cfg, login.remoteProviderId)
157             nameidfmt = provider.get_valid_nameid(login.request.nameIdPolicy)
158         except NameIdNotAllowed, e:
159             raise AuthenticationError(
160                 str(e), lasso.SAML2_STATUS_CODE_INVALID_NAME_ID_POLICY)
161         except InvalidProviderId, e:
162             raise AuthenticationError(
163                 str(e), lasso.SAML2_STATUS_CODE_AUTHN_FAILED)
164
165         # TODO: check login.request.forceAuthn
166
167         login.validateRequestMsg(not user.is_anonymous, consent)
168
169         authtime = datetime.datetime.utcnow()
170         skew = datetime.timedelta(0, 60)
171         authtime_notbefore = authtime - skew
172         authtime_notafter = authtime + skew
173
174         # TODO: get authentication type fnd name format from session
175         # need to save which login manager authenticated and map it to a
176         # saml2 authentication context
177         authn_context = lasso.SAML2_AUTHN_CONTEXT_UNSPECIFIED
178
179         timeformat = '%Y-%m-%dT%H:%M:%SZ'
180         login.buildAssertion(authn_context,
181                              authtime.strftime(timeformat),
182                              None,
183                              authtime_notbefore.strftime(timeformat),
184                              authtime_notafter.strftime(timeformat))
185
186         nameid = None
187         if nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT:
188             idpsalt = self.cfg.idp_nameid_salt
189             if idpsalt is None:
190                 raise AuthenticationError(
191                     "idp nameid salt is not set in configuration"
192                 )
193             value = hashlib.sha512()
194             value.update(idpsalt)
195             value.update(login.remoteProviderId)
196             value.update(user.name)
197             nameid = '_' + value.hexdigest()
198         elif nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_TRANSIENT:
199             nameid = '_' + uuid.uuid4().hex
200         elif nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_KERBEROS:
201             userattrs = us.get_user_attrs()
202             nameid = userattrs.get('gssapi_principal_name')
203         elif nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_EMAIL:
204             nameid = us.get_user().email
205             if not nameid:
206                 nameid = '%s@%s' % (user.name, self.cfg.default_email_domain)
207         elif nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_UNSPECIFIED:
208             nameid = provider.normalize_username(user.name)
209
210         if nameid:
211             login.assertion.subject.nameId.format = nameidfmt
212             login.assertion.subject.nameId.content = nameid
213         else:
214             self.trans.wipe()
215             raise AuthenticationError("Unavailable Name ID type",
216                                       lasso.SAML2_STATUS_CODE_AUTHN_FAILED)
217
218         # Check attribute policy and perform mapping and filtering.
219         # If the SP has its own mapping or filtering policy use that
220         # instead of the global policy.
221         if (provider.attribute_mappings is not None and
222                 len(provider.attribute_mappings) > 0):
223             attribute_mappings = provider.attribute_mappings
224         else:
225             attribute_mappings = self.cfg.default_attribute_mapping
226         if (provider.allowed_attributes is not None and
227                 len(provider.allowed_attributes) > 0):
228             allowed_attributes = provider.allowed_attributes
229         else:
230             allowed_attributes = self.cfg.default_allowed_attributes
231         self.debug("Allowed attrs: %s" % allowed_attributes)
232         self.debug("Mapping: %s" % attribute_mappings)
233         policy = Policy(attribute_mappings, allowed_attributes)
234         userattrs = us.get_user_attrs()
235         mappedattrs, _ = policy.map_attributes(userattrs)
236         attributes = policy.filter_attributes(mappedattrs)
237
238         if '_groups' in attributes and 'groups' not in attributes:
239             attributes['groups'] = attributes['_groups']
240
241         self.debug("%s's attributes: %s" % (user.name, attributes))
242
243         # The saml-core-2.0-os specification section 2.7.3 requires
244         # the AttributeStatement element to be non-empty.
245         if attributes:
246             if not login.assertion.attributeStatement:
247                 attrstat = lasso.Saml2AttributeStatement()
248                 login.assertion.attributeStatement = [attrstat]
249             else:
250                 attrstat = login.assertion.attributeStatement[0]
251             if not attrstat.attribute:
252                 attrstat.attribute = ()
253
254         for key in attributes:
255             # skip internal info
256             if key[0] == '_':
257                 continue
258             values = attributes[key]
259             if isinstance(values, dict):
260                 continue
261             if not isinstance(values, list):
262                 values = [values]
263             for value in values:
264                 attr = lasso.Saml2Attribute()
265                 attr.name = key
266                 attr.nameFormat = lasso.SAML2_ATTRIBUTE_NAME_FORMAT_BASIC
267                 value = str(value).encode('utf-8')
268                 self.debug('value %s' % value)
269                 node = lasso.MiscTextNode.newWithString(value)
270                 node.textChild = True
271                 attrvalue = lasso.Saml2AttributeValue()
272                 attrvalue.any = [node]
273                 attr.attributeValue = [attrvalue]
274                 attrstat.attribute = attrstat.attribute + (attr,)
275
276         self.debug('Assertion: %s' % login.assertion.dump())
277
278         saml_sessions = SAMLSessionFactory()
279
280         lasso_session = lasso.Session()
281         lasso_session.addAssertion(login.remoteProviderId, login.assertion)
282         saml_sessions.add_session(login.assertion.id,
283                                   login.remoteProviderId,
284                                   user.name,
285                                   lasso_session.dump())
286
287     def saml2error(self, login, code, message):
288         status = lasso.Samlp2Status()
289         status.statusCode = lasso.Samlp2StatusCode()
290         status.statusCode.value = lasso.SAML2_STATUS_CODE_RESPONDER
291         status.statusCode.statusCode = lasso.Samlp2StatusCode()
292         status.statusCode.statusCode.value = code
293         login.response.status = status
294
295     def reply(self, login):
296         if login.protocolProfile == lasso.LOGIN_PROTOCOL_PROFILE_BRWS_ART:
297             # TODO
298             raise cherrypy.HTTPError(501)
299         elif login.protocolProfile == lasso.LOGIN_PROTOCOL_PROFILE_BRWS_POST:
300             login.buildAuthnResponseMsg()
301             self.debug('POSTing back to SP [%s]' % (login.msgUrl))
302             context = {
303                 "title": 'Redirecting back to the web application',
304                 "action": login.msgUrl,
305                 "fields": [
306                     [lasso.SAML2_FIELD_RESPONSE, login.msgBody],
307                     [lasso.SAML2_FIELD_RELAYSTATE, login.msgRelayState],
308                 ],
309                 "submit": 'Return to application',
310             }
311             return self._template('saml2/post_response.html', **context)
312
313         elif login.protocolProfile == lasso.LOGIN_PROTOCOL_PROFILE_BRWS_LECP:
314             login.buildResponseMsg()
315             self.debug("Returning ECP: %s" % login.msgBody)
316             return login.msgBody
317
318         else:
319             raise cherrypy.HTTPError(500)