authentic/src/authentic2_idp_oidc/views.py

901 lines
34 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
import secrets
import time
from binascii import Error as Base64Error
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_str
from django.utils.http import urlencode
from django.utils.timezone import now, utc
from django.utils.translation import gettext 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.a2_rbac.models import OrganizationalUnit
from authentic2.custom_user.models import Profile
from authentic2.decorators import setting_enabled
from authentic2.exponential_retry_timeout import ExponentialRetryTimeout
from authentic2.utils import hooks
from authentic2.utils.misc import last_authentication_event, login_require, make_url, redirect
from authentic2.utils.service import set_service
from authentic2.utils.view_decorators import check_view_restriction
from authentic2.views import logout as a2_logout
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, extra_info=None):
if error_description:
self.error_description = error_description
self.extra_info = extra_info
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:
content['client_id'] = self.client.client_id
msg = 'idp_oidc: error "%s" in %s endpoint "%s" for client %s'
if self.extra_info:
msg += ' (%s)' % self.extra_info
logger.warning(
msg,
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,
_('OpenID Connect 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 InteractionRequired(OIDCException):
error_code = 'interaction_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 InvalidGrant(OIDCException):
error_code = 'invalid_grant'
class WrongClientSecret(InvalidClient):
error_description = _('Wrong client secret')
def __init__(self, *args, wrong_id, **kwargs):
kwargs['extra_info'] = _('received %s') % force_str(wrong_id)
super().__init__(*args, **kwargs)
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': [
'client_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')
@check_view_restriction
@setting_enabled('ENABLE', settings=app_settings)
def authorize(request, *args, **kwargs):
validated_redirect_uri = None
client_id = None
client = None
client_id = request.GET.get('client_id', '')
redirect_uri = request.GET.get('redirect_uri', '')
state = request.GET.get('state')
use_fragment = False
try:
if not client_id:
raise MissingParameter('client_id')
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)
# define the current service
set_service(request, client)
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)
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, login_hint=login_hint)
# view restriction and passive SSO
if hasattr(request, 'view_restriction_response'):
if request.user.is_authenticated and 'none' in prompt:
raise InteractionRequired(_('User profile is not complete but prompt parameter is "none"'))
return request.view_restriction_response
# 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, login_hint=login_hint)
iat = now() # iat = issued at
user_has_selectable_profiles = False
needs_scope_validation = False
profile = None
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()
authorized_profile = None
for authorization in qs:
authorized_scopes |= authorization.scope_set()
# load first authorized profile
if not authorized_profile and authorization.profile:
authorized_profile = authorization.profile
if request.user.profiles.count() and not authorized_profile:
user_has_selectable_profiles = True
else:
profile = authorized_profile
if (authorized_scopes & scopes) < scopes:
needs_scope_validation = True
if needs_scope_validation or (user_has_selectable_profiles and client.activate_user_profiles):
if request.method == 'POST':
if request.POST.get('profile-validation', ''):
try:
profile = Profile.objects.get(
user=request.user,
id=request.POST['profile-validation'],
)
except Profile.DoesNotExist:
pass
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,
profile=profile,
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:
request.journal.record(
'user.service.sso.refusal', service=client, scopes=list(sorted(scopes))
)
raise AccessDenied(_('User did not consent'))
else:
return render(
request,
'authentic2_idp_oidc/authorization.html',
{
'user_has_selectable_profiles': user_has_selectable_profiles,
'needs_scope_validation': needs_scope_validation,
'client': client,
'scopes': scopes - {'openid'},
'profile_types': set(
Profile.objects.filter(user=request.user).values_list(
'profile_type__slug', flat=True
)
),
},
)
if response_type == 'code':
code = models.OIDCCode.objects.create(
client=client,
user=request.user,
profile=profile,
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 'profile-validation' in request.POST:
try:
profile = Profile.objects.get(
id=request.POST.get('profile-validation', None),
user=request.user,
)
except Profile.DoesNotExist:
pass
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,
profile=profile,
)
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, profile=profile
)
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.headers['Authorization'].split()
if authorization[0] != 'Basic' or len(authorization) != 2:
return None, None
try:
decoded = force_str(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, wrong_id=raw_provided_client_secret)
if not secrets.compare_digest(raw_client_client_secret, raw_provided_client_secret):
raise WrongClientSecret(client=client, wrong_id=raw_provided_client_secret)
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 'authorization' in request.headers:
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:
raise InvalidRequest('missing client_id')
if not client_id:
raise InvalidClient(_('Empty client identifier'))
if not client_secret:
raise InvalidRequest('missing client_secret', client=client)
client = get_client(client_id)
if not client:
raise InvalidClient(_('Wrong client identifier: %s') % client_id)
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.headers.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')
profile_id = request.POST.get('profile')
# 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
profile = None
if profile_id:
if not client.activate_user_profiles:
raise AccessDenied(
_('User profile requested yet client does not manage profiles.'), client=client
)
try:
profile = Profile.objects.get(id=profile_id, user=user)
except Profile.DoesNotExist:
raise AccessDenied(_('Invalid profile'), client=client)
access_token = models.OIDCAccessToken.objects.create(
client=client,
user=user,
scopes=' '.join(scopes),
session_key='',
expired=iat + expires_in,
profile=profile,
)
# make id_token
id_token = utils.create_user_info(request, client, user, scopes, profile=profile, 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)
oidc_code_qs = models.OIDCCode.objects.filter(expired__gte=now()).select_related()
try:
oidc_code = oidc_code_qs.get(uuid=code)
except models.OIDCCode.DoesNotExist:
raise InvalidGrant(_('Code is unknown or has expired.'), client=client)
if oidc_code.client != client:
raise InvalidGrant(_('Code was issued to a different client.'), client=client)
if not oidc_code.is_valid():
raise InvalidGrant(_('User is disconnected or session was lost.'), client=client)
redirect_uri = request.POST.get('redirect_uri')
if oidc_code.redirect_uri != redirect_uri:
raise InvalidGrant(_('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,
profile=oidc_code.profile,
)
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,
profile=oidc_code.profile,
)
exp = start + idtoken_duration(client)
id_token.update(
{
'iss': utils.get_issuer(request),
'sub': utils.make_sub(client, oidc_code.user, profile=oidc_code.profile),
'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 'authorization' not in request.headers:
raise InvalidRequest(_('Bearer authentication is mandatory'), status=401)
authorization = request.headers['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(),
profile=access_token.profile,
)
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():
set_service(request, provider)
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
request.GET.get('id_token_hint')
return a2_logout(request, next_url=post_logout_redirect_uri, do_local=False, check_referer=False)