prompt: We are currently solving the following issue within our repository. Here is the issue text:
  --- BEGIN ISSUE ---
  Bug: Authenticator SMS Challenge response doesn't have _errors attribute
  **Describe the bug**
  It seems that something has changed in the regards to Authenticator SMS Challenge Response.
  
  **To Reproduce**
  Steps to reproduce the behavior:
  1. Have an perfectly working SMS Authenicator stage prior to upgrading to 2023.8.3
  2. Upgrade
  3. Don't see any immediate errors.
  4. See error when user tries to register using said flow.
  
  **Expected behavior**
  I would have expected it to not error out.
  
  **Logs**
  <details>
      <summary>Stacktrace from authentik</summary>
  
  ```
  Traceback (most recent call last):
    File "/authentik/flows/views/executor.py", line 298, in get
      stage_response = self.current_stage_view.get(request, *args, **kwargs)
                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    File "/authentik/stages/authenticator_sms/stage.py", line 117, in get
      response._errors.setdefault("phone_number", [])
      ^^^^^^^^^^^^^^^^
  builtins.AttributeError: 'AuthenticatorSMSChallengeResponse' object has no attribute '_errors'
  ```
  </details>
  
  
  **Version and Deployment (please complete the following information):**
  - authentik version: 2023.8.3
  - Deployment: Ansible with Docker
          
  
  --- END ISSUE ---
  
  Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
  --- BEGIN FILES ---
  Path: `authentik/stages/authenticator_sms/stage.py`
  Content:
  ```
  1 """SMS Setup stage"""
  2 from typing import Optional
  3 
  4 from django.db.models import Q
  5 from django.http import HttpRequest, HttpResponse
  6 from django.http.request import QueryDict
  7 from django.utils.translation import gettext_lazy as _
  8 from rest_framework.exceptions import ValidationError
  9 from rest_framework.fields import BooleanField, CharField, IntegerField
  10 
  11 from authentik.flows.challenge import (
  12     Challenge,
  13     ChallengeResponse,
  14     ChallengeTypes,
  15     ErrorDetailSerializer,
  16     WithUserInfoChallenge,
  17 )
  18 from authentik.flows.stage import ChallengeStageView
  19 from authentik.stages.authenticator_sms.models import (
  20     AuthenticatorSMSStage,
  21     SMSDevice,
  22     hash_phone_number,
  23 )
  24 from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
  25 
  26 SESSION_KEY_SMS_DEVICE = "authentik/stages/authenticator_sms/sms_device"
  27 
  28 
  29 class AuthenticatorSMSChallenge(WithUserInfoChallenge):
  30     """SMS Setup challenge"""
  31 
  32     # Set to true if no previous prompt stage set the phone number
  33     # this stage will also check prompt_data.phone
  34     phone_number_required = BooleanField(default=True)
  35     component = CharField(default="ak-stage-authenticator-sms")
  36 
  37 
  38 class AuthenticatorSMSChallengeResponse(ChallengeResponse):
  39     """SMS Challenge response, device is set by get_response_instance"""
  40 
  41     device: SMSDevice
  42 
  43     code = IntegerField(required=False)
  44     phone_number = CharField(required=False)
  45 
  46     component = CharField(default="ak-stage-authenticator-sms")
  47 
  48     def validate(self, attrs: dict) -> dict:
  49         """Check"""
  50         if "code" not in attrs:
  51             self.device.phone_number = attrs["phone_number"]
  52             self.stage.validate_and_send(attrs["phone_number"])
  53             return super().validate(attrs)
  54         if not self.device.verify_token(str(attrs["code"])):
  55             raise ValidationError(_("Code does not match"))
  56         self.device.confirmed = True
  57         return super().validate(attrs)
  58 
  59 
  60 class AuthenticatorSMSStageView(ChallengeStageView):
  61     """OTP sms Setup stage"""
  62 
  63     response_class = AuthenticatorSMSChallengeResponse
  64 
  65     def validate_and_send(self, phone_number: str):
  66         """Validate phone number and send message"""
  67         stage: AuthenticatorSMSStage = self.executor.current_stage
  68         hashed_number = hash_phone_number(phone_number)
  69         query = Q(phone_number=hashed_number) | Q(phone_number=phone_number)
  70         if SMSDevice.objects.filter(query, stage=stage.pk).exists():
  71             raise ValidationError(_("Invalid phone number"))
  72         # No code yet, but we have a phone number, so send a verification message
  73         device: SMSDevice = self.request.session[SESSION_KEY_SMS_DEVICE]
  74         stage.send(device.token, device)
  75 
  76     def _has_phone_number(self) -> Optional[str]:
  77         context = self.executor.plan.context
  78         if "phone" in context.get(PLAN_CONTEXT_PROMPT, {}):
  79             self.logger.debug("got phone number from plan context")
  80             return context.get(PLAN_CONTEXT_PROMPT, {}).get("phone")
  81         if SESSION_KEY_SMS_DEVICE in self.request.session:
  82             self.logger.debug("got phone number from device in session")
  83             device: SMSDevice = self.request.session[SESSION_KEY_SMS_DEVICE]
  84             if device.phone_number == "":
  85                 return None
  86             return device.phone_number
  87         return None
  88 
  89     def get_challenge(self, *args, **kwargs) -> Challenge:
  90         return AuthenticatorSMSChallenge(
  91             data={
  92                 "type": ChallengeTypes.NATIVE.value,
  93                 "phone_number_required": self._has_phone_number() is None,
  94             }
  95         )
  96 
  97     def get_response_instance(self, data: QueryDict) -> ChallengeResponse:
  98         response = super().get_response_instance(data)
  99         response.device = self.request.session[SESSION_KEY_SMS_DEVICE]
  100         return response
  101 
  102     def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
  103         user = self.get_pending_user()
  104 
  105         stage: AuthenticatorSMSStage = self.executor.current_stage
  106 
  107         if SESSION_KEY_SMS_DEVICE not in self.request.session:
  108             device = SMSDevice(user=user, confirmed=False, stage=stage, name="SMS Device")
  109             device.generate_token(commit=False)
  110             self.request.session[SESSION_KEY_SMS_DEVICE] = device
  111             if phone_number := self._has_phone_number():
  112                 device.phone_number = phone_number
  113                 try:
  114                     self.validate_and_send(phone_number)
  115                 except ValidationError as exc:
  116                     response = AuthenticatorSMSChallengeResponse()
  117                     response._errors.setdefault("phone_number", [])
  118                     response._errors["phone_number"].append(ErrorDetailSerializer(exc.detail))
  119                     return self.challenge_invalid(response)
  120         return super().get(request, *args, **kwargs)
  121 
  122     def challenge_valid(self, response: ChallengeResponse) -> HttpResponse:
  123         """SMS Token is validated by challenge"""
  124         device: SMSDevice = self.request.session[SESSION_KEY_SMS_DEVICE]
  125         if not device.confirmed:
  126             return self.challenge_invalid(response)
  127         stage: AuthenticatorSMSStage = self.executor.current_stage
  128         if stage.verify_only:
  129             self.logger.debug("Hashing number on device")
  130             device.set_hashed_number()
  131         device.save()
  132         del self.request.session[SESSION_KEY_SMS_DEVICE]
  133         return self.executor.stage_ok()
  134
  ```
  --- END FILES ---
  
  Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.
  
  Here is an example:
  
  ```diff
  diff --git a/examples/server_async.py b/examples/server_async.py
  --- a/examples/server_async.py
  +++ b/examples/server_async.py
  @@ -313,4 +313,4 @@
   
   
   if __name__ == "__main__":
  -    asyncio.run(run_async_server("."), debug=True)
  +    asyncio.run(run_async_server(), debug=True)
  diff --git a/examples/server_sync.py b/examples/server_sync.py
  --- a/examples/server_sync.py
  +++ b/examples/server_sync.py
  @@ -313,5 +313,5 @@
   
   
   if __name__ == "__main__":
  -    server = run_sync_server(".")
  +    server = run_sync_server()
       server.shutdown()
  
  ```
  
golden_diff: diff --git a/authentik/stages/authenticator_sms/stage.py b/authentik/stages/authenticator_sms/stage.py
  --- a/authentik/stages/authenticator_sms/stage.py
  +++ b/authentik/stages/authenticator_sms/stage.py
  @@ -12,7 +12,6 @@
       Challenge,
       ChallengeResponse,
       ChallengeTypes,
  -    ErrorDetailSerializer,
       WithUserInfoChallenge,
   )
   from authentik.flows.stage import ChallengeStageView
  @@ -24,6 +23,7 @@
   from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
   
   SESSION_KEY_SMS_DEVICE = "authentik/stages/authenticator_sms/sms_device"
  +PLAN_CONTEXT_PHONE = "phone"
   
   
   class AuthenticatorSMSChallenge(WithUserInfoChallenge):
  @@ -48,6 +48,8 @@
       def validate(self, attrs: dict) -> dict:
           """Check"""
           if "code" not in attrs:
  +            if "phone_number" not in attrs:
  +                raise ValidationError("phone_number required")
               self.device.phone_number = attrs["phone_number"]
               self.stage.validate_and_send(attrs["phone_number"])
               return super().validate(attrs)
  @@ -67,6 +69,7 @@
           stage: AuthenticatorSMSStage = self.executor.current_stage
           hashed_number = hash_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():
               raise ValidationError(_("Invalid phone number"))
           # No code yet, but we have a phone number, so send a verification message
  @@ -75,9 +78,9 @@
   
       def _has_phone_number(self) -> Optional[str]:
           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")
  -            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:
               self.logger.debug("got phone number from device in session")
               device: SMSDevice = self.request.session[SESSION_KEY_SMS_DEVICE]
  @@ -113,10 +116,17 @@
                   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)
  +                    # We had a phone number given already (at this point only possible from flow
  +                    # context), but an error occurred while sending a number (most likely)
  +                    # due to a duplicate device, so delete the number we got given, reset the state
  +                    # (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)
   
       def challenge_valid(self, response: ChallengeResponse) -> HttpResponse:
  
verification_info: {"golden_diff": "diff --git a/authentik/stages/authenticator_sms/stage.py b/authentik/stages/authenticator_sms/stage.py\n--- a/authentik/stages/authenticator_sms/stage.py\n+++ b/authentik/stages/authenticator_sms/stage.py\n@@ -12,7 +12,6 @@\n     Challenge,\n     ChallengeResponse,\n     ChallengeTypes,\n-    ErrorDetailSerializer,\n     WithUserInfoChallenge,\n )\n from authentik.flows.stage import ChallengeStageView\n@@ -24,6 +23,7 @@\n from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT\n \n SESSION_KEY_SMS_DEVICE = \"authentik/stages/authenticator_sms/sms_device\"\n+PLAN_CONTEXT_PHONE = \"phone\"\n \n \n class AuthenticatorSMSChallenge(WithUserInfoChallenge):\n@@ -48,6 +48,8 @@\n     def validate(self, attrs: dict) -> dict:\n         \"\"\"Check\"\"\"\n         if \"code\" not in attrs:\n+            if \"phone_number\" not in attrs:\n+                raise ValidationError(\"phone_number required\")\n             self.device.phone_number = attrs[\"phone_number\"]\n             self.stage.validate_and_send(attrs[\"phone_number\"])\n             return super().validate(attrs)\n@@ -67,6 +69,7 @@\n         stage: AuthenticatorSMSStage = self.executor.current_stage\n         hashed_number = hash_phone_number(phone_number)\n         query = Q(phone_number=hashed_number) | Q(phone_number=phone_number)\n+        print(SMSDevice.objects.filter(query, stage=stage.pk))\n         if SMSDevice.objects.filter(query, stage=stage.pk).exists():\n             raise ValidationError(_(\"Invalid phone number\"))\n         # No code yet, but we have a phone number, so send a verification message\n@@ -75,9 +78,9 @@\n \n     def _has_phone_number(self) -> Optional[str]:\n         context = self.executor.plan.context\n-        if \"phone\" in context.get(PLAN_CONTEXT_PROMPT, {}):\n+        if PLAN_CONTEXT_PHONE in context.get(PLAN_CONTEXT_PROMPT, {}):\n             self.logger.debug(\"got phone number from plan context\")\n-            return context.get(PLAN_CONTEXT_PROMPT, {}).get(\"phone\")\n+            return context.get(PLAN_CONTEXT_PROMPT, {}).get(PLAN_CONTEXT_PHONE)\n         if SESSION_KEY_SMS_DEVICE in self.request.session:\n             self.logger.debug(\"got phone number from device in session\")\n             device: SMSDevice = self.request.session[SESSION_KEY_SMS_DEVICE]\n@@ -113,10 +116,17 @@\n                 try:\n                     self.validate_and_send(phone_number)\n                 except ValidationError as exc:\n-                    response = AuthenticatorSMSChallengeResponse()\n-                    response._errors.setdefault(\"phone_number\", [])\n-                    response._errors[\"phone_number\"].append(ErrorDetailSerializer(exc.detail))\n-                    return self.challenge_invalid(response)\n+                    # We had a phone number given already (at this point only possible from flow\n+                    # context), but an error occurred while sending a number (most likely)\n+                    # due to a duplicate device, so delete the number we got given, reset the state\n+                    # (ish) and retry\n+                    device.phone_number = \"\"\n+                    self.executor.plan.context.get(PLAN_CONTEXT_PROMPT, {}).pop(\n+                        PLAN_CONTEXT_PHONE, None\n+                    )\n+                    self.request.session.pop(SESSION_KEY_SMS_DEVICE, None)\n+                    self.logger.warning(\"failed to send SMS message to pre-set number\", exc=exc)\n+                    return self.get(request, *args, **kwargs)\n         return super().get(request, *args, **kwargs)\n \n     def challenge_valid(self, response: ChallengeResponse) -> HttpResponse:\n", "issue": "Bug: Authenticator SMS Challenge response doesn't have _errors attribute\n**Describe the bug**\r\nIt seems that something has changed in the regards to Authenticator SMS Challenge Response.\r\n\r\n**To Reproduce**\r\nSteps to reproduce the behavior:\r\n1. Have an perfectly working SMS Authenicator stage prior to upgrading to 2023.8.3\r\n2. Upgrade\r\n3. Don't see any immediate errors.\r\n4. See error when user tries to register using said flow.\r\n\r\n**Expected behavior**\r\nI would have expected it to not error out.\r\n\r\n**Logs**\r\n<details>\r\n    <summary>Stacktrace from authentik</summary>\r\n\r\n```\r\nTraceback (most recent call last):\r\n  File \"/authentik/flows/views/executor.py\", line 298, in get\r\n    stage_response = self.current_stage_view.get(request, *args, **kwargs)\r\n                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\r\n  File \"/authentik/stages/authenticator_sms/stage.py\", line 117, in get\r\n    response._errors.setdefault(\"phone_number\", [])\r\n    ^^^^^^^^^^^^^^^^\r\nbuiltins.AttributeError: 'AuthenticatorSMSChallengeResponse' object has no attribute '_errors'\r\n```\r\n</details>\r\n\r\n\r\n**Version and Deployment (please complete the following information):**\r\n- authentik version: 2023.8.3\r\n- Deployment: Ansible with Docker\r\n        \n", "before_files": [{"content": "\"\"\"SMS Setup stage\"\"\"\nfrom typing import Optional\n\nfrom django.db.models import Q\nfrom django.http import HttpRequest, HttpResponse\nfrom django.http.request import QueryDict\nfrom django.utils.translation import gettext_lazy as _\nfrom rest_framework.exceptions import ValidationError\nfrom rest_framework.fields import BooleanField, CharField, IntegerField\n\nfrom authentik.flows.challenge import (\n    Challenge,\n    ChallengeResponse,\n    ChallengeTypes,\n    ErrorDetailSerializer,\n    WithUserInfoChallenge,\n)\nfrom authentik.flows.stage import ChallengeStageView\nfrom authentik.stages.authenticator_sms.models import (\n    AuthenticatorSMSStage,\n    SMSDevice,\n    hash_phone_number,\n)\nfrom authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT\n\nSESSION_KEY_SMS_DEVICE = \"authentik/stages/authenticator_sms/sms_device\"\n\n\nclass AuthenticatorSMSChallenge(WithUserInfoChallenge):\n    \"\"\"SMS Setup challenge\"\"\"\n\n    # Set to true if no previous prompt stage set the phone number\n    # this stage will also check prompt_data.phone\n    phone_number_required = BooleanField(default=True)\n    component = CharField(default=\"ak-stage-authenticator-sms\")\n\n\nclass AuthenticatorSMSChallengeResponse(ChallengeResponse):\n    \"\"\"SMS Challenge response, device is set by get_response_instance\"\"\"\n\n    device: SMSDevice\n\n    code = IntegerField(required=False)\n    phone_number = CharField(required=False)\n\n    component = CharField(default=\"ak-stage-authenticator-sms\")\n\n    def validate(self, attrs: dict) -> dict:\n        \"\"\"Check\"\"\"\n        if \"code\" not in attrs:\n            self.device.phone_number = attrs[\"phone_number\"]\n            self.stage.validate_and_send(attrs[\"phone_number\"])\n            return super().validate(attrs)\n        if not self.device.verify_token(str(attrs[\"code\"])):\n            raise ValidationError(_(\"Code does not match\"))\n        self.device.confirmed = True\n        return super().validate(attrs)\n\n\nclass AuthenticatorSMSStageView(ChallengeStageView):\n    \"\"\"OTP sms Setup stage\"\"\"\n\n    response_class = AuthenticatorSMSChallengeResponse\n\n    def validate_and_send(self, phone_number: str):\n        \"\"\"Validate phone number and send message\"\"\"\n        stage: AuthenticatorSMSStage = self.executor.current_stage\n        hashed_number = hash_phone_number(phone_number)\n        query = Q(phone_number=hashed_number) | Q(phone_number=phone_number)\n        if SMSDevice.objects.filter(query, stage=stage.pk).exists():\n            raise ValidationError(_(\"Invalid phone number\"))\n        # No code yet, but we have a phone number, so send a verification message\n        device: SMSDevice = self.request.session[SESSION_KEY_SMS_DEVICE]\n        stage.send(device.token, device)\n\n    def _has_phone_number(self) -> Optional[str]:\n        context = self.executor.plan.context\n        if \"phone\" in context.get(PLAN_CONTEXT_PROMPT, {}):\n            self.logger.debug(\"got phone number from plan context\")\n            return context.get(PLAN_CONTEXT_PROMPT, {}).get(\"phone\")\n        if SESSION_KEY_SMS_DEVICE in self.request.session:\n            self.logger.debug(\"got phone number from device in session\")\n            device: SMSDevice = self.request.session[SESSION_KEY_SMS_DEVICE]\n            if device.phone_number == \"\":\n                return None\n            return device.phone_number\n        return None\n\n    def get_challenge(self, *args, **kwargs) -> Challenge:\n        return AuthenticatorSMSChallenge(\n            data={\n                \"type\": ChallengeTypes.NATIVE.value,\n                \"phone_number_required\": self._has_phone_number() is None,\n            }\n        )\n\n    def get_response_instance(self, data: QueryDict) -> ChallengeResponse:\n        response = super().get_response_instance(data)\n        response.device = self.request.session[SESSION_KEY_SMS_DEVICE]\n        return response\n\n    def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:\n        user = self.get_pending_user()\n\n        stage: AuthenticatorSMSStage = self.executor.current_stage\n\n        if SESSION_KEY_SMS_DEVICE not in self.request.session:\n            device = SMSDevice(user=user, confirmed=False, stage=stage, name=\"SMS Device\")\n            device.generate_token(commit=False)\n            self.request.session[SESSION_KEY_SMS_DEVICE] = device\n            if phone_number := self._has_phone_number():\n                device.phone_number = phone_number\n                try:\n                    self.validate_and_send(phone_number)\n                except ValidationError as exc:\n                    response = AuthenticatorSMSChallengeResponse()\n                    response._errors.setdefault(\"phone_number\", [])\n                    response._errors[\"phone_number\"].append(ErrorDetailSerializer(exc.detail))\n                    return self.challenge_invalid(response)\n        return super().get(request, *args, **kwargs)\n\n    def challenge_valid(self, response: ChallengeResponse) -> HttpResponse:\n        \"\"\"SMS Token is validated by challenge\"\"\"\n        device: SMSDevice = self.request.session[SESSION_KEY_SMS_DEVICE]\n        if not device.confirmed:\n            return self.challenge_invalid(response)\n        stage: AuthenticatorSMSStage = self.executor.current_stage\n        if stage.verify_only:\n            self.logger.debug(\"Hashing number on device\")\n            device.set_hashed_number()\n        device.save()\n        del self.request.session[SESSION_KEY_SMS_DEVICE]\n        return self.executor.stage_ok()\n", "path": "authentik/stages/authenticator_sms/stage.py"}], "after_files": [{"content": "\"\"\"SMS Setup stage\"\"\"\nfrom typing import Optional\n\nfrom django.db.models import Q\nfrom django.http import HttpRequest, HttpResponse\nfrom django.http.request import QueryDict\nfrom django.utils.translation import gettext_lazy as _\nfrom rest_framework.exceptions import ValidationError\nfrom rest_framework.fields import BooleanField, CharField, IntegerField\n\nfrom authentik.flows.challenge import (\n    Challenge,\n    ChallengeResponse,\n    ChallengeTypes,\n    WithUserInfoChallenge,\n)\nfrom authentik.flows.stage import ChallengeStageView\nfrom authentik.stages.authenticator_sms.models import (\n    AuthenticatorSMSStage,\n    SMSDevice,\n    hash_phone_number,\n)\nfrom authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT\n\nSESSION_KEY_SMS_DEVICE = \"authentik/stages/authenticator_sms/sms_device\"\nPLAN_CONTEXT_PHONE = \"phone\"\n\n\nclass AuthenticatorSMSChallenge(WithUserInfoChallenge):\n    \"\"\"SMS Setup challenge\"\"\"\n\n    # Set to true if no previous prompt stage set the phone number\n    # this stage will also check prompt_data.phone\n    phone_number_required = BooleanField(default=True)\n    component = CharField(default=\"ak-stage-authenticator-sms\")\n\n\nclass AuthenticatorSMSChallengeResponse(ChallengeResponse):\n    \"\"\"SMS Challenge response, device is set by get_response_instance\"\"\"\n\n    device: SMSDevice\n\n    code = IntegerField(required=False)\n    phone_number = CharField(required=False)\n\n    component = CharField(default=\"ak-stage-authenticator-sms\")\n\n    def validate(self, attrs: dict) -> dict:\n        \"\"\"Check\"\"\"\n        if \"code\" not in attrs:\n            if \"phone_number\" not in attrs:\n                raise ValidationError(\"phone_number required\")\n            self.device.phone_number = attrs[\"phone_number\"]\n            self.stage.validate_and_send(attrs[\"phone_number\"])\n            return super().validate(attrs)\n        if not self.device.verify_token(str(attrs[\"code\"])):\n            raise ValidationError(_(\"Code does not match\"))\n        self.device.confirmed = True\n        return super().validate(attrs)\n\n\nclass AuthenticatorSMSStageView(ChallengeStageView):\n    \"\"\"OTP sms Setup stage\"\"\"\n\n    response_class = AuthenticatorSMSChallengeResponse\n\n    def validate_and_send(self, phone_number: str):\n        \"\"\"Validate phone number and send message\"\"\"\n        stage: AuthenticatorSMSStage = self.executor.current_stage\n        hashed_number = hash_phone_number(phone_number)\n        query = Q(phone_number=hashed_number) | Q(phone_number=phone_number)\n        print(SMSDevice.objects.filter(query, stage=stage.pk))\n        if SMSDevice.objects.filter(query, stage=stage.pk).exists():\n            raise ValidationError(_(\"Invalid phone number\"))\n        # No code yet, but we have a phone number, so send a verification message\n        device: SMSDevice = self.request.session[SESSION_KEY_SMS_DEVICE]\n        stage.send(device.token, device)\n\n    def _has_phone_number(self) -> Optional[str]:\n        context = self.executor.plan.context\n        if PLAN_CONTEXT_PHONE in context.get(PLAN_CONTEXT_PROMPT, {}):\n            self.logger.debug(\"got phone number from plan context\")\n            return context.get(PLAN_CONTEXT_PROMPT, {}).get(PLAN_CONTEXT_PHONE)\n        if SESSION_KEY_SMS_DEVICE in self.request.session:\n            self.logger.debug(\"got phone number from device in session\")\n            device: SMSDevice = self.request.session[SESSION_KEY_SMS_DEVICE]\n            if device.phone_number == \"\":\n                return None\n            return device.phone_number\n        return None\n\n    def get_challenge(self, *args, **kwargs) -> Challenge:\n        return AuthenticatorSMSChallenge(\n            data={\n                \"type\": ChallengeTypes.NATIVE.value,\n                \"phone_number_required\": self._has_phone_number() is None,\n            }\n        )\n\n    def get_response_instance(self, data: QueryDict) -> ChallengeResponse:\n        response = super().get_response_instance(data)\n        response.device = self.request.session[SESSION_KEY_SMS_DEVICE]\n        return response\n\n    def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:\n        user = self.get_pending_user()\n\n        stage: AuthenticatorSMSStage = self.executor.current_stage\n\n        if SESSION_KEY_SMS_DEVICE not in self.request.session:\n            device = SMSDevice(user=user, confirmed=False, stage=stage, name=\"SMS Device\")\n            device.generate_token(commit=False)\n            self.request.session[SESSION_KEY_SMS_DEVICE] = device\n            if phone_number := self._has_phone_number():\n                device.phone_number = phone_number\n                try:\n                    self.validate_and_send(phone_number)\n                except ValidationError as exc:\n                    # We had a phone number given already (at this point only possible from flow\n                    # context), but an error occurred while sending a number (most likely)\n                    # due to a duplicate device, so delete the number we got given, reset the state\n                    # (ish) and retry\n                    device.phone_number = \"\"\n                    self.executor.plan.context.get(PLAN_CONTEXT_PROMPT, {}).pop(\n                        PLAN_CONTEXT_PHONE, None\n                    )\n                    self.request.session.pop(SESSION_KEY_SMS_DEVICE, None)\n                    self.logger.warning(\"failed to send SMS message to pre-set number\", exc=exc)\n                    return self.get(request, *args, **kwargs)\n        return super().get(request, *args, **kwargs)\n\n    def challenge_valid(self, response: ChallengeResponse) -> HttpResponse:\n        \"\"\"SMS Token is validated by challenge\"\"\"\n        device: SMSDevice = self.request.session[SESSION_KEY_SMS_DEVICE]\n        if not device.confirmed:\n            return self.challenge_invalid(response)\n        stage: AuthenticatorSMSStage = self.executor.current_stage\n        if stage.verify_only:\n            self.logger.debug(\"Hashing number on device\")\n            device.set_hashed_number()\n        device.save()\n        del self.request.session[SESSION_KEY_SMS_DEVICE]\n        return self.executor.stage_ok()\n", "path": "authentik/stages/authenticator_sms/stage.py"}]}