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

View File

@ -14,6 +14,7 @@ class AuthenticatorMobileStageSerializer(StageSerializer):
fields = StageSerializer.Meta.fields + [
"configure_flow",
"friendly_name",
"item_matching_mode",
"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
@ -14,8 +14,8 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
("authentik_flows", "0025_alter_flowstagebinding_evaluate_on_plan_and_more"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("authentik_flows", "0027_auto_20231028_1424"),
]
operations = [
@ -34,6 +34,18 @@ class Migration(migrations.Migration):
),
),
("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",
models.ForeignKey(
@ -67,6 +79,8 @@ class Migration(migrations.Migration):
("uuid", models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
("device_id", models.TextField(unique=True)),
("firebase_token", models.TextField(blank=True)),
("state", models.JSONField(default=dict)),
("last_checkin", models.DateTimeField(auto_now=True)),
(
"stage",
models.ForeignKey(
@ -86,6 +100,30 @@ class Migration(migrations.Migration):
"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(
name="MobileDeviceToken",
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.types import UserSettingSerializer
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.stages.authenticator.models import Device
from authentik.tenants.utils import DEFAULT_TENANT
@ -41,11 +41,34 @@ def default_token_key():
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):
"""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")
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
def serializer(self) -> type[BaseSerializer]:
from authentik.stages.authenticator_mobile.api.stage import (
@ -96,6 +119,11 @@ class MobileDevice(SerializerModel, Device):
state = models.JSONField(default=dict)
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
def serializer(self) -> Serializer:
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)
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):
"""Send mobile message"""
@ -153,7 +191,7 @@ class MobileTransaction(ExpiringModel):
notification=AndroidNotification(icon="stock_ticker_update", color="#f45342"),
data={
"tx_id": str(self.tx_id),
"numbers": dumps([123, 456, 789]),
"user_decision_items": dumps(self.item_matching),
},
),
apns=APNSConfig(
@ -167,12 +205,7 @@ class MobileTransaction(ExpiringModel):
),
interruption_level="time-sensitive",
tx_id=str(self.tx_id),
numbers=[
123,
456,
789,
],
options=["foo", "bar", "baz"],
user_decision_items=self.item_matching,
),
),
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.models import Device
from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice
from authentik.stages.authenticator_mobile.models import (
MobileDevice,
MobileTransaction,
TransactionStates,
)
from authentik.stages.authenticator_mobile.models import MobileDevice, TransactionStates
from authentik.stages.authenticator_sms.models import SMSDevice
from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage, DeviceClasses
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
try:
transaction = MobileTransaction.objects.create(device=device)
transaction = device.create_transaction()
transaction.send_message(stage_view.request, **push_context)
status = transaction.wait_for_response()
if status == TransactionStates.DENY:

View File

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

View File

@ -30425,6 +30425,8 @@ components:
friendly_name:
type: string
nullable: true
item_matching_mode:
$ref: '#/components/schemas/ItemMatchingModeEnum'
firebase_config:
type: object
additionalProperties: {}
@ -30457,6 +30459,8 @@ components:
type: string
nullable: true
minLength: 1
item_matching_mode:
$ref: '#/components/schemas/ItemMatchingModeEnum'
firebase_config:
type: object
additionalProperties: {}
@ -34420,6 +34424,16 @@ components:
description: |-
* `global` - Same identifier is used for all providers
* `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:
type: object
description: KubernetesServiceConnection Serializer
@ -35426,21 +35440,12 @@ components:
tx_id:
type: string
format: uuid
status:
$ref: '#/components/schemas/MobileDeviceResponseStatusEnum'
selected_item:
type: string
minLength: 1
required:
- status
- selected_item
- tx_id
MobileDeviceResponseStatusEnum:
enum:
- wait
- accept
- deny
type: string
description: |-
* `wait` - Wait
* `accept` - Accept
* `deny` - Deny
MobileDeviceSetPushKeyRequest:
type: object
description: Set notification key
@ -38224,6 +38229,8 @@ components:
type: string
nullable: true
minLength: 1
item_matching_mode:
$ref: '#/components/schemas/ItemMatchingModeEnum'
firebase_config:
type: object
additionalProperties: {}
@ -40679,10 +40686,12 @@ components:
enum:
- ios
- android
- other
type: string
description: |-
* `ios` - iOS
* `android` - Android
* `other` - Other
PlexAuthenticationChallenge:
type: object
description: Challenge shown to the user in identification stage

View File

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

View File

@ -17,6 +17,7 @@ import {
FlowsApi,
FlowsInstancesListDesignationEnum,
FlowsInstancesListRequest,
ItemMatchingModeEnum,
StagesApi,
} from "@goauthentik/api";
@ -83,6 +84,31 @@ export class AuthenticatorMobileStageForm extends ModelForm<AuthenticatorMobileS
<ak-form-group .expanded=${true}>
<span slot="header"> ${msg("Stage-specific settings")} </span>
<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
label=${msg("Firebase config")}
?required=${false}