re-gen migrations, implement one half of number matching

this also treats accept/deny as "number" matching (we call it item matching to make it more general), since it's just a more static version of selecting the correct thing

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
Jens Langhammer 2023-12-14 21:16:44 +01:00
parent afc347ddeb
commit 97871ecd6c
No known key found for this signature in database
13 changed files with 160 additions and 163 deletions

View file

@ -21,19 +21,13 @@ from authentik.stages.authenticator_mobile.models import (
MobileDevice, MobileDevice,
MobileDeviceToken, MobileDeviceToken,
MobileTransaction, MobileTransaction,
TransactionStates,
) )
class MobileDeviceInfoSerializer(PassiveSerializer): class MobileDeviceInfoSerializer(PassiveSerializer):
"""Info about a mobile device""" """Info about a mobile device"""
platform = ChoiceField( platform = ChoiceField((("ios", "iOS"), ("android", "Android"), ("other", "Other")))
(
("ios", "iOS"),
("android", "Android"),
)
)
os_version = CharField() os_version = CharField()
model = CharField() model = CharField()
hostname = CharField() hostname = CharField()
@ -76,10 +70,7 @@ class MobileDeviceResponseSerializer(PassiveSerializer):
"""Response from push sent to phone""" """Response from push sent to phone"""
tx_id = UUIDField(required=True) tx_id = UUIDField(required=True)
status = ChoiceField( selected_item = CharField(required=True)
TransactionStates.choices,
required=True,
)
class MobileDeviceViewSet( class MobileDeviceViewSet(
@ -214,7 +205,7 @@ class MobileDeviceViewSet(
transaction = MobileTransaction.objects.filter(tx_id=data.validated_data["tx_id"]).first() transaction = MobileTransaction.objects.filter(tx_id=data.validated_data["tx_id"]).first()
if not transaction: if not transaction:
raise Http404 raise Http404
transaction.status = data.validated_data["status"] transaction.selected_item = data.validated_data["selected_item"]
transaction.save() transaction.save()
return Response(status=204) return Response(status=204)

View file

@ -14,6 +14,7 @@ class AuthenticatorMobileStageSerializer(StageSerializer):
fields = StageSerializer.Meta.fields + [ fields = StageSerializer.Meta.fields + [
"configure_flow", "configure_flow",
"friendly_name", "friendly_name",
"item_matching_mode",
"firebase_config", "firebase_config",
] ]

View file

@ -1,4 +1,4 @@
# Generated by Django 4.2.4 on 2023-09-04 13:21 # Generated by Django 4.2.7 on 2023-12-14 20:06
import uuid import uuid
@ -14,8 +14,8 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
("authentik_flows", "0025_alter_flowstagebinding_evaluate_on_plan_and_more"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("authentik_flows", "0027_auto_20231028_1424"),
] ]
operations = [ operations = [
@ -34,6 +34,18 @@ class Migration(migrations.Migration):
), ),
), ),
("friendly_name", models.TextField(null=True)), ("friendly_name", models.TextField(null=True)),
(
"item_matching_mode",
models.TextField(
choices=[
("accept_deny", "Accept Deny"),
("number_matching_2", "Number Matching 2"),
("number_matching_3", "Number Matching 3"),
],
default="number_matching_3",
),
),
("firebase_config", models.JSONField(default=dict, help_text="temp")),
( (
"configure_flow", "configure_flow",
models.ForeignKey( models.ForeignKey(
@ -67,6 +79,8 @@ class Migration(migrations.Migration):
("uuid", models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), ("uuid", models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
("device_id", models.TextField(unique=True)), ("device_id", models.TextField(unique=True)),
("firebase_token", models.TextField(blank=True)), ("firebase_token", models.TextField(blank=True)),
("state", models.JSONField(default=dict)),
("last_checkin", models.DateTimeField(auto_now=True)),
( (
"stage", "stage",
models.ForeignKey( models.ForeignKey(
@ -86,6 +100,30 @@ class Migration(migrations.Migration):
"verbose_name_plural": "Mobile Devices", "verbose_name_plural": "Mobile Devices",
}, },
), ),
migrations.CreateModel(
name="MobileTransaction",
fields=[
(
"expires",
models.DateTimeField(default=authentik.core.models.default_token_duration),
),
("expiring", models.BooleanField(default=True)),
("tx_id", models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
("decision_items", models.JSONField(default=list)),
("correct_item", models.TextField()),
("selected_item", models.TextField(default=None, null=True)),
(
"device",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="authentik_stages_authenticator_mobile.mobiledevice",
),
),
],
options={
"abstract": False,
},
),
migrations.CreateModel( migrations.CreateModel(
name="MobileDeviceToken", name="MobileDeviceToken",
fields=[ fields=[

View file

@ -1,38 +0,0 @@
# Generated by Django 4.2.4 on 2023-09-04 18:18
import uuid
import django.db.models.deletion
from django.db import migrations, models
import authentik.core.models
class Migration(migrations.Migration):
dependencies = [
("authentik_stages_authenticator_mobile", "0001_initial"),
]
operations = [
migrations.CreateModel(
name="MobileTransaction",
fields=[
(
"expires",
models.DateTimeField(default=authentik.core.models.default_token_duration),
),
("expiring", models.BooleanField(default=True)),
("tx_id", models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
(
"device",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="authentik_stages_authenticator_mobile.mobiledevice",
),
),
],
options={
"abstract": False,
},
),
]

View file

@ -1,19 +0,0 @@
# Generated by Django 4.2.4 on 2023-09-04 18:28
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_stages_authenticator_mobile", "0002_mobiletransaction"),
]
operations = [
migrations.AddField(
model_name="mobiletransaction",
name="status",
field=models.TextField(
choices=[("wait", "Wait"), ("accept", "Accept"), ("deny", "Deny")], default="wait"
),
),
]

View file

@ -1,22 +0,0 @@
# Generated by Django 4.2.4 on 2023-09-05 13:16
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_stages_authenticator_mobile", "0003_mobiletransaction_status"),
]
operations = [
migrations.AddField(
model_name="mobiledevice",
name="last_checkin",
field=models.DateTimeField(auto_now=True),
),
migrations.AddField(
model_name="mobiledevice",
name="state",
field=models.JSONField(default=dict),
),
]

View file

@ -1,20 +0,0 @@
# Generated by Django 4.2.5 on 2023-09-21 15:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
(
"authentik_stages_authenticator_mobile",
"0004_mobiledevice_last_checkin_mobiledevice_state",
),
]
operations = [
migrations.AddField(
model_name="authenticatormobilestage",
name="firebase_config",
field=models.JSONField(default=dict, help_text="temp"),
),
]

View file

@ -28,7 +28,7 @@ from structlog.stdlib import get_logger
from authentik.core.models import ExpiringModel, User from authentik.core.models import ExpiringModel, User
from authentik.core.types import UserSettingSerializer from authentik.core.types import UserSettingSerializer
from authentik.flows.models import ConfigurableStage, FriendlyNamedStage, Stage from authentik.flows.models import ConfigurableStage, FriendlyNamedStage, Stage
from authentik.lib.generators import generate_id from authentik.lib.generators import generate_code_fixed_length, generate_id
from authentik.lib.models import SerializerModel from authentik.lib.models import SerializerModel
from authentik.stages.authenticator.models import Device from authentik.stages.authenticator.models import Device
from authentik.tenants.utils import DEFAULT_TENANT from authentik.tenants.utils import DEFAULT_TENANT
@ -41,11 +41,34 @@ def default_token_key():
return generate_id(40) return generate_id(40)
class ItemMatchingMode(models.TextChoices):
"""Configure which items the app shows the user, and what the user must select"""
ACCEPT_DENY = "accept_deny"
NUMBER_MATCHING_2 = "number_matching_2"
NUMBER_MATCHING_3 = "number_matching_3"
class AuthenticatorMobileStage(ConfigurableStage, FriendlyNamedStage, Stage): class AuthenticatorMobileStage(ConfigurableStage, FriendlyNamedStage, Stage):
"""Setup Mobile authenticator devices""" """Setup Mobile authenticator devices"""
item_matching_mode = models.TextField(
choices=ItemMatchingMode.choices, default=ItemMatchingMode.NUMBER_MATCHING_3
)
firebase_config = models.JSONField(default=dict, help_text="temp") firebase_config = models.JSONField(default=dict, help_text="temp")
def create_transaction(self, device: "MobileDevice") -> "MobileTransaction":
"""Create a transaction for `device` with the config of this stage."""
transaction = MobileTransaction(device=device)
if self.item_matching_mode == ItemMatchingMode.ACCEPT_DENY:
transaction.item_matching = [TransactionStates.ACCEPT, TransactionStates.DENY]
if self.item_matching_mode == ItemMatchingMode.NUMBER_MATCHING_2:
transaction.item_matching = [generate_code_fixed_length(2)] * 3
if self.item_matching_mode == ItemMatchingMode.NUMBER_MATCHING_3:
transaction.item_matching = [generate_code_fixed_length(3)] * 3
transaction.save()
return transaction
@property @property
def serializer(self) -> type[BaseSerializer]: def serializer(self) -> type[BaseSerializer]:
from authentik.stages.authenticator_mobile.api.stage import ( from authentik.stages.authenticator_mobile.api.stage import (
@ -96,6 +119,11 @@ class MobileDevice(SerializerModel, Device):
state = models.JSONField(default=dict) state = models.JSONField(default=dict)
last_checkin = models.DateTimeField(auto_now=True) last_checkin = models.DateTimeField(auto_now=True)
def create_transaction(self) -> "MobileTransaction":
"""Create a transaction for this device with the config of its stage."""
stage: AuthenticatorMobileStage = self.stage
return stage.create_transaction(self)
@property @property
def serializer(self) -> Serializer: def serializer(self) -> Serializer:
from authentik.stages.authenticator_mobile.api.device import MobileDeviceSerializer from authentik.stages.authenticator_mobile.api.device import MobileDeviceSerializer
@ -123,8 +151,18 @@ class MobileTransaction(ExpiringModel):
tx_id = models.UUIDField(default=uuid4, primary_key=True) tx_id = models.UUIDField(default=uuid4, primary_key=True)
device = models.ForeignKey(MobileDevice, on_delete=models.CASCADE) device = models.ForeignKey(MobileDevice, on_delete=models.CASCADE)
decision_items = models.JSONField(default=list)
correct_item = models.TextField()
selected_item = models.TextField(default=None, null=True)
status = models.TextField(choices=TransactionStates.choices, default=TransactionStates.WAIT) @property
def status(self) -> TransactionStates:
"""Get the status"""
if not self.selected_item:
return TransactionStates.WAIT
if self.selected_item != self.correct_item:
return TransactionStates.DENY
return TransactionStates.ACCEPT
def send_message(self, request: Optional[HttpRequest], **context): def send_message(self, request: Optional[HttpRequest], **context):
"""Send mobile message""" """Send mobile message"""
@ -153,7 +191,7 @@ class MobileTransaction(ExpiringModel):
notification=AndroidNotification(icon="stock_ticker_update", color="#f45342"), notification=AndroidNotification(icon="stock_ticker_update", color="#f45342"),
data={ data={
"tx_id": str(self.tx_id), "tx_id": str(self.tx_id),
"numbers": dumps([123, 456, 789]), "user_decision_items": dumps(self.item_matching),
}, },
), ),
apns=APNSConfig( apns=APNSConfig(
@ -167,12 +205,7 @@ class MobileTransaction(ExpiringModel):
), ),
interruption_level="time-sensitive", interruption_level="time-sensitive",
tx_id=str(self.tx_id), tx_id=str(self.tx_id),
numbers=[ user_decision_items=self.item_matching,
123,
456,
789,
],
options=["foo", "bar", "baz"],
), ),
), ),
token=self.device.firebase_token, token=self.device.firebase_token,

View file

@ -26,11 +26,7 @@ from authentik.root.middleware import ClientIPMiddleware
from authentik.stages.authenticator import match_token from authentik.stages.authenticator import match_token
from authentik.stages.authenticator.models import Device from authentik.stages.authenticator.models import Device
from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice
from authentik.stages.authenticator_mobile.models import ( from authentik.stages.authenticator_mobile.models import MobileDevice, TransactionStates
MobileDevice,
MobileTransaction,
TransactionStates,
)
from authentik.stages.authenticator_sms.models import SMSDevice from authentik.stages.authenticator_sms.models import SMSDevice
from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage, DeviceClasses from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage, DeviceClasses
from authentik.stages.authenticator_webauthn.models import UserVerification, WebAuthnDevice from authentik.stages.authenticator_webauthn.models import UserVerification, WebAuthnDevice
@ -198,7 +194,7 @@ def validate_challenge_mobile(device_pk: str, stage_view: StageView, user: User)
).name ).name
try: try:
transaction = MobileTransaction.objects.create(device=device) transaction = device.create_transaction()
transaction.send_message(stage_view.request, **push_context) transaction.send_message(stage_view.request, **push_context)
status = transaction.wait_for_response() status = transaction.wait_for_response()
if status == TransactionStates.DENY: if status == TransactionStates.DENY:

View file

@ -6144,6 +6144,15 @@
"minLength": 1, "minLength": 1,
"title": "Friendly name" "title": "Friendly name"
}, },
"item_matching_mode": {
"type": "string",
"enum": [
"accept_deny",
"number_matching_2",
"number_matching_3"
],
"title": "Item matching mode"
},
"firebase_config": { "firebase_config": {
"type": "object", "type": "object",
"additionalProperties": true, "additionalProperties": true,

View file

@ -30425,6 +30425,8 @@ components:
friendly_name: friendly_name:
type: string type: string
nullable: true nullable: true
item_matching_mode:
$ref: '#/components/schemas/ItemMatchingModeEnum'
firebase_config: firebase_config:
type: object type: object
additionalProperties: {} additionalProperties: {}
@ -30457,6 +30459,8 @@ components:
type: string type: string
nullable: true nullable: true
minLength: 1 minLength: 1
item_matching_mode:
$ref: '#/components/schemas/ItemMatchingModeEnum'
firebase_config: firebase_config:
type: object type: object
additionalProperties: {} additionalProperties: {}
@ -34420,6 +34424,16 @@ components:
description: |- description: |-
* `global` - Same identifier is used for all providers * `global` - Same identifier is used for all providers
* `per_provider` - Each provider has a different issuer, based on the application slug. * `per_provider` - Each provider has a different issuer, based on the application slug.
ItemMatchingModeEnum:
enum:
- accept_deny
- number_matching_2
- number_matching_3
type: string
description: |-
* `accept_deny` - Accept Deny
* `number_matching_2` - Number Matching 2
* `number_matching_3` - Number Matching 3
KubernetesServiceConnection: KubernetesServiceConnection:
type: object type: object
description: KubernetesServiceConnection Serializer description: KubernetesServiceConnection Serializer
@ -35426,21 +35440,12 @@ components:
tx_id: tx_id:
type: string type: string
format: uuid format: uuid
status: selected_item:
$ref: '#/components/schemas/MobileDeviceResponseStatusEnum' type: string
minLength: 1
required: required:
- status - selected_item
- tx_id - tx_id
MobileDeviceResponseStatusEnum:
enum:
- wait
- accept
- deny
type: string
description: |-
* `wait` - Wait
* `accept` - Accept
* `deny` - Deny
MobileDeviceSetPushKeyRequest: MobileDeviceSetPushKeyRequest:
type: object type: object
description: Set notification key description: Set notification key
@ -38224,6 +38229,8 @@ components:
type: string type: string
nullable: true nullable: true
minLength: 1 minLength: 1
item_matching_mode:
$ref: '#/components/schemas/ItemMatchingModeEnum'
firebase_config: firebase_config:
type: object type: object
additionalProperties: {} additionalProperties: {}
@ -40679,10 +40686,12 @@ components:
enum: enum:
- ios - ios
- android - android
- other
type: string type: string
description: |- description: |-
* `ios` - iOS * `ios` - iOS
* `android` - Android * `android` - Android
* `other` - Other
PlexAuthenticationChallenge: PlexAuthenticationChallenge:
type: object type: object
description: Challenge shown to the user in identification stage description: Challenge shown to the user in identification stage

View file

@ -505,17 +505,9 @@ components:
type: object type: object
properties: properties:
status: status:
$ref: '#/components/schemas/MobileDeviceEnrollmentStatusStatusEnum' $ref: '#/components/schemas/StatusEnum'
required: required:
- status - status
MobileDeviceEnrollmentStatusStatusEnum:
enum:
- success
- waiting
type: string
description: |-
* `success` - Success
* `waiting` - Waiting
MobileDeviceInfo: MobileDeviceInfo:
type: object type: object
description: Info about a mobile device description: Info about a mobile device
@ -593,21 +585,12 @@ components:
tx_id: tx_id:
type: string type: string
format: uuid format: uuid
status: selected_item:
$ref: '#/components/schemas/MobileDeviceResponseStatusEnum' type: string
minLength: 1
required: required:
- status - selected_item
- tx_id - tx_id
MobileDeviceResponseStatusEnum:
enum:
- wait
- accept
- deny
type: string
description: |-
* `wait` - Wait
* `accept` - Accept
* `deny` - Deny
MobileDeviceSetPushKeyRequest: MobileDeviceSetPushKeyRequest:
type: object type: object
description: Set notification key description: Set notification key
@ -674,10 +657,20 @@ components:
enum: enum:
- ios - ios
- android - android
- other
type: string type: string
description: |- description: |-
* `ios` - iOS * `ios` - iOS
* `android` - Android * `android` - Android
* `other` - Other
StatusEnum:
enum:
- success
- waiting
type: string
description: |-
* `success` - Success
* `waiting` - Waiting
UsedBy: UsedBy:
type: object type: object
description: A list of all objects referencing the queried object description: A list of all objects referencing the queried object

View file

@ -17,6 +17,7 @@ import {
FlowsApi, FlowsApi,
FlowsInstancesListDesignationEnum, FlowsInstancesListDesignationEnum,
FlowsInstancesListRequest, FlowsInstancesListRequest,
ItemMatchingModeEnum,
StagesApi, StagesApi,
} from "@goauthentik/api"; } from "@goauthentik/api";
@ -83,6 +84,31 @@ export class AuthenticatorMobileStageForm extends ModelForm<AuthenticatorMobileS
<ak-form-group .expanded=${true}> <ak-form-group .expanded=${true}>
<span slot="header"> ${msg("Stage-specific settings")} </span> <span slot="header"> ${msg("Stage-specific settings")} </span>
<div slot="body" class="pf-c-form"> <div slot="body" class="pf-c-form">
<ak-form-element-horizontal
label=${msg("User decision mode")}
?required=${true}
name="itemMatchingMode"
>
<ak-radio
.options=${[
{
label: msg("Accept/Deny"),
value: ItemMatchingModeEnum.AcceptDeny,
},
{
label: msg("Number matching (2 digit numbers)"),
value: ItemMatchingModeEnum.NumberMatching2,
},
{
label: msg("Number matching (3 digit numbers)"),
value: ItemMatchingModeEnum.NumberMatching3,
default: true,
},
]}
.value=${this.instance?.itemMatchingMode}
>
</ak-radio>
</ak-form-element-horizontal>
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("Firebase config")} label=${msg("Firebase config")}
?required=${false} ?required=${false}