stages/authenticator_sms: fix code not being sent when phone_number is in context
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
parent
43854dc828
commit
5736a1542c
|
@ -76,13 +76,17 @@ class AuthenticatorSMSStage(ConfigurableStage, Stage):
|
||||||
return self.send_generic(token, device)
|
return self.send_generic(token, device)
|
||||||
raise ValueError(f"invalid provider {self.provider}")
|
raise ValueError(f"invalid provider {self.provider}")
|
||||||
|
|
||||||
|
def get_message(self, token: str) -> str:
|
||||||
|
"""Get SMS message"""
|
||||||
|
return _("Use this code to authenticate in authentik: %(token)s" % {"token": token})
|
||||||
|
|
||||||
def send_twilio(self, token: str, device: "SMSDevice"):
|
def send_twilio(self, token: str, device: "SMSDevice"):
|
||||||
"""send sms via twilio provider"""
|
"""send sms via twilio provider"""
|
||||||
client = Client(self.account_sid, self.auth)
|
client = Client(self.account_sid, self.auth)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
message = client.messages.create(
|
message = client.messages.create(
|
||||||
to=device.phone_number, from_=self.from_number, body=token
|
to=device.phone_number, from_=self.from_number, body=self.get_message(token)
|
||||||
)
|
)
|
||||||
LOGGER.debug("Sent SMS", to=device, message=message.sid)
|
LOGGER.debug("Sent SMS", to=device, message=message.sid)
|
||||||
except TwilioRestException as exc:
|
except TwilioRestException as exc:
|
||||||
|
@ -95,6 +99,7 @@ class AuthenticatorSMSStage(ConfigurableStage, Stage):
|
||||||
"From": self.from_number,
|
"From": self.from_number,
|
||||||
"To": device.phone_number,
|
"To": device.phone_number,
|
||||||
"Body": token,
|
"Body": token,
|
||||||
|
"Message": self.get_message(token),
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.mapping:
|
if self.mapping:
|
||||||
|
|
|
@ -12,6 +12,7 @@ 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
|
||||||
|
@ -46,15 +47,9 @@ class AuthenticatorSMSChallengeResponse(ChallengeResponse):
|
||||||
|
|
||||||
def validate(self, attrs: dict) -> dict:
|
def validate(self, attrs: dict) -> dict:
|
||||||
"""Check"""
|
"""Check"""
|
||||||
stage: AuthenticatorSMSStage = self.device.stage
|
|
||||||
if "code" not in attrs:
|
if "code" not in attrs:
|
||||||
self.device.phone_number = attrs["phone_number"]
|
self.device.phone_number = attrs["phone_number"]
|
||||||
hashed_number = hash_phone_number(self.device.phone_number)
|
self.stage.validate_and_send(attrs["phone_number"])
|
||||||
query = Q(phone_number=hashed_number) | Q(phone_number=self.device.phone_number)
|
|
||||||
if SMSDevice.objects.filter(query, stage=self.stage.executor.current_stage.pk).exists():
|
|
||||||
raise ValidationError(_("Invalid phone number"))
|
|
||||||
# No code yet, but we have a phone number, so send a verification message
|
|
||||||
stage.send(self.device.token, self.device)
|
|
||||||
return super().validate(attrs)
|
return super().validate(attrs)
|
||||||
if not self.device.verify_token(str(attrs["code"])):
|
if not self.device.verify_token(str(attrs["code"])):
|
||||||
raise ValidationError(_("Code does not match"))
|
raise ValidationError(_("Code does not match"))
|
||||||
|
@ -67,6 +62,17 @@ class AuthenticatorSMSStageView(ChallengeStageView):
|
||||||
|
|
||||||
response_class = AuthenticatorSMSChallengeResponse
|
response_class = AuthenticatorSMSChallengeResponse
|
||||||
|
|
||||||
|
def validate_and_send(self, phone_number: str):
|
||||||
|
"""Validate phone number and send message"""
|
||||||
|
stage: AuthenticatorSMSStage = self.executor.current_stage
|
||||||
|
hashed_number = hash_phone_number(phone_number)
|
||||||
|
query = Q(phone_number=hashed_number) | Q(phone_number=phone_number)
|
||||||
|
if SMSDevice.objects.filter(query, stage=stage.pk).exists():
|
||||||
|
raise ValidationError(_("Invalid phone number"))
|
||||||
|
# No code yet, but we have a phone number, so send a verification message
|
||||||
|
device: SMSDevice = self.request.session[SESSION_KEY_SMS_DEVICE]
|
||||||
|
stage.send(device.token, device)
|
||||||
|
|
||||||
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 "phone" in context.get(PLAN_CONTEXT_PROMPT, {}):
|
||||||
|
@ -96,19 +102,21 @@ class AuthenticatorSMSStageView(ChallengeStageView):
|
||||||
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||||
user = self.get_pending_user()
|
user = self.get_pending_user()
|
||||||
|
|
||||||
# Currently, this stage only supports one device per user. If the user already
|
|
||||||
# has a device, just skip to the next stage
|
|
||||||
if SMSDevice.objects.filter(user=user).exists():
|
|
||||||
return self.executor.stage_ok()
|
|
||||||
|
|
||||||
stage: AuthenticatorSMSStage = self.executor.current_stage
|
stage: AuthenticatorSMSStage = self.executor.current_stage
|
||||||
|
|
||||||
if SESSION_KEY_SMS_DEVICE not in self.request.session:
|
if SESSION_KEY_SMS_DEVICE not in self.request.session:
|
||||||
device = SMSDevice(user=user, confirmed=False, stage=stage, name="SMS Device")
|
device = SMSDevice(user=user, confirmed=False, stage=stage, name="SMS Device")
|
||||||
device.generate_token(commit=False)
|
device.generate_token(commit=False)
|
||||||
|
self.request.session[SESSION_KEY_SMS_DEVICE] = device
|
||||||
if phone_number := self._has_phone_number():
|
if phone_number := self._has_phone_number():
|
||||||
device.phone_number = phone_number
|
device.phone_number = phone_number
|
||||||
self.request.session[SESSION_KEY_SMS_DEVICE] = device
|
try:
|
||||||
|
self.validate_and_send(phone_number)
|
||||||
|
except ValidationError as exc:
|
||||||
|
response = AuthenticatorSMSChallengeResponse()
|
||||||
|
response._errors.setdefault("phone_number", [])
|
||||||
|
response._errors["phone_number"].append(ErrorDetailSerializer(exc.detail))
|
||||||
|
return self.challenge_invalid(response)
|
||||||
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:
|
||||||
|
|
|
@ -80,6 +80,39 @@ class AuthenticatorSMSStageTests(FlowTestCase):
|
||||||
phone_number_required=False,
|
phone_number_required=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_stage_context_data(self):
|
||||||
|
"""test stage context data"""
|
||||||
|
self.client.get(
|
||||||
|
reverse("authentik_flows:configure", kwargs={"stage_uuid": self.stage.stage_uuid}),
|
||||||
|
)
|
||||||
|
sms_send_mock = MagicMock()
|
||||||
|
with (
|
||||||
|
patch(
|
||||||
|
(
|
||||||
|
"authentik.stages.authenticator_sms.stage."
|
||||||
|
"AuthenticatorSMSStageView._has_phone_number"
|
||||||
|
),
|
||||||
|
MagicMock(
|
||||||
|
return_value="1234",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
patch(
|
||||||
|
"authentik.stages.authenticator_sms.models.AuthenticatorSMSStage.send",
|
||||||
|
sms_send_mock,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
response = self.client.get(
|
||||||
|
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
|
||||||
|
)
|
||||||
|
sms_send_mock.assert_called_once()
|
||||||
|
self.assertStageResponse(
|
||||||
|
response,
|
||||||
|
self.flow,
|
||||||
|
self.user,
|
||||||
|
component="ak-stage-authenticator-sms",
|
||||||
|
phone_number_required=False,
|
||||||
|
)
|
||||||
|
|
||||||
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 New Issue