def GET(self, *args, **kwargs): session = UserSession() user = session.get_user() transdata = self.trans.retrieve() self.stage = transdata['saml2_stage'] if user.is_anonymous: self.debug("User is marked anonymous?!") message = transdata.get('message') if message is not None: data = {'message': None} self.trans.store(data) # TODO: Return to SP with auth failed error raise cherrypy.HTTPError(401, message) self.debug('Continue auth for %s' % user.name) if 'saml2_request' not in transdata: self.error("Couldn't find Request dump in transaction?!") # TODO: Return to SP with auth failed error raise cherrypy.HTTPError(400) dump = transdata['saml2_request'] try: login = self.cfg.idp.get_login_handler(dump) except Exception, e: # pylint: disable=broad-except self.error('Failed to load login status from dump: %r' % e)
def GET(self, *args, **kwargs): session = UserSession() user = session.get_user() transdata = self.trans.retrieve() self.stage = transdata['saml2_stage'] if user.is_anonymous: self.debug("User is marked anonymous?!") message = transdata.get('message') if message is not None: data = {'message': None} self.trans.store(data) # TODO: Return to SP with auth failed error raise cherrypy.HTTPError(401, message) self.debug('Continue auth for %s' % user.name) if 'saml2_request' not in transdata: self.error("Couldn't find Request dump in transaction?!") # TODO: Return to SP with auth failed error raise cherrypy.HTTPError(400) dump = transdata['saml2_request'] try: login = self.cfg.idp.get_login_handler(dump) except Exception as e: # pylint: disable=broad-except self.error('Failed to load login status from dump: %r' % e) if not login: self.error("Empty login Request dump?!") # TODO: Return to SP with auth failed error raise cherrypy.HTTPError(400) return self.auth(login)
def root(self, *args, **kwargs): us = UserSession() user = us.get_user() consents = user.list_consents() for consent in consents: provname = consent['provider'] provider = self._site['provider_config'].available.get(provname, None) if provider is not None: consent['providerdn'] = provider.get_display_name() consent['clientdn'] = provider.\ get_client_display_name(consent['client']) attrs = provider.consent_to_display(consent['attrs']) else: self.debug('Consent relates to unknown provider %s' % provname) attrs = [] consent['attrs'] = attrs return self._template('user/index.html', title='', baseurl=self.url, menu=self.menu, consents=consents)
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") # if the party is explicitly whitelisted just respond if request.trust_root in self.cfg.trusted_roots: return self._respond(self._response(request, us)) allowroot = 'allow-%s' % request.trust_root try: userdata = user.load_plugin_data(self.cfg.name) expiry = int(userdata[allowroot]) except Exception, e: # pylint: disable=broad-except self.debug(e) expiry = 0
def idp_initiated_logout(self): """ Logout all SP sessions when the logout comes from the IdP. For the current user only. Only use HTTP-Redirect to start the logout. This is guaranteed to be supported in SAML 2. """ self.debug("IdP-initiated SAML2 logout") us = UserSession() user = us.get_user() saml_sessions = self.sessionfactory # pylint: disable=unused-variable (mech, session) = saml_sessions.get_next_logout( logout_mechs=[lasso.SAML2_METADATA_BINDING_REDIRECT], user=us.user) if session is None: return logout = self.idp.get_logout_handler() logout.setSessionFromDump(session.login_session) try: logout.initRequest(session.provider_id) except lasso.ServerProviderNotFoundError: self.error('Service Provider %s not found. Trying next session' % session.provider_id) return self.idp_initiated_logout() except lasso.ProfileUnsupportedProfileError: self.error('Service Provider %s does not support Single Logout. ' 'Trying next session' % session.provider_id) return self.idp_initiated_logout() try: logout.buildRequestMsg() except lasso.Error as e: self.error('failure to build logout request msg: %s' % e) raise cherrypy.HTTPRedirect(400, 'Failed to log out user: %s ' % e) # Add a fake session to indicate where the user should # be redirected to when all SP's are logged out. idpurl = self._root.instance_base_url() session_id = "_" + uuid.uuid4().hex.upper() saml_sessions.add_session(session_id, idpurl, user.name, "", "", [lasso.SAML2_METADATA_BINDING_REDIRECT]) init_session = saml_sessions.get_session_by_id(session_id) saml_sessions.start_logout(init_session, relaystate=idpurl) # Add the logout request id we just created to the session to be # logged out so that when it responds we can find the right # session. session.set_logoutstate(request_id=logout.request.id) saml_sessions.start_logout(session, initial=False) self.debug('Sending initial logout request to %s' % logout.msgUrl) raise cherrypy.HTTPRedirect(logout.msgUrl)
def revoke(self, provider, clientid): us = UserSession() user = us.get_user() provname = provider provmod = self._site['provider_config'].available.get(provname, None) if provmod is not None: if not provmod.revoke_consent(user.name, clientid): raise Exception('Provider refused to revoke') user.revoke_consent(provider, clientid) raise cherrypy.HTTPRedirect(self._master.url)
def POST(self, *args, **kwargs): us = UserSession() us.remote_login() user = us.get_user() if not user.is_anonymous: return self.lm.auth_successful(self.trans, user.name, 'password') else: error = cherrypy.request.wsgi_environ.get( 'EXTERNAL_AUTH_ERROR', 'Unknown error using external authentication') error = PAM_AUTH_ERRORS.get(error, error) cherrypy.log.error("Error: %s" % error) return self.lm.auth_failed(self.trans, error)
def POST(self, *args, **kwargs): us = UserSession() us.remote_login() user = us.get_user() if not user.is_anonymous: return self.lm.auth_successful(self.trans, user.name, 'password') else: error = cherrypy.request.wsgi_environ.get( 'EXTERNAL_AUTH_ERROR', 'Unknown error using external authentication' ) error = PAM_AUTH_ERRORS.get(error, error) cherrypy.log.error("Error: %s" % error) return self.lm.auth_failed(self.trans, error)
def root(self, *args, **kwargs): trans = self.get_valid_transaction("login", **kwargs) # If we can get here, we must be authenticated and remote_user # was set. Check the session has a user set already or error. us = UserSession() us.remote_login() self.user = us.get_user() if not self.user.is_anonymous: principal = cherrypy.request.wsgi_environ.get("GSS_NAME", None) if principal: userdata = {"gssapi_principal_name": principal} else: userdata = {"gssapi_principal_name": self.user.name} return self.lm.auth_successful(trans, self.user.name, "gssapi", userdata) else: return self.lm.auth_failed(trans)
def root(self, *args, **kwargs): trans = self.get_valid_transaction('login', **kwargs) # If we can get here, we must be authenticated and remote_user # was set. Check the session has a user set already or error. us = UserSession() us.remote_login(is_krb=True) self.user = us.get_user() if not self.user.is_anonymous: principal = cherrypy.request.wsgi_environ.get('GSS_NAME', None) if principal: userdata = {'gssapi_principal_name': principal} else: userdata = {'gssapi_principal_name': self.user.name} return self.lm.auth_successful(trans, self.user.name, 'gssapi', userdata) else: return self.lm.auth_failed(trans)
def POST(self, *args, **kwargs): if 'email' not in kwargs or 'publicKey' not in kwargs \ or 'certDuration' not in kwargs or '@' not in kwargs['email']: cherrypy.response.status = 400 raise Exception('Invalid request: %s' % kwargs) us = UserSession() user = us.get_user() if user.is_anonymous: raise cherrypy.HTTPError(401, 'Not signed in') if not self._willing_to_sign(kwargs['email'], user.name): self.log('Not willing to sign for %s, logged in as %s' % ( kwargs['email'], user.name)) raise cherrypy.HTTPError(403, 'Incorrect user') return self._persona_sign(kwargs['email'], kwargs['publicKey'], kwargs['certDuration'])
def POST(self, *args, **kwargs): if 'email' not in kwargs or 'publicKey' not in kwargs \ or 'certDuration' not in kwargs or '@' not in kwargs['email']: cherrypy.response.status = 400 raise Exception('Invalid request: %s' % kwargs) us = UserSession() user = us.get_user() if user.is_anonymous: raise cherrypy.HTTPError(401, 'Not signed in') if not self._willing_to_sign(kwargs['email'], user.name): self.log('Not willing to sign for %s, logged in as %s' % (kwargs['email'], user.name)) raise cherrypy.HTTPError(403, 'Incorrect user') return self._persona_sign(kwargs['email'], kwargs['publicKey'], kwargs['certDuration'])
def idp_initiated_logout(self): """ Logout all SP sessions when the logout comes from the IdP. For the current user only. Only use HTTP-Redirect to start the logout. This is guaranteed to be supported in SAML 2. """ self.debug("IdP-initiated SAML2 logout") us = UserSession() user = us.get_user() saml_sessions = self.sessionfactory # pylint: disable=unused-variable (mech, session) = saml_sessions.get_next_logout( logout_mechs=[lasso.SAML2_METADATA_BINDING_REDIRECT]) if session is None: return logout = self.idp.get_logout_handler() logout.setSessionFromDump(session.login_session) try: logout.initRequest(session.provider_id) except lasso.ServerProviderNotFoundError: self.error( 'Service Provider %s not found. Trying next session' % session.provider_id ) return self.idp_initiated_logout() except lasso.ProfileUnsupportedProfileError: self.error( 'Service Provider %s does not support Single Logout. ' 'Trying next session' % session.provider_id ) return self.idp_initiated_logout() try: logout.buildRequestMsg() except lasso.Error, e: self.error('failure to build logout request msg: %s' % e) raise cherrypy.HTTPRedirect(400, 'Failed to log out user: %s ' % e)
def start_authz(self, arguments): request_data = { 'scope': [], 'response_type': [], 'client_id': None, 'redirect_uri': None, 'state': None, 'response_mode': None, 'nonce': None, 'display': None, 'prompt': [], 'max_age': None, 'ui_locales': None, 'id_token_hint': None, 'login_hint': None, 'acr_values': None, 'claims': '{}' } # Get the request # Step 1: get the get query arguments for data in request_data.keys(): if arguments.get(data, None): request_data[data] = arguments[data] # This is a workaround for python not understanding the splits we # do later if request_data['prompt'] == []: request_data['prompt'] = None for required_arg in ['scope', 'response_type', 'client_id']: if request_data[required_arg] is None or \ len(request_data[required_arg]) == 0: return self._respond_error( request_data, 'invalid_request', 'missing required argument %s' % required_arg) client = self.cfg.datastore.getClient(request_data['client_id']) if not client: return self._respond_error(request_data, 'unauthorized_client', 'Unknown client ID') request_data['response_type'] = request_data.get('response_type', '').split(' ') for rtype in request_data['response_type']: if rtype not in ['id_token', 'token', 'code']: return self._respond_error( request_data, 'unsupported_response_type', 'response type %s is not supported' % rtype) if request_data['response_type'] != ['code'] and \ not request_data['nonce']: return self._respond_error(request_data, 'invalid_request', 'nonce missing in non-code flow') # Step 2: get any provided request or request_uri if 'request' in arguments or 'request_uri' in arguments: # This is a JWT-encoded request if 'request' in arguments and 'request_uri' in arguments: return self._respond_error( request_data, 'invalid_request', 'both request and request_uri ' + 'provided') if 'request' in arguments: jwt_object = arguments['request'] else: try: # FIXME: MAY cache this at client registration time and # cache permanently until client registration is changed. jwt_object = requests.get(arguments['request_uri']).text except Exception as ex: # pylint: disable=broad-except self.debug('Unable to get request: %s' % ex) return self._respond_error(request_data, 'invalid_request', 'unable to parse request_uri') jwt_request = None try: # FIXME: Implement decryption decoded = JWT(jwt=jwt_object) if client['request_object_signing_alg'] != 'none': # Client told us we need to check signature if decoded.token.jose_header['alg'] != \ client['request_object_signing_alg']: raise Exception('Invalid algorithm used: %s' % decoded.token.jose_header['alg']) if client['request_object_signing_alg'] == 'none': jwt_request = json.loads(decoded.token.objects['payload']) else: keyset = None if client['jwks']: keys = json.loads(client['jkws']) else: keys = requests.get(client['jwks_uri']).json() keyset = JWKSet() for key in keys['keys']: keyset.add(JWK(**key)) key = keyset.get_key(decoded.token.jose_header['kid']) decoded = JWT(jwt=jwt_object, key=key) jwt_request = json.loads(decoded.claims) except Exception as ex: # pylint: disable=broad-except self.debug('Unable to parse request: %s' % ex) return self._respond_error(request_data, 'invalid_request', 'unable to parse request') if 'response_type' in jwt_request: jwt_request['response_type'] = \ jwt_request['response_type'].split(' ') if jwt_request['response_type'] != \ request_data['response_type']: return self._respond_error(request_data, 'invalid_request', 'response_type does not match') if 'client_id' in jwt_request: if jwt_request['client_id'] != request_data['client_id']: return self._respond_error(request_data, 'invalid_request', 'client_id does not match') for data in request_data.keys(): if data in jwt_request: request_data[data] = jwt_request[data] # Split these options since they are space-separated lists for to_split in ['prompt', 'ui_locales', 'acr_values', 'scope']: if request_data[to_split] is not None: # We know better than pylint in this regard # pylint: disable=no-member request_data[to_split] = request_data[to_split].split(' ') else: request_data[to_split] = [] # Start checking the request if request_data['redirect_uri'] is None: if len(client['redirect_uris']) != 1: return self._respond_error(request_data, 'invalid_request', 'missing redirect_uri') else: request_data['redirect_uri'] = client['redirect_uris'][0] for scope in request_data['scope']: if scope not in self.cfg.supported_scopes: return self._respond_error( request_data, 'invalid_scope', 'unknown scope %s requested' % scope) for response_type in request_data['response_type']: if response_type not in ['code', 'id_token', 'token']: return self._respond_error( request_data, 'unsupported_response_type', 'response_type %s is unknown' % response_type) if request_data['redirect_uri'] not in client['redirect_uris']: raise InvalidRequest('Invalid redirect_uri') # Build the "claims" values from scopes try: request_data['claims'] = json.loads(request_data['claims']) except Exception as ex: # pylint: disable=broad-except return self._respond_error(request_data, 'invalid_request', 'claims malformed: %s' % ex) if 'userinfo' not in request_data['claims']: request_data['claims']['userinfo'] = {} if 'id_token' not in request_data['claims']: request_data['claims']['id_token'] = {} scopes_to_claim = { 'profile': [ 'name', 'family_name', 'given_name', 'middle_name', 'nickname', 'preferred_username', 'profile', 'picture', 'website', 'gender', 'birthdate', 'zoneinfo', 'locale', 'updated_at' ], 'email': ['email', 'email_verified'], 'address': ['address'], 'phone': ['phone_number', 'phone_number_verified'] } for scope in scopes_to_claim: if scope in request_data['scope']: for claim in scopes_to_claim[scope]: if claim not in request_data['claims']: # pylint: disable=invalid-sequence-index request_data['claims']['userinfo'][claim] = None # Add claims from extensions for n, e in self.cfg.extensions.available().items(): data = e.get_claims(request_data['scope']) self.debug('%s returned %s' % (n, repr(data))) for claim in data: # pylint: disable=invalid-sequence-index request_data['claims']['userinfo'][claim] = None # Store data so we can continue with the request us = UserSession() user = us.get_user() returl = '%s/%s/Continue?%s' % (self.basepath, URLROOT, self.trans.get_GET_arg()) data = { 'login_target': client.get('client_name', None), 'login_return': returl, 'openidc_stage': 'continue', 'openidc_request': json.dumps(request_data) } if request_data['login_hint']: data['login_username'] = request_data['login_hint'] if not data['login_target']: data['login_target'] = get_url_hostpart( request_data['redirect_uri']) # Decide what to do with the request if request_data['max_age'] is None: request_data['max_age'] = client.get('default_max_age', None) needs_auth = True if not user.is_anonymous: if request_data['max_age'] in [None, 0]: needs_auth = False else: auth_time = us.get_user_attrs()['_auth_time'] needs_auth = ((int(auth_time) + int(request_data['max_age'])) <= int(time.time())) if needs_auth or 'login' in request_data['prompt']: if 'none' in request_data['prompt']: # We were asked not to provide a UI. Answer with false. return self._respond_error(request_data, 'login_required', 'user interface required') # Either the user wasn't logged in, or we were explicitly # asked to re-auth them. Let's do so! us.logout(user) # Let the user go to auth self.trans.store(data) redirect = '%s/login?%s' % (self.basepath, self.trans.get_GET_arg()) self.debug('Redirecting: %s' % redirect) raise cherrypy.HTTPRedirect(redirect) # Return error if authz check fails authz_check_res = self._authz_stack_check(request_data, client, user.name, us.get_user_attrs()) if authz_check_res: return authz_check_res self.trans.store(data) # The user was already signed on, and no request to re-assert its # identity. Let's forward directly to /Continue/ self.debug('Redirecting: %s' % returl) raise cherrypy.HTTPRedirect(returl)
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 logout(self, message, relaystate=None, samlresponse=None): """ Handle HTTP logout. The supported logout methods are stored in each session. First all the SOAP sessions are logged out then the HTTP Redirect method is used for any remaining sessions. The basic process is this: 1. A logout request is received. It is processed and the response cached. 2. If any other SP's have also logged in as this user then the first such session is popped off and a logout request is generated and forwarded to the SP. 3. If a logout response is received then the user is marked as logged out from that SP. Repeat steps 2-3 until only the initial logout request is left unhandled, at which time the pre-generated response is sent back to the SP that originated the logout request. The final logout response is always a redirect. """ logout = self.cfg.idp.get_logout_handler() us = UserSession() saml_sessions = self.cfg.idp.sessionfactory logout_type = None try: if lasso.SAML2_FIELD_REQUEST in message: logout_type = "request" self._handle_logout_request(us, logout, saml_sessions, message) elif samlresponse: logout_type = "response" self._handle_logout_response(us, logout, saml_sessions, message, samlresponse) else: raise cherrypy.HTTPError( 400, 'Bad Request. Not a ' + 'logout request or response.') except InvalidRequest as e: raise cherrypy.HTTPError(400, 'Bad Request. %s' % e) except UnknownProvider as e: raise cherrypy.HTTPError( 400, 'Invalid logout %s: %s' % (logout_type, e)) # Fall through to handle any remaining sessions. # Find the next SP to logout and send a LogoutRequest logout_order = [ lasso.SAML2_METADATA_BINDING_SOAP, lasso.SAML2_METADATA_BINDING_REDIRECT, ] (logout_mech, session) = saml_sessions.get_next_logout(logout_mechs=logout_order, user=us.user) while session: self.debug('Going to log out %s' % session.provider_id) try: logout.setSessionFromDump(session.login_session) except lasso.ProfileBadSessionDumpError as e: self.error('Failed to load session: %s' % e) raise cherrypy.HTTPRedirect(400, 'Failed to log out user: %s ' % e) try: if logout_mech == lasso.SAML2_METADATA_BINDING_REDIRECT: logout.initRequest(session.provider_id, lasso.HTTP_METHOD_REDIRECT) else: logout.initRequest(session.provider_id, lasso.HTTP_METHOD_SOAP) except lasso.ServerProviderNotFoundError: self.error( 'Service Provider %s not found. Trying next session' % session.provider_id) saml_sessions.remove_session(session) (logout_mech, session) = saml_sessions.get_next_logout( logout_mechs=logout_order, user=us.user) continue try: logout.buildRequestMsg() except lasso.Error as e: self.error('failure to build logout request msg: %s' % e) raise cherrypy.HTTPRedirect(400, 'Failed to log out user: %s ' % e) # Set the full list of session indexes for this provider to # log out self.debug('logging out provider id %s' % session.provider_id) indexes = saml_sessions.get_session_id_by_provider_id( session.provider_id, us.user) self.debug('Requesting logout for sessions %s' % (indexes, )) req = logout.get_request() req.setSessionIndexes(indexes) session.set_logoutstate(relaystate=logout.msgUrl, request_id=logout.request.id) saml_sessions.start_logout(session, initial=False) self.debug('Request logout ID %s for session ID %s' % (logout.request.id, session.session_id)) if logout_mech == lasso.SAML2_METADATA_BINDING_REDIRECT: self.debug('Redirecting to another SP to logout on %s at %s' % (logout.remoteProviderId, logout.msgUrl)) raise cherrypy.HTTPRedirect(logout.msgUrl) else: self.debug('SOAP request to another SP to logout on %s at %s' % (logout.remoteProviderId, logout.msgUrl)) if logout.msgBody: message = self._soap_logout(logout) try: self._handle_logout_response(us, logout, saml_sessions, message, samlresponse) except Exception as e: # pylint: disable=broad-except self.error('SOAP SLO failed %s' % e) else: self.error('Provider does not support SOAP') (logout_mech, session) = saml_sessions.get_next_logout( logout_mechs=logout_order, user=us.user) # done while # All sessions should be logged out now. Respond to the # original request using the response we cached earlier. try: session = saml_sessions.get_initial_logout(us.user) except ValueError: self.debug('SLO get_last_session() unable to find last session') raise cherrypy.HTTPError(400, 'Unable to determine logout state') redirect = session.relaystate if not redirect: redirect = self.basepath saml_sessions.remove_session(session) # Log out of cherrypy session user = us.get_user() self._audit('Logged out user: %s [%s] from %s' % (user.name, user.fullname, session.provider_id)) us.logout(user) self.debug('SLO redirect to %s' % redirect) raise cherrypy.HTTPRedirect(redirect)
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 _perform_continue(self, *args, **kwargs): us = UserSession() user = us.get_user() if user.is_anonymous: raise InvalidRequest('User not authenticated at continue') transdata = self.trans.retrieve() stage = transdata.get('openidc_stage', None) request_data = transdata.get('openidc_request', None) if stage not in ['continue', 'consent'] or request_data is None: raise InvalidRequest('Invalid stage or no request') # Since we have openidc_stage continue or consent, request is sane try: request_data = json.loads(request_data) except: raise InvalidRequest('Unable to re-parse stored request') client = self.cfg.datastore.getClient(request_data['client_id']) if not client: return self._respond_error(request_data, 'unauthorized_client', 'Unknown client ID') # Return error if authz check fails authz_check_res = self._authz_stack_check(request_data, client, user.name, us.get_user_attrs()) if authz_check_res: return authz_check_res userattrs = self._source_attributes(us) if client['ipsilon_internal']['trusted']: # No consent needed, approve self.debug('Client trusted, no consent needed') return self._respond_success(request_data, client, user, userattrs) consentdata = user.get_consent('openidc', request_data['client_id']) if consentdata is not None: # Consent has already been granted self.debug('Consent already granted') consclaimset = set(consentdata['claims']) claimset = set( self._valid_claims(request_data['claims'], userattrs)) consscopeset = set(consentdata['scopes']) scopeset = set(self._valid_scopes(request_data['scope'])) if claimset.issubset(consclaimset) and \ scopeset.issubset(consscopeset): return self._respond_success(request_data, client, user, userattrs) else: self.debug('Client is asking for new claims or scopes, user ' 'must give consent again') if 'none' in request_data['prompt']: # We were asked to not show any UI return self._respond_error(request_data, 'consent_required', 'user interface required') # Now ask consent if 'form_filled' in kwargs and stage == 'consent': # The user has been shown the form, let's process his choice if 'decided_allow' in kwargs: # User allowed the request # Record the consent for the future, including the list of # claims that the user was informed of at the time. consentdata = { 'claims': self._valid_claims(request_data['claims'], userattrs), 'scopes': self._valid_scopes(request_data['scope']) } user.grant_consent('openidc', request_data['client_id'], consentdata) return self._respond_success(request_data, client, user, userattrs) else: # User denied consent self.debug('User denied consent') return self._respond_error(request_data, 'access_denied', 'user denied consent') else: # The user was not shown the form yet, let's data = { 'openidc_stage': 'consent', 'openidc_request': json.dumps(request_data) } self.trans.store(data) claim_requests = {} for claimtype in request_data['claims']: for claim in request_data['claims'][claimtype]: if claim in userattrs: essential = False if isinstance(request_data['claims'][claimtype][claim], dict): essential = \ request_data['claims'][claimtype][claim].get( 'essential', False) claim_requests[claim] = { 'display_name': self.cfg.mapping.display_name(claim), 'value': userattrs[claim], 'essential': essential } scopes = {} # Add extension data for n, e in self.cfg.extensions.available().items(): data = e.get_display_data(request_data['scope']) self.debug('%s returned %s' % (n, repr(data))) if len(data) > 0: scopes[e.get_display_name()] = data client_params = { 'name': client.get('client_name', None), 'logo': client.get('logo_uri', None), 'homepage': client.get('client_uri', None), 'policy': client.get('policy_uri', None), 'tos': client.get('tos_uri', None) } if not client_params['name']: client_params['name'] = get_url_hostpart( request_data['redirect_uri']) context = { "title": 'Consent', "action": '%s/%s/Continue' % (self.basepath, URLROOT), "client": client_params, "claim_requests": claim_requests, "scopes": scopes, "username": user.name, } context.update(dict((self.trans.get_POST_tuple(), ))) return self._template(URLROOT + '/consent_form.html', **context)