1 # Copyright (C) 2014 Simo Sorce <simo@redhat.com>
3 # see file 'COPYING' for use and warranty information
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.
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.
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/>.
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.util.user import UserSession
24 from ipsilon.util.trans import Transaction
30 class UnknownProvider(ProviderException):
32 def __init__(self, message):
33 super(UnknownProvider, self).__init__(message)
37 class AuthenticateRequest(ProviderPageBase):
39 def __init__(self, *args, **kwargs):
40 super(AuthenticateRequest, self).__init__(*args, **kwargs)
44 def _preop(self, *args, **kwargs):
46 # generate a new id or get current one
47 self.trans = Transaction('saml2', **kwargs)
48 if self.trans.cookie.value != self.trans.provider:
49 self.debug('Invalid transaction, %s != %s' % (
50 self.trans.cookie.value, self.trans.provider))
51 except Exception, e: # pylint: disable=broad-except
52 self.debug('Transaction initialization failed: %s' % repr(e))
53 raise cherrypy.HTTPError(400, 'Invalid transaction id')
55 def pre_GET(self, *args, **kwargs):
56 self._preop(*args, **kwargs)
58 def pre_POST(self, *args, **kwargs):
59 self._preop(*args, **kwargs)
61 def auth(self, login):
63 self.saml2checks(login)
64 except AuthenticationError, e:
65 self.saml2error(login, e.code, e.message)
66 return self.reply(login)
68 def _parse_request(self, message):
70 login = self.cfg.idp.get_login_handler()
73 login.processAuthnRequestMsg(message)
74 except (lasso.ProfileInvalidMsgError,
75 lasso.ProfileMissingIssuerError), e:
77 msg = 'Malformed Request %r [%r]' % (e, message)
78 raise InvalidRequest(msg)
80 except (lasso.ProfileInvalidProtocolprofileError,
83 msg = 'Invalid SAML Request: %r (%r [%r])' % (login.request,
85 raise InvalidRequest(msg)
87 except (lasso.ServerProviderNotFoundError,
88 lasso.ProfileUnknownProviderError), e:
90 msg = 'Invalid SP [%s] (%r [%r])' % (login.remoteProviderId,
92 raise UnknownProvider(msg)
94 self._debug('SP %s requested authentication' % login.remoteProviderId)
98 def saml2login(self, request):
101 raise cherrypy.HTTPError(400,
102 'SAML request token missing or empty')
105 login = self._parse_request(request)
106 except InvalidRequest, e:
108 raise cherrypy.HTTPError(400, 'Invalid SAML request token')
109 except UnknownProvider, e:
111 raise cherrypy.HTTPError(400, 'Unknown Service Provider')
112 except Exception, e: # pylint: disable=broad-except
114 raise cherrypy.HTTPError(500)
118 def saml2checks(self, login):
122 if user.is_anonymous:
123 if self.stage == 'init':
124 returl = '%s/saml2/SSO/Continue?%s' % (
125 self.basepath, self.trans.get_GET_arg())
126 data = {'saml2_stage': 'auth',
127 'saml2_request': login.dump(),
128 'login_return': returl,
129 'login_target': login.remoteProviderId}
130 self.trans.store(data)
131 redirect = '%s/login?%s' % (self.basepath,
132 self.trans.get_GET_arg())
133 raise cherrypy.HTTPRedirect(redirect)
135 raise AuthenticationError(
136 "Unknown user", lasso.SAML2_STATUS_CODE_AUTHN_FAILED)
138 self._audit("Logged in user: %s [%s]" % (user.name, user.fullname))
140 # We can wipe the transaction now, as this is the last step
143 # TODO: check if this is the first time this user access this SP
144 # If required by user prefs, ask user for consent once and then
148 # TODO: check destination
151 provider = ServiceProvider(self.cfg, login.remoteProviderId)
152 nameidfmt = provider.get_valid_nameid(login.request.nameIdPolicy)
153 except NameIdNotAllowed, e:
154 raise AuthenticationError(
155 str(e), lasso.SAML2_STATUS_CODE_INVALID_NAME_ID_POLICY)
156 except InvalidProviderId, e:
157 raise AuthenticationError(
158 str(e), lasso.SAML2_STATUS_CODE_AUTHN_FAILED)
160 # TODO: check login.request.forceAuthn
162 login.validateRequestMsg(not user.is_anonymous, consent)
164 authtime = datetime.datetime.utcnow()
165 skew = datetime.timedelta(0, 60)
166 authtime_notbefore = authtime - skew
167 authtime_notafter = authtime + skew
169 # TODO: get authentication type fnd name format from session
170 # need to save which login manager authenticated and map it to a
171 # saml2 authentication context
172 authn_context = lasso.SAML2_AUTHN_CONTEXT_UNSPECIFIED
174 timeformat = '%Y-%m-%dT%H:%M:%SZ'
175 login.buildAssertion(authn_context,
176 authtime.strftime(timeformat),
178 authtime_notbefore.strftime(timeformat),
179 authtime_notafter.strftime(timeformat))
182 if nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT:
183 # TODO map to something else ?
184 nameid = provider.normalize_username(user.name)
185 elif nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_TRANSIENT:
186 # TODO map to something else ?
187 nameid = provider.normalize_username(user.name)
188 elif nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_KERBEROS:
189 nameid = us.get_data('user', 'krb_principal_name')
190 elif nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_EMAIL:
191 nameid = us.get_user().email
193 nameid = '%s@%s' % (user.name, self.cfg.default_email_domain)
196 login.assertion.subject.nameId.format = nameidfmt
197 login.assertion.subject.nameId.content = nameid
200 raise AuthenticationError("Unavailable Name ID type",
201 lasso.SAML2_STATUS_CODE_AUTHN_FAILED)
203 # TODO: filter user attributes as policy requires from 'usersession'
204 if not login.assertion.attributeStatement:
205 attrstat = lasso.Saml2AttributeStatement()
206 login.assertion.attributeStatement = [attrstat]
208 attrstat = login.assertion.attributeStatement[0]
209 if not attrstat.attribute:
210 attrstat.attribute = ()
213 userattrs = us.get_user_attrs()
214 for key, value in userattrs.get('userdata', {}).iteritems():
215 if type(value) is str:
216 attributes[key] = value
217 if 'groups' in userattrs:
218 attributes['group'] = userattrs['groups']
219 for _, info in userattrs.get('extras', {}).iteritems():
220 for key, value in info.items():
221 attributes[key] = value
223 for key in attributes:
224 values = attributes[key]
225 if type(values) is not list:
228 attr = lasso.Saml2Attribute()
230 attr.nameFormat = lasso.SAML2_ATTRIBUTE_NAME_FORMAT_BASIC
231 value = str(value).encode('utf-8')
232 self.debug('value %s' % value)
233 node = lasso.MiscTextNode.newWithString(value)
234 node.textChild = True
235 attrvalue = lasso.Saml2AttributeValue()
236 attrvalue.any = [node]
237 attr.attributeValue = [attrvalue]
238 attrstat.attribute = attrstat.attribute + (attr,)
240 self.debug('Assertion: %s' % login.assertion.dump())
242 def saml2error(self, login, code, message):
243 status = lasso.Samlp2Status()
244 status.statusCode = lasso.Samlp2StatusCode()
245 status.statusCode.value = lasso.SAML2_STATUS_CODE_RESPONDER
246 status.statusCode.statusCode = lasso.Samlp2StatusCode()
247 status.statusCode.statusCode.value = code
248 login.response.status = status
250 def reply(self, login):
251 if login.protocolProfile == lasso.LOGIN_PROTOCOL_PROFILE_BRWS_ART:
253 raise cherrypy.HTTPError(501)
254 elif login.protocolProfile == lasso.LOGIN_PROTOCOL_PROFILE_BRWS_POST:
255 login.buildAuthnResponseMsg()
256 self._debug('POSTing back to SP [%s]' % (login.msgUrl))
258 "title": 'Redirecting back to the web application',
259 "action": login.msgUrl,
261 [lasso.SAML2_FIELD_RESPONSE, login.msgBody],
262 [lasso.SAML2_FIELD_RELAYSTATE, login.msgRelayState],
264 "submit": 'Return to application',
266 # pylint: disable=star-args
267 return self._template('saml2/post_response.html', **context)
270 raise cherrypy.HTTPError(500)