Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ Makefile
.github/copilot-instructions.md
.github/chatmodes/*
.github/prompts/*
.github/agents/*
53 changes: 53 additions & 0 deletions docs/resource-specific-documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -575,3 +575,56 @@ phoneProviders:
}
]
```

## PhoneTemplates

Phone templates allow you to customize the SMS and voice messages sent to users for phone-based authentication.
Refer to the [Management API](https://auth0.com/docs/api/management/v2/branding/get-phone-templates) for more details.

### YAML Example

```yaml
# Contents of ./tenant.yaml
phoneTemplates:
- type: otp_verify
disabled: false
content:
from: '+12341234567'
body:
text: 'Your verification code is {{ code }}'
voice: 'Your verification code is {{ code }}'
- type: otp_enroll
disabled: false
content:
from: '+12341234567'
body:
text: 'Your enrollment code is {{ code }}'
```

### Directory Example

Create individual JSON files for each template in the `phone-templates` directory:

```text
phone-templates/
├── otp_verify.json
├── otp_enroll.json
├── change_password.json
└── ...
```

Example `phone-templates/otp_verify.json`:

```json
{
"type": "otp_verify",
"disabled": false,
"content": {
"from": "+12341234567",
"body": {
"text": "Your verification code is {{ code }}",
"voice": "Your verification code is {{ code }}"
}
}
}
```
12 changes: 12 additions & 0 deletions examples/directory/phone-templates/otp_enroll.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"type": "otp_enroll",
"disabled": false,
"content": {
"syntax": "liquid",
"from": "+12341234567",
"body": {
"text": "Your enrollment code is {{ code }}",
"voice": "Your enrollment code is {{ code }}"
}
}
}
12 changes: 12 additions & 0 deletions examples/directory/phone-templates/otp_verify.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"type": "otp_verify",
"disabled": false,
"content": {
"syntax": "liquid",
"from": "+12341234567",
"body": {
"text": "Your verification code is {{ code }}",
"voice": "Your verification code is {{ code }}"
}
}
}
15 changes: 15 additions & 0 deletions examples/yaml/tenant.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,21 @@ phoneProviders:
credentials:
auth_token: "some_auth_token"

phoneTemplates:
- type: otp_verify
disabled: false
content:
from: "+12341234567"
body:
text: "Your verification code is {{ code }}"
voice: "Your verification code is {{ code }}"
- type: otp_enroll
disabled: false
content:
from: "+12341234567"
body:
text: "Your enrollment code is {{ code }}"

emailProvider:
name: "smtp"
enabled: true
Expand Down
21 changes: 21 additions & 0 deletions src/context/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,27 @@ export function phoneProviderDefaults(phoneProvider) {
return updated;
}

export function phoneTemplatesDefaults(phoneTemplate) {
const updated = { ...phoneTemplate };

// Strip read-only fields that are returned by the API but should not be included in exported config
const removeKeysFromOutput = [
'id',
'channel',
'customizable',
'tenant',
'created_at',
'updated_at',
];
removeKeysFromOutput.forEach((key) => {
if (key in updated) {
delete updated[key];
}
});

return updated;
}

export function connectionDefaults(connection) {
if (connection.options) {
// Mask secret for key: connection.options.client_secret
Expand Down
2 changes: 2 additions & 0 deletions src/context/directory/handlers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import triggers from './triggers';
import attackProtection from './attackProtection';
import branding from './branding';
import phoneProviders from './phoneProvider';
import phoneTemplates from './phoneTemplates';
import logStreams from './logStreams';
import prompts from './prompts';
import customDomains from './customDomains';
Expand Down Expand Up @@ -70,6 +71,7 @@ const directoryHandlers: {
attackProtection,
branding,
phoneProviders,
phoneTemplates,
logStreams,
prompts,
customDomains,
Expand Down
52 changes: 52 additions & 0 deletions src/context/directory/handlers/phoneTemplates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import path from 'path';
import fs from 'fs-extra';
import { constants } from '../../../tools';

import { existsMustBeDir, getFiles, dumpJSON, loadJSON } from '../../../utils';
import { DirectoryHandler } from '.';
import DirectoryContext from '..';
import { ParsedAsset } from '../../../types';
import { PhoneTemplate } from '../../../tools/auth0/handlers/phoneTemplates';
import { phoneTemplatesDefaults } from '../../defaults';

type ParsedPhoneTemplates = ParsedAsset<'phoneTemplates', PhoneTemplate[]>;

function parse(context: DirectoryContext): ParsedPhoneTemplates {
const phoneTemplatesFolder = path.join(context.filePath, constants.PHONE_TEMPLATES_DIRECTORY);
if (!existsMustBeDir(phoneTemplatesFolder)) return { phoneTemplates: null }; // Skip

const files = getFiles(phoneTemplatesFolder, ['.json']);

const phoneTemplates = files.map((f) =>
loadJSON(f, {
mappings: context.mappings,
disableKeywordReplacement: context.disableKeywordReplacement,
})
);

return { phoneTemplates };
}

async function dump(context: DirectoryContext): Promise<void> {
const { phoneTemplates } = context.assets;

if (!phoneTemplates) {
return;
} // Skip, nothing to dump

const phoneTemplatesFolder = path.join(context.filePath, constants.PHONE_TEMPLATES_DIRECTORY);
fs.ensureDirSync(phoneTemplatesFolder);

phoneTemplates.forEach((template) => {
const templateWithDefaults = phoneTemplatesDefaults(template);
const templateFile = path.join(phoneTemplatesFolder, `${template.type}.json`);
dumpJSON(templateFile, templateWithDefaults);
});
}

const phoneTemplatesHandler: DirectoryHandler<ParsedPhoneTemplates> = {
parse,
dump,
};

export default phoneTemplatesHandler;
2 changes: 2 additions & 0 deletions src/context/yaml/handlers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import triggers from './triggers';
import attackProtection from './attackProtection';
import branding from './branding';
import phoneProviders from './phoneProvider';
import phoneTemplates from './phoneTemplates';
import logStreams from './logStreams';
import prompts from './prompts';
import customDomains from './customDomains';
Expand Down Expand Up @@ -68,6 +69,7 @@ const yamlHandlers: { [key in AssetTypes]: YAMLHandler<{ [key: string]: unknown
attackProtection,
branding,
phoneProviders,
phoneTemplates,
logStreams,
prompts,
customDomains,
Expand Down
36 changes: 36 additions & 0 deletions src/context/yaml/handlers/phoneTemplates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { YAMLHandler } from '.';
import YAMLContext from '..';
import { PhoneTemplate } from '../../../tools/auth0/handlers/phoneTemplates';
import { ParsedAsset } from '../../../types';
import { phoneTemplatesDefaults } from '../../defaults';

type ParsedPhoneTemplates = ParsedAsset<'phoneTemplates', PhoneTemplate[]>;

async function parse(context: YAMLContext): Promise<ParsedPhoneTemplates> {
const { phoneTemplates } = context.assets;

if (!phoneTemplates) return { phoneTemplates: null };

return {
phoneTemplates,
};
}

async function dump(context: YAMLContext): Promise<ParsedPhoneTemplates> {
const { phoneTemplates } = context.assets;

if (!phoneTemplates) return { phoneTemplates: null };

const processedTemplates = phoneTemplates.map((template) => phoneTemplatesDefaults(template));

return {
phoneTemplates: processedTemplates,
};
}

const phoneTemplatesHandler: YAMLHandler<ParsedPhoneTemplates> = {
parse,
dump,
};

export default phoneTemplatesHandler;
2 changes: 2 additions & 0 deletions src/tools/auth0/handlers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import * as guardianPhoneFactorMessageTypes from './guardianPhoneFactorMessageTy
import * as roles from './roles';
import * as branding from './branding';
import * as phoneProviders from './phoneProvider';
import * as phoneTemplates from './phoneTemplates';
import * as prompts from './prompts';
import * as actions from './actions';
import * as triggers from './triggers';
Expand Down Expand Up @@ -59,6 +60,7 @@ const auth0ApiHandlers: { [key in AssetTypes]: any } = {
roles,
branding,
phoneProviders,
phoneTemplates,
//@ts-ignore because prompts have not been universally implemented yet
prompts,
actions,
Expand Down
Loading