diff --git a/api/authentication.py b/api/authentication.py new file mode 100644 index 00000000..4dc5a6f3 --- /dev/null +++ b/api/authentication.py @@ -0,0 +1,25 @@ +import datetime +from django.conf import settings +from django.utils.translation import ugettext_lazy as _ +from rest_framework.authentication import TokenAuthentication +from rest_framework import exceptions + +class ExpiringTokenAuthentication(TokenAuthentication): + def authenticate_credentials(self, key): + model = self.get_model() + try: + token = model.objects.select_related('user').get(key=key) + except model.DoesNotExist: + raise exceptions.AuthenticationFailed(_('Invalid token.')) + + if not token.user.is_active: + raise exceptions.AuthenticationFailed(_('User inactive or deleted.')) + + token_duration = datetime.timedelta( + seconds=settings.API_TOKEN_DURATION + ) + utc_now = datetime.datetime.now(datetime.timezone.utc) + if token.created < utc_now - token_duration: + raise exceptions.AuthenticationFailed(_('Token has expired')) + + return (token.user, token) diff --git a/api/settings.py b/api/settings.py index 8cf152f2..028ec01d 100644 --- a/api/settings.py +++ b/api/settings.py @@ -29,6 +29,7 @@ Django settings specific to the API. REST_FRAMEWORK = { 'URL_FIELD_NAME': 'api_url', 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'api.authentication.ExpiringTokenAuthentication', 'rest_framework.authentication.SessionAuthentication', ), 'DEFAULT_PERMISSION_CLASSES': ( @@ -41,3 +42,9 @@ API_CONTENT_TYPE_APP_LABEL = 'api' API_CONTENT_TYPE_MODEL = 'api' API_PERMISSION_NAME = 'Can use the API' API_PERMISSION_CODENAME = 'use_api' + +# Activate token authentication +API_APPS = ( + 'rest_framework.authtoken', +) +API_TOKEN_DURATION = 86400 # 24 hours diff --git a/api/urls.py b/api/urls.py index 662dd8c4..3379d083 100644 --- a/api/urls.py +++ b/api/urls.py @@ -42,6 +42,7 @@ router.register(r'whitelists', views.WhitelistViewSet) urlpatterns = [ url(r'^', include(router.urls)), + url(r'^token-auth/', views.ObtainExpiringAuthToken.as_view()) # # Services # url(r'^services/$', views.services), # url( diff --git a/api/views.py b/api/views.py index 00389eb7..f3e2a6c7 100644 --- a/api/views.py +++ b/api/views.py @@ -24,17 +24,15 @@ The views for the API app. They should all return JSON data and not fallback on HTML pages such as the login and index pages for a better integration. """ -from django.contrib.auth.decorators import login_required, permission_required -from django.views.decorators.csrf import csrf_exempt +import datetime +from django.conf import settings + +from rest_framework.authtoken.views import ObtainAuthToken +from rest_framework.authtoken.models import Token from rest_framework.response import Response -from rest_framework import status, mixins, generics, viewsets +from rest_framework import viewsets, status -from re2o.utils import ( - all_has_access, - all_active_assigned_interfaces, - filter_active_interfaces -) from users.models import ( User, Club, @@ -118,6 +116,30 @@ class WhitelistViewSet(viewsets.ReadOnlyModelViewSet): queryset = Whitelist.objects.all() serializer_class = WhitelistSerializer +# Subclass the standard rest_framework.auth_token.views.ObtainAuthToken +# in order to renew the lease of the token and add expiration time +class ObtainExpiringAuthToken(ObtainAuthToken): + def post(self, request, *args, **kwargs): + serializer = self.serializer_class(data=request.data) + serializer.is_valid(raise_exception=True) + user = serializer.validated_data['user'] + token, created = Token.objects.get_or_create(user=user) + + token_duration = datetime.timedelta( + seconds=settings.API_TOKEN_DURATION + ) + utc_now = datetime.datetime.now(datetime.timezone.utc) + if not created and token.created < utc_now - token_duration: + token.delete() + token = Token.objects.create(user=user) + token.created = datetime.datetime.utcnow() + token.save() + + return Response({ + 'token': token.key, + 'expiration_date': token.created + token_duration + }) + # # @csrf_exempt # @login_required diff --git a/install_utils/apache2/re2o-tls.conf b/install_utils/apache2/re2o-tls.conf index 83e2cf13..eb8f2c42 100644 --- a/install_utils/apache2/re2o-tls.conf +++ b/install_utils/apache2/re2o-tls.conf @@ -26,6 +26,7 @@ WSGIScriptAlias / PATH/re2o/wsgi.py WSGIProcessGroup re2o WSGIDaemonProcess re2o processes=2 threads=16 maximum-requests=1000 display-name=re2o + WSGIPassAuthorization On SSLCertificateFile /etc/letsencrypt/live/LE_PATH/fullchain.pem SSLCertificateKeyFile /etc/letsencrypt/live/LE_PATH/privkey.pem diff --git a/install_utils/apache2/re2o.conf b/install_utils/apache2/re2o.conf index 680fb05d..1b4e02b3 100644 --- a/install_utils/apache2/re2o.conf +++ b/install_utils/apache2/re2o.conf @@ -19,5 +19,6 @@ WSGIScriptAlias / PATH/re2o/wsgi.py WSGIProcessGroup re2o WSGIDaemonProcess re2o processes=2 threads=16 maximum-requests=1000 display-name=re2o + WSGIPassAuthorization On diff --git a/re2o/settings.py b/re2o/settings.py index ad1c4c9c..7c119a55 100644 --- a/re2o/settings.py +++ b/re2o/settings.py @@ -177,3 +177,4 @@ GRAPH_MODELS = { # Activate API if 'api' in INSTALLED_APPS: from api.settings import * + INSTALLED_APPS += API_APPS