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)
207 login.assertion.subject.nameId.format = nameidfmt
208 login.assertion.subject.nameId.content = nameid
211 raise AuthenticationError("Unavailable Name ID type",
212 lasso.SAML2_STATUS_CODE_AUTHN_FAILED)
214 # Check attribute policy and perform mapping and filtering
215 policy = Policy(self.cfg.default_attribute_mapping,
216 self.cfg.default_allowed_attributes)
217 userattrs = us.get_user_attrs()
218 mappedattrs, _ = policy.map_attributes(userattrs)
219 attributes = policy.filter_attributes(mappedattrs)
221 if '_groups' in attributes and 'groups' not in attributes:
222 attributes['groups'] = attributes['_groups']
224 self.debug("%s's attributes: %s" % (user.name, attributes))
226 # The saml-core-2.0-os specification section 2.7.3 requires
227 # the AttributeStatement element to be non-empty.
229 if not login.assertion.attributeStatement:
230 attrstat = lasso.Saml2AttributeStatement()
231 login.assertion.attributeStatement = [attrstat]
233 attrstat = login.assertion.attributeStatement[0]
234 if not attrstat.attribute:
235 attrstat.attribute = ()
237 for key in attributes:
241 values = attributes[key]
242 if isinstance(values, dict):
244 if not isinstance(values, list):
247 attr = lasso.Saml2Attribute()
249 attr.nameFormat = lasso.SAML2_ATTRIBUTE_NAME_FORMAT_BASIC
250 value = str(value).encode('utf-8')
251 self.debug('value %s' % value)
252 node = lasso.MiscTextNode.newWithString(value)
253 node.textChild = True
254 attrvalue = lasso.Saml2AttributeValue()
255 attrvalue.any = [node]
256 attr.attributeValue = [attrvalue]
257 attrstat.attribute = attrstat.attribute + (attr,)
259 self.debug('Assertion: %s' % login.assertion.dump())
261 saml_sessions = us.get_provider_data('saml2')
262 if saml_sessions is None:
263 saml_sessions = SAMLSessionsContainer()
265 session = saml_sessions.find_session_by_provider(
266 login.remoteProviderId)
269 self.debug('Login session for this user already exists!?')
272 lasso_session = lasso.Session()
273 lasso_session.addAssertion(login.remoteProviderId, login.assertion)
274 saml_sessions.add_session(login.assertion.id,
275 login.remoteProviderId,
277 us.save_provider_data('saml2', saml_sessions)
279 def saml2error(self, login, code, message):
280 status = lasso.Samlp2Status()
281 status.statusCode = lasso.Samlp2StatusCode()
282 status.statusCode.value = lasso.SAML2_STATUS_CODE_RESPONDER
283 status.statusCode.statusCode = lasso.Samlp2StatusCode()
284 status.statusCode.statusCode.value = code
285 login.response.status = status
287 def reply(self, login):
288 if login.protocolProfile == lasso.LOGIN_PROTOCOL_PROFILE_BRWS_ART:
290 raise cherrypy.HTTPError(501)
291 elif login.protocolProfile == lasso.LOGIN_PROTOCOL_PROFILE_BRWS_POST:
292 login.buildAuthnResponseMsg()
293 self._debug('POSTing back to SP [%s]' % (login.msgUrl))
295 "title": 'Redirecting back to the web application',
296 "action": login.msgUrl,
298 [lasso.SAML2_FIELD_RESPONSE, login.msgBody],
299 [lasso.SAML2_FIELD_RELAYSTATE, login.msgRelayState],
301 "submit": 'Return to application',
303 # pylint: disable=star-args
304 return self._template('saml2/post_response.html', **context)
307 raise cherrypy.HTTPError(500)