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
32 class UnknownProvider(ProviderException):
34 def __init__(self, message):
35 super(UnknownProvider, self).__init__(message)
39 class AuthenticateRequest(ProviderPageBase):
41 def __init__(self, *args, **kwargs):
42 super(AuthenticateRequest, self).__init__(*args, **kwargs)
46 def _preop(self, *args, **kwargs):
48 # generate a new id or get current one
49 self.trans = Transaction('saml2', **kwargs)
50 if self.trans.cookie.value != self.trans.provider:
51 self.debug('Invalid transaction, %s != %s' % (
52 self.trans.cookie.value, self.trans.provider))
53 except Exception, e: # pylint: disable=broad-except
54 self.debug('Transaction initialization failed: %s' % repr(e))
55 raise cherrypy.HTTPError(400, 'Invalid transaction id')
57 def pre_GET(self, *args, **kwargs):
58 self._preop(*args, **kwargs)
60 def pre_POST(self, *args, **kwargs):
61 self._preop(*args, **kwargs)
63 def auth(self, login):
65 self.saml2checks(login)
66 except AuthenticationError, e:
67 self.saml2error(login, e.code, e.message)
68 return self.reply(login)
70 def _parse_request(self, message):
72 login = self.cfg.idp.get_login_handler()
75 login.processAuthnRequestMsg(message)
76 except (lasso.ProfileInvalidMsgError,
77 lasso.ProfileMissingIssuerError), e:
79 msg = 'Malformed Request %r [%r]' % (e, message)
80 raise InvalidRequest(msg)
82 except (lasso.ProfileInvalidProtocolprofileError,
85 msg = 'Invalid SAML Request: %r (%r [%r])' % (login.request,
87 raise InvalidRequest(msg)
89 except (lasso.ServerProviderNotFoundError,
90 lasso.ProfileUnknownProviderError), e:
92 msg = 'Invalid SP [%s] (%r [%r])' % (login.remoteProviderId,
94 raise UnknownProvider(msg)
96 self._debug('SP %s requested authentication' % login.remoteProviderId)
100 def saml2login(self, request):
103 raise cherrypy.HTTPError(400,
104 'SAML request token missing or empty')
107 login = self._parse_request(request)
108 except InvalidRequest, e:
110 raise cherrypy.HTTPError(400, 'Invalid SAML request token')
111 except UnknownProvider, e:
113 raise cherrypy.HTTPError(400, 'Unknown Service Provider')
114 except Exception, e: # pylint: disable=broad-except
116 raise cherrypy.HTTPError(500)
120 def saml2checks(self, login):
124 if user.is_anonymous:
125 if self.stage == 'init':
126 returl = '%s/saml2/SSO/Continue?%s' % (
127 self.basepath, self.trans.get_GET_arg())
128 data = {'saml2_stage': 'auth',
129 'saml2_request': login.dump(),
130 'login_return': returl,
131 'login_target': login.remoteProviderId}
132 self.trans.store(data)
133 redirect = '%s/login?%s' % (self.basepath,
134 self.trans.get_GET_arg())
135 raise cherrypy.HTTPRedirect(redirect)
137 raise AuthenticationError(
138 "Unknown user", lasso.SAML2_STATUS_CODE_AUTHN_FAILED)
140 self._audit("Logged in user: %s [%s]" % (user.name, user.fullname))
142 # We can wipe the transaction now, as this is the last step
145 # TODO: check if this is the first time this user access this SP
146 # If required by user prefs, ask user for consent once and then
150 # TODO: check destination
153 provider = ServiceProvider(self.cfg, login.remoteProviderId)
154 nameidfmt = provider.get_valid_nameid(login.request.nameIdPolicy)
155 except NameIdNotAllowed, e:
156 raise AuthenticationError(
157 str(e), lasso.SAML2_STATUS_CODE_INVALID_NAME_ID_POLICY)
158 except InvalidProviderId, e:
159 raise AuthenticationError(
160 str(e), lasso.SAML2_STATUS_CODE_AUTHN_FAILED)
162 # TODO: check login.request.forceAuthn
164 login.validateRequestMsg(not user.is_anonymous, consent)
166 authtime = datetime.datetime.utcnow()
167 skew = datetime.timedelta(0, 60)
168 authtime_notbefore = authtime - skew
169 authtime_notafter = authtime + skew
171 # TODO: get authentication type fnd name format from session
172 # need to save which login manager authenticated and map it to a
173 # saml2 authentication context
174 authn_context = lasso.SAML2_AUTHN_CONTEXT_UNSPECIFIED
176 timeformat = '%Y-%m-%dT%H:%M:%SZ'
177 login.buildAssertion(authn_context,
178 authtime.strftime(timeformat),
180 authtime_notbefore.strftime(timeformat),
181 authtime_notafter.strftime(timeformat))
184 if nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT:
185 # TODO map to something else ?
186 nameid = provider.normalize_username(user.name)
187 elif nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_TRANSIENT:
188 # TODO map to something else ?
189 nameid = provider.normalize_username(user.name)
190 elif nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_KERBEROS:
191 nameid = us.get_data('user', 'krb_principal_name')
192 elif nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_EMAIL:
193 nameid = us.get_user().email
195 nameid = '%s@%s' % (user.name, self.cfg.default_email_domain)
198 login.assertion.subject.nameId.format = nameidfmt
199 login.assertion.subject.nameId.content = nameid
202 raise AuthenticationError("Unavailable Name ID type",
203 lasso.SAML2_STATUS_CODE_AUTHN_FAILED)
205 if not login.assertion.attributeStatement:
206 attrstat = lasso.Saml2AttributeStatement()
207 login.assertion.attributeStatement = [attrstat]
209 attrstat = login.assertion.attributeStatement[0]
210 if not attrstat.attribute:
211 attrstat.attribute = ()
213 # Check attribute policy and perform mapping and filtering
214 policy = Policy(self.cfg.default_attribute_mapping,
215 self.cfg.default_allowed_attributes)
216 userattrs = us.get_user_attrs()
217 mappedattrs, _ = policy.map_attributes(userattrs)
218 attributes = policy.filter_attributes(mappedattrs)
220 if '_groups' in attributes and 'groups' not in attributes:
221 attributes['groups'] = attributes['_groups']
223 self.debug("%s's attributes: %s" % (user.name, attributes))
225 for key in attributes:
229 values = attributes[key]
230 if isinstance(values, dict):
232 if not isinstance(values, list):
235 attr = lasso.Saml2Attribute()
237 attr.nameFormat = lasso.SAML2_ATTRIBUTE_NAME_FORMAT_BASIC
238 value = str(value).encode('utf-8')
239 self.debug('value %s' % value)
240 node = lasso.MiscTextNode.newWithString(value)
241 node.textChild = True
242 attrvalue = lasso.Saml2AttributeValue()
243 attrvalue.any = [node]
244 attr.attributeValue = [attrvalue]
245 attrstat.attribute = attrstat.attribute + (attr,)
247 self.debug('Assertion: %s' % login.assertion.dump())
249 saml_sessions = us.get_provider_data('saml2')
250 if saml_sessions is None:
251 saml_sessions = SAMLSessionsContainer()
253 session = saml_sessions.find_session_by_provider(
254 login.remoteProviderId)
257 self.debug('Login session for this user already exists!?')
260 lasso_session = lasso.Session()
261 lasso_session.addAssertion(login.remoteProviderId, login.assertion)
262 saml_sessions.add_session(login.assertion.id,
263 login.remoteProviderId,
265 us.save_provider_data('saml2', saml_sessions)
267 def saml2error(self, login, code, message):
268 status = lasso.Samlp2Status()
269 status.statusCode = lasso.Samlp2StatusCode()
270 status.statusCode.value = lasso.SAML2_STATUS_CODE_RESPONDER
271 status.statusCode.statusCode = lasso.Samlp2StatusCode()
272 status.statusCode.statusCode.value = code
273 login.response.status = status
275 def reply(self, login):
276 if login.protocolProfile == lasso.LOGIN_PROTOCOL_PROFILE_BRWS_ART:
278 raise cherrypy.HTTPError(501)
279 elif login.protocolProfile == lasso.LOGIN_PROTOCOL_PROFILE_BRWS_POST:
280 login.buildAuthnResponseMsg()
281 self._debug('POSTing back to SP [%s]' % (login.msgUrl))
283 "title": 'Redirecting back to the web application',
284 "action": login.msgUrl,
286 [lasso.SAML2_FIELD_RESPONSE, login.msgBody],
287 [lasso.SAML2_FIELD_RELAYSTATE, login.msgRelayState],
289 "submit": 'Return to application',
291 # pylint: disable=star-args
292 return self._template('saml2/post_response.html', **context)
295 raise cherrypy.HTTPError(500)