From 217e145d239302820ff2b507c0927c9ba90e9d59 Mon Sep 17 00:00:00 2001
From: Jens L
Date: Fri, 14 Oct 2022 12:53:01 +0300
Subject: [PATCH] stages/authenticator_sms: make sms stage payload customisable
(#3780)
* make sms stage payload customisable
Signed-off-by: Jens Langhammer
* update phrasing for webhook mapping
Signed-off-by: Jens Langhammer
Signed-off-by: Jens Langhammer
---
.../0002_alter_notificationtransport_mode.py | 4 +++
authentik/events/models.py | 8 ++---
authentik/providers/saml/models.py | 2 +-
authentik/stages/authenticator_sms/api.py | 1 +
.../0005_authenticatorsmsstage_mapping.py | 26 +++++++++++++++
authentik/stages/authenticator_sms/models.py | 30 +++++++++++++----
schema.yml | 30 ++++++++++++++---
.../AuthenticatorSMSStageForm.ts | 32 +++++++++++++++++--
.../flow/stages/authenticator_sms/index.md | 10 ++++++
9 files changed, 125 insertions(+), 18 deletions(-)
create mode 100644 authentik/stages/authenticator_sms/migrations/0005_authenticatorsmsstage_mapping.py
diff --git a/authentik/events/migrations/0002_alter_notificationtransport_mode.py b/authentik/events/migrations/0002_alter_notificationtransport_mode.py
index 5807b42e4..5faa5c457 100644
--- a/authentik/events/migrations/0002_alter_notificationtransport_mode.py
+++ b/authentik/events/migrations/0002_alter_notificationtransport_mode.py
@@ -22,4 +22,8 @@ class Migration(migrations.Migration):
default="local",
),
),
+ migrations.AlterModelOptions(
+ name="notificationwebhookmapping",
+ options={"verbose_name": "Webhook Mapping", "verbose_name_plural": "Webhook Mappings"},
+ ),
]
diff --git a/authentik/events/models.py b/authentik/events/models.py
index 629e9ba3a..f497afaed 100644
--- a/authentik/events/models.py
+++ b/authentik/events/models.py
@@ -560,7 +560,7 @@ class NotificationRule(SerializerModel, PolicyBindingModel):
class NotificationWebhookMapping(PropertyMapping):
- """Modify the schema and layout of the webhook being sent"""
+ """Modify the payload of outgoing webhook requests"""
@property
def component(self) -> str:
@@ -573,9 +573,9 @@ class NotificationWebhookMapping(PropertyMapping):
return NotificationWebhookMappingSerializer
def __str__(self):
- return f"Notification Webhook Mapping {self.name}"
+ return f"Webhook Mapping {self.name}"
class Meta:
- verbose_name = _("Notification Webhook Mapping")
- verbose_name_plural = _("Notification Webhook Mappings")
+ verbose_name = _("Webhook Mapping")
+ verbose_name_plural = _("Webhook Mappings")
diff --git a/authentik/providers/saml/models.py b/authentik/providers/saml/models.py
index 660bc9afe..172ff956c 100644
--- a/authentik/providers/saml/models.py
+++ b/authentik/providers/saml/models.py
@@ -182,7 +182,7 @@ class SAMLProvider(Provider):
class SAMLPropertyMapping(PropertyMapping):
- """Map User/Group attribute to SAML Attribute, which can be used by the Service Provider."""
+ """Map User/Group attribute to SAML Attribute, which can be used by the Service Provider"""
saml_name = models.TextField(verbose_name="SAML Name")
friendly_name = models.TextField(default=None, blank=True, null=True)
diff --git a/authentik/stages/authenticator_sms/api.py b/authentik/stages/authenticator_sms/api.py
index 6e632480d..cf5d39c89 100644
--- a/authentik/stages/authenticator_sms/api.py
+++ b/authentik/stages/authenticator_sms/api.py
@@ -27,6 +27,7 @@ class AuthenticatorSMSStageSerializer(StageSerializer):
"auth_password",
"auth_type",
"verify_only",
+ "mapping",
]
diff --git a/authentik/stages/authenticator_sms/migrations/0005_authenticatorsmsstage_mapping.py b/authentik/stages/authenticator_sms/migrations/0005_authenticatorsmsstage_mapping.py
new file mode 100644
index 000000000..bf9f8d822
--- /dev/null
+++ b/authentik/stages/authenticator_sms/migrations/0005_authenticatorsmsstage_mapping.py
@@ -0,0 +1,26 @@
+# Generated by Django 4.1.2 on 2022-10-13 20:19
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("authentik_events", "0002_alter_notificationtransport_mode"),
+ ("authentik_stages_authenticator_sms", "0004_authenticatorsmsstage_verify_only_and_more"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="authenticatorsmsstage",
+ name="mapping",
+ field=models.ForeignKey(
+ default=None,
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ to="authentik_events.notificationwebhookmapping",
+ help_text="Optionally modify the payload being sent to custom providers.",
+ ),
+ ),
+ ]
diff --git a/authentik/stages/authenticator_sms/models.py b/authentik/stages/authenticator_sms/models.py
index 4ad2e2a5f..70017b96f 100644
--- a/authentik/stages/authenticator_sms/models.py
+++ b/authentik/stages/authenticator_sms/models.py
@@ -15,7 +15,8 @@ from twilio.base.exceptions import TwilioRestException
from twilio.rest import Client
from authentik.core.types import UserSettingSerializer
-from authentik.events.models import Event, EventAction
+from authentik.events.models import Event, EventAction, NotificationWebhookMapping
+from authentik.events.utils import sanitize_item
from authentik.flows.models import ConfigurableStage, Stage
from authentik.lib.models import SerializerModel
from authentik.lib.utils.errors import exception_to_string
@@ -59,6 +60,14 @@ class AuthenticatorSMSStage(ConfigurableStage, Stage):
),
)
+ mapping = models.ForeignKey(
+ NotificationWebhookMapping,
+ null=True,
+ default=None,
+ on_delete=models.SET_NULL,
+ help_text=_("Optionally modify the payload being sent to custom providers."),
+ )
+
def send(self, token: str, device: "SMSDevice"):
"""Send message via selected provider"""
if self.provider == SMSProviders.TWILIO:
@@ -82,24 +91,33 @@ class AuthenticatorSMSStage(ConfigurableStage, Stage):
def send_generic(self, token: str, device: "SMSDevice"):
"""Send SMS via outside API"""
-
- data = {
+ payload = {
"From": self.from_number,
"To": device.phone_number,
"Body": token,
}
+ if self.mapping:
+ payload = sanitize_item(
+ self.mapping.evaluate(
+ user=device.user,
+ request=None,
+ device=device,
+ token=token,
+ stage=self,
+ )
+ )
+
if self.auth_type == SMSAuthTypes.BEARER:
response = get_http_session().post(
f"{self.account_sid}",
- json=data,
+ json=payload,
headers={"Authorization": f"Bearer {self.auth}"},
)
-
elif self.auth_type == SMSAuthTypes.BASIC:
response = get_http_session().post(
f"{self.account_sid}",
- json=data,
+ json=payload,
auth=(self.auth, self.auth_password),
)
else:
diff --git a/schema.yml b/schema.yml
index 89127066f..1fe621123 100644
--- a/schema.yml
+++ b/schema.yml
@@ -13101,7 +13101,7 @@ paths:
schema:
type: string
format: uuid
- description: A UUID string identifying this Notification Webhook Mapping.
+ description: A UUID string identifying this Webhook Mapping.
required: true
tags:
- propertymappings
@@ -13135,7 +13135,7 @@ paths:
schema:
type: string
format: uuid
- description: A UUID string identifying this Notification Webhook Mapping.
+ description: A UUID string identifying this Webhook Mapping.
required: true
tags:
- propertymappings
@@ -13175,7 +13175,7 @@ paths:
schema:
type: string
format: uuid
- description: A UUID string identifying this Notification Webhook Mapping.
+ description: A UUID string identifying this Webhook Mapping.
required: true
tags:
- propertymappings
@@ -13214,7 +13214,7 @@ paths:
schema:
type: string
format: uuid
- description: A UUID string identifying this Notification Webhook Mapping.
+ description: A UUID string identifying this Webhook Mapping.
required: true
tags:
- propertymappings
@@ -13245,7 +13245,7 @@ paths:
schema:
type: string
format: uuid
- description: A UUID string identifying this Notification Webhook Mapping.
+ description: A UUID string identifying this Webhook Mapping.
required: true
tags:
- propertymappings
@@ -18794,6 +18794,11 @@ paths:
name: from_number
schema:
type: string
+ - in: query
+ name: mapping
+ schema:
+ type: string
+ format: uuid
- in: query
name: name
schema:
@@ -25185,6 +25190,11 @@ components:
description: When enabled, the Phone number is only used during enrollment
to verify the users authenticity. Only a hash of the phone number is saved
to ensure it is not re-used in the future.
+ mapping:
+ type: string
+ format: uuid
+ nullable: true
+ description: Optionally modify the payload being sent to custom providers.
required:
- account_sid
- auth
@@ -25233,6 +25243,11 @@ components:
description: When enabled, the Phone number is only used during enrollment
to verify the users authenticity. Only a hash of the phone number is saved
to ensure it is not re-used in the future.
+ mapping:
+ type: string
+ format: uuid
+ nullable: true
+ description: Optionally modify the payload being sent to custom providers.
required:
- account_sid
- auth
@@ -32702,6 +32717,11 @@ components:
description: When enabled, the Phone number is only used during enrollment
to verify the users authenticity. Only a hash of the phone number is saved
to ensure it is not re-used in the future.
+ mapping:
+ type: string
+ format: uuid
+ nullable: true
+ description: Optionally modify the payload being sent to custom providers.
PatchedAuthenticatorStaticStageRequest:
type: object
description: AuthenticatorStaticStage Serializer
diff --git a/web/src/admin/stages/authenticator_sms/AuthenticatorSMSStageForm.ts b/web/src/admin/stages/authenticator_sms/AuthenticatorSMSStageForm.ts
index 0ad8a3918..8ae544604 100644
--- a/web/src/admin/stages/authenticator_sms/AuthenticatorSMSStageForm.ts
+++ b/web/src/admin/stages/authenticator_sms/AuthenticatorSMSStageForm.ts
@@ -16,6 +16,7 @@ import {
AuthenticatorSMSStage,
FlowsApi,
FlowsInstancesListDesignationEnum,
+ PropertymappingsApi,
ProviderEnum,
StagesApi,
} from "@goauthentik/api";
@@ -91,7 +92,8 @@ export class AuthenticatorSMSStageForm extends ModelForm {
const current = (ev.target as HTMLInputElement).value;
@@ -153,7 +155,33 @@ export class AuthenticatorSMSStageForm extends ModelForm
${t`This is the password to be used with basic auth`}
- `;
+
+
+
+
+ ${t`Modify the payload sent to the custom provider.`}
+
+
+ `;
}
renderForm(): TemplateResult {
diff --git a/website/docs/flow/stages/authenticator_sms/index.md b/website/docs/flow/stages/authenticator_sms/index.md
index faa7d34f4..656d80520 100644
--- a/website/docs/flow/stages/authenticator_sms/index.md
+++ b/website/docs/flow/stages/authenticator_sms/index.md
@@ -36,6 +36,16 @@ For the generic provider, a POST request will be sent to the URL you have specif
Authentication can either be done as HTTP Basic, or via a Bearer Token. Any response with status 400 or above is counted as failed, and will prevent the user from proceeding.
+Starting with authentik 2022.10, a custom webhook mapping can be specified to freely customise the payload of the request. For example:
+
+```python
+return {
+ "from": stage.from_number,
+ "to": device.phone_number,
+ "body": f"foo bar baz {token}".
+}
+```
+
## Verify only
:::info