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