class AuthenticateRequest(ProviderPageBase): def __init__(self, site, provider, *args, **kwargs): super(AuthenticateRequest, self).__init__(site, provider) self.stage = 'init' self.trans = None def _preop(self, *args, **kwargs): try: # generate a new id or get current one self.trans = Transaction('openid', **kwargs) if (self.trans.cookie and self.trans.cookie.value != self.trans.provider): self.debug('Invalid transaction, %s != %s' % (self.trans.cookie.value, self.trans.provider)) except Exception as e: # pylint: disable=broad-except self.debug('Transaction initialization failed: %s' % repr(e)) raise cherrypy.HTTPError(400, 'Invalid transaction id') def pre_GET(self, *args, **kwargs): self._preop(*args, **kwargs) def pre_POST(self, *args, **kwargs): self._preop(*args, **kwargs) def _get_form(self, *args): form = None if args is not None: first = args[0] if len(args) > 0 else None second = first[0] if len(first) > 0 else None if isinstance(second, dict): form = second.get('form', None) return form def auth(self, *args, **kwargs): request = None form = self._get_form(args) try: request = self._parse_request(**kwargs) return self._openid_checks(request, form, **kwargs) except InvalidRequest as e: raise cherrypy.HTTPError(e.code, e.message) except UnauthorizedRequest as e: if request is None: raise cherrypy.HTTPError(e.code, e.message) return self._respond(request.answer(False)) # get attributes, and apply policy mapping and filtering def _source_attributes(self, session): policy = Policy(self.cfg.default_attribute_mapping, self.cfg.default_allowed_attributes) userattrs = session.get_user_attrs() if 'email' not in userattrs and self.cfg.default_email_domain: userattrs['email'] = '%s@%s' % (userattrs['_username'], self.cfg.default_email_domain) mappedattrs, _ = policy.map_attributes(userattrs) attributes = policy.filter_attributes(mappedattrs) self.debug('Filterd attributes: %s' % repr(attributes)) return attributes def _parse_request(self, **kwargs): request = None try: request = self.cfg.server.decodeRequest(kwargs) except ProtocolError as openid_error: self.debug('ProtocolError: %s' % openid_error) raise InvalidRequest('Invalid OpenID request') if request is None: self.debug('No request') raise cherrypy.HTTPRedirect(self.basepath or '/') return request def _openid_checks(self, request, form, **kwargs): us = UserSession() user = us.get_user() immediate = False self.debug('Mode: %s Stage: %s User: %s' % (kwargs['openid.mode'], self.stage, user.name)) if kwargs.get('openid.mode', None) == 'checkid_setup': if user.is_anonymous: if self.stage == 'init': returl = '%s/openid/Continue?%s' % ( self.basepath, self.trans.get_GET_arg()) data = { 'openid_stage': 'auth', 'openid_request': json.dumps(kwargs), 'login_return': returl, 'login_target': request.trust_root } self.trans.store(data) redirect = '%s/login?%s' % (self.basepath, self.trans.get_GET_arg()) self.debug('Redirecting: %s' % redirect) raise cherrypy.HTTPRedirect(redirect) else: raise UnauthorizedRequest("unknown user") elif kwargs.get('openid.mode', None) == 'checkid_immediate': # This is immediate, so we need to assert or fail if user.is_anonymous: return self._respond(request.answer(False)) immediate = True else: return self._respond(self.cfg.server.handleRequest(request)) # check if this is discovery or needs identity matching checks if not request.idSelect(): idurl = self.cfg.identity_url_template % {'username': user.name} if request.identity != idurl: raise UnauthorizedRequest("User ID mismatch!") # check if the relying party is trusted if request.trust_root in self.cfg.untrusted_roots: raise UnauthorizedRequest("Untrusted Relying party") # Perform authorization check. provinfo = {'url': kwargs.get('openid.realm', None)} if not self._site['authz'].authorize_user( 'openid', provinfo, user.name, us.get_user_attrs()): self.error('Authorization denied by authorization provider') raise UnauthorizedRequest('Authorization denied') # if the party is explicitly whitelisted just respond if request.trust_root in self.cfg.trusted_roots: return self._respond(self._response(request, us)) # Add extension data to this dictionary ad = { "Trust Root": request.trust_root, } userattrs = self._source_attributes(us) for n, e in self.cfg.extensions.available().items(): data = e.get_display_data(request, userattrs) self.debug('%s returned %s' % (n, repr(data))) for key, value in data.items(): ad[self.cfg.mapping.display_name(key)] = value # We base64 encode the trust_root when looking up consent data to # ensure the client ID is safe for the cherrypy url routing consentdata = user.get_consent('openid', b64encode(request.trust_root)) if consentdata is not None: # Consent has already been granted self.debug('Consent already granted') attrlist = set(ad.keys()) consattrs = set(consentdata['attributes']) if attrlist.issubset(consattrs): return self._respond(self._response(request, us)) if immediate: raise UnauthorizedRequest("No consent for immediate") if self.stage == 'consent': if form is None: raise UnauthorizedRequest("Unintelligible consent") allow = form.get('decided_allow', False) if not allow: raise UnauthorizedRequest("User declined") # Store new consent consentdata = {'attributes': ad.keys()} user.grant_consent('openid', b64encode(request.trust_root), consentdata) # all done we consent! return self._respond(self._response(request, us)) else: data = { 'openid_stage': 'consent', 'openid_request': json.dumps(kwargs) } self.trans.store(data) context = { "title": 'Consent', "action": '%s/openid/Consent' % (self.basepath), "trustroot": request.trust_root, "username": user.name, "authz_details": ad, } context.update(dict((self.trans.get_POST_tuple(), ))) return self._template('openid/consent_form.html', **context) def _response(self, request, session): user = session.get_user() identity_url = self.cfg.identity_url_template % {'username': user.name} response = request.answer(True, identity=identity_url, claimed_id=identity_url) userattrs = self._source_attributes(session) for _, e in self.cfg.extensions.available().items(): resp = e.get_response(request, userattrs) if resp is not None: response.addExtension(resp) return response def _respond(self, response): try: self.debug('Response: %s' % response) webresponse = self.cfg.server.encodeResponse(response) resplen = len(json.dumps(webresponse.headers)) if resplen > (4 * 1024): # This is a mostly arbitrary limit, but we should be able to at # the very least encode 4k into the response header. If it # gets too much though, Apache will think we have started # sending the actual page while we're still sending headers. self.error('WARNING: Response size exceeded 4KB. Apache will ' 'most likely abort the request.') if resplen > (8 * 1024): # Over 8kb, we don't even wait for Apache to cancel us # anymore, as the chance we'll be able to send this with # success is pretty close to 0. Just show the user an error raise InvalidRequest('Response size exceeded limits') cherrypy.response.headers.update(webresponse.headers) cherrypy.response.status = webresponse.code return webresponse.body except EncodingError as encoding_error: self.debug('Unable to respond because: %s' % encoding_error) cherrypy.response.headers = { 'Content-Type': 'text/plain; charset=UTF-8' } cherrypy.response.status = 400 return encoding_error.response.encodeToKVForm()
class AuthenticateRequest(ProviderPageBase): def __init__(self, site, provider, *args, **kwargs): super(AuthenticateRequest, self).__init__(site, provider) self.stage = 'init' self.trans = None self.binding = None def _preop(self, *args, **kwargs): try: # generate a new id or get current one self.trans = Transaction('saml2', **kwargs) self.debug('self.binding=%s, transdata=%s' % (self.binding, self.trans.retrieve())) if self.binding is None: # SAML binding is unknown, try to get it from transaction transdata = self.trans.retrieve() self.binding = transdata.get('saml2_binding') else: # SAML binding known, store in transaction data = {'saml2_binding': self.binding} self.trans.store(data) # Only check for cookie for those bindings which use one if self.binding not in (metadata.SAML2_SERVICE_MAP['sso-soap'][1]): if self.trans.cookie.value != self.trans.provider: self.debug('Invalid transaction, %s != %s' % (self.trans.cookie.value, self.trans.provider)) except Exception as e: # pylint: disable=broad-except self.debug('Transaction initialization failed: %s' % repr(e)) raise cherrypy.HTTPError(400, 'Invalid transaction id') def pre_GET(self, *args, **kwargs): self._preop(*args, **kwargs) def pre_POST(self, *args, **kwargs): self._preop(*args, **kwargs) def auth(self, login): try: self.saml2checks(login) except AuthenticationError as e: self.saml2error(login, e.code, e.message) return self.reply(login) def _parse_request(self, message, hint=None, final=False): login = self.cfg.idp.get_login_handler() try: if hint: login.setSignatureVerifyHint(hint) login.processAuthnRequestMsg(message) except lasso.DsInvalidSigalgError as e: if login.remoteProviderId and not final: provider = ServiceProvider(self.cfg, login.remoteProviderId) if not provider.has_signing_keys: self.error('Invalid or missing signature, setting hint.') return self._parse_request( message, hint=provider.get_signature_hint(), final=True) msg = 'Invalid or missing signature algorithm %r [%r]' % (e, message) raise InvalidRequest(msg) except (lasso.ProfileInvalidMsgError, lasso.ProfileMissingIssuerError) as e: msg = 'Malformed Request %r [%r]' % (e, message) raise InvalidRequest(msg) except (lasso.ProfileInvalidProtocolprofileError, lasso.DsError) as e: msg = 'Invalid SAML Request: %r (%r [%r])' % (login.request, e, message) raise InvalidRequest(msg) except (lasso.ServerProviderNotFoundError, lasso.ProfileUnknownProviderError) as e: msg = 'Invalid SP [%s] (%r [%r])' % (login.remoteProviderId, e, message) raise UnknownProvider(msg) self.debug('SP %s requested authentication' % login.remoteProviderId) return login def _idp_initiated_login(self, spidentifier, relaystate): """ Perform an Idp-initiated login Exceptions are handled by the caller """ login = self.cfg.idp.get_login_handler() login.initIdpInitiatedAuthnRequest(spidentifier) # Hardcode for now, handle Artifact later login.request.protocolBinding = lasso.SAML2_METADATA_BINDING_POST login.processAuthnRequestMsg() if relaystate is not None: login.msgRelayState = relaystate else: provider = ServiceProvider(self.cfg, login.remoteProviderId) if provider.splink is not None: login.msgRelayState = provider.splink else: login.msgRelayState = login.remoteProviderId return login def saml2login(self, request, spidentifier=None, relaystate=None): """ request: the SAML request spidentifier: the provider ID for IdP-initiated login relaystate: optional string to direct user to particular place on the SP after sending POST. If one is not provided then the protected site from the SP is used, otherwise it is set to the remote provider ID. """ if not request and not spidentifier: raise cherrypy.HTTPError(400, 'SAML request token missing or empty') if spidentifier: try: login = self._idp_initiated_login(spidentifier, relaystate) except lasso.ServerProviderNotFoundError: raise cherrypy.HTTPError(400, 'Unknown Service Provider') except Exception as e: # pylint: disable=broad-except self.debug(str(e)) raise cherrypy.HTTPError(500) else: try: login = self._parse_request(request) except InvalidRequest as e: self.debug(str(e)) raise cherrypy.HTTPError(400, 'Invalid SAML request token') except UnknownProvider as e: self.debug(str(e)) raise cherrypy.HTTPError(400, 'Unknown Service Provider') except Exception as e: # pylint: disable=broad-except self.debug(str(e)) raise cherrypy.HTTPError(500) return login def saml2checks(self, login): us = UserSession() user = us.get_user() if user.is_anonymous: if self.stage == 'init': returl = '%s/saml2/SSO/Continue?%s' % ( self.basepath, self.trans.get_GET_arg()) data = { 'saml2_stage': 'auth', 'saml2_request': login.dump(), 'login_return': returl, 'login_target': login.remoteProviderId } self.trans.store(data) redirect = '%s/login?%s' % (self.basepath, self.trans.get_GET_arg()) raise cherrypy.HTTPRedirect(redirect) else: raise AuthenticationError("Unknown user", lasso.SAML2_STATUS_CODE_AUTHN_FAILED) self._audit("Logged in user: %s [%s]" % (user.name, user.fullname)) # We can wipe the transaction now, as this is the last step self.trans.wipe() # TODO: check if this is the first time this user access this SP # If required by user prefs, ask user for consent once and then # record it consent = True # TODO: check destination try: provider = ServiceProvider(self.cfg, login.remoteProviderId) nameidfmt = provider.get_valid_nameid(login.request.nameIdPolicy) except NameIdNotAllowed as e: raise AuthenticationError( str(e), lasso.SAML2_STATUS_CODE_INVALID_NAME_ID_POLICY) except InvalidProviderId as e: raise AuthenticationError(str(e), lasso.SAML2_STATUS_CODE_AUTHN_FAILED) # TODO: check login.request.forceAuthn login.validateRequestMsg(not user.is_anonymous, consent) authtime = datetime.datetime.utcnow() skew = datetime.timedelta(0, 60) authtime_notbefore = authtime - skew authtime_notafter = authtime + skew # Let's first do the attribute mapping, so we could map the username # Check attribute policy and perform mapping and filtering. # If the SP has its own mapping or filtering policy use that # instead of the global policy. if (provider.attribute_mappings is not None and len(provider.attribute_mappings) > 0): attribute_mappings = provider.attribute_mappings else: attribute_mappings = self.cfg.default_attribute_mapping if (provider.allowed_attributes is not None and len(provider.allowed_attributes) > 0): allowed_attributes = provider.allowed_attributes else: allowed_attributes = self.cfg.default_allowed_attributes self.debug("Allowed attrs: %s" % allowed_attributes) self.debug("Mapping: %s" % attribute_mappings) policy = Policy(attribute_mappings, allowed_attributes) userattrs = us.get_user_attrs() mappedattrs, _ = policy.map_attributes(userattrs) attributes = policy.filter_attributes(mappedattrs) if '_groups' in attributes and 'groups' not in attributes: attributes['groups'] = attributes['_groups'] self.debug("%s's attributes: %s" % (user.name, attributes)) # Perform authorization check. # We use the raw userattrs here so that we can make decisions based # on attributes we don't want to send to the SP provinfo = { 'name': provider.name, 'url': provider.splink, 'owner': provider.owner } if not self._site['authz'].authorize_user('saml2', provinfo, user.name, userattrs): self.trans.wipe() self.error('Authorization denied by authorization provider') raise AuthenticationError("Authorization denied", lasso.SAML2_STATUS_CODE_AUTHN_FAILED) # TODO: get authentication type fnd name format from session # need to save which login manager authenticated and map it to a # saml2 authentication context authn_context = lasso.SAML2_AUTHN_CONTEXT_UNSPECIFIED timeformat = '%Y-%m-%dT%H:%M:%SZ' login.buildAssertion(authn_context, authtime.strftime(timeformat), None, authtime_notbefore.strftime(timeformat), authtime_notafter.strftime(timeformat)) nameid = None if nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT: idpsalt = self.cfg.idp_nameid_salt if idpsalt is None: raise AuthenticationError( "idp nameid salt is not set in configuration") value = hashlib.sha512() value.update(idpsalt) value.update(login.remoteProviderId) value.update(mappedattrs.get('_username')) nameid = '_' + value.hexdigest() elif nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_TRANSIENT: nameid = '_' + uuid.uuid4().hex elif nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_KERBEROS: nameid = userattrs.get('gssapi_principal_name') elif nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_EMAIL: nameid = mappedattrs.get('email') if not nameid: nameid = '%s@%s' % (user.name, self.cfg.default_email_domain) elif nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_UNSPECIFIED: nameid = provider.normalize_username(mappedattrs.get('_username')) if nameid: login.assertion.subject.nameId.format = nameidfmt login.assertion.subject.nameId.content = nameid else: self.trans.wipe() self.error('Authentication succeeded but it was not ' + 'provided by NameID %s' % nameidfmt) raise AuthenticationError("Unavailable Name ID type", lasso.SAML2_STATUS_CODE_AUTHN_FAILED) # The saml-core-2.0-os specification section 2.7.3 requires # the AttributeStatement element to be non-empty. if attributes: if not login.assertion.attributeStatement: attrstat = lasso.Saml2AttributeStatement() login.assertion.attributeStatement = [attrstat] else: attrstat = login.assertion.attributeStatement[0] if not attrstat.attribute: attrstat.attribute = () for key in attributes: # skip internal info if key[0] == '_': continue values = attributes[key] if isinstance(values, dict): continue if not isinstance(values, list): values = [values] attr = lasso.Saml2Attribute() attr.name = key attr.nameFormat = lasso.SAML2_ATTRIBUTE_NAME_FORMAT_BASIC attr.attributeValue = [] vals = [] for value in values: if value is None: self.log('Ignoring None value for attribute %s' % key) continue self.debug('value %s' % value) node = lasso.MiscTextNode.newWithString(value) node.textChild = True attrvalue = lasso.Saml2AttributeValue() attrvalue.any = [node] vals.append(attrvalue) attr.attributeValue = vals attrstat.attribute = attrstat.attribute + (attr, ) self.debug('Assertion: %s' % login.assertion.dump()) saml_sessions = self.cfg.idp.sessionfactory lasso_session = lasso.Session() lasso_session.addAssertion(login.remoteProviderId, login.assertion) provider = ServiceProvider(self.cfg, login.remoteProviderId) saml_sessions.add_session(login.assertion.id, login.remoteProviderId, user.name, lasso_session.dump(), None, provider.logout_mechs) def saml2error(self, login, code, message): status = lasso.Samlp2Status() status.statusCode = lasso.Samlp2StatusCode() status.statusCode.value = lasso.SAML2_STATUS_CODE_RESPONDER status.statusCode.statusCode = lasso.Samlp2StatusCode() status.statusCode.statusCode.value = code login.response.status = status def reply(self, login): if login.protocolProfile == lasso.LOGIN_PROTOCOL_PROFILE_BRWS_ART: # TODO raise cherrypy.HTTPError(501) elif login.protocolProfile == lasso.LOGIN_PROTOCOL_PROFILE_BRWS_POST: login.buildAuthnResponseMsg() self.debug('POSTing back to SP [%s]' % (login.msgUrl)) fields = [[lasso.SAML2_FIELD_RESPONSE, login.msgBody]] if login.msgRelayState is not None: fields.append( [lasso.SAML2_FIELD_RELAYSTATE, login.msgRelayState]) context = { "title": 'Redirecting back to the web application', "action": login.msgUrl, "fields": fields, "submit": 'Return to application', } return self._template('saml2/post_response.html', **context) elif login.protocolProfile == lasso.LOGIN_PROTOCOL_PROFILE_BRWS_LECP: login.buildResponseMsg() self.debug("Returning ECP: %s" % login.msgBody) return login.msgBody else: raise cherrypy.HTTPError(500)