Check the NameID policy during authentication
[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
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(Exception):
31
32     def __init__(self, message, code):
33         super(AuthenticationError, self).__init__(message)
34         self.message = message
35         self.code = code
36
37     def __str__(self):
38         return repr(self.message)
39
40
41 class InvalidRequest(Exception):
42
43     def __init__(self, message):
44         super(InvalidRequest, self).__init__(message)
45         self.message = message
46
47     def __str__(self):
48         return repr(self.message)
49
50
51 class AuthenticateRequest(ProviderPageBase):
52
53     def __init__(self, *args, **kwargs):
54         super(AuthenticateRequest, self).__init__(*args, **kwargs)
55         self.STAGE_INIT = 0
56         self.STAGE_AUTH = 1
57         self.stage = self.STAGE_INIT
58         self.nameidfmt = None
59
60     def auth(self, login):
61         try:
62             self.saml2checks(login)
63             self.saml2assertion(login)
64         except AuthenticationError, e:
65             self.saml2error(login, e.code, e.message)
66         return self.reply(login)
67
68     def _parse_request(self, message):
69
70         login = lasso.Login(self.cfg.idp)
71
72         try:
73             login.processAuthnRequestMsg(message)
74         except (lasso.ProfileInvalidMsgError,
75                 lasso.ProfileMissingIssuerError), e:
76
77             msg = 'Malformed Request %r [%r]' % (e, message)
78             raise InvalidRequest(msg)
79
80         except (lasso.ProfileInvalidProtocolprofileError,
81                 lasso.DsError), e:
82
83             msg = 'Invalid SAML Request: %r (%r [%r])' % (login.request,
84                                                           e, message)
85             raise InvalidRequest(msg)
86
87         except (lasso.ServerProviderNotFoundError,
88                 lasso.ProfileUnknownProviderError), e:
89
90             msg = 'Invalid SP [%s] (%r [%r])' % (login.remoteProviderId,
91                                                  e, message)
92             raise InvalidRequest(msg)
93
94         self._debug('SP %s requested authentication' % login.remoteProviderId)
95
96         return login
97
98     def saml2login(self, request):
99
100         if not request:
101             raise cherrypy.HTTPError(400,
102                                      'SAML request token missing or empty')
103
104         try:
105             login = self._parse_request(request)
106         except InvalidRequest, e:
107             self._debug(str(e))
108             raise cherrypy.HTTPError(400, 'Invalid SAML request token')
109         except Exception, e:  # pylint: disable=broad-except
110             self._debug(str(e))
111             raise cherrypy.HTTPError(500)
112
113         return login
114
115     def saml2checks(self, login):
116
117         session = UserSession()
118         user = session.get_user()
119         if user.is_anonymous:
120             if self.stage < self.STAGE_AUTH:
121                 session.save_data('saml2', 'stage', self.STAGE_AUTH)
122                 session.save_data('saml2', 'Request', login.dump())
123                 session.save_data('login', 'Return',
124                                   '%s/saml2/SSO/Continue' % self.basepath)
125                 raise cherrypy.HTTPRedirect('%s/login' % self.basepath)
126             else:
127                 raise AuthenticationError(
128                     "Unknown user", lasso.SAML2_STATUS_CODE_AUTHN_FAILED)
129
130         self._audit("Logged in user: %s [%s]" % (user.name, user.fullname))
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             nameid = 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         self.nameidfmt = nameid
150
151         # TODO: check login.request.forceAuthn
152
153         login.validateRequestMsg(not user.is_anonymous, consent)
154
155     def saml2assertion(self, login):
156
157         authtime = datetime.datetime.utcnow()
158         skew = datetime.timedelta(0, 60)
159         authtime_notbefore = authtime - skew
160         authtime_notafter = authtime + skew
161
162         user = UserSession().get_user()
163
164         # TODO: get authentication type fnd name format from session
165         # need to save which login manager authenticated and map it to a
166         # saml2 authentication context
167         authn_context = lasso.SAML2_AUTHN_CONTEXT_UNSPECIFIED
168
169         timeformat = '%Y-%m-%dT%H:%M:%SZ'
170         login.buildAssertion(authn_context,
171                              authtime.strftime(timeformat),
172                              None,
173                              authtime_notbefore.strftime(timeformat),
174                              authtime_notafter.strftime(timeformat))
175
176         nameid = None
177         if self.nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT:
178             nameid = user.name  ## TODO map to something else ?
179         elif self.nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_TRANSIENT:
180             nameid = user.name  ## TODO map to something else ?
181
182         if nameid:
183             login.assertion.subject.nameId.format = self.nameidfmt
184             login.assertion.subject.nameId.content = nameid
185         else:
186             raise AuthenticationError("Unavailable Name ID type",
187                                       lasso.SAML2_STATUS_CODE_AUTHN_FAILED)
188
189         # TODO: add user attributes as policy requires taking from 'usersession'
190
191     def saml2error(self, login, code, message):
192         status = lasso.Samlp2Status()
193         status.statusCode = lasso.Samlp2StatusCode()
194         status.statusCode.value = lasso.SAML2_STATUS_CODE_RESPONDER
195         status.statusCode.statusCode = lasso.Samlp2StatusCode()
196         status.statusCode.statusCode.value = code
197         login.response.status = status
198
199     def reply(self, login):
200         if login.protocolProfile == lasso.LOGIN_PROTOCOL_PROFILE_BRWS_ART:
201             # TODO
202             raise cherrypy.HTTPError(501)
203         elif login.protocolProfile == lasso.LOGIN_PROTOCOL_PROFILE_BRWS_POST:
204             login.buildAuthnResponseMsg()
205             self._debug('POSTing back to SP [%s]' % (login.msgUrl))
206             context = {
207                 "title": 'Redirecting back to the web application',
208                 "action": login.msgUrl,
209                 "fields": [
210                     [lasso.SAML2_FIELD_RESPONSE, login.msgBody],
211                     [lasso.SAML2_FIELD_RELAYSTATE, login.msgRelayState],
212                 ],
213                 "submit": 'Return to application',
214             }
215             # pylint: disable=star-args
216             return self._template('saml2/post_response.html', **context)
217
218         else:
219             raise cherrypy.HTTPError(500)