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:
parent
afc347ddeb
commit
97871ecd6c
|
@ -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)
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ class AuthenticatorMobileStageSerializer(StageSerializer):
|
|||
fields = StageSerializer.Meta.fields + [
|
||||
"configure_flow",
|
||||
"friendly_name",
|
||||
"item_matching_mode",
|
||||
"firebase_config",
|
||||
]
|
||||
|
||||
|
|
|
@ -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=[
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
),
|
||||
]
|
|
@ -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"
|
||||
),
|
||||
),
|
||||
]
|
|
@ -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),
|
||||
),
|
||||
]
|
|
@ -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"),
|
||||
),
|
||||
]
|
|
@ -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,
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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,
|
||||
|
|
35
schema.yml
35
schema.yml
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
|
|
Reference in New Issue