diff --git a/authentik/root/settings.py b/authentik/root/settings.py index 4f7081723..227562c0a 100644 --- a/authentik/root/settings.py +++ b/authentik/root/settings.py @@ -130,7 +130,10 @@ SPECTACULAR_SETTINGS = { "CONTACT": { "email": "hello@goauthentik.io", }, - "AUTHENTICATION_WHITELIST": ["authentik.api.authentication.TokenAuthentication"], + "AUTHENTICATION_WHITELIST": [ + "authentik.stages.authenticator_mobile.api.auth.MobileDeviceTokenAuthentication", + "authentik.api.authentication.TokenAuthentication", + ], "LICENSE": { "name": "MIT", "url": "https://github.com/goauthentik/authentik/blob/main/LICENSE", diff --git a/authentik/stages/authenticator_mobile/api/__init__.py b/authentik/stages/authenticator_mobile/api/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/authentik/stages/authenticator_mobile/api/auth.py b/authentik/stages/authenticator_mobile/api/auth.py new file mode 100644 index 000000000..2ccecf5e5 --- /dev/null +++ b/authentik/stages/authenticator_mobile/api/auth.py @@ -0,0 +1,40 @@ +"""Mobile device token authentication""" +from typing import Any + +from drf_spectacular.extensions import OpenApiAuthenticationExtension +from rest_framework.authentication import BaseAuthentication, get_authorization_header +from rest_framework.request import Request + +from authentik.api.authentication import validate_auth +from authentik.core.models import User +from authentik.stages.authenticator_mobile.models import MobileDeviceToken + + +class MobileDeviceTokenAuthentication(BaseAuthentication): + """Mobile device token authentication""" + + def authenticate(self, request: Request) -> tuple[User, Any] | None: + """Token-based authentication using HTTP Bearer authentication""" + auth = get_authorization_header(request) + raw_token = validate_auth(auth) + device_token: MobileDeviceToken = MobileDeviceToken.objects.filter(token=raw_token).first() + if not device_token: + return None + + return (device_token.user, None) + + +class TokenSchema(OpenApiAuthenticationExtension): + """Auth schema""" + + target_class = MobileDeviceTokenAuthentication + name = "mobile_device_token" + + def get_security_definition(self, auto_schema): + """Auth schema""" + return { + "type": "apiKey", + "in": "header", + "name": "Authorization", + "scheme": "bearer", + } diff --git a/authentik/stages/authenticator_mobile/api.py b/authentik/stages/authenticator_mobile/api/device.py similarity index 50% rename from authentik/stages/authenticator_mobile/api.py rename to authentik/stages/authenticator_mobile/api/device.py index 09973e5e4..90c544ff4 100644 --- a/authentik/stages/authenticator_mobile/api.py +++ b/authentik/stages/authenticator_mobile/api/device.py @@ -1,60 +1,14 @@ """AuthenticatorMobileStage API Views""" from django_filters.rest_framework.backends import DjangoFilterBackend -from drf_spectacular.types import OpenApiTypes -from drf_spectacular.utils import extend_schema, inline_serializer from rest_framework import mixins -from rest_framework.decorators import action -from rest_framework.fields import CharField from rest_framework.filters import OrderingFilter, SearchFilter from rest_framework.permissions import IsAdminUser -from rest_framework.request import Request -from rest_framework.response import Response from rest_framework.serializers import ModelSerializer from rest_framework.viewsets import GenericViewSet, ModelViewSet from authentik.api.authorization import OwnerFilter, OwnerPermissions from authentik.core.api.used_by import UsedByMixin -from authentik.flows.api.stages import StageSerializer -from authentik.stages.authenticator_mobile.models import AuthenticatorMobileStage, MobileDevice - - -class AuthenticatorMobileStageSerializer(StageSerializer): - """AuthenticatorMobileStage Serializer""" - - class Meta: - model = AuthenticatorMobileStage - fields = StageSerializer.Meta.fields + [ - "configure_flow", - "friendly_name", - ] - - -class AuthenticatorMobileStageViewSet(UsedByMixin, ModelViewSet): - """AuthenticatorMobileStage Viewset""" - - queryset = AuthenticatorMobileStage.objects.all() - serializer_class = AuthenticatorMobileStageSerializer - filterset_fields = [ - "name", - "configure_flow", - ] - search_fields = ["name"] - ordering = ["name"] - - @extend_schema( - request=OpenApiTypes.NONE, - responses={ - 200: inline_serializer( - "MobileDeviceEnrollmentCallbackSerializer", - { - "device_token": CharField(required=True), - }, - ), - }, - ) - @action(methods=["POST"], detail=True, permission_classes=[]) - def enrollment_callback(self, request: Request, pk: str) -> Response: - """Enrollment callback""" +from authentik.stages.authenticator_mobile.models import MobileDevice class MobileDeviceSerializer(ModelSerializer): diff --git a/authentik/stages/authenticator_mobile/api/stage.py b/authentik/stages/authenticator_mobile/api/stage.py new file mode 100644 index 000000000..00533fdf3 --- /dev/null +++ b/authentik/stages/authenticator_mobile/api/stage.py @@ -0,0 +1,63 @@ +"""AuthenticatorMobileStage API Views""" +from drf_spectacular.utils import extend_schema, inline_serializer +from rest_framework.decorators import action +from rest_framework.fields import CharField +from rest_framework.request import Request +from rest_framework.response import Response +from rest_framework.viewsets import ModelViewSet + +from authentik.core.api.used_by import UsedByMixin +from authentik.flows.api.stages import StageSerializer +from authentik.stages.authenticator_mobile.api.auth import MobileDeviceTokenAuthentication +from authentik.stages.authenticator_mobile.models import AuthenticatorMobileStage + + +class AuthenticatorMobileStageSerializer(StageSerializer): + """AuthenticatorMobileStage Serializer""" + + class Meta: + model = AuthenticatorMobileStage + fields = StageSerializer.Meta.fields + [ + "configure_flow", + "friendly_name", + ] + + +class AuthenticatorMobileStageViewSet(UsedByMixin, ModelViewSet): + """AuthenticatorMobileStage Viewset""" + + queryset = AuthenticatorMobileStage.objects.all() + serializer_class = AuthenticatorMobileStageSerializer + filterset_fields = [ + "name", + "configure_flow", + ] + search_fields = ["name"] + ordering = ["name"] + + @extend_schema( + responses={ + 200: inline_serializer( + "MobileDeviceEnrollmentCallbackSerializer", + { + "device_token": CharField(required=True), + }, + ), + }, + request=inline_serializer( + "MobileDeviceEnrollmentSerializer", + { + "device_token": CharField(required=True), + }, + ), + ) + @action( + methods=["POST"], + detail=True, + permission_classes=[], + authentication_classes=[MobileDeviceTokenAuthentication], + ) + def enrollment_callback(self, request: Request, pk: str) -> Response: + """Enrollment callback""" + print(request.data) + return Response(status=204) diff --git a/authentik/stages/authenticator_mobile/models.py b/authentik/stages/authenticator_mobile/models.py index 1797d66c2..182b5fdb8 100644 --- a/authentik/stages/authenticator_mobile/models.py +++ b/authentik/stages/authenticator_mobile/models.py @@ -25,7 +25,9 @@ class AuthenticatorMobileStage(ConfigurableStage, FriendlyNamedStage, Stage): @property def serializer(self) -> type[BaseSerializer]: - from authentik.stages.authenticator_mobile.api import AuthenticatorMobileStageSerializer + from authentik.stages.authenticator_mobile.api.stage import ( + AuthenticatorMobileStageSerializer, + ) return AuthenticatorMobileStageSerializer @@ -67,7 +69,7 @@ class MobileDevice(SerializerModel, Device): @property def serializer(self) -> Serializer: - from authentik.stages.authenticator_mobile.api import MobileDeviceSerializer + from authentik.stages.authenticator_mobile.api.device import MobileDeviceSerializer return MobileDeviceSerializer diff --git a/authentik/stages/authenticator_mobile/stage.py b/authentik/stages/authenticator_mobile/stage.py index 79a7ca5cd..9dfc12084 100644 --- a/authentik/stages/authenticator_mobile/stage.py +++ b/authentik/stages/authenticator_mobile/stage.py @@ -56,7 +56,7 @@ class AuthenticatorMobileStageView(ChallengeStageView): payload = AuthenticatorMobilePayloadChallenge( data={ # TODO: use cloud gateway? - "u": self.request.get_host(), + "u": self.request.build_absolute_uri("/"), "s": str(stage.stage_uuid), "t": self.executor.plan.context[FLOW_PLAN_MOBILE_ENROLL].token, } diff --git a/authentik/stages/authenticator_mobile/urls.py b/authentik/stages/authenticator_mobile/urls.py index 38ee44b13..16ea30127 100644 --- a/authentik/stages/authenticator_mobile/urls.py +++ b/authentik/stages/authenticator_mobile/urls.py @@ -1,9 +1,9 @@ """API URLs""" -from authentik.stages.authenticator_mobile.api import ( +from authentik.stages.authenticator_mobile.api.device import ( AdminMobileDeviceViewSet, - AuthenticatorMobileStageViewSet, MobileDeviceViewSet, ) +from authentik.stages.authenticator_mobile.api.stage import AuthenticatorMobileStageViewSet api_urlpatterns = [ ("authenticators/mobile", MobileDeviceViewSet), diff --git a/schema.yml b/schema.yml index 9ec043f3a..3c4c4facf 100644 --- a/schema.yml +++ b/schema.yml @@ -23435,8 +23435,14 @@ paths: required: true tags: - stages + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/MobileDeviceEnrollmentRequest' + required: true security: - - authentik: [] + - mobile_device_token: [] responses: '200': content: @@ -35129,6 +35135,14 @@ components: type: string required: - device_token + MobileDeviceEnrollmentRequest: + type: object + properties: + device_token: + type: string + minLength: 1 + required: + - device_token MobileDeviceRequest: type: object description: Serializer for Mobile authenticator devices @@ -45091,5 +45105,10 @@ components: in: header name: Authorization scheme: bearer + mobile_device_token: + type: apiKey + in: header + name: Authorization + scheme: bearer servers: - url: /api/v3/