3 # Copyright (C) 2014 Simo Sorce <simo@redhat.com>
5 # see file 'COPYING' for use and warranty information
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.
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.
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/>.
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 from ipsilon.util.trans import Transaction
31 class AuthenticationError(ProviderException):
33 def __init__(self, message, code):
34 super(AuthenticationError, self).__init__(message)
36 self._debug('%s [%s]' % (message, code))
39 class InvalidRequest(ProviderException):
41 def __init__(self, message):
42 super(InvalidRequest, self).__init__(message)
46 class UnknownProvider(ProviderException):
48 def __init__(self, message):
49 super(UnknownProvider, self).__init__(message)
53 class AuthenticateRequest(ProviderPageBase):
55 def __init__(self, *args, **kwargs):
56 super(AuthenticateRequest, self).__init__(*args, **kwargs)
60 def _preop(self, *args, **kwargs):
62 # generate a new id or get current one
63 self.trans = Transaction('saml2', **kwargs)
64 if self.trans.cookie.value != self.trans.provider:
65 self.debug('Invalid transaction, %s != %s' % (
66 self.trans.cookie.value, self.trans.provider))
67 except Exception, e: # pylint: disable=broad-except
68 self.debug('Transaction initialization failed: %s' % repr(e))
69 raise cherrypy.HTTPError(400, 'Invalid transaction id')
71 def pre_GET(self, *args, **kwargs):
72 self._preop(*args, **kwargs)
74 def pre_POST(self, *args, **kwargs):
75 self._preop(*args, **kwargs)
77 def auth(self, login):
79 self.saml2checks(login)
80 except AuthenticationError, e:
81 self.saml2error(login, e.code, e.message)
82 return self.reply(login)
84 def _parse_request(self, message):
86 login = self.cfg.idp.get_login_handler()
89 login.processAuthnRequestMsg(message)
90 except (lasso.ProfileInvalidMsgError,
91 lasso.ProfileMissingIssuerError), e:
93 msg = 'Malformed Request %r [%r]' % (e, message)
94 raise InvalidRequest(msg)
96 except (lasso.ProfileInvalidProtocolprofileError,
99 msg = 'Invalid SAML Request: %r (%r [%r])' % (login.request,
101 raise InvalidRequest(msg)
103 except (lasso.ServerProviderNotFoundError,
104 lasso.ProfileUnknownProviderError), e:
106 msg = 'Invalid SP [%s] (%r [%r])' % (login.remoteProviderId,
108 raise UnknownProvider(msg)
110 self._debug('SP %s requested authentication' % login.remoteProviderId)
114 def saml2login(self, request):
117 raise cherrypy.HTTPError(400,
118 'SAML request token missing or empty')
121 login = self._parse_request(request)
122 except InvalidRequest, e:
124 raise cherrypy.HTTPError(400, 'Invalid SAML request token')
125 except UnknownProvider, e:
127 raise cherrypy.HTTPError(400, 'Unknown Service Provider')
128 except Exception, e: # pylint: disable=broad-except
130 raise cherrypy.HTTPError(500)
134 def saml2checks(self, login):
138 if user.is_anonymous:
139 if self.stage == 'init':
140 returl = '%s/saml2/SSO/Continue?%s' % (
141 self.basepath, self.trans.get_GET_arg())
142 data = {'saml2_stage': 'auth',
143 'saml2_request': login.dump(),
144 'login_return': returl}
145 self.trans.store(data)
146 redirect = '%s/login?%s' % (self.basepath,
147 self.trans.get_GET_arg())
148 raise cherrypy.HTTPRedirect(redirect)
150 raise AuthenticationError(
151 "Unknown user", lasso.SAML2_STATUS_CODE_AUTHN_FAILED)
153 self._audit("Logged in user: %s [%s]" % (user.name, user.fullname))
155 # We can wipe the transaction now, as this is the last step
158 # TODO: check if this is the first time this user access this SP
159 # If required by user prefs, ask user for consent once and then
163 # TODO: check destination
166 provider = ServiceProvider(self.cfg, login.remoteProviderId)
167 nameidfmt = provider.get_valid_nameid(login.request.nameIdPolicy)
168 except NameIdNotAllowed, e:
169 raise AuthenticationError(
170 str(e), lasso.SAML2_STATUS_CODE_INVALID_NAME_ID_POLICY)
171 except InvalidProviderId, e:
172 raise AuthenticationError(
173 str(e), lasso.SAML2_STATUS_CODE_AUTHN_FAILED)
175 # TODO: check login.request.forceAuthn
177 login.validateRequestMsg(not user.is_anonymous, consent)
179 authtime = datetime.datetime.utcnow()
180 skew = datetime.timedelta(0, 60)
181 authtime_notbefore = authtime - skew
182 authtime_notafter = authtime + skew
184 # TODO: get authentication type fnd name format from session
185 # need to save which login manager authenticated and map it to a
186 # saml2 authentication context
187 authn_context = lasso.SAML2_AUTHN_CONTEXT_UNSPECIFIED
189 timeformat = '%Y-%m-%dT%H:%M:%SZ'
190 login.buildAssertion(authn_context,
191 authtime.strftime(timeformat),
193 authtime_notbefore.strftime(timeformat),
194 authtime_notafter.strftime(timeformat))
197 if nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT:
198 # TODO map to something else ?
199 nameid = provider.normalize_username(user.name)
200 elif nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_TRANSIENT:
201 # TODO map to something else ?
202 nameid = provider.normalize_username(user.name)
203 elif nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_KERBEROS:
204 nameid = us.get_data('user', 'krb_principal_name')
205 elif nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_EMAIL:
206 nameid = us.get_user().email
208 nameid = '%s@%s' % (user.name, self.cfg.default_email_domain)
211 login.assertion.subject.nameId.format = nameidfmt
212 login.assertion.subject.nameId.content = nameid
215 raise AuthenticationError("Unavailable Name ID type",
216 lasso.SAML2_STATUS_CODE_AUTHN_FAILED)
218 # TODO: filter user attributes as policy requires from 'usersession'
219 if not login.assertion.attributeStatement:
220 attrstat = lasso.Saml2AttributeStatement()
221 login.assertion.attributeStatement = [attrstat]
223 attrstat = login.assertion.attributeStatement[0]
224 if not attrstat.attribute:
225 attrstat.attribute = ()
227 attributes = us.get_user_attrs()
228 for key in attributes:
229 attr = lasso.Saml2Attribute()
231 attr.nameFormat = lasso.SAML2_ATTRIBUTE_NAME_FORMAT_BASIC
232 value = str(attributes[key]).encode('utf-8')
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)