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', 'krb_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 policy = Policy(attribute_mappings, allowed_attributes)
230 userattrs = us.get_user_attrs()
231 mappedattrs, _ = policy.map_attributes(userattrs)
232 attributes = policy.filter_attributes(mappedattrs)
234 if '_groups' in attributes and 'groups' not in attributes:
235 attributes['groups'] = attributes['_groups']
237 self.debug("%s's attributes: %s" % (user.name, attributes))
239 # The saml-core-2.0-os specification section 2.7.3 requires
240 # the AttributeStatement element to be non-empty.
242 if not login.assertion.attributeStatement:
243 attrstat = lasso.Saml2AttributeStatement()
244 login.assertion.attributeStatement = [attrstat]
246 attrstat = login.assertion.attributeStatement[0]
247 if not attrstat.attribute:
248 attrstat.attribute = ()
250 for key in attributes:
254 values = attributes[key]
255 if isinstance(values, dict):
257 if not isinstance(values, list):
260 attr = lasso.Saml2Attribute()
262 attr.nameFormat = lasso.SAML2_ATTRIBUTE_NAME_FORMAT_BASIC
263 value = str(value).encode('utf-8')
264 self.debug('value %s' % value)
265 node = lasso.MiscTextNode.newWithString(value)
266 node.textChild = True
267 attrvalue = lasso.Saml2AttributeValue()
268 attrvalue.any = [node]
269 attr.attributeValue = [attrvalue]
270 attrstat.attribute = attrstat.attribute + (attr,)
272 self.debug('Assertion: %s' % login.assertion.dump())
274 saml_sessions = us.get_provider_data('saml2')
275 if saml_sessions is None:
276 saml_sessions = SAMLSessionsContainer()
278 session = saml_sessions.find_session_by_provider(
279 login.remoteProviderId)
282 self.debug('Login session for this user already exists!?')
285 lasso_session = lasso.Session()
286 lasso_session.addAssertion(login.remoteProviderId, login.assertion)
287 saml_sessions.add_session(login.assertion.id,
288 login.remoteProviderId,
290 us.save_provider_data('saml2', saml_sessions)
292 def saml2error(self, login, code, message):
293 status = lasso.Samlp2Status()
294 status.statusCode = lasso.Samlp2StatusCode()
295 status.statusCode.value = lasso.SAML2_STATUS_CODE_RESPONDER
296 status.statusCode.statusCode = lasso.Samlp2StatusCode()
297 status.statusCode.statusCode.value = code
298 login.response.status = status
300 def reply(self, login):
301 if login.protocolProfile == lasso.LOGIN_PROTOCOL_PROFILE_BRWS_ART:
303 raise cherrypy.HTTPError(501)
304 elif login.protocolProfile == lasso.LOGIN_PROTOCOL_PROFILE_BRWS_POST:
305 login.buildAuthnResponseMsg()
306 self._debug('POSTing back to SP [%s]' % (login.msgUrl))
308 "title": 'Redirecting back to the web application',
309 "action": login.msgUrl,
311 [lasso.SAML2_FIELD_RESPONSE, login.msgBody],
312 [lasso.SAML2_FIELD_RELAYSTATE, login.msgRelayState],
314 "submit": 'Return to application',
316 # pylint: disable=star-args
317 return self._template('saml2/post_response.html', **context)
320 raise cherrypy.HTTPError(500)