stages/authenticator_sms: fix error when phone number from context already exists (#7264)
This commit is contained in:
parent
6f67c1a277
commit
c0fe99714f
|
@ -12,7 +12,6 @@ from authentik.flows.challenge import (
|
||||||
Challenge,
|
Challenge,
|
||||||
ChallengeResponse,
|
ChallengeResponse,
|
||||||
ChallengeTypes,
|
ChallengeTypes,
|
||||||
ErrorDetailSerializer,
|
|
||||||
WithUserInfoChallenge,
|
WithUserInfoChallenge,
|
||||||
)
|
)
|
||||||
from authentik.flows.stage import ChallengeStageView
|
from authentik.flows.stage import ChallengeStageView
|
||||||
|
@ -24,6 +23,7 @@ from authentik.stages.authenticator_sms.models import (
|
||||||
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
|
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
|
||||||
|
|
||||||
SESSION_KEY_SMS_DEVICE = "authentik/stages/authenticator_sms/sms_device"
|
SESSION_KEY_SMS_DEVICE = "authentik/stages/authenticator_sms/sms_device"
|
||||||
|
PLAN_CONTEXT_PHONE = "phone"
|
||||||
|
|
||||||
|
|
||||||
class AuthenticatorSMSChallenge(WithUserInfoChallenge):
|
class AuthenticatorSMSChallenge(WithUserInfoChallenge):
|
||||||
|
@ -48,6 +48,8 @@ class AuthenticatorSMSChallengeResponse(ChallengeResponse):
|
||||||
def validate(self, attrs: dict) -> dict:
|
def validate(self, attrs: dict) -> dict:
|
||||||
"""Check"""
|
"""Check"""
|
||||||
if "code" not in attrs:
|
if "code" not in attrs:
|
||||||
|
if "phone_number" not in attrs:
|
||||||
|
raise ValidationError("phone_number required")
|
||||||
self.device.phone_number = attrs["phone_number"]
|
self.device.phone_number = attrs["phone_number"]
|
||||||
self.stage.validate_and_send(attrs["phone_number"])
|
self.stage.validate_and_send(attrs["phone_number"])
|
||||||
return super().validate(attrs)
|
return super().validate(attrs)
|
||||||
|
@ -67,6 +69,7 @@ class AuthenticatorSMSStageView(ChallengeStageView):
|
||||||
stage: AuthenticatorSMSStage = self.executor.current_stage
|
stage: AuthenticatorSMSStage = self.executor.current_stage
|
||||||
hashed_number = hash_phone_number(phone_number)
|
hashed_number = hash_phone_number(phone_number)
|
||||||
query = Q(phone_number=hashed_number) | Q(phone_number=phone_number)
|
query = Q(phone_number=hashed_number) | Q(phone_number=phone_number)
|
||||||
|
print(SMSDevice.objects.filter(query, stage=stage.pk))
|
||||||
if SMSDevice.objects.filter(query, stage=stage.pk).exists():
|
if SMSDevice.objects.filter(query, stage=stage.pk).exists():
|
||||||
raise ValidationError(_("Invalid phone number"))
|
raise ValidationError(_("Invalid phone number"))
|
||||||
# No code yet, but we have a phone number, so send a verification message
|
# No code yet, but we have a phone number, so send a verification message
|
||||||
|
@ -75,9 +78,9 @@ class AuthenticatorSMSStageView(ChallengeStageView):
|
||||||
|
|
||||||
def _has_phone_number(self) -> Optional[str]:
|
def _has_phone_number(self) -> Optional[str]:
|
||||||
context = self.executor.plan.context
|
context = self.executor.plan.context
|
||||||
if "phone" in context.get(PLAN_CONTEXT_PROMPT, {}):
|
if PLAN_CONTEXT_PHONE in context.get(PLAN_CONTEXT_PROMPT, {}):
|
||||||
self.logger.debug("got phone number from plan context")
|
self.logger.debug("got phone number from plan context")
|
||||||
return context.get(PLAN_CONTEXT_PROMPT, {}).get("phone")
|
return context.get(PLAN_CONTEXT_PROMPT, {}).get(PLAN_CONTEXT_PHONE)
|
||||||
if SESSION_KEY_SMS_DEVICE in self.request.session:
|
if SESSION_KEY_SMS_DEVICE in self.request.session:
|
||||||
self.logger.debug("got phone number from device in session")
|
self.logger.debug("got phone number from device in session")
|
||||||
device: SMSDevice = self.request.session[SESSION_KEY_SMS_DEVICE]
|
device: SMSDevice = self.request.session[SESSION_KEY_SMS_DEVICE]
|
||||||
|
@ -113,10 +116,17 @@ class AuthenticatorSMSStageView(ChallengeStageView):
|
||||||
try:
|
try:
|
||||||
self.validate_and_send(phone_number)
|
self.validate_and_send(phone_number)
|
||||||
except ValidationError as exc:
|
except ValidationError as exc:
|
||||||
response = AuthenticatorSMSChallengeResponse()
|
# We had a phone number given already (at this point only possible from flow
|
||||||
response._errors.setdefault("phone_number", [])
|
# context), but an error occurred while sending a number (most likely)
|
||||||
response._errors["phone_number"].append(ErrorDetailSerializer(exc.detail))
|
# due to a duplicate device, so delete the number we got given, reset the state
|
||||||
return self.challenge_invalid(response)
|
# (ish) and retry
|
||||||
|
device.phone_number = ""
|
||||||
|
self.executor.plan.context.get(PLAN_CONTEXT_PROMPT, {}).pop(
|
||||||
|
PLAN_CONTEXT_PHONE, None
|
||||||
|
)
|
||||||
|
self.request.session.pop(SESSION_KEY_SMS_DEVICE, None)
|
||||||
|
self.logger.warning("failed to send SMS message to pre-set number", exc=exc)
|
||||||
|
return self.get(request, *args, **kwargs)
|
||||||
return super().get(request, *args, **kwargs)
|
return super().get(request, *args, **kwargs)
|
||||||
|
|
||||||
def challenge_valid(self, response: ChallengeResponse) -> HttpResponse:
|
def challenge_valid(self, response: ChallengeResponse) -> HttpResponse:
|
||||||
|
|
|
@ -7,7 +7,9 @@ from requests_mock import Mocker
|
||||||
|
|
||||||
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
||||||
from authentik.flows.models import FlowStageBinding
|
from authentik.flows.models import FlowStageBinding
|
||||||
|
from authentik.flows.planner import FlowPlan
|
||||||
from authentik.flows.tests import FlowTestCase
|
from authentik.flows.tests import FlowTestCase
|
||||||
|
from authentik.flows.views.executor import SESSION_KEY_PLAN
|
||||||
from authentik.lib.generators import generate_id
|
from authentik.lib.generators import generate_id
|
||||||
from authentik.stages.authenticator_sms.models import (
|
from authentik.stages.authenticator_sms.models import (
|
||||||
AuthenticatorSMSStage,
|
AuthenticatorSMSStage,
|
||||||
|
@ -15,7 +17,8 @@ from authentik.stages.authenticator_sms.models import (
|
||||||
SMSProviders,
|
SMSProviders,
|
||||||
hash_phone_number,
|
hash_phone_number,
|
||||||
)
|
)
|
||||||
from authentik.stages.authenticator_sms.stage import SESSION_KEY_SMS_DEVICE
|
from authentik.stages.authenticator_sms.stage import PLAN_CONTEXT_PHONE, SESSION_KEY_SMS_DEVICE
|
||||||
|
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
|
||||||
|
|
||||||
|
|
||||||
class AuthenticatorSMSStageTests(FlowTestCase):
|
class AuthenticatorSMSStageTests(FlowTestCase):
|
||||||
|
@ -172,6 +175,45 @@ class AuthenticatorSMSStageTests(FlowTestCase):
|
||||||
phone_number_required=False,
|
phone_number_required=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_stage_context_data_duplicate(self):
|
||||||
|
"""test stage context data (phone number exists already)"""
|
||||||
|
self.client.get(
|
||||||
|
reverse("authentik_flows:configure", kwargs={"stage_uuid": self.stage.stage_uuid}),
|
||||||
|
)
|
||||||
|
plan: FlowPlan = self.client.session[SESSION_KEY_PLAN]
|
||||||
|
plan.context[PLAN_CONTEXT_PROMPT] = {
|
||||||
|
PLAN_CONTEXT_PHONE: "1234",
|
||||||
|
}
|
||||||
|
session = self.client.session
|
||||||
|
session[SESSION_KEY_PLAN] = plan
|
||||||
|
session.save()
|
||||||
|
SMSDevice.objects.create(
|
||||||
|
phone_number="1234",
|
||||||
|
user=self.user,
|
||||||
|
stage=self.stage,
|
||||||
|
)
|
||||||
|
sms_send_mock = MagicMock()
|
||||||
|
with (
|
||||||
|
patch(
|
||||||
|
"authentik.stages.authenticator_sms.models.AuthenticatorSMSStage.send",
|
||||||
|
sms_send_mock,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
print(self.client.session[SESSION_KEY_PLAN])
|
||||||
|
response = self.client.get(
|
||||||
|
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
|
||||||
|
)
|
||||||
|
print(response.content.decode())
|
||||||
|
self.assertStageResponse(
|
||||||
|
response,
|
||||||
|
self.flow,
|
||||||
|
self.user,
|
||||||
|
component="ak-stage-authenticator-sms",
|
||||||
|
phone_number_required=True,
|
||||||
|
)
|
||||||
|
plan: FlowPlan = self.client.session[SESSION_KEY_PLAN]
|
||||||
|
self.assertEqual(plan.context[PLAN_CONTEXT_PROMPT], {})
|
||||||
|
|
||||||
def test_stage_submit_full(self):
|
def test_stage_submit_full(self):
|
||||||
"""test stage (submit)"""
|
"""test stage (submit)"""
|
||||||
self.client.get(
|
self.client.get(
|
||||||
|
|
Reference in a new issue