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.providers.saml2.sessions import SAMLSessionsContainer
24 from ipsilon.util.user import UserSession
25 from ipsilon.util.trans import Transaction
31 class UnknownProvider(ProviderException):
33 def __init__(self, message):
34 super(UnknownProvider, self).__init__(message)
38 class AuthenticateRequest(ProviderPageBase):
40 def __init__(self, *args, **kwargs):
41 super(AuthenticateRequest, self).__init__(*args, **kwargs)
45 def _preop(self, *args, **kwargs):
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')
56 def pre_GET(self, *args, **kwargs):
57 self._preop(*args, **kwargs)
59 def pre_POST(self, *args, **kwargs):
60 self._preop(*args, **kwargs)
62 def auth(self, login):
64 self.saml2checks(login)
65 except AuthenticationError, e:
66 self.saml2error(login, e.code, e.message)
67 return self.reply(login)
69 def _parse_request(self, message):
71 login = self.cfg.idp.get_login_handler()
74 login.processAuthnRequestMsg(message)
75 except (lasso.ProfileInvalidMsgError,
76 lasso.ProfileMissingIssuerError), e:
78 msg = 'Malformed Request %r [%r]' % (e, message)
79 raise InvalidRequest(msg)
81 except (lasso.ProfileInvalidProtocolprofileError,
84 msg = 'Invalid SAML Request: %r (%r [%r])' % (login.request,
86 raise InvalidRequest(msg)
88 except (lasso.ServerProviderNotFoundError,
89 lasso.ProfileUnknownProviderError), e:
91 msg = 'Invalid SP [%s] (%r [%r])' % (login.remoteProviderId,
93 raise UnknownProvider(msg)
95 self._debug('SP %s requested authentication' % login.remoteProviderId)
99 def saml2login(self, request):
102 raise cherrypy.HTTPError(400,
103 'SAML request token missing or empty')
106 login = self._parse_request(request)
107 except InvalidRequest, e:
109 raise cherrypy.HTTPError(400, 'Invalid SAML request token')
110 except UnknownProvider, e:
112 raise cherrypy.HTTPError(400, 'Unknown Service Provider')
113 except Exception, e: # pylint: disable=broad-except
115 raise cherrypy.HTTPError(500)
119 def saml2checks(self, login):
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)
136 raise AuthenticationError(
137 "Unknown user", lasso.SAML2_STATUS_CODE_AUTHN_FAILED)
139 self._audit("Logged in user: %s [%s]" % (user.name, user.fullname))
141 # We can wipe the transaction now, as this is the last step
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
149 # TODO: check destination
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)
161 # TODO: check login.request.forceAuthn
163 login.validateRequestMsg(not user.is_anonymous, consent)
165 authtime = datetime.datetime.utcnow()
166 skew = datetime.timedelta(0, 60)
167 authtime_notbefore = authtime - skew
168 authtime_notafter = authtime + skew
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
175 timeformat = '%Y-%m-%dT%H:%M:%SZ'
176 login.buildAssertion(authn_context,
177 authtime.strftime(timeformat),
179 authtime_notbefore.strftime(timeformat),
180 authtime_notafter.strftime(timeformat))
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
194 nameid = '%s@%s' % (user.name, self.cfg.default_email_domain)
197 login.assertion.subject.nameId.format = nameidfmt
198 login.assertion.subject.nameId.content = nameid
201 raise AuthenticationError("Unavailable Name ID type",
202 lasso.SAML2_STATUS_CODE_AUTHN_FAILED)
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]
209 attrstat = login.assertion.attributeStatement[0]
210 if not attrstat.attribute:
211 attrstat.attribute = ()
213 attributes = us.get_user_attrs()
215 for key in attributes:
216 values = attributes[key]
217 if isinstance(values, dict):
219 if not isinstance(values, list):
222 attr = lasso.Saml2Attribute()
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,)
234 self.debug('Assertion: %s' % login.assertion.dump())
236 saml_sessions = us.get_provider_data('saml2')
237 if saml_sessions is None:
238 saml_sessions = SAMLSessionsContainer()
240 session = saml_sessions.find_session_by_provider(
241 login.remoteProviderId)
244 self.debug('Login session for this user already exists!?')
247 lasso_session = lasso.Session()
248 lasso_session.addAssertion(login.remoteProviderId, login.assertion)
249 saml_sessions.add_session(login.assertion.id,
250 login.remoteProviderId,
252 us.save_provider_data('saml2', saml_sessions)
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
262 def reply(self, login):
263 if login.protocolProfile == lasso.LOGIN_PROTOCOL_PROFILE_BRWS_ART:
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))
270 "title": 'Redirecting back to the web application',
271 "action": login.msgUrl,
273 [lasso.SAML2_FIELD_RESPONSE, login.msgBody],
274 [lasso.SAML2_FIELD_RELAYSTATE, login.msgRelayState],
276 "submit": 'Return to application',
278 # pylint: disable=star-args
279 return self._template('saml2/post_response.html', **context)
282 raise cherrypy.HTTPError(500)