diff --git a/src/aseb/api/auth/viewsets.py b/src/aseb/api/auth/viewsets.py index 61741fc..53661e0 100644 --- a/src/aseb/api/auth/viewsets.py +++ b/src/aseb/api/auth/viewsets.py @@ -1,11 +1,9 @@ from drf_yasg import openapi from drf_yasg.utils import swagger_auto_schema -from rest_framework.authtoken.serializers import AuthTokenSerializer from rest_framework.decorators import action from rest_framework.response import Response from aseb.api import viewsets -from aseb.apps.users.models import User from ..users.serializers import UserSerializer from .serializers import RegisterSerializer @@ -15,37 +13,9 @@ class AuthViewSet(viewsets.ViewSet): authentication_classes = () permission_classes = () - @swagger_auto_schema( - request_body=openapi.Schema( - type=openapi.TYPE_OBJECT, - properties={ - "username": openapi.Schema(type=openapi.TYPE_STRING), - "password": openapi.Schema( - type=openapi.TYPE_STRING, format=openapi.FORMAT_PASSWORD - ), - }, - ), - ) - @action(detail=False, methods=["post"]) - def login(self, request, *args, **kwargs): - serializer = AuthTokenSerializer(data=request.data, context={"request": request}) - serializer.is_valid(raise_exception=True) - - user: User = serializer.validated_data["user"] - user.tokens.all().delete() # TODO: Be smart about rotating tokens on every login - token = user.tokens.create(user=user) - - return Response( - { - "token": token.token, - "expires": token.expires, - "scopes": token.scopes, - "user": UserSerializer(context={"request": request}).to_representation(user), - } - ) - @swagger_auto_schema( request_body=RegisterSerializer, + tags=["auth"], responses={ 200: openapi.Schema( type=openapi.TYPE_OBJECT, diff --git a/src/aseb/api/oauth/viewsets.py b/src/aseb/api/oauth/viewsets.py new file mode 100644 index 0000000..a0ff2f7 --- /dev/null +++ b/src/aseb/api/oauth/viewsets.py @@ -0,0 +1,25 @@ +from oauth2_provider import views + + +class AuthorizationView(views.AuthorizationView): + ... + + +class TokenView(views.TokenView): + ... + + +class RevokeTokenView(views.RevokeTokenView): + ... + + +class IntrospectTokenView(views.IntrospectTokenView): + ... + + +class AuthorizedTokenDeleteView(views.AuthorizedTokenDeleteView): + ... + + +class AuthorizedTokensListView(views.AuthorizedTokensListView): + ... diff --git a/src/aseb/api/urls.py b/src/aseb/api/urls.py index 75475d5..15b08b7 100644 --- a/src/aseb/api/urls.py +++ b/src/aseb/api/urls.py @@ -1,5 +1,15 @@ +from django.urls import re_path + from .auth.viewsets import AuthViewSet from .members.viewsets import MemberViewSet +from .oauth.viewsets import ( + AuthorizationView, + AuthorizedTokenDeleteView, + AuthorizedTokensListView, + IntrospectTokenView, + RevokeTokenView, + TokenView, +) from .pages.viewsets import PageViewSet from .router import Router from .topics.viewsets import TopicViewSet @@ -13,6 +23,21 @@ router.register("topics", TopicViewSet, basename="topic") router.register("pages", PageViewSet, basename="page") -urlpatterns = [ - *router.urls, +urlpatterns = [*router.urls] + +oauth2_urlpatterns = [ + re_path(r"^oauth/authorize/$", AuthorizationView.as_view(), name="authorize"), + re_path(r"^oauth/token/$", TokenView.as_view(), name="token"), + re_path(r"^oauth/revoke_token/$", RevokeTokenView.as_view(), name="revoke-token"), + re_path(r"^oauth/introspect/$", IntrospectTokenView.as_view(), name="introspect"), + re_path( + r"^oauth/authorized-tokens/$", + AuthorizedTokensListView.as_view(), + name="authorized-token-list", + ), + re_path( + r"^oauth/authorized-tokens/(?P[\w-]+)/delete/$", + AuthorizedTokenDeleteView.as_view(), + name="authorized-token-delete", + ), ] diff --git a/src/aseb/api/users/viewsets.py b/src/aseb/api/users/viewsets.py index ac06772..2817926 100644 --- a/src/aseb/api/users/viewsets.py +++ b/src/aseb/api/users/viewsets.py @@ -1,6 +1,5 @@ from drf_yasg.utils import swagger_auto_schema from rest_framework import parsers, permissions -from rest_framework.decorators import action from rest_framework.response import Response from aseb.api import viewsets @@ -18,10 +17,7 @@ def list(self, request): UserSerializer(context={"request": request}).to_representation(request.user) ) - @swagger_auto_schema( - request_body=UserUpdateSerializer, - responses={200: UserSerializer()}, - ) + @swagger_auto_schema(request_body=UserUpdateSerializer, responses={200: UserSerializer()}) def create(self, request): serializer = UserUpdateSerializer( context={"request": request}, @@ -33,11 +29,6 @@ def create(self, request): return Response(UserSerializer(context={"request": request}).to_representation(user)) - @action(detail=False, methods=["post"]) - def logout(self, request, *args, **kwargs): - request.user.tokens.all().delete() - return Response() - class UserViewSet(viewsets.ModelViewSet): permission_classes = [permissions.IsAuthenticated, permissions.IsAdminUser] diff --git a/src/aseb/apps/users/urls.py b/src/aseb/apps/users/urls.py index 9038c2e..66abfa4 100644 --- a/src/aseb/apps/users/urls.py +++ b/src/aseb/apps/users/urls.py @@ -1,7 +1,14 @@ from django.contrib.auth import views from django.urls import path, re_path -from oauth2_provider.views import AuthorizedTokensListView, AuthorizedTokenDeleteView -from oauth2_provider.urls import base_urlpatterns as oauth2_urlpatterns + +from .views import ( + AuthorizationView, + IntrospectTokenView, + RevokeTokenView, + TokenView, + AuthorizedTokenDeleteView, + AuthorizedTokensListView, +) urlpatterns = [ path( @@ -24,15 +31,4 @@ views.PasswordResetCompleteView.as_view(), name="password_reset_complete", ), - *oauth2_urlpatterns, - re_path( - r"^authorized_tokens/$", - AuthorizedTokensListView.as_view(), - name="authorized-token-list", - ), - re_path( - r"^authorized_tokens/(?P[\w-]+)/delete/$", - AuthorizedTokenDeleteView.as_view(), - name="authorized-token-delete", - ), ] diff --git a/src/aseb/apps/users/views.py b/src/aseb/apps/users/views.py new file mode 100644 index 0000000..2887c20 --- /dev/null +++ b/src/aseb/apps/users/views.py @@ -0,0 +1,35 @@ +from oauth2_provider import views +from rest_framework.schemas import AutoSchema +from rest_framework.views import APIView +from django.utils.decorators import method_decorator +from drf_yasg.utils import swagger_auto_schema + + +class AuthorizationView(views.AuthorizationView): + ... + + +@method_decorator(name="post", decorator=swagger_auto_schema(tags=["tokens"])) +class TokenView(APIView, views.TokenView): + ... + + +@method_decorator(name="post", decorator=swagger_auto_schema(tags=["tokens"])) +class RevokeTokenView(APIView, views.RevokeTokenView): + ... + + +@method_decorator(name="get", decorator=swagger_auto_schema(tags=["tokens"])) +@method_decorator(name="post", decorator=swagger_auto_schema(tags=["tokens"])) +class IntrospectTokenView(APIView, views.IntrospectTokenView): + ... + + +@method_decorator(name="delete", decorator=swagger_auto_schema(tags=["tokens"])) +class AuthorizedTokenDeleteView(APIView, views.AuthorizedTokenDeleteView): + ... + + +@method_decorator(name="get", decorator=swagger_auto_schema(tags=["tokens"])) +class AuthorizedTokensListView(APIView, views.AuthorizedTokensListView): + ... diff --git a/src/aseb/fixtures/oauth2_provider.json b/src/aseb/fixtures/oauth2_provider.json new file mode 100644 index 0000000..5104224 --- /dev/null +++ b/src/aseb/fixtures/oauth2_provider.json @@ -0,0 +1,19 @@ +[ + { + "model": "oauth2_provider.application", + "pk": 1, + "fields": { + "client_id": "CLIENT_ID", + "user": null, + "redirect_uris": "http://localhost:8000/static/drf-yasg/swagger-ui-dist/oauth2-redirect.html", + "client_type": "confidential", + "authorization_grant_type": "authorization-code", + "client_secret": "CLIENT_SECRET", + "name": "aseb.bo", + "skip_authorization": true, + "created": "2021-09-12T02:14:57.377Z", + "updated": "2021-09-12T02:19:14.020Z", + "algorithm": "" + } + } +] diff --git a/src/aseb/settings/base.py b/src/aseb/settings/base.py index a80ba70..3c94e43 100644 --- a/src/aseb/settings/base.py +++ b/src/aseb/settings/base.py @@ -168,8 +168,8 @@ "SECURITY_DEFINITIONS": { "ASEB API": { "type": "oauth2", - "authorizationUrl": "/auth/authorize", - "tokenUrl": "/auth/token/", + "authorizationUrl": "/oauth/authorize", + "tokenUrl": "/oauth/token/", "flow": "accessCode", "scopes": { "read": "read", @@ -178,8 +178,8 @@ } }, "OAUTH2_CONFIG": { - "clientId": "yourAppClientId", - "clientSecret": "yourAppClientSecret", + "clientId": env("OAUTH2_CLIENT_ID", ""), + "clientSecret": env("OAUTH2_CLIENT_SECRET", ""), "appName": "aseb.bo", }, } diff --git a/src/aseb/settings/develop.py b/src/aseb/settings/develop.py index c431fec..11bfe7a 100644 --- a/src/aseb/settings/develop.py +++ b/src/aseb/settings/develop.py @@ -15,12 +15,17 @@ "debug_toolbar.middleware.DebugToolbarMiddleware", ] +SWAGGER_SETTINGS["OAUTH2_CONFIG"]["clientId"] = "CLIENT_ID" # noqa: +SWAGGER_SETTINGS["OAUTH2_CONFIG"]["clientSecret"] = "CLIENT_SECRET" # noqa: +SWAGGER_SETTINGS[ # noqa: + "OAUTH2_REDIRECT_URL" +] = "http://localhost:8000/static/drf-yasg/swagger-ui-dist/oauth2-redirect.html" + EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" DEBUG_TOOLBAR_CONFIG = { "DISABLE_PANELS": ["debug_toolbar.panels.redirects.RedirectsPanel"], "SHOW_TEMPLATE_CONTEXT": True, - "SHOW_TOOLBAR_CALLBACK": lambda request: True, } DEBUG_TOOLBAR_PANELS = [ diff --git a/src/aseb/urls.py b/src/aseb/urls.py index da77703..4f96172 100644 --- a/src/aseb/urls.py +++ b/src/aseb/urls.py @@ -3,8 +3,9 @@ from django.conf import settings from django.conf.urls.static import static from django.contrib import admin -from django.urls import include, path +from django.urls import include, path, re_path from django.views.static import serve +from aseb.api.urls import oauth2_urlpatterns handler404 = "aseb.core.views.not_found" @@ -12,8 +13,8 @@ path("admin/docs/", include("django.contrib.admindocs.urls")), path("admin/", admin.site.urls), path("auth/", include("aseb.apps.users.urls")), - path("oauth/", include("oauth2_provider.urls", namespace="oauth2_provider")), - path("v1/", include("aseb.api.urls")), + *oauth2_urlpatterns, # ^oauth/ + re_path("^v1/", include("aseb.api.urls")), ] if settings.DEBUG: