# 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.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')


@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)
