diff --git a/authentik/stages/authenticator_mobile/api/device.py b/authentik/stages/authenticator_mobile/api/device.py index 90c544ff4..eece494c2 100644 --- a/authentik/stages/authenticator_mobile/api/device.py +++ b/authentik/stages/authenticator_mobile/api/device.py @@ -5,9 +5,15 @@ from rest_framework.filters import OrderingFilter, SearchFilter from rest_framework.permissions import IsAdminUser from rest_framework.serializers import ModelSerializer from rest_framework.viewsets import GenericViewSet, ModelViewSet +from drf_spectacular.utils import extend_schema, inline_serializer +from rest_framework.decorators import action +from rest_framework.fields import CharField, UUIDField +from rest_framework.request import Request +from rest_framework.response import Response from authentik.api.authorization import OwnerFilter, OwnerPermissions from authentik.core.api.used_by import UsedByMixin +from authentik.stages.authenticator_mobile.api.auth import MobileDeviceTokenAuthentication from authentik.stages.authenticator_mobile.models import MobileDevice @@ -38,6 +44,69 @@ class MobileDeviceViewSet( permission_classes = [OwnerPermissions] filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter] + @extend_schema( + responses={ + 204: "", + }, + request=inline_serializer( + "MobileDeviceSetPushKeySerializer", + { + "firebase_key": CharField(required=True), + }, + ), + ) + @action( + methods=["POST"], + detail=True, + permission_classes=[], + authentication_classes=[MobileDeviceTokenAuthentication], + ) + def set_notification_key(self): + """Called by the phone whenever the firebase key changes and we need to update it""" + device = self.get_object() + print(self.request.user) + + @action( + methods=["POST"], + detail=True, + permission_classes=[], + authentication_classes=[MobileDeviceTokenAuthentication], + ) + def receive_response(): + """Get response from notification on phone""" + pass + + @extend_schema( + responses={ + 200: inline_serializer( + "MobileDeviceEnrollmentCallbackSerializer", + { + "device_token": CharField(required=True), + "device_uuid": UUIDField(required=True) + }, + ), + }, + request=inline_serializer( + "MobileDeviceEnrollmentSerializer", + { + # New API token (that will be rotated at some point) + # also used by the backend to sign requests to the cloud broker + # also used by the app to check the signature of incoming requests + "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) + class AdminMobileDeviceViewSet(ModelViewSet): """Viewset for Mobile authenticator devices (for admins)""" diff --git a/authentik/stages/authenticator_mobile/api/stage.py b/authentik/stages/authenticator_mobile/api/stage.py index 00533fdf3..61fc78bcd 100644 --- a/authentik/stages/authenticator_mobile/api/stage.py +++ b/authentik/stages/authenticator_mobile/api/stage.py @@ -1,14 +1,8 @@ """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 @@ -34,30 +28,3 @@ class AuthenticatorMobileStageViewSet(UsedByMixin, ModelViewSet): ] 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 182b5fdb8..99e28d450 100644 --- a/authentik/stages/authenticator_mobile/models.py +++ b/authentik/stages/authenticator_mobile/models.py @@ -87,3 +87,5 @@ class MobileDeviceToken(ExpiringModel): device = models.ForeignKey(MobileDevice, on_delete=models.CASCADE, null=True) user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE) token = models.TextField(default=default_token_key) + + firebase_token = models.TextField(blank=True) diff --git a/authentik/stages/authenticator_mobile/stage.py b/authentik/stages/authenticator_mobile/stage.py index 9dfc12084..5ef187e37 100644 --- a/authentik/stages/authenticator_mobile/stage.py +++ b/authentik/stages/authenticator_mobile/stage.py @@ -10,7 +10,7 @@ from authentik.flows.challenge import ( WithUserInfoChallenge, ) from authentik.flows.stage import ChallengeStageView -from authentik.stages.authenticator_mobile.models import AuthenticatorMobileStage, MobileDeviceToken +from authentik.stages.authenticator_mobile.models import AuthenticatorMobileStage, MobileDevice, MobileDeviceToken FLOW_PLAN_MOBILE_ENROLL = "authentik/stages/authenticator_mobile/enroll" @@ -45,19 +45,23 @@ class AuthenticatorMobileStageView(ChallengeStageView): """Prepare the token""" if FLOW_PLAN_MOBILE_ENROLL in self.executor.plan.context: return + device = MobileDevice.objects.create( + user=self.get_pending_user(), + stage=self.executor.current_stage, + ) token = MobileDeviceToken.objects.create( user=self.get_pending_user(), + device=device, ) self.executor.plan.context[FLOW_PLAN_MOBILE_ENROLL] = token def get_challenge(self, *args, **kwargs) -> Challenge: - stage: AuthenticatorMobileStage = self.executor.current_stage self.prepare() payload = AuthenticatorMobilePayloadChallenge( data={ # TODO: use cloud gateway? "u": self.request.build_absolute_uri("/"), - "s": str(stage.stage_uuid), + "s": self.executor.plan.context[FLOW_PLAN_MOBILE_ENROLL].device.pk, "t": self.executor.plan.context[FLOW_PLAN_MOBILE_ENROLL].token, } ) diff --git a/schema.yml b/schema.yml index 10b6b1a08..a5bcff26d 100644 --- a/schema.yml +++ b/schema.yml @@ -2157,6 +2157,123 @@ paths: schema: $ref: '#/components/schemas/GenericError' description: '' + /authenticators/mobile/{id}/enrollment_callback/: + post: + operationId: authenticators_mobile_enrollment_callback_create + description: Enrollment callback + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this Mobile Device. + required: true + tags: + - authenticators + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/MobileDeviceEnrollmentRequest' + required: true + security: + - mobile_device_token: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/MobileDeviceEnrollmentCallback' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + /authenticators/mobile/{id}/receive_response/: + post: + operationId: authenticators_mobile_receive_response_create + description: Get response from notification on phone + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this Mobile Device. + required: true + tags: + - authenticators + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/MobileDeviceRequest' + required: true + security: + - mobile_device_token: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/MobileDevice' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + /authenticators/mobile/{id}/set_notification_key/: + post: + operationId: authenticators_mobile_set_notification_key_create + description: Called by the phone whenever the firebase key changes and we need + to update it + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this Mobile Device. + required: true + tags: + - authenticators + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/MobileDeviceSetPushKeyRequest' + required: true + security: + - mobile_device_token: [] + responses: + '204': + description: No response body + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' /authenticators/mobile/{id}/used_by/: get: operationId: authenticators_mobile_used_by_list @@ -22597,47 +22714,6 @@ paths: schema: $ref: '#/components/schemas/GenericError' description: '' - /stages/authenticator/mobile/{stage_uuid}/enrollment_callback/: - post: - operationId: stages_authenticator_mobile_enrollment_callback_create - description: Enrollment callback - parameters: - - in: path - name: stage_uuid - schema: - type: string - format: uuid - description: A UUID string identifying this Mobile Authenticator Setup Stage. - required: true - tags: - - stages - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/MobileDeviceEnrollmentRequest' - required: true - security: - - mobile_device_token: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/MobileDeviceEnrollmentCallback' - description: '' - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/ValidationError' - description: '' - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/GenericError' - description: '' /stages/authenticator/mobile/{stage_uuid}/used_by/: get: operationId: stages_authenticator_mobile_used_by_list @@ -34145,16 +34221,20 @@ components: properties: device_token: type: string + device_uuid: + type: string + format: uuid required: - device_token + - device_uuid MobileDeviceEnrollmentRequest: type: object properties: - device_token: + token: type: string minLength: 1 required: - - device_token + - token MobileDeviceRequest: type: object description: Serializer for Mobile authenticator devices @@ -34166,6 +34246,14 @@ components: maxLength: 64 required: - name + MobileDeviceSetPushKeyRequest: + type: object + properties: + firebase_key: + type: string + minLength: 1 + required: + - firebase_key ModelEnum: enum: - authentik_crypto.certificatekeypair @@ -35353,30 +35441,7 @@ components: type: object properties: pagination: - type: object - properties: - next: - type: number - previous: - type: number - count: - type: number - current: - type: number - total_pages: - type: number - start_index: - type: number - end_index: - type: number - required: - - next - - previous - - count - - current - - total_pages - - start_index - - end_index + $ref: '#/components/schemas/Pagination' results: type: array items: @@ -35772,30 +35837,7 @@ components: type: object properties: pagination: - type: object - properties: - next: - type: number - previous: - type: number - count: - type: number - current: - type: number - total_pages: - type: number - start_index: - type: number - end_index: - type: number - required: - - next - - previous - - count - - current - - total_pages - - start_index - - end_index + $ref: '#/components/schemas/Pagination' results: type: array items: