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.policy import Policy
25 from ipsilon.util.user import UserSession
26 from ipsilon.util.trans import Transaction
33 class UnknownProvider(ProviderException):
35 def __init__(self, message):
36 super(UnknownProvider, self).__init__(message)
40 class AuthenticateRequest(ProviderPageBase):
42 def __init__(self, *args, **kwargs):
43 super(AuthenticateRequest, self).__init__(*args, **kwargs)
47 def _preop(self, *args, **kwargs):
49 # generate a new id or get current one
50 self.trans = Transaction('saml2', **kwargs)
51 if self.trans.cookie.value != self.trans.provider:
52 self.debug('Invalid transaction, %s != %s' % (
53 self.trans.cookie.value, self.trans.provider))
54 except Exception, e: # pylint: disable=broad-except
55 self.debug('Transaction initialization failed: %s' % repr(e))
56 raise cherrypy.HTTPError(400, 'Invalid transaction id')
58 def pre_GET(self, *args, **kwargs):
59 self._preop(*args, **kwargs)
61 def pre_POST(self, *args, **kwargs):
62 self._preop(*args, **kwargs)
64 def auth(self, login):
66 self.saml2checks(login)
67 except AuthenticationError, e:
68 self.saml2error(login, e.code, e.message)
69 return self.reply(login)
71 def _parse_request(self, message):
73 login = self.cfg.idp.get_login_handler()
76 login.processAuthnRequestMsg(message)
77 except (lasso.ProfileInvalidMsgError,
78 lasso.ProfileMissingIssuerError), e:
80 msg = 'Malformed Request %r [%r]' % (e, message)
81 raise InvalidRequest(msg)
83 except (lasso.ProfileInvalidProtocolprofileError,
86 msg = 'Invalid SAML Request: %r (%r [%r])' % (login.request,
88 raise InvalidRequest(msg)
90 except (lasso.ServerProviderNotFoundError,
91 lasso.ProfileUnknownProviderError), e:
93 msg = 'Invalid SP [%s] (%r [%r])' % (login.remoteProviderId,
95 raise UnknownProvider(msg)
97 self._debug('SP %s requested authentication' % login.remoteProviderId)
101 def saml2login(self, request):
104 raise cherrypy.HTTPError(400,
105 'SAML request token missing or empty')
108 login = self._parse_request(request)
109 except InvalidRequest, e:
111 raise cherrypy.HTTPError(400, 'Invalid SAML request token')
112 except UnknownProvider, e:
114 raise cherrypy.HTTPError(400, 'Unknown Service Provider')
115 except Exception, e: # pylint: disable=broad-except
117 raise cherrypy.HTTPError(500)
121 def saml2checks(self, login):
125 if user.is_anonymous:
126 if self.stage == 'init':
127 returl = '%s/saml2/SSO/Continue?%s' % (
128 self.basepath, self.trans.get_GET_arg())
129 data = {'saml2_stage': 'auth',
130 'saml2_request': login.dump(),
131 'login_return': returl,
132 'login_target': login.remoteProviderId}
133 self.trans.store(data)
134 redirect = '%s/login?%s' % (self.basepath,
135 self.trans.get_GET_arg())
136 raise cherrypy.HTTPRedirect(redirect)
138 raise AuthenticationError(
139 "Unknown user", lasso.SAML2_STATUS_CODE_AUTHN_FAILED)
141 self._audit("Logged in user: %s [%s]" % (user.name, user.fullname))
143 # We can wipe the transaction now, as this is the last step
146 # TODO: check if this is the first time this user access this SP
147 # If required by user prefs, ask user for consent once and then
151 # TODO: check destination
154 provider = ServiceProvider(self.cfg, login.remoteProviderId)
155 nameidfmt = provider.get_valid_nameid(login.request.nameIdPolicy)
156 except NameIdNotAllowed, e:
157 raise AuthenticationError(
158 str(e), lasso.SAML2_STATUS_CODE_INVALID_NAME_ID_POLICY)
159 except InvalidProviderId, e:
160 raise AuthenticationError(
161 str(e), lasso.SAML2_STATUS_CODE_AUTHN_FAILED)
163 # TODO: check login.request.forceAuthn
165 login.validateRequestMsg(not user.is_anonymous, consent)
167 authtime = datetime.datetime.utcnow()
168 skew = datetime.timedelta(0, 60)
169 authtime_notbefore = authtime - skew
170 authtime_notafter = authtime + skew
172 # TODO: get authentication type fnd name format from session
173 # need to save which login manager authenticated and map it to a
174 # saml2 authentication context
175 authn_context = lasso.SAML2_AUTHN_CONTEXT_UNSPECIFIED
177 timeformat = '%Y-%m-%dT%H:%M:%SZ'
178 login.buildAssertion(authn_context,
179 authtime.strftime(timeformat),
181 authtime_notbefore.strftime(timeformat),
182 authtime_notafter.strftime(timeformat))
185 if nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT:
186 # TODO map to something else ?
187 nameid = provider.normalize_username(user.name)
188 elif nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_TRANSIENT:
189 nameid = '_' + uuid.uuid4().hex
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
195 nameid = '%s@%s' % (user.name, self.cfg.default_email_domain)
198 login.assertion.subject.nameId.format = nameidfmt
199 login.assertion.subject.nameId.content = nameid
202 raise AuthenticationError("Unavailable Name ID type",
203 lasso.SAML2_STATUS_CODE_AUTHN_FAILED)
205 # Check attribute policy and perform mapping and filtering
206 policy = Policy(self.cfg.default_attribute_mapping,
207 self.cfg.default_allowed_attributes)
208 userattrs = us.get_user_attrs()
209 mappedattrs, _ = policy.map_attributes(userattrs)
210 attributes = policy.filter_attributes(mappedattrs)
212 if '_groups' in attributes and 'groups' not in attributes:
213 attributes['groups'] = attributes['_groups']
215 self.debug("%s's attributes: %s" % (user.name, attributes))
217 # The saml-core-2.0-os specification section 2.7.3 requires
218 # the AttributeStatement element to be non-empty.
220 if not login.assertion.attributeStatement:
221 attrstat = lasso.Saml2AttributeStatement()
222 login.assertion.attributeStatement = [attrstat]
224 attrstat = login.assertion.attributeStatement[0]
225 if not attrstat.attribute:
226 attrstat.attribute = ()
228 for key in attributes:
232 values = attributes[key]
233 if isinstance(values, dict):
235 if not isinstance(values, list):
238 attr = lasso.Saml2Attribute()
240 attr.nameFormat = lasso.SAML2_ATTRIBUTE_NAME_FORMAT_BASIC
241 value = str(value).encode('utf-8')
242 self.debug('value %s' % value)
243 node = lasso.MiscTextNode.newWithString(value)
244 node.textChild = True
245 attrvalue = lasso.Saml2AttributeValue()
246 attrvalue.any = [node]
247 attr.attributeValue = [attrvalue]
248 attrstat.attribute = attrstat.attribute + (attr,)
250 self.debug('Assertion: %s' % login.assertion.dump())
252 saml_sessions = us.get_provider_data('saml2')
253 if saml_sessions is None:
254 saml_sessions = SAMLSessionsContainer()
256 session = saml_sessions.find_session_by_provider(
257 login.remoteProviderId)
260 self.debug('Login session for this user already exists!?')
263 lasso_session = lasso.Session()
264 lasso_session.addAssertion(login.remoteProviderId, login.assertion)
265 saml_sessions.add_session(login.assertion.id,
266 login.remoteProviderId,
268 us.save_provider_data('saml2', saml_sessions)
270 def saml2error(self, login, code, message):
271 status = lasso.Samlp2Status()
272 status.statusCode = lasso.Samlp2StatusCode()
273 status.statusCode.value = lasso.SAML2_STATUS_CODE_RESPONDER
274 status.statusCode.statusCode = lasso.Samlp2StatusCode()
275 status.statusCode.statusCode.value = code
276 login.response.status = status
278 def reply(self, login):
279 if login.protocolProfile == lasso.LOGIN_PROTOCOL_PROFILE_BRWS_ART:
281 raise cherrypy.HTTPError(501)
282 elif login.protocolProfile == lasso.LOGIN_PROTOCOL_PROFILE_BRWS_POST:
283 login.buildAuthnResponseMsg()
284 self._debug('POSTing back to SP [%s]' % (login.msgUrl))
286 "title": 'Redirecting back to the web application',
287 "action": login.msgUrl,
289 [lasso.SAML2_FIELD_RESPONSE, login.msgBody],
290 [lasso.SAML2_FIELD_RELAYSTATE, login.msgRelayState],
292 "submit": 'Return to application',
294 # pylint: disable=star-args
295 return self._template('saml2/post_response.html', **context)
298 raise cherrypy.HTTPError(500)