def GET(self, *args, **kwargs): session = UserSession() session.logout(None) # return to the caller if any transdata = self.get_valid_transaction('login', **kwargs).retrieve() if 'login_return' not in transdata: raise cherrypy.HTTPError(401) raise cherrypy.HTTPRedirect(transdata['login_return'])
def root(self, *args, **kwargs): us = UserSession() if us.user is not None: for provider in self.handlers: self.debug("Calling logout for provider %s" % provider) obj = self.handlers[provider] obj() us.logout(self.user) return self._template('logout.html', title='Logout')
def auth_failed(self, trans, message=None): # try with next module next_login = self.next_login() data = {'message': message} trans.store(data) if next_login: return self.redirect_to_path(next_login.path, trans) # return to the caller if any session = UserSession() transdata = trans.retrieve() # on direct login the UI (ie not redirected by a provider) we ned to # remove the transaction cookie as it won't be needed anymore if trans.provider == 'login': trans.wipe() # destroy session and return error if 'login_return' not in transdata: session.logout(None) raise cherrypy.HTTPError(401, message) raise cherrypy.HTTPRedirect(transdata['login_return'])
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 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)