feat: Add custom token exchange delegation and impersonation support#891
feat: Add custom token exchange delegation and impersonation support#891utkrishtsahu wants to merge 2 commits into
Conversation
|
Warning Review limit reached
Next review available in: 36 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (1)
WalkthroughThis PR adds actor token (delegation/impersonation) support to Custom Token Exchange across the auth0_flutter and auth0_flutter_platform_interface packages. It introduces a ChangesActor Token Delegation Support
Estimated code review effort: 3 (Moderate) | ~30 minutes Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
auth0_flutter/lib/src/mobile/authentication_api.dart (1)
427-450: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueValidation and passthrough look correct.
XOR check on
actorToken/actorTokenTypematches the platform interface contract AuthCustomTokenExchangeOptions defines actorToken/actorTokenType fields and includes them in toMap only when non-null. Confirmed via search thatactor_token/actor_token_typeon the wire andapi.authentication.setActor()for theactclaim are the correct Auth0 concepts being documented here.The same XOR validation block is duplicated verbatim in
auth0_flutter_web.dart(mobile vs web). Consider extracting a small shared helper (e.g.validateActorTokenPair(actorToken, actorTokenType)) to avoid the two copies drifting (e.g. inconsistent error message wording) over time.♻️ Optional dedup suggestion
// lib/src/utils/actor_token_validation.dart void validateActorTokenPair(final String? actorToken, final String? actorTokenType) { if ((actorToken == null) != (actorTokenType == null)) { throw ArgumentError( 'actorToken and actorTokenType must be provided together.'); } }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@auth0_flutter/lib/src/mobile/authentication_api.dart` around lines 427 - 450, The actorToken/actorTokenType XOR validation is duplicated in customTokenExchange across mobile and web, so extract it into a shared helper to keep the auth0_flutter APIs consistent. Add a small reusable validation function (for example, a helper used by AuthenticationApi.customTokenExchange and the web equivalent) that enforces the paired actorToken/actorTokenType rule and throws the same ArgumentError message from one place.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@auth0_flutter_platform_interface/lib/src/auth/auth_custom_token_exchange_options.dart`:
- Around line 35-36: `AuthCustomTokenExchangeOptions.toMap()` currently
serializes `actorToken` and `actorTokenType` independently, which can emit only
one of the pair even though both must be provided together. Update the `toMap()`
logic in `AuthCustomTokenExchangeOptions` so both keys are included only when
both `actorToken` and `actorTokenType` are non-null, and consider adding the
suggested constructor assert on `AuthCustomTokenExchangeOptions` for early
validation in debug mode.
In `@auth0_flutter_platform_interface/lib/src/user_actor.dart`:
- Around line 64-68: The UserActor.toMap() implementation lets extraClaims
overwrite the explicit sub and act entries because the spread is applied last.
Update UserActor.toMap() so sub and act cannot be replaced by extraClaims,
either by filtering those keys out before spreading or by placing the protected
fields after the spread; keep the behavior consistent with UserActor.fromMap()
and the constructor’s arbitrary extraClaims input.
In `@auth0_flutter_platform_interface/test/user_profile_test.dart`:
- Around line 140-175: Add a test that covers recursive delegation chains in
UserProfile.fromMap and UserActor, since current coverage only verifies a
single-level act claim. Create a map where act contains another nested act
object, then assert the outer actor is parsed with its actor field populated
correctly and any extraClaims preserved. Keep the new test near the existing
fromMap/toMap cases so the nested parsing behavior is validated alongside the
current UserProfile and UserActor serialization tests.
In
`@auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/api/CustomTokenExchangeApiRequestHandler.kt`:
- Around line 38-39: The subject-token extraction in
CustomTokenExchangeApiRequestHandler still uses unsafe casts for
subjectTokenType and subjectToken, which can crash with ClassCastException when
MethodChannel args have the wrong type. Update the request parsing in the
handler to use the same safe-cast pattern already used for actor token fields
(the relevant symbols are handleRequest and the subjectTokenType/subjectToken
reads), and make sure invalid types are handled gracefully instead of throwing.
In
`@auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/UserProfileExtensions.kt`:
- Around line 7-13: UserProfile.toMap() is still missing the act claim, so the
actor data produced by ActorClaim.toMap() never makes it into the returned
profile map. Update UserProfile.toMap() to include act (or equivalent actor
serialization) alongside the existing claims, and make sure the nested mapping
matches the shape produced by ActorClaim.toMap() so the Kotlin/Dart contract
stays aligned.
---
Outside diff comments:
In `@auth0_flutter/lib/src/mobile/authentication_api.dart`:
- Around line 427-450: The actorToken/actorTokenType XOR validation is
duplicated in customTokenExchange across mobile and web, so extract it into a
shared helper to keep the auth0_flutter APIs consistent. Add a small reusable
validation function (for example, a helper used by
AuthenticationApi.customTokenExchange and the web equivalent) that enforces the
paired actorToken/actorTokenType rule and throws the same ArgumentError message
from one place.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: e569034a-f0dc-4c7b-850a-90347807338c
⛔ Files ignored due to path filters (2)
auth0_flutter/example/ios/Tests/AuthAPI/AuthAPICustomTokenExchangeMethodHandlerTests.swiftis excluded by!**/example/**auth0_flutter/example/ios/Tests/AuthAPI/AuthAPISpies.swiftis excluded by!**/example/**
📒 Files selected for processing (22)
auth0_flutter/EXAMPLES.mdauth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/UserProfileExtensions.ktauth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/api/CustomTokenExchangeApiRequestHandler.ktauth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/UserProfileExtensionsTest.ktauth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/api/CustomTokenExchangeApiRequestHandlerTest.ktauth0_flutter/darwin/Classes/AuthAPI/AuthAPICustomTokenExchangeMethodHandler.swiftauth0_flutter/darwin/Classes/Extensions.swiftauth0_flutter/darwin/Classes/Properties.swiftauth0_flutter/lib/auth0_flutter_web.dartauth0_flutter/lib/src/mobile/authentication_api.dartauth0_flutter/lib/src/web/extensions/exchange_token_options_extension.dartauth0_flutter/lib/src/web/js_interop.dartauth0_flutter/test/mobile/authentication_api_test.dartauth0_flutter/test/web/auth0_flutter_web_test.dartauth0_flutter_platform_interface/lib/auth0_flutter_platform_interface.dartauth0_flutter_platform_interface/lib/src/auth/auth_custom_token_exchange_options.dartauth0_flutter_platform_interface/lib/src/user_actor.dartauth0_flutter_platform_interface/lib/src/user_profile.dartauth0_flutter_platform_interface/lib/src/web/exchange_token_options.dartauth0_flutter_platform_interface/test/auth_custom_token_exchange_options_test.dartauth0_flutter_platform_interface/test/user_actor_test.dartauth0_flutter_platform_interface/test/user_profile_test.dart
| if (actorToken != null) 'actorToken': actorToken, | ||
| if (actorTokenType != null) 'actorTokenType': actorTokenType, |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
Make actorToken/actorTokenType serialization atomic in toMap().
The doc comments state both fields must be provided together, but toMap() independently includes each field. If only one is set, the native handler silently drops the actor token — user intent is lost without error. Make the serialization conditional on both being present.
🔒 Proposed fix
if (organization != null) 'organization': organization,
- if (actorToken != null) 'actorToken': actorToken,
- if (actorTokenType != null) 'actorTokenType': actorTokenType,
+ if (actorToken != null && actorTokenType != null) ...{
+ 'actorToken': actorToken,
+ 'actorTokenType': actorTokenType,
+ },
};Optionally, add a constructor assert for early feedback in debug mode:
const AuthCustomTokenExchangeOptions({
required this.subjectToken,
required this.subjectTokenType,
this.audience,
this.scopes = const {},
this.organization,
this.actorToken,
this.actorTokenType,
}) : assert(
(actorToken == null) == (actorTokenType == null),
'actorToken and actorTokenType must be provided together',
);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@auth0_flutter_platform_interface/lib/src/auth/auth_custom_token_exchange_options.dart`
around lines 35 - 36, `AuthCustomTokenExchangeOptions.toMap()` currently
serializes `actorToken` and `actorTokenType` independently, which can emit only
one of the pair even though both must be provided together. Update the `toMap()`
logic in `AuthCustomTokenExchangeOptions` so both keys are included only when
both `actorToken` and `actorTokenType` are non-null, and consider adding the
suggested constructor assert on `AuthCustomTokenExchangeOptions` for early
validation in debug mode.
Source: Path instructions
| Map<String, dynamic> toMap() => { | ||
| 'sub': sub, | ||
| if (actor != null) 'act': actor!.toMap(), | ||
| ...extraClaims, | ||
| }; |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🔵 Trivial | 💤 Low value
extraClaims spread can overwrite sub and act in toMap().
If extraClaims contains a sub or act key, the spread at line 67 overwrites the explicitly set values at lines 65–66. While fromMap prevents this by excluding those keys, the constructor allows arbitrary extraClaims. Consider filtering or documenting this constraint.
🛡️ Optional guard
Map<String, dynamic> toMap() => {
'sub': sub,
if (actor != null) 'act': actor!.toMap(),
- ...extraClaims,
+ ...extraClaims..remove('sub')..remove('act'),
};📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| Map<String, dynamic> toMap() => { | |
| 'sub': sub, | |
| if (actor != null) 'act': actor!.toMap(), | |
| ...extraClaims, | |
| }; | |
| Map<String, dynamic> toMap() => { | |
| 'sub': sub, | |
| if (actor != null) 'act': actor!.toMap(), | |
| ...extraClaims..remove('sub')..remove('act'), | |
| }; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@auth0_flutter_platform_interface/lib/src/user_actor.dart` around lines 64 -
68, The UserActor.toMap() implementation lets extraClaims overwrite the explicit
sub and act entries because the spread is applied last. Update UserActor.toMap()
so sub and act cannot be replaced by extraClaims, either by filtering those keys
out before spreading or by placing the protected fields after the spread; keep
the behavior consistent with UserActor.fromMap() and the constructor’s arbitrary
extraClaims input.
|
|
||
| test('fromMap parses the act claim into an actor', () { | ||
| final map = { | ||
| 'sub': 'user123', | ||
| 'act': { | ||
| 'sub': 'actor-agent-123', | ||
| 'org': 'auth0', | ||
| 'role': 'support', | ||
| }, | ||
| }; | ||
|
|
||
| final userProfile = UserProfile.fromMap(map); | ||
|
|
||
| expect(userProfile.actor, isNotNull); | ||
| expect(userProfile.actor!.sub, 'actor-agent-123'); | ||
| expect(userProfile.actor!.actor, isNull); | ||
| expect(userProfile.actor!.extraClaims['org'], 'auth0'); | ||
| expect(userProfile.actor!.extraClaims['role'], 'support'); | ||
| }); | ||
|
|
||
| test('fromMap leaves actor null when act claim is absent', () { | ||
| final userProfile = UserProfile.fromMap({'sub': 'user123'}); | ||
|
|
||
| expect(userProfile.actor, isNull); | ||
| }); | ||
|
|
||
| test('toMap emits the act claim when actor is present', () { | ||
| const userProfile = UserProfile( | ||
| sub: 'user123', | ||
| actor: UserActor(sub: 'actor-agent-123', extraClaims: {'org': 'auth0'}), | ||
| ); | ||
|
|
||
| final map = userProfile.toMap(); | ||
|
|
||
| expect(map['act'], {'sub': 'actor-agent-123', 'org': 'auth0'}); | ||
| }); |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
Add test coverage for nested act claims (recursive delegation chains).
The UserActor model supports nested actors via the actor field, but no test exercises a recursive act claim. This is a core feature of the delegation/impersonation support.
🧪 Suggested test for nested actor
test('fromMap parses the act claim into an actor', () {
final map = {
'sub': 'user123',
'act': {
'sub': 'actor-agent-123',
'org': 'auth0',
'role': 'support',
},
};
final userProfile = UserProfile.fromMap(map);
expect(userProfile.actor, isNotNull);
expect(userProfile.actor!.sub, 'actor-agent-123');
expect(userProfile.actor!.actor, isNull);
expect(userProfile.actor!.extraClaims['org'], 'auth0');
expect(userProfile.actor!.extraClaims['role'], 'support');
});
+ test('fromMap parses nested act claims into a delegation chain', () {
+ final map = {
+ 'sub': 'user123',
+ 'act': {
+ 'sub': 'actor-agent-123',
+ 'org': 'auth0',
+ 'act': {
+ 'sub': 'delegated-agent-456',
+ 'role': 'admin',
+ },
+ },
+ };
+
+ final userProfile = UserProfile.fromMap(map);
+
+ expect(userProfile.actor, isNotNull);
+ expect(userProfile.actor!.sub, 'actor-agent-123');
+ expect(userProfile.actor!.extraClaims['org'], 'auth0');
+ expect(userProfile.actor!.actor, isNotNull);
+ expect(userProfile.actor!.actor!.sub, 'delegated-agent-456');
+ expect(userProfile.actor!.actor!.extraClaims['role'], 'admin');
+ expect(userProfile.actor!.actor!.actor, isNull);
+ });
+
+ test('toMap emits nested act claims when delegation chain is present', () {
+ const userProfile = UserProfile(
+ sub: 'user123',
+ actor: UserActor(
+ sub: 'actor-agent-123',
+ extraClaims: {'org': 'auth0'},
+ actor: UserActor(sub: 'delegated-agent-456', extraClaims: {'role': 'admin'}),
+ ),
+ );
+
+ final map = userProfile.toMap();
+
+ expect(map['act'], {
+ 'sub': 'actor-agent-123',
+ 'org': 'auth0',
+ 'act': {
+ 'sub': 'delegated-agent-456',
+ 'role': 'admin',
+ },
+ });
+ });📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| test('fromMap parses the act claim into an actor', () { | |
| final map = { | |
| 'sub': 'user123', | |
| 'act': { | |
| 'sub': 'actor-agent-123', | |
| 'org': 'auth0', | |
| 'role': 'support', | |
| }, | |
| }; | |
| final userProfile = UserProfile.fromMap(map); | |
| expect(userProfile.actor, isNotNull); | |
| expect(userProfile.actor!.sub, 'actor-agent-123'); | |
| expect(userProfile.actor!.actor, isNull); | |
| expect(userProfile.actor!.extraClaims['org'], 'auth0'); | |
| expect(userProfile.actor!.extraClaims['role'], 'support'); | |
| }); | |
| test('fromMap leaves actor null when act claim is absent', () { | |
| final userProfile = UserProfile.fromMap({'sub': 'user123'}); | |
| expect(userProfile.actor, isNull); | |
| }); | |
| test('toMap emits the act claim when actor is present', () { | |
| const userProfile = UserProfile( | |
| sub: 'user123', | |
| actor: UserActor(sub: 'actor-agent-123', extraClaims: {'org': 'auth0'}), | |
| ); | |
| final map = userProfile.toMap(); | |
| expect(map['act'], {'sub': 'actor-agent-123', 'org': 'auth0'}); | |
| }); | |
| test('fromMap parses the act claim into an actor', () { | |
| final map = { | |
| 'sub': 'user123', | |
| 'act': { | |
| 'sub': 'actor-agent-123', | |
| 'org': 'auth0', | |
| 'role': 'support', | |
| }, | |
| }; | |
| final userProfile = UserProfile.fromMap(map); | |
| expect(userProfile.actor, isNotNull); | |
| expect(userProfile.actor!.sub, 'actor-agent-123'); | |
| expect(userProfile.actor!.actor, isNull); | |
| expect(userProfile.actor!.extraClaims['org'], 'auth0'); | |
| expect(userProfile.actor!.extraClaims['role'], 'support'); | |
| }); | |
| test('fromMap parses nested act claims into a delegation chain', () { | |
| final map = { | |
| 'sub': 'user123', | |
| 'act': { | |
| 'sub': 'actor-agent-123', | |
| 'org': 'auth0', | |
| 'act': { | |
| 'sub': 'delegated-agent-456', | |
| 'role': 'admin', | |
| }, | |
| }, | |
| }; | |
| final userProfile = UserProfile.fromMap(map); | |
| expect(userProfile.actor, isNotNull); | |
| expect(userProfile.actor!.sub, 'actor-agent-123'); | |
| expect(userProfile.actor!.extraClaims['org'], 'auth0'); | |
| expect(userProfile.actor!.actor, isNotNull); | |
| expect(userProfile.actor!.actor!.sub, 'delegated-agent-456'); | |
| expect(userProfile.actor!.actor!.extraClaims['role'], 'admin'); | |
| expect(userProfile.actor!.actor!.actor, isNull); | |
| }); | |
| test('fromMap leaves actor null when act claim is absent', () { | |
| final userProfile = UserProfile.fromMap({'sub': 'user123'}); | |
| expect(userProfile.actor, isNull); | |
| }); | |
| test('toMap emits nested act claims when delegation chain is present', () { | |
| const userProfile = UserProfile( | |
| sub: 'user123', | |
| actor: UserActor( | |
| sub: 'actor-agent-123', | |
| extraClaims: {'org': 'auth0'}, | |
| actor: UserActor(sub: 'delegated-agent-456', extraClaims: {'role': 'admin'}), | |
| ), | |
| ); | |
| final map = userProfile.toMap(); | |
| expect(map['act'], { | |
| 'sub': 'actor-agent-123', | |
| 'org': 'auth0', | |
| 'act': { | |
| 'sub': 'delegated-agent-456', | |
| 'role': 'admin', | |
| }, | |
| }); | |
| }); | |
| test('toMap emits the act claim when actor is present', () { | |
| const userProfile = UserProfile( | |
| sub: 'user123', | |
| actor: UserActor(sub: 'actor-agent-123', extraClaims: {'org': 'auth0'}), | |
| ); | |
| final map = userProfile.toMap(); | |
| expect(map['act'], {'sub': 'actor-agent-123', 'org': 'auth0'}); | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@auth0_flutter_platform_interface/test/user_profile_test.dart` around lines
140 - 175, Add a test that covers recursive delegation chains in
UserProfile.fromMap and UserActor, since current coverage only verifies a
single-level act claim. Create a map where act contains another nested act
object, then assert the outer actor is parsed with its actor field populated
correctly and any extraClaims preserved. Keep the new test near the existing
fromMap/toMap cases so the nested parsing behavior is validated alongside the
current UserProfile and UserActor serialization tests.
Source: Path instructions
| args["subjectTokenType"] as String, | ||
| args["subjectToken"] as String, |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Replace force casts with safe casts for subjectTokenType and subjectToken.
assertHasProperties on line 25 checks key presence but not type. If a non-String value is passed for either key, as String throws ClassCastException — a known crash pattern in MethodChannel handlers. The new actor-token code on lines 29–30 correctly uses as? String; the subject-token casts should follow the same pattern.
As per path instructions: "ClassCastException from unsafe casts in MethodChannel handlers has caused crashes in the past — treat any unchecked cast as a bug."
🔒 Proposed fix
val args = request.data
- assertHasProperties(listOf("subjectToken", "subjectTokenType"), args)
+ assertHasProperties(listOf("subjectToken", "subjectTokenType"), args)
+
+ val subjectTokenType = args["subjectTokenType"] as? String
+ ?: return result.error("invalid_argument", "subjectTokenType must be a String", null)
+ val subjectToken = args["subjectToken"] as? String
+ ?: return result.error("invalid_argument", "subjectToken must be a String", null)
val organization = if (args["organization"] is String) args["organization"] as String else null
val actorTokenValue = args["actorToken"] as? String
val actorTokenType = args["actorTokenType"] as? String
val actorToken = if (actorTokenValue != null && actorTokenType != null) {
ActorToken(actorTokenValue, actorTokenType)
} else {
null
}
val builder = api.customTokenExchange(
- args["subjectTokenType"] as String,
- args["subjectToken"] as String,
+ subjectTokenType,
+ subjectToken,
organization,
actorToken
).apply {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| args["subjectTokenType"] as String, | |
| args["subjectToken"] as String, | |
| val subjectTokenType = args["subjectTokenType"] as? String | |
| ?: return result.error("invalid_argument", "subjectTokenType must be a String", null) | |
| val subjectToken = args["subjectToken"] as? String | |
| ?: return result.error("invalid_argument", "subjectToken must be a String", null) |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/api/CustomTokenExchangeApiRequestHandler.kt`
around lines 38 - 39, The subject-token extraction in
CustomTokenExchangeApiRequestHandler still uses unsafe casts for
subjectTokenType and subjectToken, which can crash with ClassCastException when
MethodChannel args have the wrong type. Update the request parsing in the
handler to use the same safe-cast pattern already used for actor token fields
(the relevant symbols are handleRequest and the subjectTokenType/subjectToken
reads), and make sure invalid types are handled gracefully instead of throwing.
Source: Path instructions
📋 Changes
This PR adds Custom Token Exchange (CTE) Delegation & Impersonation support (RFC 8693), achieving parity with the native SDKs (auth0-spa-js #1608, Auth0.swift #1190, Auth0.Android #975). The base CTE (
customTokenExchangewith a subject token) already shipped; this adds the actor token request params and exposes theactclaim on the user profile.Public API additions:
customTokenExchange(bothAuth0().api.customTokenExchangeon mobile and the web equivalent) now accepts two optional params:actorToken(String) — the acting party's tokenactorTokenType(String) — the acting party's token typeArgumentError.UserActormodel (sub, nestedactor, andextraClaims) representing theactclaim, including nested delegation chains.UserProfilegains anactorfield, populated from theactclaim.Behavioral note:* when an actor token is present, Auth0 does not issue a refresh token regardless of the
offline_accessscope — the SDK handles a null refresh token gracefully.Implementation by layer:
UserActormodel + export;UserProfile.actor;actorToken/actorTokenTypeon the mobile and web option types.customTokenExchange.ActorTokenand calls the actor-aware overload;UserProfileExtensionsmaps theactclaim recursively.ActorTokenand calls theactorToken:overload;Extensions/Propertiesmap theactclaim intoUserInfo.actor_token/actor_token_typepassed through the auth0-spa-js JS interop.EXAMPLES.md.No native SDK version bump is required — Auth0.Android
3.20.0and Auth0.swift2.23.0already ship the actor APIs.📎 References
SDK-8925
🎯 Testing
Automated (added in this PR):
UserActor(fromMap/toMap, nested chains, missingsub),UserProfileact-claim parsing/emission, and options serialization.customTokenExchangepasses actor token/type through, leaves them null when omitted, and throws when only one of the pair is supplied.ActorTokento the SDK (and omits it when incomplete);UserProfileExtensionsmaps theactclaim incl. nested chains.The Dart and Web suites pass locally (
flutter analyzeclean; platform-interface, mobile, and web tests green). The Android and iOS/macOS native tests were verified against the pinned SDK sources.Manual end-to-end (for reviewers with a configured tenant):
cte_actor_tokenfeature flag on the tenant and set up an Action that callsapi.authentication.setActor().customTokenExchangewith a valid subject token plusactorToken/actorTokenType.UserProfile.actorreflects the expectedactclaim (including nested actors for delegation chains).Summary by CodeRabbit
New Features
actclaim, including nested delegation data.Bug Fixes
Documentation