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