a65b52abbf7ed266e7a73ee390270518806f9f85
[cascardo/ipsilon.git] / ipsilon / providers / saml2 / auth.py
1 # Copyright (C) 2014  Simo Sorce <simo@redhat.com>
2 #
3 # see file 'COPYING' for use and warranty information
4 #
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation, either version 3 of the License, or
8 # (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
17
18 from ipsilon.providers.common import ProviderPageBase, ProviderException
19 from ipsilon.providers.common import AuthenticationError, InvalidRequest
20 from ipsilon.providers.saml2.provider import ServiceProvider
21 from ipsilon.providers.saml2.provider import InvalidProviderId
22 from ipsilon.providers.saml2.provider import NameIdNotAllowed
23 from ipsilon.providers.saml2.sessions import SAMLSessionsContainer
24 from ipsilon.util.user import UserSession
25 from ipsilon.util.trans import Transaction
26 import cherrypy
27 import datetime
28 import lasso
29
30
31 class UnknownProvider(ProviderException):
32
33     def __init__(self, message):
34         super(UnknownProvider, self).__init__(message)
35         self._debug(message)
36
37
38 class AuthenticateRequest(ProviderPageBase):
39
40     def __init__(self, *args, **kwargs):
41         super(AuthenticateRequest, self).__init__(*args, **kwargs)
42         self.stage = 'init'
43         self.trans = None
44
45     def _preop(self, *args, **kwargs):
46         try:
47             # generate a new id or get current one
48             self.trans = Transaction('saml2', **kwargs)
49             if self.trans.cookie.value != self.trans.provider:
50                 self.debug('Invalid transaction, %s != %s' % (
51                            self.trans.cookie.value, self.trans.provider))
52         except Exception, e:  # pylint: disable=broad-except
53             self.debug('Transaction initialization failed: %s' % repr(e))
54             raise cherrypy.HTTPError(400, 'Invalid transaction id')
55
56     def pre_GET(self, *args, **kwargs):
57         self._preop(*args, **kwargs)
58
59     def pre_POST(self, *args, **kwargs):
60         self._preop(*args, **kwargs)
61
62     def auth(self, login):
63         try:
64             self.saml2checks(login)
65         except AuthenticationError, e:
66             self.saml2error(login, e.code, e.message)
67         return self.reply(login)
68
69     def _parse_request(self, message):
70
71         login = self.cfg.idp.get_login_handler()
72
73         try:
74             login.processAuthnRequestMsg(message)
75         except (lasso.ProfileInvalidMsgError,
76                 lasso.ProfileMissingIssuerError), e:
77
78             msg = 'Malformed Request %r [%r]' % (e, message)
79             raise InvalidRequest(msg)
80
81         except (lasso.ProfileInvalidProtocolprofileError,
82                 lasso.DsError), e:
83
84             msg = 'Invalid SAML Request: %r (%r [%r])' % (login.request,
85                                                           e, message)
86             raise InvalidRequest(msg)
87
88         except (lasso.ServerProviderNotFoundError,
89                 lasso.ProfileUnknownProviderError), e:
90
91             msg = 'Invalid SP [%s] (%r [%r])' % (login.remoteProviderId,
92                                                  e, message)
93             raise UnknownProvider(msg)
94
95         self._debug('SP %s requested authentication' % login.remoteProviderId)
96
97         return login
98
99     def saml2login(self, request):
100
101         if not request:
102             raise cherrypy.HTTPError(400,
103                                      'SAML request token missing or empty')
104
105         try:
106             login = self._parse_request(request)
107         except InvalidRequest, e:
108             self._debug(str(e))
109             raise cherrypy.HTTPError(400, 'Invalid SAML request token')
110         except UnknownProvider, e:
111             self._debug(str(e))
112             raise cherrypy.HTTPError(400, 'Unknown Service Provider')
113         except Exception, e:  # pylint: disable=broad-except
114             self._debug(str(e))
115             raise cherrypy.HTTPError(500)
116
117         return login
118
119     def saml2checks(self, login):
120
121         us = UserSession()
122         user = us.get_user()
123         if user.is_anonymous:
124             if self.stage == 'init':
125                 returl = '%s/saml2/SSO/Continue?%s' % (
126                     self.basepath, self.trans.get_GET_arg())
127                 data = {'saml2_stage': 'auth',
128                         'saml2_request': login.dump(),
129                         'login_return': returl,
130                         'login_target': login.remoteProviderId}
131                 self.trans.store(data)
132                 redirect = '%s/login?%s' % (self.basepath,
133                                             self.trans.get_GET_arg())
134                 raise cherrypy.HTTPRedirect(redirect)
135             else:
136                 raise AuthenticationError(
137                     "Unknown user", lasso.SAML2_STATUS_CODE_AUTHN_FAILED)
138
139         self._audit("Logged in user: %s [%s]" % (user.name, user.fullname))
140
141         # We can wipe the transaction now, as this is the last step
142         self.trans.wipe()
143
144         # TODO: check if this is the first time this user access this SP
145         # If required by user prefs, ask user for consent once and then
146         # record it
147         consent = True
148
149         # TODO: check destination
150
151         try:
152             provider = ServiceProvider(self.cfg, login.remoteProviderId)
153             nameidfmt = provider.get_valid_nameid(login.request.nameIdPolicy)
154         except NameIdNotAllowed, e:
155             raise AuthenticationError(
156                 str(e), lasso.SAML2_STATUS_CODE_INVALID_NAME_ID_POLICY)
157         except InvalidProviderId, e:
158             raise AuthenticationError(
159                 str(e), lasso.SAML2_STATUS_CODE_AUTHN_FAILED)
160
161         # TODO: check login.request.forceAuthn
162
163         login.validateRequestMsg(not user.is_anonymous, consent)
164
165         authtime = datetime.datetime.utcnow()
166         skew = datetime.timedelta(0, 60)
167         authtime_notbefore = authtime - skew
168         authtime_notafter = authtime + skew
169
170         # TODO: get authentication type fnd name format from session
171         # need to save which login manager authenticated and map it to a
172         # saml2 authentication context
173         authn_context = lasso.SAML2_AUTHN_CONTEXT_UNSPECIFIED
174
175         timeformat = '%Y-%m-%dT%H:%M:%SZ'
176         login.buildAssertion(authn_context,
177                              authtime.strftime(timeformat),
178                              None,
179                              authtime_notbefore.strftime(timeformat),
180                              authtime_notafter.strftime(timeformat))
181
182         nameid = None
183         if nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT:
184             # TODO map to something else ?
185             nameid = provider.normalize_username(user.name)
186         elif nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_TRANSIENT:
187             # TODO map to something else ?
188             nameid = provider.normalize_username(user.name)
189         elif nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_KERBEROS:
190             nameid = us.get_data('user', 'krb_principal_name')
191         elif nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_EMAIL:
192             nameid = us.get_user().email
193             if not nameid:
194                 nameid = '%s@%s' % (user.name, self.cfg.default_email_domain)
195
196         if nameid:
197             login.assertion.subject.nameId.format = nameidfmt
198             login.assertion.subject.nameId.content = nameid
199         else:
200             self.trans.wipe()
201             raise AuthenticationError("Unavailable Name ID type",
202                                       lasso.SAML2_STATUS_CODE_AUTHN_FAILED)
203
204         # TODO: filter user attributes as policy requires from 'usersession'
205         if not login.assertion.attributeStatement:
206             attrstat = lasso.Saml2AttributeStatement()
207             login.assertion.attributeStatement = [attrstat]
208         else:
209             attrstat = login.assertion.attributeStatement[0]
210         if not attrstat.attribute:
211             attrstat.attribute = ()
212
213         attributes = us.get_user_attrs()
214
215         for key in attributes:
216             values = attributes[key]
217             if isinstance(values, dict):
218                 continue
219             if not isinstance(values, list):
220                 values = [values]
221             for value in values:
222                 attr = lasso.Saml2Attribute()
223                 attr.name = key
224                 attr.nameFormat = lasso.SAML2_ATTRIBUTE_NAME_FORMAT_BASIC
225                 value = str(value).encode('utf-8')
226                 self.debug('value %s' % value)
227                 node = lasso.MiscTextNode.newWithString(value)
228                 node.textChild = True
229                 attrvalue = lasso.Saml2AttributeValue()
230                 attrvalue.any = [node]
231                 attr.attributeValue = [attrvalue]
232                 attrstat.attribute = attrstat.attribute + (attr,)
233
234         self.debug('Assertion: %s' % login.assertion.dump())
235
236         saml_sessions = us.get_provider_data('saml2')
237         if saml_sessions is None:
238             saml_sessions = SAMLSessionsContainer()
239
240         session = saml_sessions.find_session_by_provider(
241             login.remoteProviderId)
242         if session:
243             # TODO: something...
244             self.debug('Login session for this user already exists!?')
245             session.dump()
246
247         lasso_session = lasso.Session()
248         lasso_session.addAssertion(login.remoteProviderId, login.assertion)
249         saml_sessions.add_session(login.assertion.id,
250                                   login.remoteProviderId,
251                                   lasso_session)
252         us.save_provider_data('saml2', saml_sessions)
253
254     def saml2error(self, login, code, message):
255         status = lasso.Samlp2Status()
256         status.statusCode = lasso.Samlp2StatusCode()
257         status.statusCode.value = lasso.SAML2_STATUS_CODE_RESPONDER
258         status.statusCode.statusCode = lasso.Samlp2StatusCode()
259         status.statusCode.statusCode.value = code
260         login.response.status = status
261
262     def reply(self, login):
263         if login.protocolProfile == lasso.LOGIN_PROTOCOL_PROFILE_BRWS_ART:
264             # TODO
265             raise cherrypy.HTTPError(501)
266         elif login.protocolProfile == lasso.LOGIN_PROTOCOL_PROFILE_BRWS_POST:
267             login.buildAuthnResponseMsg()
268             self._debug('POSTing back to SP [%s]' % (login.msgUrl))
269             context = {
270                 "title": 'Redirecting back to the web application',
271                 "action": login.msgUrl,
272                 "fields": [
273                     [lasso.SAML2_FIELD_RESPONSE, login.msgBody],
274                     [lasso.SAML2_FIELD_RELAYSTATE, login.msgRelayState],
275                 ],
276                 "submit": 'Return to application',
277             }
278             # pylint: disable=star-args
279             return self._template('saml2/post_response.html', **context)
280
281         else:
282             raise cherrypy.HTTPError(500)