diff --git a/docs/resource-specific-documentation.md b/docs/resource-specific-documentation.md index 7d2353647..c071d1bff 100644 --- a/docs/resource-specific-documentation.md +++ b/docs/resource-specific-documentation.md @@ -364,6 +364,67 @@ connections: } ``` +## Connections (Cross App Access — Resource Application) + +> **Early Access:** Part of the XAA (Cross App Access) — Auth0 as Resource Application Authorization Server feature. + +The Deploy CLI supports configuring a connection as a Resource Application for Cross App Access via the top-level `cross_app_access_resource_app` field. This is supported for enterprise connections including SAML (`strategy: samlp`) and OIDC (`strategy: oidc`). + +- `cross_app_access_resource_app.status` (`"enabled"` | `"disabled"`): Enables or disables the connection as a Resource Application for Cross App Access. + +For SAML connections, the `discovery_url` and `oidc_metadata` connection options — previously only supported for OIDC connections — are now also accepted under `options`. + +**YAML Example** + +```yaml +connections: + - name: enterprise-saml + strategy: samlp + cross_app_access_resource_app: + status: enabled + options: + discovery_url: https://example-idp.com/.well-known/openid-configuration + oidc_metadata: + issuer: https://example-idp.com +``` + +**Directory Example** + +``` +./connections/enterprise-saml.json +``` + +```json +{ + "name": "enterprise-saml", + "strategy": "samlp", + "cross_app_access_resource_app": { + "status": "enabled" + }, + "options": { + "discovery_url": "https://example-idp.com/.well-known/openid-configuration", + "oidc_metadata": { + "issuer": "https://example-idp.com" + } + } +} +``` + +## Clients (Cross App Access — Identity Assertion Authorization Grant) + +> **Early Access:** Part of the XAA (Cross App Access) — Auth0 as Resource Application Authorization Server feature. + +The Deploy CLI supports the `identity_assertion_authorization_grant` property on clients, which enables the client to participate in Cross App Access (ID-JAG) token exchange. + +- `identity_assertion_authorization_grant.active` (boolean): Set to `true` to enable ID-JAG exchange for the client. + +```yaml +clients: + - name: My XAA Client + identity_assertion_authorization_grant: + active: true +``` + ## Databases When managing database connections, the values of `options.customScripts` point to specific javascript files relative to diff --git a/src/tools/auth0/handlers/clients.ts b/src/tools/auth0/handlers/clients.ts index f8f6c9289..d00aea3bc 100644 --- a/src/tools/auth0/handlers/clients.ts +++ b/src/tools/auth0/handlers/clients.ts @@ -232,6 +232,20 @@ export const schema = { }, additionalProperties: true, }, + identity_assertion_authorization_grant: { + type: 'object', + description: + 'Settings for Cross App Access (ID-JAG) token exchange. Early Access. Enables the client to request identity assertion authorization grants.', + properties: { + active: { + type: 'boolean', + description: + 'Indicates whether the client can request an identity assertion authorization grant (ID-JAG) via token exchange.', + }, + }, + required: ['active'], + additionalProperties: false, + }, app_type: { type: 'string', description: 'The type of application this client represents', diff --git a/src/tools/auth0/handlers/connections.ts b/src/tools/auth0/handlers/connections.ts index 696518882..ea58ba74c 100644 --- a/src/tools/auth0/handlers/connections.ts +++ b/src/tools/auth0/handlers/connections.ts @@ -104,6 +104,14 @@ export const schema = { required: ['active'], additionalProperties: false, }, + cross_app_access_resource_app: { + type: 'object', + properties: { + status: { type: 'string', enum: ['enabled', 'disabled'] }, + }, + required: ['status'], + additionalProperties: false, + }, directory_provisioning_configuration: { type: 'object', properties: { diff --git a/test/tools/auth0/handlers/clients.tests.js b/test/tools/auth0/handlers/clients.tests.js index 265c5f6f8..2130d230b 100644 --- a/test/tools/auth0/handlers/clients.tests.js +++ b/test/tools/auth0/handlers/clients.tests.js @@ -531,6 +531,42 @@ describe('#clients handler', () => { expect(wasCreateCalled).to.be.true; }); + it('should allow valid identity_assertion_authorization_grant property in client', async () => { + const clientWithIdJag = { + name: 'clientWithIdJag', + identity_assertion_authorization_grant: { + active: true, + }, + }; + let wasCreateCalled = false; + const auth0 = { + clients: { + create: function (data) { + wasCreateCalled = true; + expect(data).to.be.an('object'); + expect(data.name).to.equal('clientWithIdJag'); + expect(data.identity_assertion_authorization_grant).to.deep.equal({ + active: true, + }); + return Promise.resolve({ data }); + }, + update: () => Promise.resolve({ data: [] }), + delete: () => Promise.resolve({ data: [] }), + list: (params) => mockPagedData(params, 'clients', []), + }, + connectionProfiles: { list: (params) => mockPagedData(params, 'connectionProfiles', []) }, + userAttributeProfiles: { + list: (params) => mockPagedData(params, 'userAttributeProfiles', []), + }, + pool, + }; + const handler = new clients.default({ client: pageClient(auth0), config }); + const stageFn = Object.getPrototypeOf(handler).processChanges; + await stageFn.apply(handler, [{ clients: [clientWithIdJag] }]); + // eslint-disable-next-line no-unused-expressions + expect(wasCreateCalled).to.be.true; + }); + it('should create resource server client', async () => { let wasCreateCalled = false; const resourceServerClient = { diff --git a/test/tools/auth0/handlers/connections.tests.js b/test/tools/auth0/handlers/connections.tests.js index da63e4823..9caa2112a 100644 --- a/test/tools/auth0/handlers/connections.tests.js +++ b/test/tools/auth0/handlers/connections.tests.js @@ -70,6 +70,14 @@ describe('#connections handler', () => { dpop_signing_alg: 'Ed25519', }, }, + { + name: 'samlp-connection', + strategy: 'samlp', + options: { + discovery_url: 'https://example-idp.com/.well-known/openid-configuration', + oidc_metadata: { issuer: 'https://example-idp.com' }, + }, + }, ]; const valid = ajv.validate(connections.schema, assets); @@ -130,6 +138,43 @@ describe('#connections handler', () => { expect(valid).to.equal(false); expect(ajv.errors).to.not.be.null; }); + + it('should allow cross_app_access_resource_app with status enum', () => { + const ajv = new Ajv({ useDefaults: true, nullable: true }); + const assets = [ + { + name: 'samlp-connection', + strategy: 'samlp', + cross_app_access_resource_app: { status: 'enabled' }, + }, + { + name: 'oidc-connection', + strategy: 'oidc', + cross_app_access_resource_app: { status: 'disabled' }, + }, + ]; + + const valid = ajv.validate(connections.schema, assets); + + expect(valid).to.equal(true); + expect(ajv.errors).to.be.null; + }); + + it('should reject cross_app_access_resource_app with invalid status', () => { + const ajv = new Ajv({ useDefaults: true, nullable: true }); + const assets = [ + { + name: 'samlp-connection', + strategy: 'samlp', + cross_app_access_resource_app: { status: 'on' }, + }, + ]; + + const valid = ajv.validate(connections.schema, assets); + + expect(valid).to.equal(false); + expect(ajv.errors).to.not.be.null; + }); }); describe('#connections validate', () => { @@ -596,6 +641,46 @@ describe('#connections handler', () => { await stageFn.apply(handler, [{ connections: data }]); }); + it('should pass cross_app_access_resource_app through in the update payload', async () => { + const auth0 = { + connections: { + create: () => Promise.resolve({ data: {} }), + update: function (id, data) { + expect(id).to.equal('con1'); + expect(data).to.have.deep.property('cross_app_access_resource_app', { + status: 'enabled', + }); + return Promise.resolve({ ...data, id }); + }, + delete: () => Promise.resolve({ data: [] }), + list: (params) => + mockPagedData(params, 'connections', [ + { name: 'someConnection', id: 'con1', strategy: 'custom' }, + ]), + _getRestClient: () => ({}), + clients: { + update: () => Promise.resolve({}), + }, + }, + clients: { + list: (params) => mockPagedData(params, 'clients', []), + }, + pool, + }; + + const handler = new connections.default({ client: pageClient(auth0), config }); + const stageFn = Object.getPrototypeOf(handler).processChanges; + const data = [ + { + name: 'someConnection', + strategy: 'custom', + cross_app_access_resource_app: { status: 'enabled' }, + }, + ]; + + await stageFn.apply(handler, [{ connections: data }]); + }); + it('should convert client name with ID in idpinitiated.client_id', async () => { const auth0 = { connections: {