Skip to content

feat: Add custom token exchange delegation and impersonation support#891

Open
utkrishtsahu wants to merge 2 commits into
mainfrom
feat/cte-delegation-impersonation
Open

feat: Add custom token exchange delegation and impersonation support#891
utkrishtsahu wants to merge 2 commits into
mainfrom
feat/cte-delegation-impersonation

Conversation

@utkrishtsahu

@utkrishtsahu utkrishtsahu commented Jul 9, 2026

Copy link
Copy Markdown
Contributor
  • 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

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 (customTokenExchange with a subject token) already shipped; this adds the actor token request params and exposes the act claim on the user profile.

Public API additions:

  • customTokenExchange (both Auth0().api.customTokenExchange on mobile and the web equivalent) now accepts two optional params:
    • actorToken (String) — the acting party's token
    • actorTokenType (String) — the acting party's token type
    • Both must be provided together; supplying only one throws an ArgumentError.
  • New UserActor model (sub, nested actor, and extraClaims) representing the act claim, including nested delegation chains.
  • UserProfile gains an actor field, populated from the act claim.
    Behavioral note:* when an actor token is present, Auth0 does not issue a refresh token regardless of the offline_access scope — the SDK handles a null refresh token gracefully.

Implementation by layer:

  • Platform interface: new UserActor model + export; UserProfile.actor; actorToken/actorTokenType on the mobile and web option types.
  • Mobile Dart API: params + pairing validation on customTokenExchange.
  • Android: handler builds ActorToken and calls the actor-aware overload; UserProfileExtensions maps the act claim recursively.
  • iOS/macOS: handler builds ActorToken and calls the actorToken: overload; Extensions/Properties map the act claim into UserInfo.
  • Web: actor_token/actor_token_type passed through the auth0-spa-js JS interop.
  • Docs: new Delegation & Impersonation section in EXAMPLES.md.

No native SDK version bump is required — Auth0.Android 3.20.0 and Auth0.swift 2.23.0 already ship the actor APIs.

📎 References

SDK-8925

🎯 Testing

Automated (added in this PR):

  • Platform interface: UserActor (fromMap/toMap, nested chains, missing sub), UserProfile act-claim parsing/emission, and options serialization.
  • Mobile Dart: customTokenExchange passes actor token/type through, leaves them null when omitted, and throws when only one of the pair is supplied.
  • Web: actor token passthrough and pairing-validation errors.
  • Android: handler passes ActorToken to the SDK (and omits it when incomplete); UserProfileExtensions maps the act claim incl. nested chains.
  • iOS/macOS: handler test asserts actor params reach the SDK via the exchange call, plus success/error/optional-argument coverage.

The Dart and Web suites pass locally (flutter analyze clean; 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):

  1. Enable the cte_actor_token feature flag on the tenant and set up an Action that calls api.authentication.setActor().
  2. Call customTokenExchange with a valid subject token plus actorToken/actorTokenType.
  3. Confirm credentials are returned and that the decoded ID token / UserProfile.actor reflects the expected act claim (including nested actors for delegation chains).

Summary by CodeRabbit

  • New Features

    • Added support for delegated/impersonation sign-in flows with optional actor token details.
    • User profiles can now include and preserve actor information from the act claim, including nested delegation data.
  • Bug Fixes

    • Improved validation so actor token values must be provided together.
    • When actor details are supplied, the app now reflects that no refresh token is returned in this flow.
  • Documentation

    • Expanded examples and API guidance for mobile and web usage, including how to read actor data.

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@utkrishtsahu, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 36 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 968597b2-d967-4ba7-ba97-ee3e6ce97d5e

📥 Commits

Reviewing files that changed from the base of the PR and between 2438d6f and f5b6564.

⛔ Files ignored due to path filters (1)
  • auth0_flutter/example/ios/Tests/AuthAPI/AuthAPIUserInfoMethodHandlerTests.swift is excluded by !**/example/**
📒 Files selected for processing (1)
  • auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/UserProfileExtensions.kt

Walkthrough

This PR adds actor token (delegation/impersonation) support to Custom Token Exchange across the auth0_flutter and auth0_flutter_platform_interface packages. It introduces a UserActor model, extends UserProfile, AuthCustomTokenExchangeOptions, and ExchangeTokenOptions with actorToken/actorTokenType, and updates Android, iOS, and Dart mobile/web implementations plus tests and docs.

Changes

Actor Token Delegation Support

Layer / File(s) Summary
UserActor model and options/contract updates
auth0_flutter_platform_interface/lib/src/user_actor.dart, .../user_profile.dart, .../auth/auth_custom_token_exchange_options.dart, .../web/exchange_token_options.dart, auth0_flutter_platform_interface.dart, test/user_actor_test.dart, test/user_profile_test.dart, test/auth_custom_token_exchange_options_test.dart
Adds UserActor model with fromMap/toMap, wires an actor field into UserProfile, and adds actorToken/actorTokenType fields to AuthCustomTokenExchangeOptions and ExchangeTokenOptions, with corresponding tests.
Android actor token handling
UserProfileExtensions.kt, CustomTokenExchangeApiRequestHandler.kt, related test files
Builds an ActorToken from request args when both fields are present and maps ActorClaim into an act map, with new tests.
iOS actor token handling
AuthAPICustomTokenExchangeMethodHandler.swift, Extensions.swift, Properties.swift
Parses actorToken/actorTokenType into an ActorToken, forwards it to customTokenExchange, and serializes ActClaim into the act dictionary key.
Dart mobile API
authentication_api.dart, test/mobile/authentication_api_test.dart
Adds actorToken/actorTokenType parameters with paired-presence validation and passthrough to AuthCustomTokenExchangeOptions, with tests.
Dart web API and JS interop
auth0_flutter_web.dart, exchange_token_options_extension.dart, js_interop.dart, test/web/auth0_flutter_web_test.dart
Adds paired-presence validation and forwards actor_token/actor_token_type through ExchangeTokenOptions into the JS interop layer, with tests.
Documentation
EXAMPLES.md
Documents the delegation/impersonation flow with Mobile and Web code examples.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Suggested reviewers: sanchitmehtagit, pmathew92

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: custom token exchange now supports delegation and impersonation.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/cte-delegation-impersonation

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 value

Validation and passthrough look correct.

XOR check on actorToken/actorTokenType matches the platform interface contract AuthCustomTokenExchangeOptions defines actorToken/actorTokenType fields and includes them in toMap only when non-null. Confirmed via search that actor_token/actor_token_type on the wire and api.authentication.setActor() for the act claim 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

📥 Commits

Reviewing files that changed from the base of the PR and between 00d4c13 and 2438d6f.

⛔ Files ignored due to path filters (2)
  • auth0_flutter/example/ios/Tests/AuthAPI/AuthAPICustomTokenExchangeMethodHandlerTests.swift is excluded by !**/example/**
  • auth0_flutter/example/ios/Tests/AuthAPI/AuthAPISpies.swift is excluded by !**/example/**
📒 Files selected for processing (22)
  • auth0_flutter/EXAMPLES.md
  • auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/UserProfileExtensions.kt
  • auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/api/CustomTokenExchangeApiRequestHandler.kt
  • auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/UserProfileExtensionsTest.kt
  • auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/api/CustomTokenExchangeApiRequestHandlerTest.kt
  • auth0_flutter/darwin/Classes/AuthAPI/AuthAPICustomTokenExchangeMethodHandler.swift
  • auth0_flutter/darwin/Classes/Extensions.swift
  • auth0_flutter/darwin/Classes/Properties.swift
  • auth0_flutter/lib/auth0_flutter_web.dart
  • auth0_flutter/lib/src/mobile/authentication_api.dart
  • auth0_flutter/lib/src/web/extensions/exchange_token_options_extension.dart
  • auth0_flutter/lib/src/web/js_interop.dart
  • auth0_flutter/test/mobile/authentication_api_test.dart
  • auth0_flutter/test/web/auth0_flutter_web_test.dart
  • auth0_flutter_platform_interface/lib/auth0_flutter_platform_interface.dart
  • auth0_flutter_platform_interface/lib/src/auth/auth_custom_token_exchange_options.dart
  • auth0_flutter_platform_interface/lib/src/user_actor.dart
  • auth0_flutter_platform_interface/lib/src/user_profile.dart
  • auth0_flutter_platform_interface/lib/src/web/exchange_token_options.dart
  • auth0_flutter_platform_interface/test/auth_custom_token_exchange_options_test.dart
  • auth0_flutter_platform_interface/test/user_actor_test.dart
  • auth0_flutter_platform_interface/test/user_profile_test.dart

Comment on lines +35 to +36
if (actorToken != null) 'actorToken': actorToken,
if (actorTokenType != null) 'actorTokenType': actorTokenType,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ 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

Comment on lines +64 to +68
Map<String, dynamic> toMap() => {
'sub': sub,
if (actor != null) 'act': actor!.toMap(),
...extraClaims,
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ 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.

Suggested change
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.

Comment on lines +140 to +175

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'});
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 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.

Suggested change
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

Comment on lines 38 to 39
args["subjectTokenType"] as String,
args["subjectToken"] as 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.

🩺 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.

Suggested change
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

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.

1 participant