implement more
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
parent
628b130122
commit
5e2ed504bf
|
@ -4,7 +4,7 @@ from django_filters.rest_framework.backends import DjangoFilterBackend
|
||||||
from drf_spectacular.utils import OpenApiResponse, extend_schema, inline_serializer
|
from drf_spectacular.utils import OpenApiResponse, extend_schema, inline_serializer
|
||||||
from rest_framework import mixins
|
from rest_framework import mixins
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.fields import CharField, ChoiceField
|
from rest_framework.fields import CharField, ChoiceField, UUIDField
|
||||||
from rest_framework.filters import OrderingFilter, SearchFilter
|
from rest_framework.filters import OrderingFilter, SearchFilter
|
||||||
from rest_framework.permissions import IsAdminUser
|
from rest_framework.permissions import IsAdminUser
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
|
@ -70,7 +70,7 @@ class MobileDeviceSetPushKeySerializer(PassiveSerializer):
|
||||||
class MobileDeviceResponseSerializer(PassiveSerializer):
|
class MobileDeviceResponseSerializer(PassiveSerializer):
|
||||||
"""Response from push sent to phone"""
|
"""Response from push sent to phone"""
|
||||||
|
|
||||||
tx_id = CharField(required=True)
|
tx_id = UUIDField(required=True)
|
||||||
status = ChoiceField(
|
status = ChoiceField(
|
||||||
TransactionStates.choices,
|
TransactionStates.choices,
|
||||||
required=True,
|
required=True,
|
||||||
|
@ -205,11 +205,12 @@ class MobileDeviceViewSet(
|
||||||
def receive_response(self, request: Request, pk: str) -> Response:
|
def receive_response(self, request: Request, pk: str) -> Response:
|
||||||
"""Get response from notification on phone"""
|
"""Get response from notification on phone"""
|
||||||
data = MobileDeviceResponseSerializer(data=request.data)
|
data = MobileDeviceResponseSerializer(data=request.data)
|
||||||
data.is_valid()
|
data.is_valid(raise_exception=True)
|
||||||
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.status = data.validated_data["status"]
|
||||||
|
transaction.save()
|
||||||
return Response(status=204)
|
return Response(status=204)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
"""Mobile authenticator stage"""
|
"""Mobile authenticator stage"""
|
||||||
|
from time import sleep
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
|
@ -128,9 +129,6 @@ class MobileTransaction(ExpiringModel):
|
||||||
branding = request.tenant.branding_title
|
branding = request.tenant.branding_title
|
||||||
domain = request.get_host()
|
domain = request.get_host()
|
||||||
message = Message(
|
message = Message(
|
||||||
data={
|
|
||||||
"tx_id": str(self.tx_id),
|
|
||||||
},
|
|
||||||
notification=Notification(
|
notification=Notification(
|
||||||
title=__("%(brand)s authentication request" % {"brand": branding}),
|
title=__("%(brand)s authentication request" % {"brand": branding}),
|
||||||
body=__(
|
body=__(
|
||||||
|
@ -155,6 +153,7 @@ class MobileTransaction(ExpiringModel):
|
||||||
category="cat_authentik_push_authorization",
|
category="cat_authentik_push_authorization",
|
||||||
),
|
),
|
||||||
interruption_level="time-sensitive",
|
interruption_level="time-sensitive",
|
||||||
|
tx_id=str(self.tx_id),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
token=self.device.firebase_token,
|
token=self.device.firebase_token,
|
||||||
|
@ -166,6 +165,20 @@ class MobileTransaction(ExpiringModel):
|
||||||
LOGGER.warning("failed to push", exc=exc)
|
LOGGER.warning("failed to push", exc=exc)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def wait_for_response(self, max_checks=30) -> TransactionStates:
|
||||||
|
"""Wait for a change in status"""
|
||||||
|
checks = 0
|
||||||
|
while True:
|
||||||
|
self.refresh_from_db()
|
||||||
|
if self.status in [TransactionStates.accept, TransactionStates.deny]:
|
||||||
|
self.delete()
|
||||||
|
return self.status
|
||||||
|
checks += 1
|
||||||
|
if checks > max_checks:
|
||||||
|
self.delete()
|
||||||
|
raise TimeoutError()
|
||||||
|
sleep(1)
|
||||||
|
|
||||||
|
|
||||||
class MobileDeviceToken(ExpiringModel):
|
class MobileDeviceToken(ExpiringModel):
|
||||||
"""Mobile device token"""
|
"""Mobile device token"""
|
||||||
|
|
|
@ -26,7 +26,11 @@ 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 MobileDevice, MobileTransaction
|
from authentik.stages.authenticator_mobile.models import (
|
||||||
|
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
|
||||||
|
@ -194,20 +198,22 @@ def validate_challenge_mobile(device_pk: str, stage_view: StageView, user: User)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
tx = MobileTransaction.objects.create(device=device)
|
tx = MobileTransaction.objects.create(device=device)
|
||||||
response = tx.send_message(stage_view.request, **push_context)
|
tx.send_message(stage_view.request, **push_context)
|
||||||
# {'result': 'allow', 'status': 'allow', 'status_msg': 'Success. Logging you in...'}
|
status = tx.wait_for_response()
|
||||||
if not response:
|
if status == TransactionStates.deny:
|
||||||
LOGGER.debug("mobile push response", result=response)
|
LOGGER.debug("mobile push response", result=status)
|
||||||
login_failed.send(
|
login_failed.send(
|
||||||
sender=__name__,
|
sender=__name__,
|
||||||
credentials={"username": user.username},
|
credentials={"username": user.username},
|
||||||
request=stage_view.request,
|
request=stage_view.request,
|
||||||
stage=stage_view.executor.current_stage,
|
stage=stage_view.executor.current_stage,
|
||||||
device_class=DeviceClasses.MOBILE.value,
|
device_class=DeviceClasses.MOBILE.value,
|
||||||
mobile_response=response,
|
mobile_response=status,
|
||||||
)
|
)
|
||||||
raise ValidationError("Mobile denied access", code="denied")
|
raise ValidationError("Mobile denied access", code="denied")
|
||||||
return device
|
return device
|
||||||
|
except TimeoutError:
|
||||||
|
raise ValidationError("Mobile push notification timed out.")
|
||||||
except RuntimeError as exc:
|
except RuntimeError as exc:
|
||||||
Event.new(
|
Event.new(
|
||||||
EventAction.CONFIGURATION_ERROR,
|
EventAction.CONFIGURATION_ERROR,
|
||||||
|
|
|
@ -34352,7 +34352,7 @@ components:
|
||||||
properties:
|
properties:
|
||||||
tx_id:
|
tx_id:
|
||||||
type: string
|
type: string
|
||||||
minLength: 1
|
format: uuid
|
||||||
status:
|
status:
|
||||||
$ref: '#/components/schemas/MobileDeviceResponseStatusEnum'
|
$ref: '#/components/schemas/MobileDeviceResponseStatusEnum'
|
||||||
required:
|
required:
|
||||||
|
|
Reference in New Issue