pam: use a pam object method instead of pam module function
[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.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
12 import cherrypy
13 import datetime
14 import lasso
15 import uuid
16 import hashlib
17
18
19 class UnknownProvider(ProviderException):
20
21     def __init__(self, message):
22         super(UnknownProvider, self).__init__(message)
23         self.debug(message)
24
25
26 class AuthenticateRequest(ProviderPageBase):
27
28     def __init__(self, site, provider, *args, **kwargs):
29         super(AuthenticateRequest, self).__init__(site, provider)
30         self.stage = 'init'
31         self.trans = None
32         self.binding = None
33
34     def _preop(self, *args, **kwargs):
35         try:
36             # generate a new id or get current one
37             self.trans = Transaction('saml2', **kwargs)
38
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')
45             else:
46                 # SAML binding known, store in transaction
47                 data = {'saml2_binding': self.binding}
48                 self.trans.store(data)
49
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')
58
59     def pre_GET(self, *args, **kwargs):
60         self._preop(*args, **kwargs)
61
62     def pre_POST(self, *args, **kwargs):
63         self._preop(*args, **kwargs)
64
65     def auth(self, login):
66         try:
67             self.saml2checks(login)
68         except AuthenticationError, e:
69             self.saml2error(login, e.code, e.message)
70         return self.reply(login)
71
72     def _parse_request(self, message):
73
74         login = self.cfg.idp.get_login_handler()
75
76         try:
77             login.processAuthnRequestMsg(message)
78         except (lasso.ProfileInvalidMsgError,
79                 lasso.ProfileMissingIssuerError), e:
80
81             msg = 'Malformed Request %r [%r]' % (e, message)
82             raise InvalidRequest(msg)
83
84         except (lasso.ProfileInvalidProtocolprofileError,
85                 lasso.DsError), e:
86
87             msg = 'Invalid SAML Request: %r (%r [%r])' % (login.request,
88                                                           e, message)
89             raise InvalidRequest(msg)
90
91         except (lasso.ServerProviderNotFoundError,
92                 lasso.ProfileUnknownProviderError), e:
93
94             msg = 'Invalid SP [%s] (%r [%r])' % (login.remoteProviderId,
95                                                  e, message)
96             raise UnknownProvider(msg)
97
98         self.debug('SP %s requested authentication' % login.remoteProviderId)
99
100         return login
101
102     def _idp_initiated_login(self, spidentifier, relaystate):
103         """
104         Perform an Idp-initiated login
105
106         Exceptions are handled by the caller
107         """
108         login = self.cfg.idp.get_login_handler()
109
110         login.initIdpInitiatedAuthnRequest(spidentifier)
111
112         # Hardcode for now, handle Artifact later
113         login.request.protocolBinding = lasso.SAML2_METADATA_BINDING_POST
114
115         login.processAuthnRequestMsg()
116
117         if relaystate is not None:
118             login.msgRelayState = relaystate
119         else:
120             provider = ServiceProvider(self.cfg, login.remoteProviderId)
121             if provider.splink is not None:
122                 login.msgRelayState = provider.splink
123             else:
124                 login.msgRelayState = login.remoteProviderId
125
126         return login
127
128     def saml2login(self, request, spidentifier=None, relaystate=None):
129         """
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.
136         """
137         if not request and not spidentifier:
138             raise cherrypy.HTTPError(400,
139                                      'SAML request token missing or empty')
140
141         if spidentifier:
142             try:
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
147                 self.debug(str(e))
148                 raise cherrypy.HTTPError(500)
149         else:
150             try:
151                 login = self._parse_request(request)
152             except InvalidRequest, e:
153                 self.debug(str(e))
154                 raise cherrypy.HTTPError(400, 'Invalid SAML request token')
155             except UnknownProvider, e:
156                 self.debug(str(e))
157                 raise cherrypy.HTTPError(400, 'Unknown Service Provider')
158             except Exception, e:  # pylint: disable=broad-except
159                 self.debug(str(e))
160                 raise cherrypy.HTTPError(500)
161
162         return login
163
164     def saml2checks(self, login):
165
166         us = UserSession()
167         user = us.get_user()
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)
180             else:
181                 raise AuthenticationError(
182                     "Unknown user", lasso.SAML2_STATUS_CODE_AUTHN_FAILED)
183
184         self._audit("Logged in user: %s [%s]" % (user.name, user.fullname))
185
186         # We can wipe the transaction now, as this is the last step
187         self.trans.wipe()
188
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
191         # record it
192         consent = True
193
194         # TODO: check destination
195
196         try:
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)
205
206         # TODO: check login.request.forceAuthn
207
208         login.validateRequestMsg(not user.is_anonymous, consent)
209
210         authtime = datetime.datetime.utcnow()
211         skew = datetime.timedelta(0, 60)
212         authtime_notbefore = authtime - skew
213         authtime_notafter = authtime + skew
214
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
219
220         timeformat = '%Y-%m-%dT%H:%M:%SZ'
221         login.buildAssertion(authn_context,
222                              authtime.strftime(timeformat),
223                              None,
224                              authtime_notbefore.strftime(timeformat),
225                              authtime_notafter.strftime(timeformat))
226
227         nameid = None
228         if nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT:
229             idpsalt = self.cfg.idp_nameid_salt
230             if idpsalt is None:
231                 raise AuthenticationError(
232                     "idp nameid salt is not set in configuration"
233                 )
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
246             if not nameid:
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)
250
251         if nameid:
252             login.assertion.subject.nameId.format = nameidfmt
253             login.assertion.subject.nameId.content = nameid
254         else:
255             self.trans.wipe()
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)
260
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
267         else:
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
272         else:
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)
280
281         if '_groups' in attributes and 'groups' not in attributes:
282             attributes['groups'] = attributes['_groups']
283
284         self.debug("%s's attributes: %s" % (user.name, attributes))
285
286         # The saml-core-2.0-os specification section 2.7.3 requires
287         # the AttributeStatement element to be non-empty.
288         if attributes:
289             if not login.assertion.attributeStatement:
290                 attrstat = lasso.Saml2AttributeStatement()
291                 login.assertion.attributeStatement = [attrstat]
292             else:
293                 attrstat = login.assertion.attributeStatement[0]
294             if not attrstat.attribute:
295                 attrstat.attribute = ()
296
297         for key in attributes:
298             # skip internal info
299             if key[0] == '_':
300                 continue
301             values = attributes[key]
302             if isinstance(values, dict):
303                 continue
304             if not isinstance(values, list):
305                 values = [values]
306             for value in values:
307                 attr = lasso.Saml2Attribute()
308                 attr.name = key
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,)
318
319         self.debug('Assertion: %s' % login.assertion.dump())
320
321         saml_sessions = self.cfg.idp.sessionfactory
322
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,
328                                   user.name,
329                                   lasso_session.dump(),
330                                   None,
331                                   provider.logout_mechs)
332
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
340
341     def reply(self, login):
342         if login.protocolProfile == lasso.LOGIN_PROTOCOL_PROFILE_BRWS_ART:
343             # TODO
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))
348             context = {
349                 "title": 'Redirecting back to the web application',
350                 "action": login.msgUrl,
351                 "fields": [
352                     [lasso.SAML2_FIELD_RESPONSE, login.msgBody],
353                     [lasso.SAML2_FIELD_RELAYSTATE, login.msgRelayState],
354                 ],
355                 "submit": 'Return to application',
356             }
357             return self._template('saml2/post_response.html', **context)
358
359         elif login.protocolProfile == lasso.LOGIN_PROTOCOL_PROFILE_BRWS_LECP:
360             login.buildResponseMsg()
361             self.debug("Returning ECP: %s" % login.msgBody)
362             return login.msgBody
363
364         else:
365             raise cherrypy.HTTPError(500)