Unsplit checking functions
[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
59     def auth(self, login):
60         try:
61             self.saml2checks(login)
62         except AuthenticationError, e:
63             self.saml2error(login, e.code, e.message)
64         return self.reply(login)
65
66     def _parse_request(self, message):
67
68         login = lasso.Login(self.cfg.idp)
69
70         try:
71             login.processAuthnRequestMsg(message)
72         except (lasso.ProfileInvalidMsgError,
73                 lasso.ProfileMissingIssuerError), e:
74
75             msg = 'Malformed Request %r [%r]' % (e, message)
76             raise InvalidRequest(msg)
77
78         except (lasso.ProfileInvalidProtocolprofileError,
79                 lasso.DsError), e:
80
81             msg = 'Invalid SAML Request: %r (%r [%r])' % (login.request,
82                                                           e, message)
83             raise InvalidRequest(msg)
84
85         except (lasso.ServerProviderNotFoundError,
86                 lasso.ProfileUnknownProviderError), e:
87
88             msg = 'Invalid SP [%s] (%r [%r])' % (login.remoteProviderId,
89                                                  e, message)
90             raise InvalidRequest(msg)
91
92         self._debug('SP %s requested authentication' % login.remoteProviderId)
93
94         return login
95
96     def saml2login(self, request):
97
98         if not request:
99             raise cherrypy.HTTPError(400,
100                                      'SAML request token missing or empty')
101
102         try:
103             login = self._parse_request(request)
104         except InvalidRequest, e:
105             self._debug(str(e))
106             raise cherrypy.HTTPError(400, 'Invalid SAML request token')
107         except Exception, e:  # pylint: disable=broad-except
108             self._debug(str(e))
109             raise cherrypy.HTTPError(500)
110
111         return login
112
113     def saml2checks(self, login):
114
115         session = UserSession()
116         user = session.get_user()
117         if user.is_anonymous:
118             if self.stage < self.STAGE_AUTH:
119                 session.save_data('saml2', 'stage', self.STAGE_AUTH)
120                 session.save_data('saml2', 'Request', login.dump())
121                 session.save_data('login', 'Return',
122                                   '%s/saml2/SSO/Continue' % self.basepath)
123                 raise cherrypy.HTTPRedirect('%s/login' % self.basepath)
124             else:
125                 raise AuthenticationError(
126                     "Unknown user", lasso.SAML2_STATUS_CODE_AUTHN_FAILED)
127
128         self._audit("Logged in user: %s [%s]" % (user.name, user.fullname))
129
130         # TODO: check if this is the first time this user access this SP
131         # If required by user prefs, ask user for consent once and then
132         # record it
133         consent = True
134
135         # TODO: check destination
136
137         try:
138             provider = ServiceProvider(self.cfg, login.remoteProviderId)
139             nameidfmt = provider.get_valid_nameid(login.request.nameIdPolicy)
140         except NameIdNotAllowed, e:
141             raise AuthenticationError(
142                 str(e), lasso.SAML2_STATUS_CODE_INVALID_NAME_ID_POLICY)
143         except InvalidProviderId, e:
144             raise AuthenticationError(
145                 str(e), lasso.SAML2_STATUS_CODE_AUTHN_FAILED)
146
147         # TODO: check login.request.forceAuthn
148
149         login.validateRequestMsg(not user.is_anonymous, consent)
150
151         authtime = datetime.datetime.utcnow()
152         skew = datetime.timedelta(0, 60)
153         authtime_notbefore = authtime - skew
154         authtime_notafter = authtime + skew
155
156         us = UserSession()
157         user = us.get_user()
158
159         # TODO: get authentication type fnd name format from session
160         # need to save which login manager authenticated and map it to a
161         # saml2 authentication context
162         authn_context = lasso.SAML2_AUTHN_CONTEXT_UNSPECIFIED
163
164         timeformat = '%Y-%m-%dT%H:%M:%SZ'
165         login.buildAssertion(authn_context,
166                              authtime.strftime(timeformat),
167                              None,
168                              authtime_notbefore.strftime(timeformat),
169                              authtime_notafter.strftime(timeformat))
170
171         nameid = None
172         if nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT:
173             nameid = user.name  ## TODO map to something else ?
174         elif nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_TRANSIENT:
175             nameid = user.name  ## TODO map to something else ?
176         elif nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_KERBEROS:
177             nameid = us.get_data('user', 'krb_principal_name')
178         elif nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_EMAIL:
179             nameid = us.get_user().email
180             if not nameid:
181                 nameid = '%s@%s' % (user.name, self.cfg.default_email_domain)
182
183         if nameid:
184             login.assertion.subject.nameId.format = nameidfmt
185             login.assertion.subject.nameId.content = nameid
186         else:
187             raise AuthenticationError("Unavailable Name ID type",
188                                       lasso.SAML2_STATUS_CODE_AUTHN_FAILED)
189
190         # TODO: add user attributes as policy requires taking from 'usersession'
191
192     def saml2error(self, login, code, message):
193         status = lasso.Samlp2Status()
194         status.statusCode = lasso.Samlp2StatusCode()
195         status.statusCode.value = lasso.SAML2_STATUS_CODE_RESPONDER
196         status.statusCode.statusCode = lasso.Samlp2StatusCode()
197         status.statusCode.statusCode.value = code
198         login.response.status = status
199
200     def reply(self, login):
201         if login.protocolProfile == lasso.LOGIN_PROTOCOL_PROFILE_BRWS_ART:
202             # TODO
203             raise cherrypy.HTTPError(501)
204         elif login.protocolProfile == lasso.LOGIN_PROTOCOL_PROFILE_BRWS_POST:
205             login.buildAuthnResponseMsg()
206             self._debug('POSTing back to SP [%s]' % (login.msgUrl))
207             context = {
208                 "title": 'Redirecting back to the web application',
209                 "action": login.msgUrl,
210                 "fields": [
211                     [lasso.SAML2_FIELD_RESPONSE, login.msgBody],
212                     [lasso.SAML2_FIELD_RELAYSTATE, login.msgRelayState],
213                 ],
214                 "submit": 'Return to application',
215             }
216             # pylint: disable=star-args
217             return self._template('saml2/post_response.html', **context)
218
219         else:
220             raise cherrypy.HTTPError(500)