diff --git a/docs/resource-specific-documentation.md b/docs/resource-specific-documentation.md index 7d2353647..12e2db5ab 100644 --- a/docs/resource-specific-documentation.md +++ b/docs/resource-specific-documentation.md @@ -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. diff --git a/package.json b/package.json index 01910d5a2..89fe637ba 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/tools/auth0/handlers/tenant.ts b/src/tools/auth0/handlers/tenant.ts index 8f5e6dc5e..ace164794 100644 --- a/src/tools/auth0/handlers/tenant.ts +++ b/src/tools/auth0/handlers/tenant.ts @@ -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, + }, }, }; diff --git a/src/tools/auth0/handlers/themes.ts b/src/tools/auth0/handlers/themes.ts index abdcc3674..4091d88e6 100644 --- a/src/tools/auth0/handlers/themes.ts +++ b/src/tools/auth0/handlers/themes.ts @@ -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: { diff --git a/test/tools/auth0/handlers/tenant.tests.ts b/test/tools/auth0/handlers/tenant.tests.ts index 598451bf2..029ff8360 100644 --- a/test/tools/auth0/handlers/tenant.tests.ts +++ b/test/tools/auth0/handlers/tenant.tests.ts @@ -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: { diff --git a/test/tools/auth0/handlers/themes.tests.js b/test/tools/auth0/handlers/themes.tests.js index 4b11f2a02..4c2a416f8 100644 --- a/test/tools/auth0/handlers/themes.tests.js +++ b/test/tools/auth0/handlers/themes.tests.js @@ -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 () => {