Commit 8193af5b authored by Lukas Burgey's avatar Lukas Burgey

Rework auth flow

Also rework the way errors are communicated to the webpage
parent fead5a1c
......@@ -5,7 +5,6 @@ import json
from urllib.error import HTTPError
from urllib.request import Request, urlopen
from rest_framework.authentication import BaseAuthentication, SessionAuthentication
from rest_framework.exceptions import AuthenticationFailed
from . import utils
from .models import OIDCConfig
......@@ -71,9 +70,8 @@ class OIDCTokenAuthBackend:
def authenticate(self, request):
if 'HTTP_AUTHORIZATION' not in request.META:
raise AuthenticationFailed(
detail='No token. Set the HTTP Authorization header to the value of your access token.',
)
request.session['auth_error'] = 'Missing auth header'
return None
access_token = request.META['HTTP_AUTHORIZATION']
......@@ -82,9 +80,8 @@ class OIDCTokenAuthBackend:
try:
idp = self.get_idp(request)
except OIDCConfig.DoesNotExist:
raise AuthenticationFailed(
detail='Unable to determine IdP. Set \'X-Issuer\' header to the issuer URI of your access token.',
)
request.session['auth_error'] = 'Unable to determine IdP'
return None
# get the user info from the idp
try:
......@@ -92,23 +89,34 @@ class OIDCTokenAuthBackend:
idp,
access_token,
)
except HTTPError as http_error:
raise AuthenticationFailed(detail='Error retrieving user info: {}'.format(http_error))
except HTTPError as exception:
request.session['auth_error'] = 'HTTP when retrieving user info: {}'.format(exception)
return None
try:
user = User.get_user(
userinfo,
idp,
)
if not user.is_active:
LOGGER.info('Login attempt: %s', user)
utils.set_session(request, 'msg', 'Account deactivated')
utils.set_session(request, 'deactivated', True)
return None
LOGGER.info('Authenticated: %s', user)
# reset all values we possibly set in the session
utils.del_session(request, ['deactivated', 'auth_error'])
return user
except User.DoesNotExist:
raise AuthenticationFailed(detail='No such user')
return None
# raise AuthenticationFailed(detail='No such user')
except Exception as exception:
LOGGER.error(exception)
raise AuthenticationFailed(detail='Error during authentication')
return None
class CsrfExemptSessionAuthentication(SessionAuthentication):
......
import logging
from django.db.utils import OperationalError
LOGGER = logging.getLogger(__name__)
......@@ -20,4 +22,10 @@ def set_session(request, key, value):
try:
value = request.session[key] = value
except OperationalError as exp:
raise OperationalError('get_session: Error setting in session: {}'.format(exp))
raise OperationalError('set_session: Error setting in session: {}'.format(exp))
def del_session(request, keys):
for key in keys:
if key in request.session:
del request.session[key]
......@@ -5,11 +5,14 @@ import urllib
from django.contrib.auth import authenticate, login, logout
from django.core.exceptions import ImproperlyConfigured
from django.http import HttpResponse
from django.shortcuts import redirect
from django.views import View
from oic import rndstr
from oic.oic.message import AuthorizationResponse
from oic.oic.message import AuthorizationResponse, MissingRequiredAttribute, MissingRequiredValue
from oic.oauth2.exception import HttpError
from rest_framework import generics, views
from rest_framework.permissions import AllowAny
......@@ -47,12 +50,16 @@ class AuthFlowException(Exception):
pass
def error_response(request, msg='Server Error'):
request.session['error'] = msg
return redirect('/')
def error_response(request, msg='Server Error', redirect_back=True):
if redirect_back:
return redirect('/')
return HttpResponse(msg, status=403)
class Auth(View):
permission_classes = (AllowAny,)
def get(self, request):
try:
state = rndstr()
......@@ -84,79 +91,82 @@ class Auth(View):
return error_response(request)
except Exception as exception:
except Exception:
return error_response(request)
class AuthCallback(View):
permission_classes = (AllowAny,)
@staticmethod
def retry_flow():
LOGGER.debug('Retrying auth flow')
return redirect('login')
@staticmethod
def flow_failed(request, msg='Authentication failed', auth_error=''):
LOGGER.error('Auth flow failed: %s', auth_error)
request.session['msg'] = msg
request.session['auth_error'] = 'States do not match'
return redirect('/')
def get(self, request):
state = utils.get_session(request, 'state', None)
if state is None:
return redirect('login')
idp_id = utils.get_session(request, 'idp_id', None)
if idp_id is None:
return self.retry_flow()
try:
state = utils.get_session(request, 'state', None)
if state is None:
return redirect('login')
idp_id = utils.get_session(request, 'idp_id', None)
if idp_id is None:
LOGGER.error("Session for %s does not contain an idp_id. Hence we don't now which idp authenticated the user", state)
try:
oidc_config = OIDCConfig.objects.get(id=idp_id)
oidc_client = oidc_config.oidc_client
except OIDCConfig.DoesNotExist:
raise AuthFlowException('Unable to determine idp')
aresp = oidc_client.parse_response(
AuthorizationResponse,
info=json.dumps(request.GET),
)
if 'error' in aresp:
LOGGER.debug('AuthCallback: error response: %s', aresp)
raise AuthFlowException('Erroneous callback from IdP {}'.format(oidc_config))
if not state == aresp['state']:
raise AuthFlowException('AuthCallback: States do not match')
if 'code' not in aresp:
raise AuthFlowException('AuthCallback: Did not receive a code')
access_token_response = None
try:
access_token_response = (
oidc_client.do_access_token_request(
state=state,
request_args=aresp,
)
)
except HttpError as exception:
LOGGER.error('AuthCallback: Access Token Request: %s', exception)
oidc_config = OIDCConfig.objects.get(id=idp_id)
oidc_client = oidc_config.oidc_client
except OIDCConfig.DoesNotExist:
return self.flow_failed(request, auth_error='Unable to determine IdP')
return error_response(request, msg='Server communication error')
aresp = oidc_client.parse_response(
AuthorizationResponse,
info=json.dumps(request.GET),
)
if 'error' in aresp:
return self.flow_failed(request, auth_error=aresp['error'])
access_token = access_token_response.get('access_token', '')
request.META['HTTP_AUTHORIZATION'] = 'Bearer '+access_token
if not state == aresp.get('state', ''):
return self.flow_failed(request, auth_error='States do not match')
user = authenticate(request)
if 'code' not in aresp:
return self.flow_failed(request, auth_error='Did not receive a code')
if user is None:
# authentication failed -> "401"
LOGGER.error('User failed to log in')
return error_response(request, msg='Authentication failed')
access_token_response = None
try:
access_token_response = (
oidc_client.do_access_token_request(
state=state,
request_args=aresp,
)
)
except (MissingRequiredAttribute, MissingRequiredValue):
return self.retry_flow()
if not user.is_active:
# user is deactivated -> "403"
LOGGER.info('%s tried to log in', user)
return error_response(request, msg='Account deactivated')
except HttpError as exception:
return self.flow_failed(
request,
auth_error='HTTP error on do_access_token_request: {}'.format(exception),
)
# user authenticated -> back to frontend
login(request, user)
access_token = access_token_response.get('access_token', '')
request.META['HTTP_AUTHORIZATION'] = 'Bearer '+access_token
# if user is None authenticate sets 'auth_error' in the session
user = authenticate(request)
if user is None:
return redirect('/')
except AuthFlowException as exception:
LOGGER.error('AuthCallback: %s', exception)
return error_response(request)
# log user in for session authentication
login(request, user)
return redirect('/')
class LogoutView(views.APIView):
......@@ -169,7 +179,8 @@ class LogoutView(views.APIView):
class AuthInfo(generics.RetrieveAPIView):
permission_classes = (AllowAny,)
authorization_classes = ()
permission_classes = ()
serializer_class = AuthInfoSerializer
def get_object(self):
......
......@@ -155,7 +155,13 @@ class UserStateSerializer(serializers.ModelSerializer):
class StateSerializer(serializers.Serializer):
error = serializers.CharField(allow_blank=True, required=False)
msg = serializers.CharField(
allow_blank=True,
required=False,
)
session = serializers.JSONField(
required=False,
)
user = UserStateSerializer()
......
......@@ -22,16 +22,15 @@ def _api_error_response(error):
def state_view_data(request):
data = {
'user': None,
'user': request.user,
'session': request.session,
}
if request.user.is_authenticated:
data['user'] = request.user
if 'error' in request.session:
data['error'] = request.session['error']
if 'msg' in request.session:
data['msg'] = request.session.get('msg')
# we display errors only once
del request.session['error']
# we display msg only once
del request.session['msg']
return data
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment