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 policy = Policy(self.cfg.default_attribute_mapping,
218 self.cfg.default_allowed_attributes)
219 userattrs = us.get_user_attrs()
220 mappedattrs, _ = policy.map_attributes(userattrs)
221 attributes = policy.filter_attributes(mappedattrs)
223 if '_groups' in attributes and 'groups' not in attributes:
224 attributes['groups'] = attributes['_groups']
226 self.debug("%s's attributes: %s" % (user.name, attributes))
228 # The saml-core-2.0-os specification section 2.7.3 requires
229 # the AttributeStatement element to be non-empty.
231 if not login.assertion.attributeStatement:
232 attrstat = lasso.Saml2AttributeStatement()
233 login.assertion.attributeStatement = [attrstat]
235 attrstat = login.assertion.attributeStatement[0]
236 if not attrstat.attribute:
237 attrstat.attribute = ()
239 for key in attributes:
243 values = attributes[key]
244 if isinstance(values, dict):
246 if not isinstance(values, list):
249 attr = lasso.Saml2Attribute()
251 attr.nameFormat = lasso.SAML2_ATTRIBUTE_NAME_FORMAT_BASIC
252 value = str(value).encode('utf-8')
253 self.debug('value %s' % value)
254 node = lasso.MiscTextNode.newWithString(value)
255 node.textChild = True
256 attrvalue = lasso.Saml2AttributeValue()
257 attrvalue.any = [node]
258 attr.attributeValue = [attrvalue]
259 attrstat.attribute = attrstat.attribute + (attr,)
261 self.debug('Assertion: %s' % login.assertion.dump())
263 saml_sessions = us.get_provider_data('saml2')
264 if saml_sessions is None:
265 saml_sessions = SAMLSessionsContainer()
267 session = saml_sessions.find_session_by_provider(
268 login.remoteProviderId)
271 self.debug('Login session for this user already exists!?')
274 lasso_session = lasso.Session()
275 lasso_session.addAssertion(login.remoteProviderId, login.assertion)
276 saml_sessions.add_session(login.assertion.id,
277 login.remoteProviderId,
279 us.save_provider_data('saml2', saml_sessions)
281 def saml2error(self, login, code, message):
282 status = lasso.Samlp2Status()
283 status.statusCode = lasso.Samlp2StatusCode()
284 status.statusCode.value = lasso.SAML2_STATUS_CODE_RESPONDER
285 status.statusCode.statusCode = lasso.Samlp2StatusCode()
286 status.statusCode.statusCode.value = code
287 login.response.status = status
289 def reply(self, login):
290 if login.protocolProfile == lasso.LOGIN_PROTOCOL_PROFILE_BRWS_ART:
292 raise cherrypy.HTTPError(501)
293 elif login.protocolProfile == lasso.LOGIN_PROTOCOL_PROFILE_BRWS_POST:
294 login.buildAuthnResponseMsg()
295 self._debug('POSTing back to SP [%s]' % (login.msgUrl))
297 "title": 'Redirecting back to the web application',
298 "action": login.msgUrl,
300 [lasso.SAML2_FIELD_RESPONSE, login.msgBody],
301 [lasso.SAML2_FIELD_RELAYSTATE, login.msgRelayState],
303 "submit": 'Return to application',
305 # pylint: disable=star-args
306 return self._template('saml2/post_response.html', **context)
309 raise cherrypy.HTTPError(500)