Add way to return Kerberos nameid if available
[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         us = UserSession()
163         user = us.get_user()
164
165         # TODO: get authentication type fnd name format from session
166         # need to save which login manager authenticated and map it to a
167         # saml2 authentication context
168         authn_context = lasso.SAML2_AUTHN_CONTEXT_UNSPECIFIED
169
170         timeformat = '%Y-%m-%dT%H:%M:%SZ'
171         login.buildAssertion(authn_context,
172                              authtime.strftime(timeformat),
173                              None,
174                              authtime_notbefore.strftime(timeformat),
175                              authtime_notafter.strftime(timeformat))
176
177         nameid = None
178         if self.nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT:
179             nameid = user.name  ## TODO map to something else ?
180         elif self.nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_TRANSIENT:
181             nameid = user.name  ## TODO map to something else ?
182         elif self.nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_KERBEROS:
183             nameid = us.get_data('user', 'krb_principal_name')
184
185         if nameid:
186             login.assertion.subject.nameId.format = self.nameidfmt
187             login.assertion.subject.nameId.content = nameid
188         else:
189             raise AuthenticationError("Unavailable Name ID type",
190                                       lasso.SAML2_STATUS_CODE_AUTHN_FAILED)
191
192         # TODO: add user attributes as policy requires taking from 'usersession'
193
194     def saml2error(self, login, code, message):
195         status = lasso.Samlp2Status()
196         status.statusCode = lasso.Samlp2StatusCode()
197         status.statusCode.value = lasso.SAML2_STATUS_CODE_RESPONDER
198         status.statusCode.statusCode = lasso.Samlp2StatusCode()
199         status.statusCode.statusCode.value = code
200         login.response.status = status
201
202     def reply(self, login):
203         if login.protocolProfile == lasso.LOGIN_PROTOCOL_PROFILE_BRWS_ART:
204             # TODO
205             raise cherrypy.HTTPError(501)
206         elif login.protocolProfile == lasso.LOGIN_PROTOCOL_PROFILE_BRWS_POST:
207             login.buildAuthnResponseMsg()
208             self._debug('POSTing back to SP [%s]' % (login.msgUrl))
209             context = {
210                 "title": 'Redirecting back to the web application',
211                 "action": login.msgUrl,
212                 "fields": [
213                     [lasso.SAML2_FIELD_RESPONSE, login.msgBody],
214                     [lasso.SAML2_FIELD_RELAYSTATE, login.msgRelayState],
215                 ],
216                 "submit": 'Return to application',
217             }
218             # pylint: disable=star-args
219             return self._template('saml2/post_response.html', **context)
220
221         else:
222             raise cherrypy.HTTPError(500)