Skip to content

feat(auth0-auth-js): add actor token support and act claim to Custom Token Exchange#175

Merged
cschetan77 merged 10 commits into
mainfrom
feature/support-act-claim
Jun 12, 2026
Merged

feat(auth0-auth-js): add actor token support and act claim to Custom Token Exchange#175
cschetan77 merged 10 commits into
mainfrom
feature/support-act-claim

Conversation

@cschetan77

@cschetan77 cschetan77 commented May 25, 2026

Copy link
Copy Markdown
Contributor

Description

Extends the Custom Token Exchange (CTE) implementation in auth0-auth-js to support RFC 8693 delegation flows via actor tokens.

  • Adds actorToken and actorTokenType optional fields to ExchangeProfileOptions, forwarded as actor_token and actor_token_type in the token request. Both remain in PARAM_DENYLIST to prevent injection via extra — callers must use the typed fields.
  • Adds the ActClaim interface ({ sub: string; [key: string]: unknown }) per RFC 8693 Section 4.1. The index signature allows for additional server-provided fields (e.g. chained delegation, iss).
  • Adds act?: ActClaim to TokenResponse, populated exclusively inside #exchangeProfileToken() (not in the shared fromTokenEndpointResponse()). Prefers the act claim from the verified ID token; falls back to decoding the JWT access token for M2M flows where no ID token is issued. Wrapped in try/catch to handle opaque access tokens gracefully.
  • No client-side URI validation for subjectTokenType or actorTokenType — invalid URIs are rejected by the Auth0 server, consistent with Swift, Android, and spa-js behaviour.

Examples

Added a full "Custom Token Exchange" section to EXAMPLES.md covering basic exchange, delegation with actor token, reading act, M2M fallback behaviour, and error handling.

Testing

Unit Tests

  • should send actor_token and actor_token_type when provided: verifies request params and result.act are correctly wired
  • should expose act claim from id_token on TokenResponse: verifies extra fields (e.g. iss) on act are accessible
  • should not send actor_token or actor_token_type when omitted: verifies no regression on existing exchanges
  • should throw TokenExchangeError when actorToken is provided without actorTokenType
  • should not set act when access token is opaque and no id_token is returned
  • should prefer act from id_token over act from access token when both are present
  • should expose act claim from access token when no id_token is returned (M2M delegation)

Manual Tests

  • Tested end-to-end against a live Auth0 tenant using a locally built version.

  • The test covered a delegation exchange where a service actor exchanges a user's token on their behalf using actorToken and actorTokenType parameters. The following behaviours were verified:

    • The exchange completes successfully and returns a valid access token and ID token
    • The act claim is present on the TokenResponse, correctly populated from the ID token with the actor identity set by the CTE Action
    • The refresh token is suppressed by Auth0 when an actor_token is present in the request, and the SDK handles the absence gracefully

Checklist

  • I have added documentation for new/changed functionality in this PR or in auth0.com/docs
  • All active GitHub checks for tests, formatting, and security are passing
  • The correct base branch is being used, if not the default branch

@cschetan77 cschetan77 force-pushed the feature/support-act-claim branch from 77b32f4 to 1ef94d5 Compare June 3, 2026 11:45
Comment thread packages/auth0-auth-js/src/types.ts Outdated

tokenResponse.tokenType = response.token_type;
tokenResponse.issuedTokenType = (response as typeof response & { issued_token_type?: string }).issued_token_type;
const atClaims = decodeJwt(response.access_token);

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.

What will happen if Auth0 issues an opaque token.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

yes, this condition needs to be handled. Updated to catch the error when it's a opaque token and populate the act claim to be undefined.

Comment thread packages/auth0-auth-js/src/types.ts Outdated
tokenResponse.tokenType = response.token_type;
tokenResponse.issuedTokenType = (response as typeof response & { issued_token_type?: string }).issued_token_type;
const atClaims = decodeJwt(response.access_token);
tokenResponse.act = (claims?.act ?? atClaims.act) as ActClaim | undefined;

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.

Does that mean, tokenResponse would always have act ?
Because fromTokenEndpointResponse is called from multiple places.

@cschetan77 cschetan77 Jun 8, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

yes, tokenResponse from any exchange would populate the act claim, but it will be undefined except response from exchangeProfileToken.

I agree, the static method fromTokenEndpointResponse is called at many places. And none of them need the act claim population logic except exchangeProfileToken.
It seems, a better design choice will be to keep the act claim population logic in that specific method itself.

Update: The act claim population logic has been -

  1. Removed from fromTokenEndpointResponse static method.
  2. Added in exchangeProfileToken method, which is the only method that would ever populate it.

What do you suggest!

@nandan-bhat

Copy link
Copy Markdown
Contributor
  • Please add tests for opaque token scenarios.
  • Please update documentation (EXAMPLES.md) elaborating this feature.

Comment thread packages/auth0-auth-js/src/types.ts
* the acting party on whose behalf the subject token was exchanged.
*
* @see {@link https://www.rfc-editor.org/rfc/rfc8693#section-4.1 RFC 8693 Section 4.1}
*/

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 as the ActClaim interface above: act is defined in Section 4.4, not 4.1. Should be #section-4.4.


validateSubjectToken(options.subjectToken);
validateTokenTypeUri(options.subjectTokenType, 'subjectTokenType');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Two concerns here:

  • The team aligned that token-type URI validation stays server-side, no other SDK (Swift, Android, spa-js) does client-side URI validation. Adding it here for subjectTokenType (which previously had none) breaks that alignment and is a behavior change for existing callers.

  • new URL() is the wrong validator and risks false rejections. It can reject values the Auth0 server would actually accept, turning a server-side concern into a client-side breaking change on upgrade. (new URL('urn:acme:custom-token') happens to pass, but the general class of valid token-type identifiers isn't guaranteed to parse as a WHATWG URL.)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, that makes complete sense.
Removed the entire URI validation logic.

Comment thread packages/auth0-auth-js/src/auth-client.ts
@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@cschetan77, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 6 minutes and 43 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more credits in the billing tab to continue.

⌛ How to resolve this issue?

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.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6795b868-8bbc-4247-960c-79fd85735d32

📥 Commits

Reviewing files that changed from the base of the PR and between ed6e9c8 and 2340f75.

📒 Files selected for processing (2)
  • packages/auth0-auth-js/src/auth-client.spec.ts
  • packages/auth0-auth-js/src/auth-client.ts
📝 Walkthrough

Walkthrough

Adds RFC 8693 actor-token support: new types, request validation/wiring to include actor_token/actor_token_type, extraction of the act claim from id_token or JWT access_token, expanded tests, and example docs; several tests were refactored to use generated access tokens.

Changes

Actor Token Delegation Feature

Layer / File(s) Summary
Type definitions and contracts
packages/auth0-auth-js/src/types.ts
ExchangeProfileOptions adds actorToken? and actorTokenType?; new ActClaim interface; TokenResponse adds optional act property.
JWT import and docs
packages/auth0-auth-js/src/auth-client.ts
Import decodeJwt from jose, import ActClaim type, and update PARAM_DENYLIST comment to clarify actor parameter handling.
Exchange request construction with actor token
packages/auth0-auth-js/src/auth-client.ts
When actorToken is provided require actorTokenType; conditionally append actor_token and actor_token_type to the token endpoint request.
Extract and populate act claim from response
packages/auth0-auth-js/src/auth-client.ts
After token endpoint response, prefer act from id_token; if absent, attempt to decode access_token as JWT to extract act, ignoring decode errors for opaque tokens.
AuthClient exchangeToken actor tests
packages/auth0-auth-js/src/auth-client.spec.ts
New exchangeToken — actor token support tests: verify actor params are sent when provided, omitted when not provided, enforce actorTokenType requirement, and assert act extraction precedence and M2M behavior; minor comment cleanups.
Update api-client exchange tests with dynamic tokens
packages/auth0-api-js/src/api-client.spec.ts
Refactor getAccessTokenForConnection, getTokenByExchangeProfile, and getTokenOnBehalfOf tests to generate access tokens dynamically and assert returned tokens against generated values.
Update mfa-client verify tests with dynamic tokens
packages/auth0-auth-js/src/mfa/mfa-client.spec.ts
Generate distinct access tokens for OTP, OOB, recovery-code, and a generic token; update token endpoint mocks and assertions to use generated values.
Documentation: Custom Token Exchange examples
packages/auth0-auth-js/EXAMPLES.md
Add “Custom Token Exchange” section and TOC entries with examples for basic exchange, delegation with actor token, reading tokens.act, M2M delegation behavior, and error handling.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • pmathew92

Poem

🐰 I hopped through tokens, claims in hand,

actor parameters now take a stand,
id or access, the act will show,
tests refreshed so tokens flow,
RFC 8693 — a tidy land.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.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
Title check ✅ Passed The title 'feat(auth0-auth-js): add actor token support and act claim to Custom Token Exchange' accurately reflects the main changes: adding actor token parameters (actorToken, actorTokenType) and the act claim support to the token exchange implementation.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/support-act-claim

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 and usage tips.

@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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/auth0-auth-js/src/auth-client.ts (1)

772-799: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate actor-token pairs symmetrically.

Line 772 treats actorToken: '' as “provided”, but Line 796 drops it because the append branch is truthy-based. That silently downgrades a delegation exchange into a plain subject-token exchange instead of failing fast. actorTokenType without actorToken is also accepted and then ignored, even though the public contract says the pair must be used together.

Proposed fix
-    if (options.actorToken !== undefined && options.actorTokenType === undefined) {
-      throw new TokenExchangeError('actorTokenType is required when actorToken is provided');
-    }
-    if (options.actorTokenType !== undefined) {
-      validateTokenTypeUri(options.actorTokenType, 'actorTokenType');
+    if ((options.actorToken === undefined) !== (options.actorTokenType === undefined)) {
+      throw new TokenExchangeError('actorToken and actorTokenType must be provided together');
+    }
+    if (options.actorToken !== undefined) {
+      if (options.actorToken.trim().length === 0) {
+        throw new TokenExchangeError('actorToken cannot be blank or whitespace');
+      }
+      validateTokenTypeUri(options.actorTokenType, 'actorTokenType');
     }
@@
-    if (options.actorToken) {
+    if (options.actorToken !== undefined) {
       tokenRequestParams.append('actor_token', options.actorToken);
       tokenRequestParams.append('actor_token_type', options.actorTokenType!);
     }
🤖 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 `@packages/auth0-auth-js/src/auth-client.ts` around lines 772 - 799, The code
treats options.actorToken='' as provided but later ignores falsy values when
appending, and allows actorTokenType without actorToken; update the validation
in the token exchange branch so the actor pair is treated symmetrically: treat
an empty string as "provided" (i.e. if actorToken is !== undefined but
actorToken === '' or actorTokenType is missing, throw TokenExchangeError), and
likewise if actorTokenType is provided without actorToken throw; only call
validateTokenTypeUri('actorTokenType') and append actor_token/actor_token_type
to tokenRequestParams when both options.actorToken and options.actorTokenType
are present and non-empty (use explicit !== undefined and !== '' checks), and
keep references to TokenExchangeError, validateTokenTypeUri, options.actorToken,
options.actorTokenType, and tokenRequestParams to locate the changes.
🧹 Nitpick comments (1)
packages/auth0-auth-js/src/auth-client.spec.ts (1)

3036-3059: ⚡ Quick win

Consider adding test coverage for opaque access tokens.

The test suite covers M2M delegation when the access token is a JWT, but there's no explicit test for opaque (non-JWT) access tokens. Based on the implementation (context snippet shows a try-catch around decodeJwt), opaque tokens should be handled gracefully with act remaining undefined.

🧪 Suggested test for opaque token handling

Add a test after line 3059:

+  test('should handle opaque access tokens gracefully (act undefined when decode fails)', async () => {
+    const authClient = new AuthClient({ domain, clientId: '<client_id>', clientSecret: '<client_secret>' });
+
+    const opaqueAccessToken = 'opaque-token-not-a-jwt';
+
+    server.use(
+      http.post(mockOpenIdConfiguration.token_endpoint, async () => {
+        return HttpResponse.json({
+          access_token: opaqueAccessToken,
+          expires_in: 3600,
+          token_type: 'Bearer',
+          scope: 'read:data',
+          // no id_token — M2M flow
+        });
+      })
+    );
+
+    const result = await authClient.exchangeToken(baseOptions);
+
+    expect(result.accessToken).toBe(opaqueAccessToken);
+    expect(result.act).toBeUndefined();
+  });
🤖 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 `@packages/auth0-auth-js/src/auth-client.spec.ts` around lines 3036 - 3059, Add
a new unit test after the existing M2M JWT test that verifies opaque access
tokens are handled: simulate the token endpoint returning a non-JWT opaque
access_token (e.g., a random string like 'opaque-token-123') with no id_token,
call AuthClient.exchangeToken (same baseOptions) and assert that the returned
result.act is undefined; this exercises the decodeJwt error path and ensures no
exception is thrown. Reference the existing test pattern and functions:
AuthClient, exchangeToken, and the token endpoint mock used in the file.
🤖 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 `@packages/auth0-auth-js/src/types.ts`:
- Around line 635-643: The docblock for the act property is inaccurate: update
the comment for the act?: ActClaim on TokenResponse to reflect runtime behavior
of `#exchangeProfileToken`() — it can be populated from the id_token or, if no
id_token is issued, from the JWT access_token. Mention both sources (id_token
and JWT access_token) and keep the reference to ActClaim and RFC 8693 for
context so consumers see the correct runtime semantics for TokenResponse.act.

---

Outside diff comments:
In `@packages/auth0-auth-js/src/auth-client.ts`:
- Around line 772-799: The code treats options.actorToken='' as provided but
later ignores falsy values when appending, and allows actorTokenType without
actorToken; update the validation in the token exchange branch so the actor pair
is treated symmetrically: treat an empty string as "provided" (i.e. if
actorToken is !== undefined but actorToken === '' or actorTokenType is missing,
throw TokenExchangeError), and likewise if actorTokenType is provided without
actorToken throw; only call validateTokenTypeUri('actorTokenType') and append
actor_token/actor_token_type to tokenRequestParams when both options.actorToken
and options.actorTokenType are present and non-empty (use explicit !== undefined
and !== '' checks), and keep references to TokenExchangeError,
validateTokenTypeUri, options.actorToken, options.actorTokenType, and
tokenRequestParams to locate the changes.

---

Nitpick comments:
In `@packages/auth0-auth-js/src/auth-client.spec.ts`:
- Around line 3036-3059: Add a new unit test after the existing M2M JWT test
that verifies opaque access tokens are handled: simulate the token endpoint
returning a non-JWT opaque access_token (e.g., a random string like
'opaque-token-123') with no id_token, call AuthClient.exchangeToken (same
baseOptions) and assert that the returned result.act is undefined; this
exercises the decodeJwt error path and ensures no exception is thrown. Reference
the existing test pattern and functions: AuthClient, exchangeToken, and the
token endpoint mock used in the file.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6098b860-e4e0-41f3-969c-9809b79d5bef

📥 Commits

Reviewing files that changed from the base of the PR and between 4cf0944 and 0959214.

📒 Files selected for processing (5)
  • packages/auth0-api-js/src/api-client.spec.ts
  • packages/auth0-auth-js/src/auth-client.spec.ts
  • packages/auth0-auth-js/src/auth-client.ts
  • packages/auth0-auth-js/src/mfa/mfa-client.spec.ts
  • packages/auth0-auth-js/src/types.ts

Comment thread packages/auth0-auth-js/src/types.ts
kishore7snehil
kishore7snehil previously approved these changes Jun 10, 2026

@kishore7snehil kishore7snehil 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.

LGTM!

@cschetan77 cschetan77 force-pushed the feature/support-act-claim branch from c44ba1d to af8e6d6 Compare June 10, 2026 12:49
  - Added ActClaim interface (sub: string + open index signature for extra claims per RFC 8693)
  - Added actorToken? and actorTokenType? to ExchangeProfileOptions
  - Added act?: ActClaim property to TokenResponse
  - Populated act from parsed ID token claims in fromTokenEndpointResponse()

  auth-client.ts
  - Removed actor_token and actor_token_type from PARAM_DENYLIST (they now have explicit typed params)
  - Updated the denylist comment block to remove the stale bullet
  - Added validateTokenTypeUri() — syntactic-only new URL() check, throws TokenExchangeError with a clear message
  - In #exchangeProfileToken(): calls validateTokenTypeUri for both subjectTokenType and actorTokenType, then appends actor_token/actor_token_type to the request when present

  auth-client.spec.ts
  - New describe('exchangeToken — actor token support') block with 5 tests covering: params wired + act claim populated, act claim with extra fields, params absent when not provided, invalid URI validation (both params in one test), valid URI acceptance
…_toke and actor_token_type to param deny list
@cschetan77 cschetan77 merged commit d179cae into main Jun 12, 2026
15 of 16 checks passed
@cschetan77 cschetan77 deleted the feature/support-act-claim branch June 12, 2026 08:42
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