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 SAMLSessionsContainer
9 from ipsilon.util.policy import Policy
10 from ipsilon.util.user import UserSession
11 from ipsilon.util.trans import Transaction
19 class UnknownProvider(ProviderException):
21 def __init__(self, message):
22 super(UnknownProvider, self).__init__(message)
26 class AuthenticateRequest(ProviderPageBase):
28 def __init__(self, *args, **kwargs):
29 super(AuthenticateRequest, self).__init__(*args, **kwargs)
33 def _preop(self, *args, **kwargs):
35 # generate a new id or get current one
36 self.trans = Transaction('saml2', **kwargs)
37 if self.trans.cookie.value != self.trans.provider:
38 self.debug('Invalid transaction, %s != %s' % (
39 self.trans.cookie.value, self.trans.provider))
40 except Exception, e: # pylint: disable=broad-except
41 self.debug('Transaction initialization failed: %s' % repr(e))
42 raise cherrypy.HTTPError(400, 'Invalid transaction id')
44 def pre_GET(self, *args, **kwargs):
45 self._preop(*args, **kwargs)
47 def pre_POST(self, *args, **kwargs):
48 self._preop(*args, **kwargs)
50 def auth(self, login):
52 self.saml2checks(login)
53 except AuthenticationError, e:
54 self.saml2error(login, e.code, e.message)
55 return self.reply(login)
57 def _parse_request(self, message):
59 login = self.cfg.idp.get_login_handler()
62 login.processAuthnRequestMsg(message)
63 except (lasso.ProfileInvalidMsgError,
64 lasso.ProfileMissingIssuerError), e:
66 msg = 'Malformed Request %r [%r]' % (e, message)
67 raise InvalidRequest(msg)
69 except (lasso.ProfileInvalidProtocolprofileError,
72 msg = 'Invalid SAML Request: %r (%r [%r])' % (login.request,
74 raise InvalidRequest(msg)
76 except (lasso.ServerProviderNotFoundError,
77 lasso.ProfileUnknownProviderError), e:
79 msg = 'Invalid SP [%s] (%r [%r])' % (login.remoteProviderId,
81 raise UnknownProvider(msg)
83 self.debug('SP %s requested authentication' % login.remoteProviderId)
87 def saml2login(self, request):
90 raise cherrypy.HTTPError(400,
91 'SAML request token missing or empty')
94 login = self._parse_request(request)
95 except InvalidRequest, e:
97 raise cherrypy.HTTPError(400, 'Invalid SAML request token')
98 except UnknownProvider, e:
100 raise cherrypy.HTTPError(400, 'Unknown Service Provider')
101 except Exception, e: # pylint: disable=broad-except
103 raise cherrypy.HTTPError(500)
107 def saml2checks(self, login):
111 if user.is_anonymous:
112 if self.stage == 'init':
113 returl = '%s/saml2/SSO/Continue?%s' % (
114 self.basepath, self.trans.get_GET_arg())
115 data = {'saml2_stage': 'auth',
116 'saml2_request': login.dump(),
117 'login_return': returl,
118 'login_target': login.remoteProviderId}
119 self.trans.store(data)
120 redirect = '%s/login?%s' % (self.basepath,
121 self.trans.get_GET_arg())
122 raise cherrypy.HTTPRedirect(redirect)
124 raise AuthenticationError(
125 "Unknown user", lasso.SAML2_STATUS_CODE_AUTHN_FAILED)
127 self._audit("Logged in user: %s [%s]" % (user.name, user.fullname))
129 # We can wipe the transaction now, as this is the last step
132 # TODO: check if this is the first time this user access this SP
133 # If required by user prefs, ask user for consent once and then
137 # TODO: check destination
140 provider = ServiceProvider(self.cfg, login.remoteProviderId)
141 nameidfmt = provider.get_valid_nameid(login.request.nameIdPolicy)
142 except NameIdNotAllowed, e:
143 raise AuthenticationError(
144 str(e), lasso.SAML2_STATUS_CODE_INVALID_NAME_ID_POLICY)
145 except InvalidProviderId, e:
146 raise AuthenticationError(
147 str(e), lasso.SAML2_STATUS_CODE_AUTHN_FAILED)
149 # TODO: check login.request.forceAuthn
151 login.validateRequestMsg(not user.is_anonymous, consent)
153 authtime = datetime.datetime.utcnow()
154 skew = datetime.timedelta(0, 60)
155 authtime_notbefore = authtime - skew
156 authtime_notafter = authtime + skew
158 # TODO: get authentication type fnd name format from session
159 # need to save which login manager authenticated and map it to a
160 # saml2 authentication context
161 authn_context = lasso.SAML2_AUTHN_CONTEXT_UNSPECIFIED
163 timeformat = '%Y-%m-%dT%H:%M:%SZ'
164 login.buildAssertion(authn_context,
165 authtime.strftime(timeformat),
167 authtime_notbefore.strftime(timeformat),
168 authtime_notafter.strftime(timeformat))
171 if nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT:
172 idpsalt = self.cfg.idp_nameid_salt
174 raise AuthenticationError(
175 "idp nameid salt is not set in configuration"
177 value = hashlib.sha512()
178 value.update(idpsalt)
179 value.update(login.remoteProviderId)
180 value.update(user.name)
181 nameid = '_' + value.hexdigest()
182 elif nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_TRANSIENT:
183 nameid = '_' + uuid.uuid4().hex
184 elif nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_KERBEROS:
185 userattrs = us.get_user_attrs()
186 nameid = userattrs.get('gssapi_principal_name')
187 elif nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_EMAIL:
188 nameid = us.get_user().email
190 nameid = '%s@%s' % (user.name, self.cfg.default_email_domain)
191 elif nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_UNSPECIFIED:
192 nameid = provider.normalize_username(user.name)
195 login.assertion.subject.nameId.format = nameidfmt
196 login.assertion.subject.nameId.content = nameid
199 raise AuthenticationError("Unavailable Name ID type",
200 lasso.SAML2_STATUS_CODE_AUTHN_FAILED)
202 # Check attribute policy and perform mapping and filtering.
203 # If the SP has its own mapping or filtering policy use that
204 # instead of the global policy.
205 if (provider.attribute_mappings is not None and
206 len(provider.attribute_mappings) > 0):
207 attribute_mappings = provider.attribute_mappings
209 attribute_mappings = self.cfg.default_attribute_mapping
210 if (provider.allowed_attributes is not None and
211 len(provider.allowed_attributes) > 0):
212 allowed_attributes = provider.allowed_attributes
214 allowed_attributes = self.cfg.default_allowed_attributes
215 self.debug("Allowed attrs: %s" % allowed_attributes)
216 self.debug("Mapping: %s" % attribute_mappings)
217 policy = Policy(attribute_mappings, allowed_attributes)
218 userattrs = us.get_user_attrs()
219 mappedattrs, _ = policy.map_attributes(userattrs)
220 attributes = policy.filter_attributes(mappedattrs)
222 if '_groups' in attributes and 'groups' not in attributes:
223 attributes['groups'] = attributes['_groups']
225 self.debug("%s's attributes: %s" % (user.name, attributes))
227 # The saml-core-2.0-os specification section 2.7.3 requires
228 # the AttributeStatement element to be non-empty.
230 if not login.assertion.attributeStatement:
231 attrstat = lasso.Saml2AttributeStatement()
232 login.assertion.attributeStatement = [attrstat]
234 attrstat = login.assertion.attributeStatement[0]
235 if not attrstat.attribute:
236 attrstat.attribute = ()
238 for key in attributes:
242 values = attributes[key]
243 if isinstance(values, dict):
245 if not isinstance(values, list):
248 attr = lasso.Saml2Attribute()
250 attr.nameFormat = lasso.SAML2_ATTRIBUTE_NAME_FORMAT_BASIC
251 value = str(value).encode('utf-8')
252 self.debug('value %s' % value)
253 node = lasso.MiscTextNode.newWithString(value)
254 node.textChild = True
255 attrvalue = lasso.Saml2AttributeValue()
256 attrvalue.any = [node]
257 attr.attributeValue = [attrvalue]
258 attrstat.attribute = attrstat.attribute + (attr,)
260 self.debug('Assertion: %s' % login.assertion.dump())
262 saml_sessions = us.get_provider_data('saml2')
263 if saml_sessions is None:
264 saml_sessions = SAMLSessionsContainer()
266 session = saml_sessions.find_session_by_provider(
267 login.remoteProviderId)
270 self.debug('Login session for this user already exists!?')
273 lasso_session = lasso.Session()
274 lasso_session.addAssertion(login.remoteProviderId, login.assertion)
275 saml_sessions.add_session(login.assertion.id,
276 login.remoteProviderId,
278 us.save_provider_data('saml2', saml_sessions)
280 def saml2error(self, login, code, message):
281 status = lasso.Samlp2Status()
282 status.statusCode = lasso.Samlp2StatusCode()
283 status.statusCode.value = lasso.SAML2_STATUS_CODE_RESPONDER
284 status.statusCode.statusCode = lasso.Samlp2StatusCode()
285 status.statusCode.statusCode.value = code
286 login.response.status = status
288 def reply(self, login):
289 if login.protocolProfile == lasso.LOGIN_PROTOCOL_PROFILE_BRWS_ART:
291 raise cherrypy.HTTPError(501)
292 elif login.protocolProfile == lasso.LOGIN_PROTOCOL_PROFILE_BRWS_POST:
293 login.buildAuthnResponseMsg()
294 self.debug('POSTing back to SP [%s]' % (login.msgUrl))
296 "title": 'Redirecting back to the web application',
297 "action": login.msgUrl,
299 [lasso.SAML2_FIELD_RESPONSE, login.msgBody],
300 [lasso.SAML2_FIELD_RELAYSTATE, login.msgRelayState],
302 "submit": 'Return to application',
304 return self._template('saml2/post_response.html', **context)
307 raise cherrypy.HTTPError(500)