authentic/src/authentic2_idp_oidc/views.py

816 lines
30 KiB
Python

# authentic2 - versatile identity manager
# Copyright (C) 2010-2019 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import base64
import datetime
import logging
import math
from binascii import Error as Base64Error
try:
from secrets import compare_digest
except ImportError:
def compare_digest(a, b):
return a == b
import time
from django.conf import settings
from django.contrib import messages
from django.contrib.auth import authenticate
from django.http import HttpResponse, HttpResponseNotAllowed, JsonResponse
from django.shortcuts import render
from django.urls import reverse
from django.utils.encoding import force_text
from django.utils.http import urlencode
from django.utils.timezone import now, utc
from django.utils.translation import ugettext as _
from django.views.decorators.csrf import csrf_exempt
from ratelimit.utils import is_ratelimited
from authentic2 import app_settings as a2_app_settings
from authentic2 import hooks
from authentic2.decorators import setting_enabled
from authentic2.exponential_retry_timeout import ExponentialRetryTimeout
from authentic2.utils.misc import last_authentication_event, login_require, make_url, redirect
from authentic2.utils.view_decorators import enable_view_restriction
from authentic2.views import logout as a2_logout
from django_rbac.utils import get_ou_model
from . import app_settings, models, utils
logger = logging.getLogger(__name__)
class OIDCException(Exception):
error_code = None
error_description = None
show_message = True
def __init__(self, error_description=None, status=400, client=None, show_message=None):
if error_description:
self.error_description = error_description
self.status = status
self.client = client
if show_message is not None:
self.show_message = show_message
def json_response(self, request, endpoint):
content = {
'error': self.error_code,
}
if self.error_description:
content['error_description'] = self.error_description
if self.client:
logger.warning(
'idp_oidc: error "%s" in %s endpoint "%s" for client %s',
self.error_code,
endpoint,
self.error_description,
self.client,
)
else:
logger.warning(
'idp_oidc: error "%s" in %s endpoint "%s"', self.error_code, endpoint, self.error_description
)
return JsonResponse(content, status=self.status)
def redirect_response(self, request, redirect_uri=None, use_fragment=None, state=None, client=None):
params = {
'error': self.error_code,
'error_description': self.error_description,
}
if state is not None:
params['state'] = state
log_method = logger.warning
if not self.show_message:
# errors not shown as Django messages are regular events, no need to log as warning
log_method = logger.info
client = client or self.client
if client:
log_method(
'idp_oidc: error "%s" in authorize endpoint for client %s": %s',
self.error_code,
client,
self.error_description,
)
else:
log_method(
'idp_oidc: error "%s" in authorize endpoint: %s', self.error_code, self.error_description
)
if self.show_message:
messages.error(
request,
_('OpenIDConnect Error "%(error_code)s": %(error_description)s')
% {'error_code': self.error_code, 'error_description': self.error_description},
)
if redirect_uri:
if use_fragment:
return redirect(request, redirect_uri + '#%s' % urlencode(params), resolve=False)
else:
return redirect(request, redirect_uri, params=params, resolve=False)
else:
return redirect(request, 'continue', resolve=True)
class InvalidRequest(OIDCException):
error_code = 'invalid_request'
class InvalidToken(OIDCException):
error_code = 'invalid_token'
class MissingParameter(InvalidRequest):
def __init__(self, parameter, **kwargs):
super().__init__(error_description=_('Missing parameter "%s"') % parameter, **kwargs)
class UnsupportedResponseType(OIDCException):
error_code = 'unsupported_response_type'
class InvalidScope(OIDCException):
error_code = 'invalid_scope'
class LoginRequired(OIDCException):
error_code = 'login_required'
show_message = False
class ConsentRequired(OIDCException):
error_code = 'consent_required'
show_message = False
class AccessDenied(OIDCException):
error_code = 'access_denied'
show_message = False
class UnauthorizedClient(OIDCException):
error_code = 'unauthorized_client'
class InvalidClient(OIDCException):
error_code = 'invalid_client'
class WrongClientId(InvalidClient):
error_description = _('Wrong client identifier')
class WrongClientSecret(InvalidClient):
error_description = _('Wrong client secret')
def idtoken_duration(client):
return client.idtoken_duration or datetime.timedelta(seconds=app_settings.IDTOKEN_DURATION)
def allowed_scopes(client):
return client.scope_set() or app_settings.SCOPES or ['openid', 'email', 'profile']
def is_scopes_allowed(scopes, client):
return scopes <= set(allowed_scopes(client))
@setting_enabled('ENABLE', settings=app_settings)
def openid_configuration(request, *args, **kwargs):
metadata = {
'issuer': utils.get_issuer(request),
'authorization_endpoint': request.build_absolute_uri(reverse('oidc-authorize')),
'token_endpoint': request.build_absolute_uri(reverse('oidc-token')),
'jwks_uri': request.build_absolute_uri(reverse('oidc-certs')),
'end_session_endpoint': request.build_absolute_uri(reverse('oidc-logout')),
'response_types_supported': ['code', 'token', 'token id_token'],
'subject_types_supported': ['public', 'pairwise'],
'token_endpoint_auth_methods_supported': [
'clien_secret_post',
'client_secret_basic',
],
'id_token_signing_alg_values_supported': [
'RS256',
'HS256',
'ES256',
],
'userinfo_endpoint': request.build_absolute_uri(reverse('oidc-user-info')),
'frontchannel_logout_supported': True,
'frontchannel_logout_session_supported': True,
}
return JsonResponse(metadata)
@setting_enabled('ENABLE', settings=app_settings)
def certs(request, *args, **kwargs):
return HttpResponse(utils.get_jwkset().export(private_keys=False), content_type='application/json')
@enable_view_restriction
@setting_enabled('ENABLE', settings=app_settings)
def authorize(request, *args, **kwargs):
validated_redirect_uri = None
client_id = None
client = None
try:
client_id = request.GET.get('client_id', '')
if not client_id:
raise MissingParameter('client_id')
redirect_uri = request.GET.get('redirect_uri', '')
if not redirect_uri:
raise MissingParameter('redirect_uri')
client = get_client(client_id=client_id)
if not client:
raise InvalidRequest(_('Unknown client identifier: "%s"') % client_id)
try:
client.validate_redirect_uri(redirect_uri)
except ValueError:
error_description = _('Redirect URI "%s" is unknown.') % redirect_uri
if settings.DEBUG:
error_description += _(' Known redirect URIs are: %s') % ', '.join(
client.redirect_uris.split()
)
raise InvalidRequest(error_description)
state = request.GET.get('state')
use_fragment = client.authorization_flow == client.FLOW_IMPLICIT
validated_redirect_uri = redirect_uri
return authorize_for_client(request, client, validated_redirect_uri)
except OIDCException as e:
return e.redirect_response(
request,
redirect_uri=validated_redirect_uri,
state=validated_redirect_uri and state,
use_fragment=validated_redirect_uri and use_fragment,
client=client,
)
def authorize_for_client(request, client, redirect_uri):
hooks.call_hooks('event', name='sso-request', idp='oidc', service=client)
state = request.GET.get('state')
nonce = request.GET.get('nonce')
login_hint = set(request.GET.get('login_hint', '').split())
prompt = set(filter(None, request.GET.get('prompt', '').split()))
# check response_type
response_type = request.GET.get('response_type', '')
if not response_type:
raise MissingParameter('response_type')
if client.authorization_flow == client.FLOW_RESOURCE_OWNER_CRED:
raise InvalidRequest(
_(
'Client is configured for resource owner password credentials grant, '
'authorize endpoint is not usable'
)
)
if client.authorization_flow == client.FLOW_AUTHORIZATION_CODE:
if response_type != 'code':
raise UnsupportedResponseType(_('Response type must be "code"'))
elif client.authorization_flow == client.FLOW_IMPLICIT:
if not set(filter(None, response_type.split())) in ({'id_token', 'token'}, {'id_token'}):
raise UnsupportedResponseType(_('Response type must be "id_token token" or "id_token"'))
else:
raise NotImplementedError
# check scope
scope = request.GET.get('scope', '')
if not scope:
raise MissingParameter('scope')
scopes = utils.scope_set(scope)
if 'openid' not in scopes:
raise InvalidScope(_('Scope must contain "openid", received "%s"') % ', '.join(sorted(scopes)))
if not is_scopes_allowed(scopes, client):
raise InvalidScope(
_('Scope may contain "%(allowed_scopes)s" scope(s), received "%(scopes)s"')
% {
'allowed_scopes': ', '.join(sorted(allowed_scopes(client))),
'scopes': ', '.join(sorted(scopes)),
}
)
# check max_age
max_age = request.GET.get('max_age')
if max_age:
try:
max_age = int(max_age)
if max_age < 0:
raise ValueError
except ValueError:
raise InvalidRequest(_('Parameter "max_age" must be a positive integer'))
# authentication canceled by user
if 'cancel' in request.GET:
raise AccessDenied(_('Authentication cancelled by user'))
if not request.user.is_authenticated or 'login' in prompt:
if 'none' in prompt:
raise LoginRequired(_('Login is required but prompt parameter is "none"'))
params = {}
if nonce is not None:
params['nonce'] = nonce
return login_require(request, params=params, service=client, login_hint=login_hint)
# if user not authorized, a ServiceAccessDenied exception
# is raised and handled by ServiceAccessMiddleware
client.authorize(request.user)
last_auth = last_authentication_event(request=request)
if max_age is not None and time.time() - last_auth['when'] >= max_age:
if 'none' in prompt:
raise LoginRequired(_('Login is required because of max_age, but prompt parameter is "none"'))
params = {}
if nonce is not None:
params['nonce'] = nonce
return login_require(request, params=params, service=client, login_hint=login_hint)
iat = now() # iat = issued at
if client.authorization_mode != client.AUTHORIZATION_MODE_NONE or 'consent' in prompt:
# authorization by user is mandatory, as per local configuration or per explicit request by
# the RP
if client.authorization_mode in (
client.AUTHORIZATION_MODE_NONE,
client.AUTHORIZATION_MODE_BY_SERVICE,
):
auth_manager = client.authorizations
elif client.authorization_mode == client.AUTHORIZATION_MODE_BY_OU:
auth_manager = client.ou.oidc_authorizations
qs = auth_manager.filter(user=request.user)
if 'consent' in prompt:
# if consent is asked we delete existing authorizations
# it seems to be the safer option
qs.delete()
qs = auth_manager.none()
else:
qs = qs.filter(expired__gte=iat)
authorized_scopes = set()
for authorization in qs:
authorized_scopes |= authorization.scope_set()
if (authorized_scopes & scopes) < scopes:
if 'none' in prompt:
raise ConsentRequired(_('Consent is required but prompt parameter is "none"'))
if request.method == 'POST':
if 'accept' in request.POST:
if 'do_not_ask_again' in request.POST:
pk_to_deletes = []
for authorization in qs:
# clean obsolete authorizations
if authorization.scope_set() <= scopes:
pk_to_deletes.append(authorization.pk)
auth_manager.create(
user=request.user,
scopes=' '.join(sorted(scopes)),
expired=iat + datetime.timedelta(days=365),
)
if pk_to_deletes:
auth_manager.filter(pk__in=pk_to_deletes).delete()
request.journal.record(
'user.service.sso.authorization', service=client, scopes=list(sorted(scopes))
)
logger.info(
'idp_oidc: authorized scopes %s saved for service %s', ' '.join(scopes), client
)
else:
logger.info('idp_oidc: authorized scopes %s for service %s', ' '.join(scopes), client)
else:
raise AccessDenied(_('User did not consent'))
else:
return render(
request,
'authentic2_idp_oidc/authorization.html',
{
'client': client,
'scopes': scopes - {'openid'},
},
)
if response_type == 'code':
code = models.OIDCCode.objects.create(
client=client,
user=request.user,
scopes=' '.join(scopes),
state=state,
nonce=nonce,
redirect_uri=redirect_uri,
expired=iat + datetime.timedelta(seconds=30),
auth_time=datetime.datetime.fromtimestamp(last_auth['when'], utc),
session_key=request.session.session_key,
)
logger.info(
'idp_oidc: sending code %s for scopes %s for service %s', code.uuid, ' '.join(scopes), client
)
params = {
'code': str(code.uuid),
}
if state is not None:
params['state'] = state
response = redirect(request, redirect_uri, params=params, resolve=False)
else:
need_access_token = 'token' in response_type.split()
if need_access_token:
if client.access_token_duration is None:
expires_in = datetime.timedelta(seconds=request.session.get_expiry_age())
expired = None
else:
expires_in = client.access_token_duration
expired = iat + client.access_token_duration
access_token = models.OIDCAccessToken.objects.create(
client=client,
user=request.user,
scopes=' '.join(scopes),
session_key=request.session.session_key,
expired=expired,
)
acr = '0'
if nonce is not None and last_auth.get('nonce') == nonce:
acr = '1'
id_token = utils.create_user_info(request, client, request.user, scopes, id_token=True)
exp = iat + idtoken_duration(client)
id_token.update(
{
'iss': utils.get_issuer(request),
'aud': client.client_id,
'exp': int(exp.timestamp()),
'iat': int(iat.timestamp()),
'auth_time': last_auth['when'],
'acr': acr,
'sid': utils.get_session_id(request, client),
}
)
if nonce is not None:
id_token['nonce'] = nonce
params = {
'id_token': utils.make_idtoken(client, id_token),
}
if state is not None:
params['state'] = state
if need_access_token:
params.update(
{
'access_token': access_token.uuid,
'token_type': 'Bearer',
'expires_in': int(expires_in.total_seconds()),
}
)
# query is transfered through the hashtag
response = redirect(request, redirect_uri + '#%s' % urlencode(params), resolve=False)
request.journal.record('user.service.sso', service=client, how=last_auth and last_auth.get('how'))
hooks.call_hooks('event', name='sso-success', idp='oidc', service=client, user=request.user)
utils.add_oidc_session(request, client)
return response
def parse_http_basic(request):
authorization = request.META['HTTP_AUTHORIZATION'].split()
if authorization[0] != 'Basic' or len(authorization) != 2:
return None, None
try:
decoded = force_text(base64.b64decode(authorization[1]))
except Base64Error:
return None, None
parts = decoded.split(':')
if len(parts) != 2:
return None, None
return parts
def get_client(client_id, client=None):
if not client:
try:
client = models.OIDCClient.objects.get(client_id=client_id)
except models.OIDCClient.DoesNotExist:
return None
else:
if client.client_id != client_id:
return None
return client
def authenticate_client_secret(client, client_secret):
raw_client_client_secret = client.client_secret.encode('utf-8')
raw_provided_client_secret = client_secret.encode('utf-8')
if len(raw_client_client_secret) != len(raw_provided_client_secret):
raise WrongClientSecret(client=client)
if not compare_digest(raw_client_client_secret, raw_provided_client_secret):
raise WrongClientSecret(client=client)
return client
def is_ro_cred_grant_ratelimited(request, key='ip', increment=True):
return is_ratelimited(
request,
group='ro-cred-grant',
increment=increment,
key=key,
rate=app_settings.PASSWORD_GRANT_RATELIMIT,
)
def authenticate_client(request, ratelimit=False, client=None):
'''Authenticate client on the token endpoint'''
if 'HTTP_AUTHORIZATION' in request.META:
client_id, client_secret = parse_http_basic(request)
elif 'client_id' in request.POST:
client_id = request.POST.get('client_id', '')
client_secret = request.POST.get('client_secret', '')
else:
return None
if not client_id:
raise WrongClientId
if not client_secret:
raise InvalidRequest('missing client_secret', client=client_id)
client = get_client(client_id)
if not client:
raise WrongClientId
return authenticate_client_secret(client, client_secret)
def idtoken_from_user_credential(request):
# if rate limit by ip is exceeded, do not even try client authentication
if is_ro_cred_grant_ratelimited(request, increment=False):
raise InvalidRequest('Rate limit exceeded for IP address "%s"' % request.META.get('REMOTE_ADDR', ''))
try:
client = authenticate_client(request, ratelimit=True, client=None)
except InvalidClient:
# increment rate limit by IP
if is_ro_cred_grant_ratelimited(request):
raise InvalidRequest(
_('Rate limit exceeded for IP address "%s"') % request.META.get('REMOTE_ADDR', '')
)
raise
# check rate limit by client id
if is_ro_cred_grant_ratelimited(request, key=lambda group, request: client.client_id):
raise InvalidClient(
_('Rate limit of %(ratelimit)s exceeded for client "%(client)s"')
% {'ratelimit': app_settings.PASSWORD_GRANT_RATELIMIT, 'client': client},
client=client,
)
if request.META.get('CONTENT_TYPE') != 'application/x-www-form-urlencoded':
raise InvalidRequest(
_('Wrong content type. request content type must be ' '\'application/x-www-form-urlencoded\''),
client=client,
)
username = request.POST.get('username')
scope = request.POST.get('scope')
OrganizationalUnit = get_ou_model()
# scope is ignored, we used the configured scope
if not all((username, request.POST.get('password'))):
raise InvalidRequest(
_(
'Request must bear both username and password as '
'parameters using the "application/x-www-form-urlencoded" '
'media type'
),
client=client,
)
if client.authorization_flow != models.OIDCClient.FLOW_RESOURCE_OWNER_CRED:
raise UnauthorizedClient(
_('Client is not configured for resource owner password ' 'credential grant'), client=client
)
exponential_backoff = ExponentialRetryTimeout(
key_prefix='idp-oidc-ro-cred-grant',
duration=a2_app_settings.A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_DURATION,
factor=a2_app_settings.A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_FACTOR,
)
backoff_keys = (username, client.client_id)
seconds_to_wait = exponential_backoff.seconds_to_wait(*backoff_keys)
if seconds_to_wait > a2_app_settings.A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_MAX_DURATION:
seconds_to_wait = a2_app_settings.A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_MAX_DURATION
if seconds_to_wait:
raise InvalidRequest(
_('Too many attempts with erroneous RO password, you must wait ' '%s seconds to try again.')
% int(math.ceil(seconds_to_wait)),
client=client,
)
ou = None
if 'ou_slug' in request.POST:
try:
ou = OrganizationalUnit.objects.get(slug=request.POST.get('ou_slug'))
except OrganizationalUnit.DoesNotExist:
raise InvalidRequest(
_('Parameter "ou_slug" does not match an existing organizational unit'), client=client
)
user = authenticate(request, username=username, password=request.POST.get('password'), ou=ou)
if not user:
exponential_backoff.failure(*backoff_keys)
raise AccessDenied(_('Invalid user credentials'), client=client)
# limit requested scopes
if scope is not None:
scopes = utils.scope_set(scope) & client.scope_set()
else:
scopes = client.scope_set()
exponential_backoff.success(*backoff_keys)
iat = now() # iat = issued at
# make access_token
if client.access_token_duration is None:
expires_in = datetime.timedelta(seconds=app_settings.ACCESS_TOKEN_DURATION)
else:
expires_in = client.access_token_duration
access_token = models.OIDCAccessToken.objects.create(
client=client, user=user, scopes=' '.join(scopes), session_key='', expired=iat + expires_in
)
# make id_token
id_token = utils.create_user_info(request, client, user, scopes, id_token=True)
exp = iat + idtoken_duration(client)
id_token.update(
{
'iss': utils.get_issuer(request),
'aud': client.client_id,
'exp': int(exp.timestamp()),
'iat': int(iat.timestamp()),
'auth_time': int(iat.timestamp()),
'acr': '0',
}
)
return JsonResponse(
{
'access_token': str(access_token.uuid),
'token_type': 'Bearer',
'expires_in': int(expires_in.total_seconds()),
'id_token': utils.make_idtoken(client, id_token),
}
)
def tokens_from_authz_code(request):
client = authenticate_client(request)
code = request.POST.get('code')
if not code:
raise MissingParameter('code', client=client)
try:
oidc_code = models.OIDCCode.objects.select_related().get(uuid=code)
except models.OIDCCode.DoesNotExist:
raise InvalidRequest(_('Parameter "code" is invalid'), client=client)
if not oidc_code.is_valid():
raise InvalidRequest(_('Parameter "code" has expired or user is disconnected'), client=client)
models.OIDCCode.objects.filter(uuid=code).delete()
redirect_uri = request.POST.get('redirect_uri')
if oidc_code.redirect_uri != redirect_uri:
raise InvalidRequest(_('Parameter "redirect_uri" does not match the code.'), client=client)
if client.access_token_duration is None:
expires_in = datetime.timedelta(seconds=oidc_code.session.get_expiry_age())
expired = None
else:
expires_in = client.access_token_duration
expired = oidc_code.created + expires_in
access_token = models.OIDCAccessToken.objects.create(
client=client,
user=oidc_code.user,
scopes=oidc_code.scopes,
session_key=oidc_code.session_key,
expired=expired,
)
start = now()
acr = '0'
if (
oidc_code.nonce is not None
and last_authentication_event(session=oidc_code.session).get('nonce') == oidc_code.nonce
):
acr = '1'
# prefill id_token with user info
id_token = utils.create_user_info(request, client, oidc_code.user, oidc_code.scope_set(), id_token=True)
exp = start + idtoken_duration(client)
id_token.update(
{
'iss': utils.get_issuer(request),
'sub': utils.make_sub(client, oidc_code.user),
'aud': client.client_id,
'exp': int(exp.timestamp()),
'iat': int(start.timestamp()),
'auth_time': int(oidc_code.auth_time.timestamp()),
'acr': acr,
}
)
if oidc_code.nonce is not None:
id_token['nonce'] = oidc_code.nonce
return JsonResponse(
{
'access_token': str(access_token.uuid),
'token_type': 'Bearer',
'expires_in': int(expires_in.total_seconds()),
'id_token': utils.make_idtoken(client, id_token),
}
)
@setting_enabled('ENABLE', settings=app_settings)
@csrf_exempt
def token(request, *args, **kwargs):
if request.method != 'POST':
return HttpResponseNotAllowed(['POST'])
grant_type = request.POST.get('grant_type')
try:
if grant_type == 'password':
response = idtoken_from_user_credential(request)
elif grant_type == 'authorization_code':
response = tokens_from_authz_code(request)
else:
raise InvalidRequest('grant_type must be either authorization_code or password')
response['Cache-Control'] = 'no-store'
response['Pragma'] = 'no-cache'
return response
except OIDCException as e:
response = e.json_response(request, endpoint='token')
# special case of client authentication error with HTTP Basic
if 'HTTP_AUTHORIZATION' in request and e.error_code == 'invalid_client':
response['WWW-Authenticate'] = 'Basic'
response.status_code = 401
return response
def authenticate_access_token(request):
if 'HTTP_AUTHORIZATION' not in request.META:
raise InvalidRequest(_('Bearer authentication is mandatory'), status=401)
authorization = request.META['HTTP_AUTHORIZATION'].split()
if authorization[0] != 'Bearer' or len(authorization) != 2:
raise InvalidRequest(_('Invalid Bearer authentication'), status=401)
try:
access_token = models.OIDCAccessToken.objects.select_related().get(uuid=authorization[1])
except models.OIDCAccessToken.DoesNotExist:
raise InvalidToken(_('Token unknown'), status=401)
if not access_token.is_valid():
raise InvalidToken(_('Token expired or user disconnected'), status=401)
return access_token
@setting_enabled('ENABLE', settings=app_settings)
@csrf_exempt
def user_info(request, *args, **kwargs):
try:
access_token = authenticate_access_token(request)
user_info = utils.create_user_info(
request, access_token.client, access_token.user, access_token.scope_set()
)
return JsonResponse(user_info)
except OIDCException as e:
error_response = e.json_response(request, endpoint='user_info')
if e.status == 401:
error_response['WWW-Authenticate'] = 'Bearer error="%s", error_description="%s"' % (
e.error_code,
e.error_description,
)
return error_response
@setting_enabled('ENABLE', settings=app_settings)
def logout(request):
post_logout_redirect_uri = request.GET.get('post_logout_redirect_uri')
state = request.GET.get('state')
if post_logout_redirect_uri:
providers = models.OIDCClient.objects.filter(
post_logout_redirect_uris__contains=post_logout_redirect_uri
)
for provider in providers:
if post_logout_redirect_uri in provider.post_logout_redirect_uris.split():
break
else:
messages.warning(request, _('Invalid post logout URI'))
return redirect(request, settings.LOGIN_REDIRECT_URL)
if state:
post_logout_redirect_uri = make_url(post_logout_redirect_uri, params={'state': state})
# FIXME: do something with id_token_hint
id_token_hint = request.GET.get('id_token_hint')
return a2_logout(request, next_url=post_logout_redirect_uri, do_local=False, check_referer=False)