Beispiel #1
0
    def _soap_logout(self, logout):
        """
        Send a SOAP logout request over HTTP and return the result.
        """
        headers = {'Content-Type': SOAP_MEDIA_TYPE}
        try:
            response = requests.post(logout.msgUrl,
                                     data=logout.msgBody,
                                     headers=headers)
        except Exception as e:  # pylint: disable=broad-except
            self.error('SOAP HTTP request failed: (%s) (on %s)' %
                       (e, logout.msgUrl))
            raise

        if response.status_code != 200:
            self.error('SOAP error (%s) (on %s)' %
                       (response.status_code, logout.msgUrl))
            raise InvalidRequest('SOAP HTTP error code %s' %
                                 response.status_code)

        if not response.text:
            self.error('Empty SOAP response')
            raise InvalidRequest('No content in SOAP response')

        return response.text
Beispiel #2
0
 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()
Beispiel #3
0
    def GET(self, *args, **kwargs):
        transdata = self.trans.retrieve()
        self.stage = transdata.get('openid_stage', None)
        openid_request = transdata.get('openid_request', None)
        if self.stage is None or openid_request is None:
            raise InvalidRequest("unknown state")

        kwargs = json.loads(openid_request)
        return self.auth(**kwargs)
Beispiel #4
0
    def _respond_error(self, request, error, message):
        self.log('Responding with error: %s, message: %s' % (error, message))
        if request.get('redirect_uri') is None:
            self.log('No valid redirect URI')
            raise InvalidRequest('Request is missing redirct_uri')

        return self._respond(request, {
            'error': error,
            'error_description': message
        })
Beispiel #5
0
    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
Beispiel #6
0
    def _respond(self, request, contents):
        url = request['redirect_uri']
        response_mode = request.get('response_mode', None)
        response_type = request.get('response_type', [])
        if 'none' in response_type:
            response_mode = 'none'
            self.debug('none response_type, using none response_mode')
        elif 'id_token' in response_type or 'token' in response_type:
            if response_mode in [None, 'query']:
                # If no response_mode or query response_mode is selected,
                # fall back to the default for id_token or token requests,
                # which is fragment encoding. The query override is because
                # the specifications specify that with id_token or token,
                # query MUST NOT be used.
                response_mode = 'fragment'
                self.debug('id_token requesed, fragment response_mode forced')
        elif not response_mode:
            # We still have no response_mode, fall back to query
            # This also happens in case we were unable to parse the request,
            # and as such were unable to get the response_mode the client
            # preferred
            response_mode = 'query'
            self.debug('Using default query response mode')

        # If the client sent a state, we need to pass that back
        if 'state' in request and request['state']:
            contents['state'] = request['state']

        # Build a response-string, which is sent with either query, form
        # or fragment responses
        if response_mode in ['query', 'fragment']:

            separator = '?'
            if response_mode == 'fragment':
                separator = '#'
            if separator not in url:
                url += separator
            else:
                url += '&'

            url += urllib.urlencode(contents)

        if response_mode in ['query', 'fragment', 'none']:
            raise cherrypy.HTTPRedirect(url)
        elif response_mode == 'form_post':
            context = {
                "title": "Continue",
                "redirect_url": url,
                "response_info": contents
            }
            return self._template(URLROOT + '/form_response.html', **context)
        else:
            raise InvalidRequest('Invalid response_mode requested')
Beispiel #7
0
    def _not_logged_in(self, logout, message):
        """
        The user requested a logout but isn't logged in, or we can't
        find a session for the user. Try to be nice and redirect them
        back to the RelayState in the logout request.

        We are only nice in the case of a valid logout request. If the
        request is invalid (not signed, unknown SP, etc) then an
        exception is raised.
        """
        self.error('Logout attempt without being logged in.')

        if logout.msgRelayState is not None:
            raise cherrypy.HTTPRedirect(logout.msgRelayState)

        try:
            logout.processRequestMsg(message)
        except lasso.DsInvalidSigalgError as e:
            msg = 'Invalid or missing signature algorithm %r [%r]' % (e,
                                                                      message)
            raise InvalidRequest(msg)
        except (lasso.ServerProviderNotFoundError,
                lasso.ProfileUnknownProviderError) as e:
            msg = 'Invalid SP [%s] (%r [%r])' % (logout.remoteProviderId, e,
                                                 message)
            self.error(msg)
            raise UnknownProvider(msg)
        except (lasso.ProfileInvalidProtocolprofileError, lasso.DsError) as e:
            msg = 'Invalid SAML Request: %r (%r [%r])' % (logout.request, e,
                                                          message)
            self.error(msg)
            raise InvalidRequest(msg)
        except lasso.Error as e:
            self.error('SLO unknown error: %s' % message)
            raise cherrypy.HTTPError(400, 'Invalid logout request')

        if logout.msgRelayState:
            raise cherrypy.HTTPRedirect(logout.msgRelayState)
        else:
            raise cherrypy.HTTPError(400, 'Not logged in')
Beispiel #8
0
    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
Beispiel #9
0
    def _handle_logout_response(self, us, logout, saml_sessions, message,
                                samlresponse):

        self.debug('Logout response')

        try:
            logout.processResponseMsg(message)
        except lasso.ProfileCannotVerifySignatureError as e:
            msg = 'Invalid or missing signature algorithm %r [%r]' % (e,
                                                                      message)
            raise InvalidRequest(msg)
        except getattr(lasso, 'ProfileRequestDeniedError',
                       lasso.LogoutRequestDeniedError):
            self.error('Logout request denied by %s' % logout.remoteProviderId)
            # Fall through to next provider
        except (lasso.ProfileInvalidMsgError,
                lasso.LogoutPartialLogoutError) as e:
            self.error('Logout request from %s failed: %s' %
                       (logout.remoteProviderId, e))
        else:
            self.debug('Processing SLO Response from %s' %
                       logout.remoteProviderId)

            self.debug('SLO response to request id %s' %
                       logout.response.inResponseTo)

            session = saml_sessions.get_session_by_request_id(
                logout.response.inResponseTo)

            if session is not None:
                self.debug('Logout response session logout id is: %s' %
                           session.session_id)
                saml_sessions.remove_session(session)
                user = us.get_user()
                self._audit(
                    'Logged out user: %s [%s] from %s' %
                    (user.name, user.fullname, logout.remoteProviderId))
            else:
                return self._not_logged_in(logout, message)

        return
Beispiel #10
0
    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)
Beispiel #11
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)
Beispiel #12
0
    def _handle_logout_request(self, us, logout, saml_sessions, message):
        self.debug('Logout request')

        try:
            logout.processRequestMsg(message)
        except (lasso.ServerProviderNotFoundError,
                lasso.ProfileUnknownProviderError) as e:
            msg = 'Invalid SP [%s] (%r [%r])' % (logout.remoteProviderId, e,
                                                 message)
            self.error(msg)
            raise UnknownProvider(msg)
        except lasso.DsInvalidSigalgError as e:
            msg = 'Invalid SAML Request: missing or invalid signature ' \
                  'algorithm'
            self.error(msg)
            raise InvalidRequest(msg)
        except (lasso.ProfileInvalidProtocolprofileError, lasso.DsError) as e:
            msg = 'Invalid SAML Request: %r (%r [%r])' % (logout.request, e,
                                                          message)
            self.error(msg)
            raise InvalidRequest(msg)
        except lasso.Error as e:
            self.error('SLO unknown error: %s' % message)
            raise cherrypy.HTTPError(400, 'Invalid logout request')

        session_indexes = logout.request.sessionIndexes
        self.debug('SLO from %s with %s sessions' %
                   (logout.remoteProviderId, session_indexes))

        # Find the first session being asked to log out. Later we loop over
        # all the session indexes and mark them as logging out but only one
        # is needed to handle the request.
        if len(session_indexes) < 1:
            self.error('SLO empty session Indexes')
            raise cherrypy.HTTPError(400, 'Invalid logout request')
        session = saml_sessions.get_session_by_id(session_indexes[0])
        if session:
            try:
                logout.setSessionFromDump(session.login_session)
            except lasso.ProfileBadSessionDumpError as e:
                self.error('loading session failed: %s' % e)
                raise cherrypy.HTTPError(400, 'Invalid logout session')
        else:
            return self._not_logged_in(logout, message)

        try:
            logout.validateRequest()
        except lasso.ProfileSessionNotFoundError as e:
            self.error('Logout failed. No sessions for %s' %
                       logout.remoteProviderId)
            return self._not_logged_in(logout, message)
        except lasso.LogoutUnsupportedProfileError:
            self.error('Logout failed. Unsupported profile %s' %
                       logout.remoteProviderId)
            raise cherrypy.HTTPError(400, 'Profile does not support logout')
        except lasso.Error as e:
            self.error('SLO validation failed: %s' % e)
            raise cherrypy.HTTPError(400, 'Failed to validate logout request')

        try:
            logout.buildResponseMsg()
        except lasso.ProfileUnsupportedProfileError:
            self.error('Unsupported profile for %s' % logout.remoteProviderId)
            raise cherrypy.HTTPError(400, 'Profile does not support logout')
        except lasso.Error as e:
            self.error('SLO failed to build logout response: %s' % e)

        for ind in session_indexes:
            session = saml_sessions.get_session_by_id(ind)
            if session:
                session.set_logoutstate(relaystate=logout.msgUrl,
                                        request=message)
                saml_sessions.start_logout(session)
            else:
                self.error('SLO request to log out non-existent session: %s' %
                           ind)

        return