From 634375c43f0e23b9fd5ac6da07f3969075b5f16d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Skyler=20M=C3=A4ntysaari?= Date: Thu, 14 Oct 2021 11:13:39 +0300 Subject: [PATCH] stages/authenticator_sms: add generic provider (#1595) * stages/sms: New SMS provider, aka wrapper for outside API * web/pages/authenicator_sms: Conditionally show options based on provider. * stages/authenicator_sms: Fixing up the model. * Whoops * stages/authenicator_sms: Adding supported auth types for Generic provider. * web/pages/stages/authenicator_sms: Added auth type for generic provider * web/pages/stages/authenicator_sms: Fixing up my generic provider options. * stages/authenicator/sms: Working version of generic provider. * stages/authenicator/sms: Cleanup and creating an event on error. * web/ages/stages/authenicator_sms: Made a default for Auth Type and cleaned up the non-needed name attribute. * stages/authenicator_validate: Fixing up the migration as it had no SMS. * stages/authenicator_sms: Removd non-needed migration and better error code handling. * stages/authenicator_sms: Removd non-needed migration and better error code handling. * web/pages/stages/authenicator_sms: Provider default is not empty anymore. Signed-off-by: Jens Langhammer --- authentik/stages/authenticator_sms/api.py | 6 +- .../migrations/0003_auto_20211014_0813.py | 38 ++++++ authentik/stages/authenticator_sms/models.py | 69 +++++++++- schema.yml | 76 ++++++++--- .../AuthenticatorSMSStageForm.ts | 128 ++++++++++++++++-- 5 files changed, 283 insertions(+), 34 deletions(-) create mode 100644 authentik/stages/authenticator_sms/migrations/0003_auto_20211014_0813.py diff --git a/authentik/stages/authenticator_sms/api.py b/authentik/stages/authenticator_sms/api.py index 712d10b18..661c5939d 100644 --- a/authentik/stages/authenticator_sms/api.py +++ b/authentik/stages/authenticator_sms/api.py @@ -22,8 +22,10 @@ class AuthenticatorSMSStageSerializer(StageSerializer): "configure_flow", "provider", "from_number", - "twilio_account_sid", - "twilio_auth", + "account_sid", + "auth", + "auth_password", + "auth_type", ] diff --git a/authentik/stages/authenticator_sms/migrations/0003_auto_20211014_0813.py b/authentik/stages/authenticator_sms/migrations/0003_auto_20211014_0813.py new file mode 100644 index 000000000..cdb50c21e --- /dev/null +++ b/authentik/stages/authenticator_sms/migrations/0003_auto_20211014_0813.py @@ -0,0 +1,38 @@ +# Generated by Django 3.2.8 on 2021-10-14 08:13 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("authentik_stages_authenticator_sms", "0002_authenticatorsmsstage_from_number"), + ] + + operations = [ + migrations.RenameField( + model_name="authenticatorsmsstage", + old_name="twilio_account_sid", + new_name="account_sid", + ), + migrations.RenameField( + model_name="authenticatorsmsstage", + old_name="twilio_auth", + new_name="auth", + ), + migrations.AddField( + model_name="authenticatorsmsstage", + name="auth_password", + field=models.TextField(null=True), + ), + migrations.AddField( + model_name="authenticatorsmsstage", + name="auth_type", + field=models.TextField(choices=[("bearer", "Bearer"), ("basic", "Basic")], null=True), + ), + migrations.AlterField( + model_name="authenticatorsmsstage", + name="provider", + field=models.TextField(choices=[("twilio", "Twilio"), ("generic", "Generic")]), + ), + ] diff --git a/authentik/stages/authenticator_sms/models.py b/authentik/stages/authenticator_sms/models.py index 2053c6b36..d9aeed30a 100644 --- a/authentik/stages/authenticator_sms/models.py +++ b/authentik/stages/authenticator_sms/models.py @@ -12,7 +12,9 @@ from rest_framework.serializers import BaseSerializer from structlog.stdlib import get_logger from authentik.core.types import UserSettingSerializer +from authentik.events.models import Event, EventAction from authentik.flows.models import ConfigurableStage, Stage +from authentik.lib.utils.errors import exception_to_string from authentik.lib.utils.http import get_http_session LOGGER = get_logger() @@ -22,6 +24,14 @@ class SMSProviders(models.TextChoices): """Supported SMS Providers""" TWILIO = "twilio" + GENERIC = "generic" + + +class SMSAuthTypes(models.TextChoices): + """Supported SMS Auth Types""" + + BEARER = "bearer" + BASIC = "basic" class AuthenticatorSMSStage(ConfigurableStage, Stage): @@ -31,25 +41,29 @@ class AuthenticatorSMSStage(ConfigurableStage, Stage): from_number = models.TextField() - twilio_account_sid = models.TextField() - twilio_auth = models.TextField() + account_sid = models.TextField() + auth = models.TextField() + auth_password = models.TextField(null=True) + auth_type = models.TextField(choices=SMSAuthTypes.choices, null=True) def send(self, token: str, device: "SMSDevice"): """Send message via selected provider""" if self.provider == SMSProviders.TWILIO: return self.send_twilio(token, device) + if self.provider == SMSProviders.GENERIC: + return self.send_generic(token, device) raise ValueError(f"invalid provider {self.provider}") def send_twilio(self, token: str, device: "SMSDevice"): """send sms via twilio provider""" response = get_http_session().post( - f"https://api.twilio.com/2010-04-01/Accounts/{self.twilio_account_sid}/Messages.json", + f"https://api.twilio.com/2010-04-01/Accounts/{self.account_sid}/Messages.json", data={ "From": self.from_number, "To": device.phone_number, "Body": token, }, - auth=(self.twilio_account_sid, self.twilio_auth), + auth=(self.account_sid, self.auth), ) LOGGER.debug("Sent SMS", to=device.phone_number) try: @@ -65,6 +79,52 @@ class AuthenticatorSMSStage(ConfigurableStage, Stage): LOGGER.warning("Error sending token by Twilio SMS", message=message) raise Exception(message) + def send_generic(self, token: str, device: "SMSDevice"): + """Send SMS via outside API""" + + data = { + "From": self.from_number, + "To": device.phone_number, + "Body": token, + } + + if self.auth_type == SMSAuthTypes.BEARER: + response = get_http_session().post( + f"{self.account_sid}", + json=data, + headers={"Authorization": f"Bearer {self.auth}"}, + ) + + elif self.auth_type == SMSAuthTypes.BASIC: + response = get_http_session().post( + f"{self.account_sid}", + json=data, + auth=(self.auth, self.auth_password), + ) + else: + raise ValueError(f"Invalid Auth type '{self.auth_type}'") + + LOGGER.debug("Sent SMS", to=device.phone_number) + try: + response.raise_for_status() + except RequestException as exc: + LOGGER.warning( + "Error sending token by generic SMS", + exc=exc, + status=response.status_code, + body=response.text[:100], + ) + Event.new( + EventAction.CONFIGURATION_ERROR, + message="Error sending SMS", + exc=exception_to_string(exc), + status_code=response.status_code, + body=response.text, + ).set_user(device.user).save() + if response.status_code >= 400: + raise ValidationError(response.text) + raise + @property def serializer(self) -> BaseSerializer: from authentik.stages.authenticator_sms.api import AuthenticatorSMSStageSerializer @@ -113,6 +173,5 @@ class SMSDevice(SideChannelDevice): return self.name or str(self.user) class Meta: - verbose_name = _("SMS Device") verbose_name_plural = _("SMS Devices") diff --git a/schema.yml b/schema.yml index df9d2ca1e..ab5420c01 100644 --- a/schema.yml +++ b/schema.yml @@ -14141,6 +14141,26 @@ paths: operationId: stages_authenticator_sms_list description: AuthenticatorSMSStage Viewset parameters: + - in: query + name: account_sid + schema: + type: string + - in: query + name: auth + schema: + type: string + - in: query + name: auth_password + schema: + type: string + - in: query + name: auth_type + schema: + type: string + nullable: true + enum: + - basic + - bearer - in: query name: configure_flow schema: @@ -14177,6 +14197,7 @@ paths: schema: type: string enum: + - generic - twilio - name: search required: false @@ -14189,14 +14210,6 @@ paths: schema: type: string format: uuid - - in: query - name: twilio_account_sid - schema: - type: string - - in: query - name: twilio_auth - schema: - type: string tags: - stages security: @@ -18859,6 +18872,11 @@ components: required: - name - slug + AuthTypeEnum: + enum: + - bearer + - basic + type: string AuthenticateWebAuthnStage: type: object description: AuthenticateWebAuthnStage Serializer @@ -19183,18 +19201,25 @@ components: $ref: '#/components/schemas/ProviderEnum' from_number: type: string - twilio_account_sid: + account_sid: type: string - twilio_auth: + auth: type: string + auth_password: + type: string + nullable: true + auth_type: + allOf: + - $ref: '#/components/schemas/AuthTypeEnum' + nullable: true required: + - account_sid + - auth - component - from_number - name - pk - provider - - twilio_account_sid - - twilio_auth - verbose_name - verbose_name_plural AuthenticatorSMSStageRequest: @@ -19217,16 +19242,23 @@ components: $ref: '#/components/schemas/ProviderEnum' from_number: type: string - twilio_account_sid: + account_sid: type: string - twilio_auth: + auth: type: string + auth_password: + type: string + nullable: true + auth_type: + allOf: + - $ref: '#/components/schemas/AuthTypeEnum' + nullable: true required: + - account_sid + - auth - from_number - name - provider - - twilio_account_sid - - twilio_auth AuthenticatorStaticChallenge: type: object description: Static authenticator challenge @@ -25963,10 +25995,17 @@ components: $ref: '#/components/schemas/ProviderEnum' from_number: type: string - twilio_account_sid: + account_sid: type: string - twilio_auth: + auth: type: string + auth_password: + type: string + nullable: true + auth_type: + allOf: + - $ref: '#/components/schemas/AuthTypeEnum' + nullable: true PatchedAuthenticatorStaticStageRequest: type: object description: AuthenticatorStaticStage Serializer @@ -28147,6 +28186,7 @@ components: ProviderEnum: enum: - twilio + - generic type: string ProviderRequest: type: object diff --git a/web/src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts b/web/src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts index cf40d2830..49ecf7107 100644 --- a/web/src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts +++ b/web/src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts @@ -1,16 +1,17 @@ import { t } from "@lingui/macro"; import { html, TemplateResult } from "lit"; -import { customElement } from "lit/decorators"; +import { customElement, property } from "lit/decorators"; import { ifDefined } from "lit/directives/if-defined"; import { until } from "lit/directives/until"; import { - FlowsApi, - StagesApi, - FlowsInstancesListDesignationEnum, AuthenticatorSMSStage, + AuthTypeEnum, + FlowsApi, + FlowsInstancesListDesignationEnum, ProviderEnum, + StagesApi, } from "@goauthentik/api"; import { DEFAULT_CONFIG } from "../../../api/Config"; @@ -26,6 +27,14 @@ export class AuthenticatorSMSStageForm extends ModelForm
@@ -68,13 +97,25 @@ export class AuthenticatorSMSStageForm extends ModelForm - { + const current = (ev.target as HTMLInputElement).value; + this.onProviderChange(current); + }} + > + + @@ -109,12 +152,13 @@ export class AuthenticatorSMSStageForm extends ModelForm @@ -122,6 +166,72 @@ export class AuthenticatorSMSStageForm extends ModelForm + { + const current = (ev.target as HTMLInputElement).value; + this.onAuthTypeChange(current); + }} + ?required=${true} + name="authType" + > + + + + +

+ ${t`This is the full endpoint to send POST requests to.`} +

+
+ + +

+ ${t`This is the username to be used with basic auth or the token when used with bearer token`} +

+
+ + +

+ ${t`This is the password to be used with basic auth`} +

+