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