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 <jens.langhammer@beryju.org>
This commit is contained in:
Skyler Mäntysaari 2021-10-14 11:13:39 +03:00 committed by Jens Langhammer
parent 10fc33f7d3
commit 634375c43f
5 changed files with 283 additions and 34 deletions

View file

@ -22,8 +22,10 @@ class AuthenticatorSMSStageSerializer(StageSerializer):
"configure_flow",
"provider",
"from_number",
"twilio_account_sid",
"twilio_auth",
"account_sid",
"auth",
"auth_password",
"auth_type",
]

View file

@ -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")]),
),
]

View file

@ -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")

View file

@ -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

View file

@ -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<AuthenticatorSMSStage,
});
}
@property({ type: Boolean })
shouldShowTwilio = false;
@property({ type: Boolean })
shouldShowGeneric = false;
@property({ type: Boolean })
shouldShowAuthPassword = false;
getSuccessMessage(): string {
if (this.instance) {
return t`Successfully updated stage.`;
@ -47,6 +56,26 @@ export class AuthenticatorSMSStageForm extends ModelForm<AuthenticatorSMSStage,
}
};
onProviderChange(provider: string): void {
if (provider === ProviderEnum.Twilio) {
this.shouldShowTwilio = true;
this.shouldShowGeneric = false;
}
if (provider === ProviderEnum.Generic) {
this.shouldShowGeneric = true;
this.shouldShowTwilio = false;
}
}
onAuthTypeChange(auth_type: string): void {
if (auth_type === AuthTypeEnum.Basic) {
this.shouldShowAuthPassword = true;
}
if (auth_type === AuthTypeEnum.Bearer) {
this.shouldShowAuthPassword = false;
}
}
renderForm(): TemplateResult {
return html`<form class="pf-c-form pf-m-horizontal">
<div class="form-help-text">
@ -68,13 +97,25 @@ export class AuthenticatorSMSStageForm extends ModelForm<AuthenticatorSMSStage,
?required=${true}
name="provider"
>
<select name="users" class="pf-c-form-control">
<select
class="pf-c-form-control"
@change=${(ev: Event) => {
const current = (ev.target as HTMLInputElement).value;
this.onProviderChange(current);
}}
>
<option
value="${ProviderEnum.Twilio}"
?selected=${this.instance?.provider === ProviderEnum.Twilio}
>
${t`Twilio`}
</option>
<option
value="${ProviderEnum.Generic}"
?selected=${this.instance?.provider === ProviderEnum.Generic}
>
${t`Generic`}
</option>
</select>
</ak-form-element-horizontal>
<ak-form-element-horizontal
@ -92,14 +133,16 @@ export class AuthenticatorSMSStageForm extends ModelForm<AuthenticatorSMSStage,
${t`Number the SMS will be sent from.`}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${t`Twilio Account SID`}
?hidden=${!this.shouldShowTwilio}
?required=${true}
name="twilioAccountSid"
name="accountSid"
>
<input
type="text"
value="${ifDefined(this.instance?.twilioAccountSid || "")}"
value="${ifDefined(this.instance?.accountSid || "")}"
class="pf-c-form-control"
required
/>
@ -109,12 +152,13 @@ export class AuthenticatorSMSStageForm extends ModelForm<AuthenticatorSMSStage,
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${t`Twilio Auth Token`}
?hidden=${!this.shouldShowTwilio}
?required=${true}
name="twilioAuth"
name="auth"
>
<input
type="text"
value="${ifDefined(this.instance?.twilioAuth || "")}"
value="${ifDefined(this.instance?.auth || "")}"
class="pf-c-form-control"
required
/>
@ -122,6 +166,72 @@ export class AuthenticatorSMSStageForm extends ModelForm<AuthenticatorSMSStage,
${t`Get this value from https://console.twilio.com`}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${t`Auth Type`}
?hidden=${!this.shouldShowGeneric}
@change=${(ev: Event) => {
const current = (ev.target as HTMLInputElement).value;
this.onAuthTypeChange(current);
}}
?required=${true}
name="authType"
>
<select class="pf-c-form-control">
<option
value="${AuthTypeEnum.Bearer}"
?selected=${this.instance?.authType === AuthTypeEnum.Bearer}
>
${t`Bearer Token`}
</option>
<option value="${AuthTypeEnum.Basic}">${t`Basic Auth`}</option>
</select>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${t`External API URL`}
?hidden=${!this.shouldShowGeneric}
?required=${true}
name="accountSid"
>
<input
type="text"
value="${ifDefined(this.instance?.accountSid || "")}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${t`This is the full endpoint to send POST requests to.`}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${t`API Auth Username`}
?hidden=${!this.shouldShowGeneric}
?required=${true}
name="auth"
>
<input
type="text"
value="${ifDefined(this.instance?.auth || "")}"
class="pf-c-form-control"
/>
<p class="pf-c-form__helper-text">
${t`This is the username to be used with basic auth or the token when used with bearer token`}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${t`API Auth password`}
?hidden=${!this.shouldShowGeneric || !this.shouldShowAuthPassword}
?required=${false}
name="authPassword"
>
<input
type="text"
value="${ifDefined(this.instance?.authPassword || "null")}"
class="pf-c-form-control"
/>
<p class="pf-c-form__helper-text">
${t`This is the password to be used with basic auth`}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${t`Configuration flow`} name="configureFlow">
<select class="pf-c-form-control">
<option