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 nameid = us.get_data('user', 'gssapi_principal_name')
201 elif nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_EMAIL:
202 nameid = us.get_user().email
204 nameid = '%s@%s' % (user.name, self.cfg.default_email_domain)
205 elif nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_UNSPECIFIED:
206 nameid = provider.normalize_username(user.name)
209 login.assertion.subject.nameId.format = nameidfmt
210 login.assertion.subject.nameId.content = nameid
213 raise AuthenticationError("Unavailable Name ID type",
214 lasso.SAML2_STATUS_CODE_AUTHN_FAILED)
216 # Check attribute policy and perform mapping and filtering.
217 # If the SP has its own mapping or filtering policy use that
218 # instead of the global policy.
219 if (provider.attribute_mappings is not None and
220 len(provider.attribute_mappings) > 0):
221 attribute_mappings = provider.attribute_mappings
223 attribute_mappings = self.cfg.default_attribute_mapping
224 if (provider.allowed_attributes is not None and
225 len(provider.allowed_attributes) > 0):
226 allowed_attributes = provider.allowed_attributes
228 allowed_attributes = self.cfg.default_allowed_attributes
229 self.debug("Allowed attrs: %s" % allowed_attributes)
230 self.debug("Mapping: %s" % attribute_mappings)
231 policy = Policy(attribute_mappings, allowed_attributes)
232 userattrs = us.get_user_attrs()
233 mappedattrs, _ = policy.map_attributes(userattrs)
234 attributes = policy.filter_attributes(mappedattrs)
236 if '_groups' in attributes and 'groups' not in attributes:
237 attributes['groups'] = attributes['_groups']
239 self.debug("%s's attributes: %s" % (user.name, attributes))
241 # The saml-core-2.0-os specification section 2.7.3 requires
242 # the AttributeStatement element to be non-empty.
244 if not login.assertion.attributeStatement:
245 attrstat = lasso.Saml2AttributeStatement()
246 login.assertion.attributeStatement = [attrstat]
248 attrstat = login.assertion.attributeStatement[0]
249 if not attrstat.attribute:
250 attrstat.attribute = ()
252 for key in attributes:
256 values = attributes[key]
257 if isinstance(values, dict):
259 if not isinstance(values, list):
262 attr = lasso.Saml2Attribute()
264 attr.nameFormat = lasso.SAML2_ATTRIBUTE_NAME_FORMAT_BASIC
265 value = str(value).encode('utf-8')
266 self.debug('value %s' % value)
267 node = lasso.MiscTextNode.newWithString(value)
268 node.textChild = True
269 attrvalue = lasso.Saml2AttributeValue()
270 attrvalue.any = [node]
271 attr.attributeValue = [attrvalue]
272 attrstat.attribute = attrstat.attribute + (attr,)
274 self.debug('Assertion: %s' % login.assertion.dump())
276 saml_sessions = us.get_provider_data('saml2')
277 if saml_sessions is None:
278 saml_sessions = SAMLSessionsContainer()
280 session = saml_sessions.find_session_by_provider(
281 login.remoteProviderId)
284 self.debug('Login session for this user already exists!?')
287 lasso_session = lasso.Session()
288 lasso_session.addAssertion(login.remoteProviderId, login.assertion)
289 saml_sessions.add_session(login.assertion.id,
290 login.remoteProviderId,
292 us.save_provider_data('saml2', saml_sessions)
294 def saml2error(self, login, code, message):
295 status = lasso.Samlp2Status()
296 status.statusCode = lasso.Samlp2StatusCode()
297 status.statusCode.value = lasso.SAML2_STATUS_CODE_RESPONDER
298 status.statusCode.statusCode = lasso.Samlp2StatusCode()
299 status.statusCode.statusCode.value = code
300 login.response.status = status
302 def reply(self, login):
303 if login.protocolProfile == lasso.LOGIN_PROTOCOL_PROFILE_BRWS_ART:
305 raise cherrypy.HTTPError(501)
306 elif login.protocolProfile == lasso.LOGIN_PROTOCOL_PROFILE_BRWS_POST:
307 login.buildAuthnResponseMsg()
308 self.debug('POSTing back to SP [%s]' % (login.msgUrl))
310 "title": 'Redirecting back to the web application',
311 "action": login.msgUrl,
313 [lasso.SAML2_FIELD_RESPONSE, login.msgBody],
314 [lasso.SAML2_FIELD_RELAYSTATE, login.msgRelayState],
316 "submit": 'Return to application',
318 # pylint: disable=star-args
319 return self._template('saml2/post_response.html', **context)
322 raise cherrypy.HTTPError(500)