diff --git a/authentik/stages/authenticator_mobile/models.py b/authentik/stages/authenticator_mobile/models.py index d0bb2f824..211bbfe87 100644 --- a/authentik/stages/authenticator_mobile/models.py +++ b/authentik/stages/authenticator_mobile/models.py @@ -7,12 +7,19 @@ from django.utils.translation import gettext_lazy as _ from django.views import View from django_otp.models import Device from rest_framework.serializers import BaseSerializer, Serializer +from authentik.core.models import ExpiringModel from authentik.core.types import UserSettingSerializer from authentik.flows.models import ConfigurableStage, FriendlyNamedStage, Stage +from authentik.lib.generators import generate_id from authentik.lib.models import SerializerModel +def default_token_key(): + """Default token key""" + return generate_id(40) + + class AuthenticatorMobileStage(ConfigurableStage, FriendlyNamedStage, Stage): """Setup Duo authenticator devices""" @@ -70,3 +77,9 @@ class MobileDevice(SerializerModel, Device): class Meta: verbose_name = _("Mobile Device") verbose_name_plural = _("Mobile Devices") + +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) diff --git a/authentik/stages/authenticator_mobile/stage.py b/authentik/stages/authenticator_mobile/stage.py index 0bf772dca..bdd4912da 100644 --- a/authentik/stages/authenticator_mobile/stage.py +++ b/authentik/stages/authenticator_mobile/stage.py @@ -2,6 +2,7 @@ from django.http import HttpResponse from django.utils.timezone import now from rest_framework.fields import CharField +from authentik.core.api.utils import PassiveSerializer from authentik.events.models import Event, EventAction from authentik.flows.challenge import ( @@ -11,16 +12,22 @@ from authentik.flows.challenge import ( WithUserInfoChallenge, ) from authentik.flows.stage import ChallengeStageView -from authentik.stages.authenticator_mobile.models import AuthenticatorMobileStage +from authentik.stages.authenticator_mobile.models import AuthenticatorMobileStage, MobileDeviceToken -SESSION_KEY_MOBILE_ENROLL = "authentik/stages/authenticator_mobile/enroll" +FLOW_PLAN_MOBILE_ENROLL = "authentik/stages/authenticator_mobile/enroll" +class AuthenticatorMobilePayloadChallenge(PassiveSerializer): + """Payload within the QR code given to the mobile app, hence the short variable names""" + + u = CharField(required=False, help_text="Server URL") + s = CharField(required=False, help_text="Stage UUID") + t = CharField(required=False, help_text="Initial Token") + class AuthenticatorMobileChallenge(WithUserInfoChallenge): """Mobile Challenge""" - authentik_url = CharField(required=True) - stage_uuid = CharField(required=True) + payload = AuthenticatorMobilePayloadChallenge(required=True) component = CharField(default="ak-stage-authenticator-mobile") @@ -35,13 +42,28 @@ class AuthenticatorMobileStageView(ChallengeStageView): response_class = AuthenticatorMobileChallengeResponse + def prepare(self): + if FLOW_PLAN_MOBILE_ENROLL in self.executor.plan.context: + return + token = MobileDeviceToken.objects.create( + user=self.get_pending_user(), + ) + 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.get_host(), + "s": str(stage.stage_uuid), + "t": self.executor.plan[FLOW_PLAN_MOBILE_ENROLL].token, + }) + payload.is_valid() return AuthenticatorMobileChallenge( data={ "type": ChallengeTypes.NATIVE.value, - "authentik_url": self.request.get_host(), - "stage_uuid": str(stage.stage_uuid), + "payload": payload.validated_data, } ) diff --git a/schema.yml b/schema.yml index 7bd0907dd..3b66b428d 100644 --- a/schema.yml +++ b/schema.yml @@ -30183,15 +30183,12 @@ components: type: string pending_user_avatar: type: string - authentik_url: - type: string - stage_uuid: - type: string + payload: + $ref: '#/components/schemas/AuthenticatorMobilePayloadChallenge' required: - - authentik_url + - payload - pending_user - pending_user_avatar - - stage_uuid - type AuthenticatorMobileChallengeResponseRequest: type: object @@ -30201,6 +30198,20 @@ components: type: string minLength: 1 default: ak-stage-authenticator-mobile + AuthenticatorMobilePayloadChallenge: + type: object + description: Payload within the QR code given to the mobile app, hence the short + variable names + properties: + u: + type: string + description: Server URL + s: + type: string + description: Stage UUID + t: + type: string + description: Initial Token AuthenticatorMobileStage: type: object description: AuthenticatorMobileStage Serializer diff --git a/web/src/flow/FlowExecutor.ts b/web/src/flow/FlowExecutor.ts index ad8b740a1..d3585b1a0 100644 --- a/web/src/flow/FlowExecutor.ts +++ b/web/src/flow/FlowExecutor.ts @@ -338,7 +338,9 @@ export class FlowExecutor extends Interface implements StageHost { .challenge=${this.challenge} >`; case "ak-stage-authenticator-mobile": - await import("@goauthentik/flow/stages/authenticator_mobile/AuthenticatorMobileStage"); + await import( + "@goauthentik/flow/stages/authenticator_mobile/AuthenticatorMobileStage" + ); return html`
- +