1 # Copyright (C) 2014 Ipsilon project Contributors, for license see COPYING
3 from ipsilon.providers.common import ProviderPageBase, ProviderException
4 from ipsilon.providers.common import AuthenticationError, InvalidRequest
5 from ipsilon.providers.saml2.provider import ServiceProvider
6 from ipsilon.providers.saml2.provider import InvalidProviderId
7 from ipsilon.providers.saml2.provider import NameIdNotAllowed
8 from ipsilon.providers.saml2.sessions import SAMLSessionFactory
9 from ipsilon.tools import saml2metadata as metadata
10 from ipsilon.util.policy import Policy
11 from ipsilon.util.user import UserSession
12 from ipsilon.util.trans import Transaction
20 class UnknownProvider(ProviderException):
22 def __init__(self, message):
23 super(UnknownProvider, self).__init__(message)
27 class AuthenticateRequest(ProviderPageBase):
29 def __init__(self, *args, **kwargs):
30 super(AuthenticateRequest, self).__init__(*args, **kwargs)
35 def _preop(self, *args, **kwargs):
37 # generate a new id or get current one
38 self.trans = Transaction('saml2', **kwargs)
40 self.debug('self.binding=%s, transdata=%s' %
41 (self.binding, self.trans.retrieve()))
42 if self.binding is None:
43 # SAML binding is unknown, try to get it from transaction
44 transdata = self.trans.retrieve()
45 self.binding = transdata.get('saml2_binding')
47 # SAML binding known, store in transaction
48 data = {'saml2_binding': self.binding}
49 self.trans.store(data)
51 # Only check for cookie for those bindings which use one
52 if self.binding not in (metadata.SAML2_SERVICE_MAP['sso-soap'][1]):
53 if self.trans.cookie.value != self.trans.provider:
54 self.debug('Invalid transaction, %s != %s' % (
55 self.trans.cookie.value, self.trans.provider))
56 except Exception, e: # pylint: disable=broad-except
57 self.debug('Transaction initialization failed: %s' % repr(e))
58 raise cherrypy.HTTPError(400, 'Invalid transaction id')
60 def pre_GET(self, *args, **kwargs):
61 self._preop(*args, **kwargs)
63 def pre_POST(self, *args, **kwargs):
64 self._preop(*args, **kwargs)
66 def auth(self, login):
68 self.saml2checks(login)
69 except AuthenticationError, e:
70 self.saml2error(login, e.code, e.message)
71 return self.reply(login)
73 def _parse_request(self, message):
75 login = self.cfg.idp.get_login_handler()
78 login.processAuthnRequestMsg(message)
79 except (lasso.ProfileInvalidMsgError,
80 lasso.ProfileMissingIssuerError), e:
82 msg = 'Malformed Request %r [%r]' % (e, message)
83 raise InvalidRequest(msg)
85 except (lasso.ProfileInvalidProtocolprofileError,
88 msg = 'Invalid SAML Request: %r (%r [%r])' % (login.request,
90 raise InvalidRequest(msg)
92 except (lasso.ServerProviderNotFoundError,
93 lasso.ProfileUnknownProviderError), e:
95 msg = 'Invalid SP [%s] (%r [%r])' % (login.remoteProviderId,
97 raise UnknownProvider(msg)
99 self.debug('SP %s requested authentication' % login.remoteProviderId)
103 def saml2login(self, request):
106 raise cherrypy.HTTPError(400,
107 'SAML request token missing or empty')
110 login = self._parse_request(request)
111 except InvalidRequest, e:
113 raise cherrypy.HTTPError(400, 'Invalid SAML request token')
114 except UnknownProvider, e:
116 raise cherrypy.HTTPError(400, 'Unknown Service Provider')
117 except Exception, e: # pylint: disable=broad-except
119 raise cherrypy.HTTPError(500)
123 def saml2checks(self, login):
127 if user.is_anonymous:
128 if self.stage == 'init':
129 returl = '%s/saml2/SSO/Continue?%s' % (
130 self.basepath, self.trans.get_GET_arg())
131 data = {'saml2_stage': 'auth',
132 'saml2_request': login.dump(),
133 'login_return': returl,
134 'login_target': login.remoteProviderId}
135 self.trans.store(data)
136 redirect = '%s/login?%s' % (self.basepath,
137 self.trans.get_GET_arg())
138 raise cherrypy.HTTPRedirect(redirect)
140 raise AuthenticationError(
141 "Unknown user", lasso.SAML2_STATUS_CODE_AUTHN_FAILED)
143 self._audit("Logged in user: %s [%s]" % (user.name, user.fullname))
145 # We can wipe the transaction now, as this is the last step
148 # TODO: check if this is the first time this user access this SP
149 # If required by user prefs, ask user for consent once and then
153 # TODO: check destination
156 provider = ServiceProvider(self.cfg, login.remoteProviderId)
157 nameidfmt = provider.get_valid_nameid(login.request.nameIdPolicy)
158 except NameIdNotAllowed, e:
159 raise AuthenticationError(
160 str(e), lasso.SAML2_STATUS_CODE_INVALID_NAME_ID_POLICY)
161 except InvalidProviderId, e:
162 raise AuthenticationError(
163 str(e), lasso.SAML2_STATUS_CODE_AUTHN_FAILED)
165 # TODO: check login.request.forceAuthn
167 login.validateRequestMsg(not user.is_anonymous, consent)
169 authtime = datetime.datetime.utcnow()
170 skew = datetime.timedelta(0, 60)
171 authtime_notbefore = authtime - skew
172 authtime_notafter = authtime + skew
174 # TODO: get authentication type fnd name format from session
175 # need to save which login manager authenticated and map it to a
176 # saml2 authentication context
177 authn_context = lasso.SAML2_AUTHN_CONTEXT_UNSPECIFIED
179 timeformat = '%Y-%m-%dT%H:%M:%SZ'
180 login.buildAssertion(authn_context,
181 authtime.strftime(timeformat),
183 authtime_notbefore.strftime(timeformat),
184 authtime_notafter.strftime(timeformat))
187 if nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT:
188 idpsalt = self.cfg.idp_nameid_salt
190 raise AuthenticationError(
191 "idp nameid salt is not set in configuration"
193 value = hashlib.sha512()
194 value.update(idpsalt)
195 value.update(login.remoteProviderId)
196 value.update(user.name)
197 nameid = '_' + value.hexdigest()
198 elif nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_TRANSIENT:
199 nameid = '_' + uuid.uuid4().hex
200 elif nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_KERBEROS:
201 userattrs = us.get_user_attrs()
202 nameid = userattrs.get('gssapi_principal_name')
203 elif nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_EMAIL:
204 nameid = us.get_user().email
206 nameid = '%s@%s' % (user.name, self.cfg.default_email_domain)
207 elif nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_UNSPECIFIED:
208 nameid = provider.normalize_username(user.name)
211 login.assertion.subject.nameId.format = nameidfmt
212 login.assertion.subject.nameId.content = nameid
215 raise AuthenticationError("Unavailable Name ID type",
216 lasso.SAML2_STATUS_CODE_AUTHN_FAILED)
218 # Check attribute policy and perform mapping and filtering.
219 # If the SP has its own mapping or filtering policy use that
220 # instead of the global policy.
221 if (provider.attribute_mappings is not None and
222 len(provider.attribute_mappings) > 0):
223 attribute_mappings = provider.attribute_mappings
225 attribute_mappings = self.cfg.default_attribute_mapping
226 if (provider.allowed_attributes is not None and
227 len(provider.allowed_attributes) > 0):
228 allowed_attributes = provider.allowed_attributes
230 allowed_attributes = self.cfg.default_allowed_attributes
231 self.debug("Allowed attrs: %s" % allowed_attributes)
232 self.debug("Mapping: %s" % attribute_mappings)
233 policy = Policy(attribute_mappings, allowed_attributes)
234 userattrs = us.get_user_attrs()
235 mappedattrs, _ = policy.map_attributes(userattrs)
236 attributes = policy.filter_attributes(mappedattrs)
238 if '_groups' in attributes and 'groups' not in attributes:
239 attributes['groups'] = attributes['_groups']
241 self.debug("%s's attributes: %s" % (user.name, attributes))
243 # The saml-core-2.0-os specification section 2.7.3 requires
244 # the AttributeStatement element to be non-empty.
246 if not login.assertion.attributeStatement:
247 attrstat = lasso.Saml2AttributeStatement()
248 login.assertion.attributeStatement = [attrstat]
250 attrstat = login.assertion.attributeStatement[0]
251 if not attrstat.attribute:
252 attrstat.attribute = ()
254 for key in attributes:
258 values = attributes[key]
259 if isinstance(values, dict):
261 if not isinstance(values, list):
264 attr = lasso.Saml2Attribute()
266 attr.nameFormat = lasso.SAML2_ATTRIBUTE_NAME_FORMAT_BASIC
267 value = str(value).encode('utf-8')
268 self.debug('value %s' % value)
269 node = lasso.MiscTextNode.newWithString(value)
270 node.textChild = True
271 attrvalue = lasso.Saml2AttributeValue()
272 attrvalue.any = [node]
273 attr.attributeValue = [attrvalue]
274 attrstat.attribute = attrstat.attribute + (attr,)
276 self.debug('Assertion: %s' % login.assertion.dump())
278 saml_sessions = SAMLSessionFactory()
280 lasso_session = lasso.Session()
281 lasso_session.addAssertion(login.remoteProviderId, login.assertion)
282 saml_sessions.add_session(login.assertion.id,
283 login.remoteProviderId,
285 lasso_session.dump())
287 def saml2error(self, login, code, message):
288 status = lasso.Samlp2Status()
289 status.statusCode = lasso.Samlp2StatusCode()
290 status.statusCode.value = lasso.SAML2_STATUS_CODE_RESPONDER
291 status.statusCode.statusCode = lasso.Samlp2StatusCode()
292 status.statusCode.statusCode.value = code
293 login.response.status = status
295 def reply(self, login):
296 if login.protocolProfile == lasso.LOGIN_PROTOCOL_PROFILE_BRWS_ART:
298 raise cherrypy.HTTPError(501)
299 elif login.protocolProfile == lasso.LOGIN_PROTOCOL_PROFILE_BRWS_POST:
300 login.buildAuthnResponseMsg()
301 self.debug('POSTing back to SP [%s]' % (login.msgUrl))
303 "title": 'Redirecting back to the web application',
304 "action": login.msgUrl,
306 [lasso.SAML2_FIELD_RESPONSE, login.msgBody],
307 [lasso.SAML2_FIELD_RELAYSTATE, login.msgRelayState],
309 "submit": 'Return to application',
311 return self._template('saml2/post_response.html', **context)
313 elif login.protocolProfile == lasso.LOGIN_PROTOCOL_PROFILE_BRWS_LECP:
314 login.buildResponseMsg()
315 self.debug("Returning ECP: %s" % login.msgBody)
319 raise cherrypy.HTTPError(500)