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