Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
93 changes: 93 additions & 0 deletions docs/resource-specific-documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -784,6 +784,99 @@ For `universal_login` template `templates/` will be created.
}
```

## Themes (Identifier display settings)

> **Early Access:** Requires the `universal_login_theme_identifiers` feature flag to be enabled on the tenant. When the flag is off, the Auth0 API rejects a theme write that includes `identifiers` and strips the field from responses.

The Deploy CLI supports configuring identifier display settings on the branding theme via the top-level `identifiers` object. All three members are required when `identifiers` is supplied:

- `identifiers.login_display` (string): Login display mode. One of `separate`, `unified`.
- `identifiers.otp_autocomplete` (boolean): Whether OTP autocomplete is enabled.
- `identifiers.phone_display` (object): Phone number display settings.
- `identifiers.phone_display.formatting` (string): One of `international`, `regional`.
- `identifiers.phone_display.masking` (string): One of `hide_country_code`, `mask_digits`, `show_all`.

**YAML Example**

```yaml
themes:
- displayName: Default theme
borders: { ... }
colors: { ... }
fonts: { ... }
page_background: { ... }
widget: { ... }
identifiers:
login_display: unified
otp_autocomplete: true
phone_display:
masking: mask_digits
formatting: international
```

**Directory Example**

```
./themes/Default theme.json
```

```json
{
"displayName": "Default theme",
"borders": { "...": "..." },
"colors": { "...": "..." },
"fonts": { "...": "..." },
"page_background": { "...": "..." },
"widget": { "...": "..." },
"identifiers": {
"login_display": "unified",
"otp_autocomplete": true,
"phone_display": {
"masking": "mask_digits",
"formatting": "international"
}
}
}
```

## Tenant Settings (Country codes)

> **Early Access:** Requires the `tenant_country_codes_filtering` feature flag to be enabled on the tenant. When the flag is off, the Auth0 API rejects a tenant settings write that includes `country_codes`.

The Deploy CLI supports configuring phone country code filtering for identifier input via the top-level `country_codes` object in tenant settings:

- `country_codes.list` (array of string): ISO 3166-1 alpha-2 codes (e.g. `US`, `GB`). Must be non-empty and unique.
- `country_codes.mode` (string): Whether the list is an allowlist or denylist. One of `allow`, `deny`.

Set `country_codes: null` to remove filtering (allow all countries).

**YAML Example**

```yaml
tenant:
country_codes:
list:
- US
- GB
- CA
mode: allow
```

**Directory Example**

```
./tenant.json
```

```json
{
"country_codes": {
"list": ["US", "GB", "CA"],
"mode": "allow"
}
}
```

## Custom Domains

Custom domains allow you to use your own domain for authentication instead of the default Auth0 domain. The Deploy CLI supports managing custom domains in both directory and YAML modes.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
"dependencies": {
"@clack/prompts": "1.4.0",
"ajv": "^6.12.6",
"auth0": "^6.0.0",
"auth0": "^6.1.0",
"chalk": "5.6.2",
"dot-prop": "^5.3.0",
"fs-extra": "^10.1.0",
Expand Down
24 changes: 24 additions & 0 deletions src/tools/auth0/handlers/tenant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,30 @@ export const schema = {
description:
'Indicates the security mode for new clients created through the Dynamic Client Registration endpoint.',
},
country_codes: {
type: ['object', 'null'],
description:
'Phone country code configuration for identifier input. Set to `null` to remove filtering (allow all countries). Requires the `tenant_country_codes_filtering` feature flag to be enabled on the tenant.',
properties: {
list: {
type: 'array',
description: 'ISO 3166-1 alpha-2 country codes (e.g. `US`, `GB`).',
items: {
type: 'string',
pattern: '^[A-Z]{2}$',
},
minItems: 1,
uniqueItems: true,
},
mode: {
type: 'string',
enum: ['allow', 'deny'],
description: 'Whether the list is an allowlist or denylist.',
},
},
required: ['list', 'mode'],
additionalProperties: false,
},
},
};

Expand Down
36 changes: 36 additions & 0 deletions src/tools/auth0/handlers/themes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,42 @@ export const schema = {
pattern: '^[^<>]*$',
type: 'string',
},
identifiers: {
additionalProperties: false,
description:
'Identifier display settings on the theme. Requires the `universal_login_theme_identifiers` feature flag to be enabled on the tenant.',
properties: {
login_display: {
description: 'Login display mode',
enum: ['separate', 'unified'],
type: 'string',
},
otp_autocomplete: {
description: 'Whether OTP autocomplete is enabled',
type: 'boolean',
},
phone_display: {
additionalProperties: false,
description: 'Phone number display settings',
properties: {
formatting: {
description: 'Phone number formatting style',
enum: ['international', 'regional'],
type: 'string',
},
masking: {
description: 'Phone number masking strategy',
enum: ['hide_country_code', 'mask_digits', 'show_all'],
type: 'string',
},
},
required: ['formatting', 'masking'],
type: 'object',
},
},
required: ['login_display', 'otp_autocomplete', 'phone_display'],
type: 'object',
},
fonts: {
additionalProperties: false,
properties: {
Expand Down
48 changes: 48 additions & 0 deletions test/tools/auth0/handlers/tenant.tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,54 @@ describe('#tenant handler', () => {
expect(updatedData.dynamic_client_registration_security_mode).to.equal('strict');
});

it('should update tenant with country_codes', async () => {
let updatedData = null;
const auth0 = {
tenants: {
settings: {
update: (data) => {
updatedData = data;
return Promise.resolve(data);
},
},
},
};

// @ts-ignore
const handler = new tenantHandler({ client: auth0 });
const stageFn = Object.getPrototypeOf(handler).processChanges;

await stageFn.apply(handler, [
{ tenant: { country_codes: { list: ['US', 'GB', 'CA'], mode: 'allow' } } },
]);

expect(updatedData).to.not.be.null;
expect(updatedData.country_codes).to.deep.equal({ list: ['US', 'GB', 'CA'], mode: 'allow' });
});

it('should update tenant with country_codes set to null to remove filtering', async () => {
let updatedData = null;
const auth0 = {
tenants: {
settings: {
update: (data) => {
updatedData = data;
return Promise.resolve(data);
},
},
},
};

// @ts-ignore
const handler = new tenantHandler({ client: auth0 });
const stageFn = Object.getPrototypeOf(handler).processChanges;

await stageFn.apply(handler, [{ tenant: { country_codes: null } }]);

expect(updatedData).to.not.be.null;
expect(updatedData.country_codes).to.equal(null);
});

it('should allow valid default_token_quota property in tenant', async () => {
const tenantWithDefaultTokenQuota = {
default_token_quota: {
Expand Down
68 changes: 68 additions & 0 deletions test/tools/auth0/handlers/themes.tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,74 @@ describe('#themes handler', () => {
expect(auth0.branding.themes.create.called).to.equal(false);
expect(auth0.branding.themes.delete.called).to.equal(false);
});

it('should create the theme with identifiers when default theme does not exist', async () => {
const theme = mockTheme();
theme.identifiers = {
login_display: 'unified',
otp_autocomplete: true,
phone_display: {
masking: 'mask_digits',
formatting: 'international',
},
};

const auth0 = {
branding: {
themes: {
getDefault: stub().returns(Promise.reject(errorWithStatusCode(404))),
create: stub().returns(Promise.resolve(theme)),
update: stub().returns(Promise.reject(new Error('update should not have been called'))),
delete: stub().returns(Promise.reject(new Error('delete should not have been called'))),
},
},
};

const handler = new ThemesHandler({ client: auth0 });
const assets = { themes: [theme] };

await handler.processChanges(assets);

expect(auth0.branding.themes.create.called).to.equal(true);
expect(auth0.branding.themes.create.callCount).to.equal(1);
expect(auth0.branding.themes.create.calledWith(theme)).to.equal(true);
expect(auth0.branding.themes.update.called).to.equal(false);
});

it('should update the theme with identifiers when default exists', async () => {
const theme = mockTheme({ withThemeId: 'myThemeId' });
theme.identifiers = {
login_display: 'separate',
otp_autocomplete: false,
phone_display: {
masking: 'show_all',
formatting: 'regional',
},
};

const auth0 = {
branding: {
themes: {
getDefault: stub().returns(theme),
create: stub().returns(Promise.reject(new Error('create should not have been called'))),
update: stub().returns(Promise.resolve(theme)),
delete: stub().returns(Promise.reject(new Error('delete should not have been called'))),
},
},
};

const handler = new ThemesHandler({ client: auth0 });
const assets = { themes: [omit(theme, 'themeId')] };

await handler.processChanges(assets);

expect(auth0.branding.themes.update.called).to.equal(true);
expect(auth0.branding.themes.update.callCount).to.equal(1);
expect(
auth0.branding.themes.update.calledWith('myThemeId', omit(theme, 'themeId'))
).to.deep.equal(true);
expect(auth0.branding.themes.create.called).to.equal(false);
});
});

it('should delete the theme when default theme exists and AUTH0_ALLOW_DELETE: true', async () => {
Expand Down