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
34 class UnknownProvider(ProviderException):
36 def __init__(self, message):
37 super(UnknownProvider, self).__init__(message)
41 class AuthenticateRequest(ProviderPageBase):
43 def __init__(self, *args, **kwargs):
44 super(AuthenticateRequest, self).__init__(*args, **kwargs)
48 def _preop(self, *args, **kwargs):
50 # generate a new id or get current one
51 self.trans = Transaction('saml2', **kwargs)
52 if self.trans.cookie.value != self.trans.provider:
53 self.debug('Invalid transaction, %s != %s' % (
54 self.trans.cookie.value, self.trans.provider))
55 except Exception, e: # pylint: disable=broad-except
56 self.debug('Transaction initialization failed: %s' % repr(e))
57 raise cherrypy.HTTPError(400, 'Invalid transaction id')
59 def pre_GET(self, *args, **kwargs):
60 self._preop(*args, **kwargs)
62 def pre_POST(self, *args, **kwargs):
63 self._preop(*args, **kwargs)
65 def auth(self, login):
67 self.saml2checks(login)
68 except AuthenticationError, e:
69 self.saml2error(login, e.code, e.message)
70 return self.reply(login)
72 def _parse_request(self, message):
74 login = self.cfg.idp.get_login_handler()
77 login.processAuthnRequestMsg(message)
78 except (lasso.ProfileInvalidMsgError,
79 lasso.ProfileMissingIssuerError), e:
81 msg = 'Malformed Request %r [%r]' % (e, message)
82 raise InvalidRequest(msg)
84 except (lasso.ProfileInvalidProtocolprofileError,
87 msg = 'Invalid SAML Request: %r (%r [%r])' % (login.request,
89 raise InvalidRequest(msg)
91 except (lasso.ServerProviderNotFoundError,
92 lasso.ProfileUnknownProviderError), e:
94 msg = 'Invalid SP [%s] (%r [%r])' % (login.remoteProviderId,
96 raise UnknownProvider(msg)
98 self.debug('SP %s requested authentication' % login.remoteProviderId)
102 def saml2login(self, request):
105 raise cherrypy.HTTPError(400,
106 'SAML request token missing or empty')
109 login = self._parse_request(request)
110 except InvalidRequest, e:
112 raise cherrypy.HTTPError(400, 'Invalid SAML request token')
113 except UnknownProvider, e:
115 raise cherrypy.HTTPError(400, 'Unknown Service Provider')
116 except Exception, e: # pylint: disable=broad-except
118 raise cherrypy.HTTPError(500)
122 def saml2checks(self, login):
126 if user.is_anonymous:
127 if self.stage == 'init':
128 returl = '%s/saml2/SSO/Continue?%s' % (
129 self.basepath, self.trans.get_GET_arg())
130 data = {'saml2_stage': 'auth',
131 'saml2_request': login.dump(),
132 'login_return': returl,
133 'login_target': login.remoteProviderId}
134 self.trans.store(data)
135 redirect = '%s/login?%s' % (self.basepath,
136 self.trans.get_GET_arg())
137 raise cherrypy.HTTPRedirect(redirect)
139 raise AuthenticationError(
140 "Unknown user", lasso.SAML2_STATUS_CODE_AUTHN_FAILED)
142 self._audit("Logged in user: %s [%s]" % (user.name, user.fullname))
144 # We can wipe the transaction now, as this is the last step
147 # TODO: check if this is the first time this user access this SP
148 # If required by user prefs, ask user for consent once and then
152 # TODO: check destination
155 provider = ServiceProvider(self.cfg, login.remoteProviderId)
156 nameidfmt = provider.get_valid_nameid(login.request.nameIdPolicy)
157 except NameIdNotAllowed, e:
158 raise AuthenticationError(
159 str(e), lasso.SAML2_STATUS_CODE_INVALID_NAME_ID_POLICY)
160 except InvalidProviderId, e:
161 raise AuthenticationError(
162 str(e), lasso.SAML2_STATUS_CODE_AUTHN_FAILED)
164 # TODO: check login.request.forceAuthn
166 login.validateRequestMsg(not user.is_anonymous, consent)
168 authtime = datetime.datetime.utcnow()
169 skew = datetime.timedelta(0, 60)
170 authtime_notbefore = authtime - skew
171 authtime_notafter = authtime + skew
173 # TODO: get authentication type fnd name format from session
174 # need to save which login manager authenticated and map it to a
175 # saml2 authentication context
176 authn_context = lasso.SAML2_AUTHN_CONTEXT_UNSPECIFIED
178 timeformat = '%Y-%m-%dT%H:%M:%SZ'
179 login.buildAssertion(authn_context,
180 authtime.strftime(timeformat),
182 authtime_notbefore.strftime(timeformat),
183 authtime_notafter.strftime(timeformat))
186 if nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT:
187 idpsalt = self.cfg.idp_nameid_salt
189 raise AuthenticationError(
190 "idp nameid salt is not set in configuration"
192 value = hashlib.sha512()
193 value.update(idpsalt)
194 value.update(login.remoteProviderId)
195 value.update(user.name)
196 nameid = '_' + value.hexdigest()
197 elif nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_TRANSIENT:
198 nameid = '_' + uuid.uuid4().hex
199 elif nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_KERBEROS:
200 userattrs = us.get_user_attrs()
201 nameid = userattrs.get('gssapi_principal_name')
202 elif nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_EMAIL:
203 nameid = us.get_user().email
205 nameid = '%s@%s' % (user.name, self.cfg.default_email_domain)
206 elif nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_UNSPECIFIED:
207 nameid = provider.normalize_username(user.name)
210 login.assertion.subject.nameId.format = nameidfmt
211 login.assertion.subject.nameId.content = nameid
214 raise AuthenticationError("Unavailable Name ID type",
215 lasso.SAML2_STATUS_CODE_AUTHN_FAILED)
217 # Check attribute policy and perform mapping and filtering.
218 # If the SP has its own mapping or filtering policy use that
219 # instead of the global policy.
220 if (provider.attribute_mappings is not None and
221 len(provider.attribute_mappings) > 0):
222 attribute_mappings = provider.attribute_mappings
224 attribute_mappings = self.cfg.default_attribute_mapping
225 if (provider.allowed_attributes is not None and
226 len(provider.allowed_attributes) > 0):
227 allowed_attributes = provider.allowed_attributes
229 allowed_attributes = self.cfg.default_allowed_attributes
230 self.debug("Allowed attrs: %s" % allowed_attributes)
231 self.debug("Mapping: %s" % attribute_mappings)
232 policy = Policy(attribute_mappings, allowed_attributes)
233 userattrs = us.get_user_attrs()
234 mappedattrs, _ = policy.map_attributes(userattrs)
235 attributes = policy.filter_attributes(mappedattrs)
237 if '_groups' in attributes and 'groups' not in attributes:
238 attributes['groups'] = attributes['_groups']
240 self.debug("%s's attributes: %s" % (user.name, attributes))
242 # The saml-core-2.0-os specification section 2.7.3 requires
243 # the AttributeStatement element to be non-empty.
245 if not login.assertion.attributeStatement:
246 attrstat = lasso.Saml2AttributeStatement()
247 login.assertion.attributeStatement = [attrstat]
249 attrstat = login.assertion.attributeStatement[0]
250 if not attrstat.attribute:
251 attrstat.attribute = ()
253 for key in attributes:
257 values = attributes[key]
258 if isinstance(values, dict):
260 if not isinstance(values, list):
263 attr = lasso.Saml2Attribute()
265 attr.nameFormat = lasso.SAML2_ATTRIBUTE_NAME_FORMAT_BASIC
266 value = str(value).encode('utf-8')
267 self.debug('value %s' % value)
268 node = lasso.MiscTextNode.newWithString(value)
269 node.textChild = True
270 attrvalue = lasso.Saml2AttributeValue()
271 attrvalue.any = [node]
272 attr.attributeValue = [attrvalue]
273 attrstat.attribute = attrstat.attribute + (attr,)
275 self.debug('Assertion: %s' % login.assertion.dump())
277 saml_sessions = us.get_provider_data('saml2')
278 if saml_sessions is None:
279 saml_sessions = SAMLSessionsContainer()
281 session = saml_sessions.find_session_by_provider(
282 login.remoteProviderId)
285 self.debug('Login session for this user already exists!?')
288 lasso_session = lasso.Session()
289 lasso_session.addAssertion(login.remoteProviderId, login.assertion)
290 saml_sessions.add_session(login.assertion.id,
291 login.remoteProviderId,
293 us.save_provider_data('saml2', saml_sessions)
295 def saml2error(self, login, code, message):
296 status = lasso.Samlp2Status()
297 status.statusCode = lasso.Samlp2StatusCode()
298 status.statusCode.value = lasso.SAML2_STATUS_CODE_RESPONDER
299 status.statusCode.statusCode = lasso.Samlp2StatusCode()
300 status.statusCode.statusCode.value = code
301 login.response.status = status
303 def reply(self, login):
304 if login.protocolProfile == lasso.LOGIN_PROTOCOL_PROFILE_BRWS_ART:
306 raise cherrypy.HTTPError(501)
307 elif login.protocolProfile == lasso.LOGIN_PROTOCOL_PROFILE_BRWS_POST:
308 login.buildAuthnResponseMsg()
309 self.debug('POSTing back to SP [%s]' % (login.msgUrl))
311 "title": 'Redirecting back to the web application',
312 "action": login.msgUrl,
314 [lasso.SAML2_FIELD_RESPONSE, login.msgBody],
315 [lasso.SAML2_FIELD_RELAYSTATE, login.msgRelayState],
317 "submit": 'Return to application',
319 # pylint: disable=star-args
320 return self._template('saml2/post_response.html', **context)
323 raise cherrypy.HTTPError(500)