Commit a0aa6bfd authored by Lukas Burgey's avatar Lukas Burgey

Rework auth backends

parent c66fafaa
......@@ -9,7 +9,7 @@ DEBUG = True
DEBUG_AUTH = False
DEBUG_PUBLISHING = False
DEBUG_CREDENTIALS = True
DEBUG_CREDENTIALS = False
ALLOWED_HOSTS = [
'hdf-portal-dev.data.kit.edu',
......@@ -105,13 +105,14 @@ CORS_ORIGIN_ALLOW_ALL = True
# AUTHENTICATION AND AUTHORIZATION
AUTHENTICATION_BACKENDS = [
'feudal.backend.auth.v1.models.OIDCTokenAuthBackend',
'feudal.backend.auth.v1.OIDCTokenAuthBackend',
'django.contrib.auth.backends.ModelBackend',
]
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'feudal.backend.auth.v1.auth_class.CsrfExemptSessionAuthentication',
'feudal.backend.auth.v1.CsrfExemptSessionAuthentication',
'feudal.backend.auth.v1.OIDCTokenAuthHTTPBackend',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
......
import logging
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
LOGGER = logging.getLogger(__name__)
class OIDCTokenAuthBackend:
def get_userinfo(self, oidc_client, access_token):
req = Request(
oidc_client.provider_info['userinfo_endpoint'],
)
req.add_header('Authorization', access_token)
# execute request
userinfo_bytes = urlopen(req).read()
user_info = json.loads(userinfo_bytes.decode('UTF-8'))
user_info['iss'] = oidc_client.provider_info['issuer']
return user_info
# raises OIDCConfig.DoesNotExist if no idp can be determined
def get_idp(self, request):
# OPTION 1: issuer set in the 'X-Issuer' header
if 'HTTP_X_ISSUER' in request.META:
return OIDCConfig.objects.get(issuer_uri=request.META['HTTP_X_ISSUER'])
# OPTION 2: issuer set in users session (before redirecting to IdP)
idp_id = utils.get_session(request, 'idp_id', None)
if idp_id is not None:
try:
return OIDCConfig.objects.get(id=idp_id)
except OIDCConfig.DoesNotExist:
pass
raise OIDCConfig.DoesNotExist('Unable to determine IdP')
def get_user(self, user_id):
from feudal.backend.models.users import User
query = User.objects.filter(
user_type='oidcuser',
pk=user_id,
)
if query.exists():
if len(query) == 1:
return query.first()
LOGGER.error(
'OIDCTokenAuthBackend: query for user id %s: %s results',
user_id,
len(query),
)
return None
LOGGER.error(
'OIDCTokenAuthBackend: query for user id %s: no results',
user_id,
)
return None
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.',
)
access_token = request.META['HTTP_AUTHORIZATION']
from feudal.backend.models.users import User
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.',
)
# get the user info from the idp
try:
userinfo = self.get_userinfo(
idp,
access_token,
)
except HTTPError as http_error:
raise AuthenticationFailed(detail='Error retrieving user info: {}'.format(http_error))
try:
user = User.get_user(
userinfo,
idp,
)
LOGGER.info('Authenticated: %s', user)
return user
except User.DoesNotExist:
raise AuthenticationFailed(detail='No such user')
except Exception as exception:
LOGGER.error(exception)
raise AuthenticationFailed(detail='Error during authentication')
class CsrfExemptSessionAuthentication(SessionAuthentication):
def enforce_csrf(self, request):
# No CSRF enforcing
return
class OIDCTokenAuthHTTPBackend(BaseAuthentication):
def authenticate_header(self, request):
return 'Bearer realm="openidconnect"'
def authenticate(self, request):
return (OIDCTokenAuthBackend().authenticate(request), None)
import logging
import json
from urllib.request import Request, urlopen
import re
from django.db import models as db_models
from django.core.exceptions import ImproperlyConfigured
from django.db import models as db_models
from django_mysql.models import JSONField
from oic.oic import Client
from oic.oic.message import RegistrationResponse
from oic.utils.authn.client import CLIENT_AUTHN_METHOD
from .. import utils
LOGGER = logging.getLogger(__name__)
BEARER_TOKEN_EXTRACTOR = re.compile('^Bearer (.+)$')
OIDC_CLIENT = {}
......@@ -106,85 +106,3 @@ def default_idp():
if not available_idps.exists():
raise ImproperlyConfigured('No OIDCConfig available')
return available_idps.first()
class OIDCTokenAuthBackend:
IdPException = Exception
AuthException = Exception('Unable to authenticate user')
def get_userinfo(self, oidc_client, access_token):
user_info = None
# do the user info request
req = Request(
oidc_client.provider_info['userinfo_endpoint'],
)
auth = ('Bearer ' + access_token)
req.add_header('Authorization', auth)
userinfo_bytes = urlopen(req).read()
user_info = json.loads(userinfo_bytes.decode('UTF-8'))
user_info['iss'] = oidc_client.provider_info['issuer']
return user_info
# raises OIDCConfig.DoesNotExist if no idp can be determined
def get_idp(self, idp_id=None, issuer_uri=None):
if idp_id is None and issuer_uri is None:
raise OIDCConfig.DoesNotExist('Need idp_id or issuer_uri to determine IdP')
if issuer_uri is not None:
return OIDCConfig.objects.get(issuer_uri=issuer_uri)
return OIDCConfig.objects.get(id=idp_id)
def authenticate(self, request, token=None, issuer_uri=None):
if token is None:
LOGGER.error('Cannot authenticate without access token')
return None
idp_id = utils.get_session(request, 'idp_id', None)
try:
oidc_client = self.get_idp(idp_id=idp_id, issuer_uri=issuer_uri)
# get the user info from the idp
userinfo = self.get_userinfo(
oidc_client,
token,
)
from feudal.backend.models.users import User
return User.get_user(
userinfo,
oidc_client,
)
except OIDCConfig.DoesNotExist:
LOGGER.error('Unable to determine IdP for authentication')
return None
def get_user(self, user_id):
from feudal.backend.models.users import User
query = User.objects.filter(
user_type='oidcuser',
pk=user_id,
)
if query.exists():
if len(query) == 1:
return query.first()
LOGGER.error(
'OIDCTokenAuthBackend: query for user id %s: %s results',
user_id,
len(query),
)
return None
LOGGER.error(
'OIDCTokenAuthBackend: query for user id %s: no results',
user_id,
)
return None
......@@ -13,8 +13,6 @@ from oic.oauth2.exception import HttpError
from rest_framework import generics, views
from rest_framework.permissions import AllowAny
from feudal.backend.views.webpage import state_view_data
from .. import utils
from ..models import OIDCConfig, default_idp
from ..models.serializers import AuthInfoSerializer
......@@ -24,10 +22,6 @@ LOGGER = logging.getLogger(__name__)
IDP_COOKIE_NAME = 'idp_id'
class AuthException(Exception):
pass
def select_oidc_config(request):
issuer_uri_urlenc = request.GET.get('idp', None)
idp_id = request.COOKIES.get(IDP_COOKIE_NAME, None)
......@@ -39,7 +33,7 @@ def select_oidc_config(request):
return OIDCConfig.objects.get(issuer_uri=issuer_uri)
# IdP selection using a cookie
elif idp_id is not None:
if idp_id is not None:
return OIDCConfig.objects.get(id=idp_id)
return default_idp()
......@@ -49,6 +43,15 @@ def select_oidc_config(request):
return default_idp()
class AuthFlowException(Exception):
pass
def error_response(request, msg='Server Error'):
request.session['error'] = msg
return redirect('/')
class Auth(View):
def get(self, request):
try:
......@@ -67,8 +70,6 @@ class Auth(View):
state,
)
LOGGER.debug('Auth: redirecting %s to IdP %s', state, oidc_config)
# include query parameters in the redirect to the idp
if len(list(request.GET.items())) > 1:
urlparams = request.GET.copy()
......@@ -81,17 +82,16 @@ class Auth(View):
except ImproperlyConfigured:
LOGGER.error('No OIDCConfig is not available')
# the error is deleted from the session when the state is delivered
request.session['error'] = 'Server Error'
return redirect('/')
return error_response(request)
except:
# the error is deleted from the session when the state is delivered
request.session['error'] = 'Server Error'
return redirect('/')
except Exception as exception:
return error_response(request)
class AuthCallback(View):
permission_classes = (AllowAny,)
def get(self, request):
try:
state = utils.get_session(request, 'state', None)
......@@ -102,9 +102,11 @@ class AuthCallback(View):
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)
oidc_config = OIDCConfig.objects.get(id=idp_id)
oidc_client = oidc_config.oidc_client
LOGGER.debug('AuthCallback: %s returned from IdP %s', state, oidc_config)
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,
......@@ -112,63 +114,49 @@ class AuthCallback(View):
)
if 'error' in aresp:
LOGGER.debug('AuthCallback: error response: %s', aresp)
raise AuthException('Erroneous callback from IdP {}'.format(oidc_config))
raise AuthFlowException('Erroneous callback from IdP {}'.format(oidc_config))
if not state == aresp['state']:
raise AuthException('AuthCallback: States do not match')
raise AuthFlowException('AuthCallback: States do not match')
if 'code' not in aresp:
raise AuthFlowException('AuthCallback: Did not receive a code')
ac_token_response = None
access_token_response = None
try:
ac_token_response = (
access_token_response = (
oidc_client.do_access_token_request(
state=state,
request_args={
'code': aresp['code']
},
request_args=aresp,
)
)
except HttpError as exception:
# this exception is throw
LOGGER.error('AuthCallback: Access Token Request: %s', exception)
# the error is deleted from the session when the state is delivered
request.session['error'] = 'Server Communication Error'
return redirect('/')
return error_response(request, msg='Server communication error')
# TODO retry in the future
# pyoidc_userinfo = oidc_client.do_user_info_request(
# state=aresp["state"],
# )
# LOGGER.debug("pyoidc: %s", pyoidc_userinfo)
access_token = access_token_response.get('access_token', '')
request.META['HTTP_AUTHORIZATION'] = 'Bearer '+access_token
user = authenticate(
request,
token=ac_token_response['access_token'],
)
response = redirect('/')
user = authenticate(request)
if user is None:
# authentication failed -> "401"
LOGGER.error('User failed to log in')
request.session['error'] = 'Authentication failed'
elif not user.is_active:
return error_response(request, msg='Authentication failed')
if not user.is_active:
# user is deactivated -> "403"
LOGGER.info('%s tried to log in', user)
request.session['error'] = 'Account deactivated'
else:
# user authenticated -> back to frontend
login(request, user)
LOGGER.info('IdP %s authenticated user as %s', oidc_config, user)
return error_response(request, msg='Account deactivated')
return response
# user authenticated -> back to frontend
login(request, user)
return redirect('/')
except AuthException as exception:
except AuthFlowException as exception:
LOGGER.error('AuthCallback: %s', exception)
# the error is deleted from the session when the state is delivered
request.session['error'] = 'Server Error'
return redirect('/')
return error_response(request)
class LogoutView(views.APIView):
......
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