Example #1
0
    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'])
Example #2
0
    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')
Example #3
0
    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'])
Example #4
0
    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)
Example #5
0
    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)