Skip to content

feat: CTE delegation and impersonation support#1190

Open
NandanPrabhu wants to merge 2 commits into
masterfrom
feat/cte-delegation-impersonation
Open

feat: CTE delegation and impersonation support#1190
NandanPrabhu wants to merge 2 commits into
masterfrom
feat/cte-delegation-impersonation

Conversation

@NandanPrabhu
Copy link
Copy Markdown
Contributor

@NandanPrabhu NandanPrabhu commented May 29, 2026

  • All new/changed/fixed functionality is covered by tests (or N/A)
  • I have added documentation for all new/changed functionality (or N/A)

📋 Changes

Adds delegation and impersonation support for Custom Token Exchange (CTE) per RFC 8693, allowing an actor (e.g. an AI agent or support rep) to act on behalf of a user.

Types added:

  • ActorToken — A struct that bundles token and tokenType together, ensuring both are always provided when performing delegation/impersonation token exchanges (per RFC 8693).
  • ActClaim — A final class representing the act (actor) claim from an ID token. Supports recursive nesting for multi-hop delegation chains. Exposes sub, nested act, and additionalClaims.

Methods changed:

  • Authentication.customTokenExchange(...) — Added optional actorToken: ActorToken? parameter. When provided, actor_token and actor_token_type are sent in the token exchange request.

Properties added:

  • UserInfo.act: ActClaim? — Parses the act claim from the ID token JSON body. Also added "act" to UserInfo.publicClaims so it is excluded from customClaims.

Documentation:

  • Updated EXAMPLES.md with sections on actor token delegation and reading the act claim.
  • Updated README.md to mention custom token exchange in the comprehensive guides note.

📎 References

🎯 Testing

  • Unit tests added in AuthenticationSpec for actor token parameter in CTE requests.
  • Unit tests added in UserInfoSpec for act claim parsing: nil when absent, basic parsing, nested delegation chains, and exclusion from custom claims.
  • Run swift test to verify all tests pass.

@NandanPrabhu NandanPrabhu requested a review from a team as a code owner May 29, 2026 05:01
Comment thread Auth0/ActClaim.swift
///
/// - [RFC 8693: OAuth 2.0 Token Exchange - act Claim](https://tools.ietf.org/html/rfc8693#section-4.1)
/// - [Custom Token Exchange Documentation](https://auth0.com/docs/authenticate/custom-token-exchange)
public final class ActClaim: Sendable {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could this be struct instead

Comment thread Auth0/ActClaim.swift
/// ## See Also
///
/// - [RFC 8693: OAuth 2.0 Token Exchange - act Claim](https://tools.ietf.org/html/rfc8693#section-4.1)
/// - [Custom Token Exchange Documentation](https://auth0.com/docs/authenticate/custom-token-exchange)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This references RFC 8693 Section 4.1, but the act claim is defined in Section 4.4 ("Delegation Semantics"). Section 4.1 covers the token exchange response structure.

Suggestion: https://tools.ietf.org/html/rfc8693#section-4.4

Comment thread Auth0/ActClaim.swift

/// The subject identifier of the acting party.
public let sub: String?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per RFC 8693 Section 4.4: "The sub claim is required" within an act claim. The Android SDK (ActorClaim) makes sub non-optional and returns null for the entire object if sub is missing from the JSON.

Here, sub is String? which means callers always need to nil-check even though a valid act claim per the RFC will always have it. This creates inconsistent behavior across platforms.

Consider either:

  1. Making sub non-optional (String) and returning nil from init? if sub is absent, or
  2. Documenting why the lenient approach was chosen (e.g., forward-compatibility with potential spec changes)

Comment thread Auth0/ActClaim.swift
var additional: [String: String] = [:]
for (key, value) in json where key != "sub" && key != "act" {
if let stringValue = value as? String {
additional[key] = stringValue
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

additionalClaims is [String: String] and the initializer only keeps values that cast to String, silently dropping non-string JSON values (booleans, numbers, objects, arrays). While RFC 8693 act claims primarily use string values like sub, custom Auth0 Actions could set non-string values via api.authentication.setActor() that would be lost here.

scope: String,
organization: String?,
actorToken: ActorToken?,
parameters: [String: Any]) -> Request<Credentials, AuthenticationError>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding actorToken: ActorToken? as a required parameter to the public Authentication protocol is a source-breaking change for anyone who has created a custom conformance . Their implementation will fail to compile until they add the new parameter.

Consider noting it in the changelog, or providing a default implementation in a protocol extension to avoid breaking custom conformers.

}
}
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test verifies actor_token/actor_token_type ARE sent when provided, but there's no corresponding test verifying they are NOT present in the request body when actorToken is nil (the default).

The hasAllOf matcher only checks for presence of specified keys, not absence of others. Consider adding a test like:

it("should not include actor token params when actorToken is nil") {
    // ... call customTokenExchange without actorToken
    // assert body does NOT contain "actor_token" or "actor_token_type"
}

The Android PR includes this test (shouldSendCTERequestWithoutActorToken).

Comment thread Auth0/UserInfo.swift
///
/// - ``ActClaim``
/// - [RFC 8693: OAuth 2.0 Token Exchange](https://tools.ietf.org/html/rfc8693#section-4.1)
public let act: ActClaim?
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue as in ActClaim.swift: this links to Section 4.1 but the act claim is defined in Section 4.4 ("Delegation Semantics"). Section 4.1 covers the token exchange response structure.

Should be: https://tools.ietf.org/html/rfc8693#section-4.4

Comment thread EXAMPLES.md
#### Reading the `act` claim from the ID token

When a token exchange involves an actor token, Auth0 may include an `act` (actor) claim in the resulting ID token. This claim identifies who is acting on behalf of the subject and may be nested to represent delegation chains.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This says "Auth0 may include an act claim" but doesn't explain when it will. The act claim only appears when an Action is configured server-side that calls api.authentication.setActor(). Without that, the response will never have an act claim regardless of whether actor_token is sent.

Consider adding a note like:

The act claim is set server-side via an Auth0 Action that calls api.authentication.setActor(). Without this Action configured, the ID token will not contain an act claim even when an actor token is provided in the request.

Comment thread EXAMPLES.md

```swift
import JWTDecode

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JWTDecode is a separate package (JWTDecode.swift) that needs to be added as a dependency. Developers copying this example may not know they need to add it to their project.

Consider adding a brief note: "This example uses JWTDecode.swift — add it to your project if you haven't already."

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants