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