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:
parent
10fc33f7d3
commit
634375c43f
|
@ -22,8 +22,10 @@ class AuthenticatorSMSStageSerializer(StageSerializer):
|
|||
"configure_flow",
|
||||
"provider",
|
||||
"from_number",
|
||||
"twilio_account_sid",
|
||||
"twilio_auth",
|
||||
"account_sid",
|
||||
"auth",
|
||||
"auth_password",
|
||||
"auth_type",
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -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")]),
|
||||
),
|
||||
]
|
|
@ -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")
|
||||
|
|
76
schema.yml
76
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
|
||||
|
|
|
@ -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
|
||||
|
|
Reference in a new issue