Move accessory functions to a generic tools module
[cascardo/ipsilon.git] / ipsilon / providers / saml2 / auth.py
1 #!/usr/bin/python
2 #
3 # Copyright (C) 2014  Simo Sorce <simo@redhat.com>
4 #
5 # see file 'COPYING' for use and warranty information
6 #
7 # This program is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
19
20 from ipsilon.providers.common import ProviderPageBase, ProviderException
21 from ipsilon.providers.saml2.provider import ServiceProvider
22 from ipsilon.providers.saml2.provider import InvalidProviderId
23 from ipsilon.providers.saml2.provider import NameIdNotAllowed
24 from ipsilon.util.user import UserSession
25 import cherrypy
26 import datetime
27 import lasso
28
29
30 class AuthenticationError(ProviderException):
31
32     def __init__(self, message, code):
33         super(AuthenticationError, self).__init__(message)
34         self.code = code
35         self._debug('%s [%s]' % (message, code))
36
37
38 class InvalidRequest(ProviderException):
39
40     def __init__(self, message):
41         super(InvalidRequest, self).__init__(message)
42         self._debug(message)
43
44
45 class UnknownProvider(ProviderException):
46
47     def __init__(self, message):
48         super(UnknownProvider, self).__init__(message)
49         self._debug(message)
50
51
52 class AuthenticateRequest(ProviderPageBase):
53
54     def __init__(self, *args, **kwargs):
55         super(AuthenticateRequest, self).__init__(*args, **kwargs)
56         self.STAGE_INIT = 0
57         self.STAGE_AUTH = 1
58         self.stage = self.STAGE_INIT
59
60     def auth(self, login):
61         try:
62             self.saml2checks(login)
63         except AuthenticationError, e:
64             self.saml2error(login, e.code, e.message)
65         return self.reply(login)
66
67     def _parse_request(self, message):
68
69         login = self.cfg.idp.get_login_handler()
70
71         try:
72             login.processAuthnRequestMsg(message)
73         except (lasso.ProfileInvalidMsgError,
74                 lasso.ProfileMissingIssuerError), e:
75
76             msg = 'Malformed Request %r [%r]' % (e, message)
77             raise InvalidRequest(msg)
78
79         except (lasso.ProfileInvalidProtocolprofileError,
80                 lasso.DsError), e:
81
82             msg = 'Invalid SAML Request: %r (%r [%r])' % (login.request,
83                                                           e, message)
84             raise InvalidRequest(msg)
85
86         except (lasso.ServerProviderNotFoundError,
87                 lasso.ProfileUnknownProviderError), e:
88
89             msg = 'Invalid SP [%s] (%r [%r])' % (login.remoteProviderId,
90                                                  e, message)
91             raise UnknownProvider(msg)
92
93         self._debug('SP %s requested authentication' % login.remoteProviderId)
94
95         return login
96
97     def saml2login(self, request):
98
99         if not request:
100             raise cherrypy.HTTPError(400,
101                                      'SAML request token missing or empty')
102
103         try:
104             login = self._parse_request(request)
105         except InvalidRequest, e:
106             self._debug(str(e))
107             raise cherrypy.HTTPError(400, 'Invalid SAML request token')
108         except UnknownProvider, e:
109             self._debug(str(e))
110             raise cherrypy.HTTPError(400, 'Unknown Service Provider')
111         except Exception, e:  # pylint: disable=broad-except
112             self._debug(str(e))
113             raise cherrypy.HTTPError(500)
114
115         return login
116
117     def saml2checks(self, login):
118
119         session = UserSession()
120         user = session.get_user()
121         if user.is_anonymous:
122             if self.stage < self.STAGE_AUTH:
123                 session.save_data('saml2', 'stage', self.STAGE_AUTH)
124                 session.save_data('saml2', 'Request', login.dump())
125                 session.save_data('login', 'Return',
126                                   '%s/saml2/SSO/Continue' % self.basepath)
127                 raise cherrypy.HTTPRedirect('%s/login' % self.basepath)
128             else:
129                 raise AuthenticationError(
130                     "Unknown user", lasso.SAML2_STATUS_CODE_AUTHN_FAILED)
131
132         self._audit("Logged in user: %s [%s]" % (user.name, user.fullname))
133
134         # TODO: check if this is the first time this user access this SP
135         # If required by user prefs, ask user for consent once and then
136         # record it
137         consent = True
138
139         # TODO: check destination
140
141         try:
142             provider = ServiceProvider(self.cfg, login.remoteProviderId)
143             nameidfmt = provider.get_valid_nameid(login.request.nameIdPolicy)
144         except NameIdNotAllowed, e:
145             raise AuthenticationError(
146                 str(e), lasso.SAML2_STATUS_CODE_INVALID_NAME_ID_POLICY)
147         except InvalidProviderId, e:
148             raise AuthenticationError(
149                 str(e), lasso.SAML2_STATUS_CODE_AUTHN_FAILED)
150
151         # TODO: check login.request.forceAuthn
152
153         login.validateRequestMsg(not user.is_anonymous, consent)
154
155         authtime = datetime.datetime.utcnow()
156         skew = datetime.timedelta(0, 60)
157         authtime_notbefore = authtime - skew
158         authtime_notafter = authtime + skew
159
160         us = UserSession()
161         user = us.get_user()
162
163         # TODO: get authentication type fnd name format from session
164         # need to save which login manager authenticated and map it to a
165         # saml2 authentication context
166         authn_context = lasso.SAML2_AUTHN_CONTEXT_UNSPECIFIED
167
168         timeformat = '%Y-%m-%dT%H:%M:%SZ'
169         login.buildAssertion(authn_context,
170                              authtime.strftime(timeformat),
171                              None,
172                              authtime_notbefore.strftime(timeformat),
173                              authtime_notafter.strftime(timeformat))
174
175         nameid = None
176         if nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT:
177             ## TODO map to something else ?
178             nameid = provider.normalize_username(user.name)
179         elif nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_TRANSIENT:
180             ## TODO map to something else ?
181             nameid = provider.normalize_username(user.name)
182         elif nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_KERBEROS:
183             nameid = us.get_data('user', 'krb_principal_name')
184         elif nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_EMAIL:
185             nameid = us.get_user().email
186             if not nameid:
187                 nameid = '%s@%s' % (user.name, self.cfg.default_email_domain)
188
189         if nameid:
190             login.assertion.subject.nameId.format = nameidfmt
191             login.assertion.subject.nameId.content = nameid
192         else:
193             raise AuthenticationError("Unavailable Name ID type",
194                                       lasso.SAML2_STATUS_CODE_AUTHN_FAILED)
195
196         # TODO: add user attributes as policy requires from 'usersession'
197
198     def saml2error(self, login, code, message):
199         status = lasso.Samlp2Status()
200         status.statusCode = lasso.Samlp2StatusCode()
201         status.statusCode.value = lasso.SAML2_STATUS_CODE_RESPONDER
202         status.statusCode.statusCode = lasso.Samlp2StatusCode()
203         status.statusCode.statusCode.value = code
204         login.response.status = status
205
206     def reply(self, login):
207         if login.protocolProfile == lasso.LOGIN_PROTOCOL_PROFILE_BRWS_ART:
208             # TODO
209             raise cherrypy.HTTPError(501)
210         elif login.protocolProfile == lasso.LOGIN_PROTOCOL_PROFILE_BRWS_POST:
211             login.buildAuthnResponseMsg()
212             self._debug('POSTing back to SP [%s]' % (login.msgUrl))
213             context = {
214                 "title": 'Redirecting back to the web application',
215                 "action": login.msgUrl,
216                 "fields": [
217                     [lasso.SAML2_FIELD_RESPONSE, login.msgBody],
218                     [lasso.SAML2_FIELD_RELAYSTATE, login.msgRelayState],
219                 ],
220                 "submit": 'Return to application',
221             }
222             # pylint: disable=star-args
223             return self._template('saml2/post_response.html', **context)
224
225         else:
226             raise cherrypy.HTTPError(500)