Skip to content

feat(auth0-server-js): add loginWithCustomTokenExchange and customTokenExchange methods#176

Merged
nandan-bhat merged 16 commits into
mainfrom
feat/custom-token-exchange
Jun 15, 2026
Merged

feat(auth0-server-js): add loginWithCustomTokenExchange and customTokenExchange methods#176
nandan-bhat merged 16 commits into
mainfrom
feat/custom-token-exchange

Conversation

@cschetan77

@cschetan77 cschetan77 commented May 25, 2026

Copy link
Copy Markdown
Contributor

Description

Adds first-class Custom Token Exchange (CTE) support to ServerClient in auth0-server-js, building on the actor token and act claim foundation added to auth0-auth-js.

  • loginWithCustomTokenExchange(options, storeOptions?): exchanges a custom external token (e.g. a Google ID token, a legacy system token) for Auth0 tokens and persists the result as a user session in the StateStore. Equivalent to completing an interactive login flow, but without a browser redirect. Calls stateStore.set with removeIfExists: true to prevent session fixation. Returns Promise<LoginWithCustomTokenExchangeResult>, surfacing authorizationDetails from the token response for callers using RAR (authorization_details in extra).
  • customTokenExchange(options, storeOptions?): performs the same RFC 8693 token exchange but returns the raw TokenResponse without touching the StateStore. Designed for delegation and impersonation flows where downstream tokens are needed but no session should be created or modified.
  • LoginWithCustomTokenExchangeOptions and CustomTokenExchangeOptions type aliases in types.ts, both derived from ExchangeProfileOptions in auth0-auth-js. This keeps the types consistent across the monorepo and inherits actorToken, actorTokenType, and all other exchange options automatically. Also added LoginWithCustomTokenExchangeResult interface to types.ts

Both methods follow the same domain resolution and AuthClient routing pattern used by loginBackchannel, supporting both static domain and resolver mode configurations.

The return of Promise<LoginWithCustomTokenExchangeResult> containing authorizationDetails is kept in consistent with completeInteractiveLogin and loginBackchannel methods. Though the case for returning it is narrow.

Testing

Unit tests

  • loginWithCustomTokenExchange - should persist session after successful exchange
  • loginWithCustomTokenExchange - should use "default" audience when none provided
  • loginWithCustomTokenExchange - should persist domain on session
  • loginWithCustomTokenExchange - should call stateStore.set with removeIfExists=true (session fixation)
  • loginWithCustomTokenExchange - should throw when exchange fails
  • loginWithCustomTokenExchange - should return authorizationDetails when RAR was used
  • loginWithCustomTokenExchange - should resolve domain via resolver function
  • loginWithCustomTokenExchange - should allow getAccessToken to return the token after login
  • loginWithCustomTokenExchange - should persist act claim on session user when actor token is used
  • customTokenExchange - should return token response without persisting session
  • customTokenExchange - should throw when exchange fails
  • customTokenExchange - should return act claim when actor token is used

Manual Testing

customTokenExchange(): stateless delegation flow
  • Called with a subject token and an actor token
  • Exchange succeeded and returned a TokenResponse
  • act.sub populated correctly (set by api.authentication.setActor() in the Action)
  • refreshToken correctly absent, suppressed when actor_token is present per RFC 8693
loginWithCustomTokenExchange(): session-persisting
  • Called with a subject token only (no actor token)
  • Exchange succeeded and session was written to the state store
  • session.user confirmed to be the subject token user
  • accessToken, idToken, and refreshToken all present in the session

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

Summary by CodeRabbit

Release Notes

  • New Features

    • Added custom token exchange APIs (RFC 8693) for exchanging external tokens into Auth0 sessions.
    • Supports actor-based delegation, and exposes returned authorization details when using RAR.
    • Added a non-session “raw” exchange mode that returns token data without persisting session state.
  • Documentation

    • Added a “Login using Custom Token Exchange” guide, including required scope behavior and storeOptions usage.
  • Tests

    • Added coverage for session persistence, domain resolution via resolvers, and error handling for failed exchanges.

@cschetan77 cschetan77 force-pushed the feat/custom-token-exchange branch from bd5cc6f to 12cfbca Compare June 2, 2026 08:02
@cschetan77 cschetan77 force-pushed the feature/support-act-claim branch from 77b32f4 to 1ef94d5 Compare June 3, 2026 11:45
@cschetan77 cschetan77 force-pushed the feat/custom-token-exchange branch from ba61a9a to 97b20dc Compare June 3, 2026 11:49
@cschetan77 cschetan77 force-pushed the feature/support-act-claim branch 2 times, most recently from af8e6d6 to ed6e9c8 Compare June 12, 2026 07:21
@cschetan77 cschetan77 force-pushed the feat/custom-token-exchange branch from 97b20dc to e2d556d Compare June 12, 2026 07:49
@coderabbitai

coderabbitai Bot commented Jun 12, 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 24 minutes and 2 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ 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: 6880e62a-fe3e-4376-909f-17925de8040d

📥 Commits

Reviewing files that changed from the base of the PR and between ad51996 and c6ba55f.

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

Walkthrough

This PR introduces RFC 8693 custom token exchange support to the Auth0 ServerClient. It adds type definitions for exchange options and results, implements two new public methods for session-creating and delegation-only token exchanges, includes comprehensive test coverage for both methods with authorization request (RAR) and domain resolver support, and documents the feature with usage examples.

Changes

Custom Token Exchange Support

Layer / File(s) Summary
Type contracts for token exchange
packages/auth0-server-js/src/types.ts
Imports ExchangeProfileOptions from @auth0/auth0-auth-js and defines LoginWithCustomTokenExchangeOptions, CustomTokenExchangeOptions (both aliased to ExchangeProfileOptions), and LoginWithCustomTokenExchangeResult interface with optional authorizationDetails field.
ServerClient token exchange methods
packages/auth0-server-js/src/server-client.ts
Implements loginWithCustomTokenExchange to exchange custom tokens for Auth0 tokens and persist session state including audience/access token, and customTokenExchange to exchange without persisting session. Both methods resolve domain via optional resolver function using provided store options.
Test coverage for exchange methods
packages/auth0-server-js/src/server-client.spec.ts
Tests verify successful session persistence with audience and access token defaults, removeIfExists=true for session fixation mitigation, TokenExchangeError rejection on failure, customTokenExchange returning raw token response without session writes, actor-token delegation persisting/returning the act claim, RAR support returning authorizationDetails, and resolver-based domain resolution with store options.
Documentation and examples
packages/auth0-server-js/EXAMPLES.md
New section describing loginWithCustomTokenExchange for session-creating exchanges with required parameters (subjectToken, subjectTokenType) and optional fields (audience, scope), customTokenExchange for delegation without session modification, actor-token delegation via actorToken/actorTokenType, and storeOptions usage for domain resolution.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • auth0/auth0-auth-js#175: Implements RFC 8693 custom token exchange options and support in the auth0-auth-js package, which ServerClient directly depends on for the ExchangeProfileOptions type contract and underlying exchange functionality.

Suggested reviewers

  • nandan-bhat

Poem

🐰 A token exchange, oh what a day!
Custom credentials now find their way,
Session persists, or delegation flows free,
RFC 8693 hops merrily!
Domain resolves like a rabbit's own quest,
Auth0's exchange puts ServerClient to the test! 🎯

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the two main features added: loginWithCustomTokenExchange and customTokenExchange methods to auth0-server-js.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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 feat/custom-token-exchange

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.

@cschetan77 cschetan77 force-pushed the feat/custom-token-exchange branch from d72d8f3 to 9141b4c Compare June 12, 2026 08:22
Base automatically changed from feature/support-act-claim to main June 12, 2026 08:42
  - 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
@cschetan77 cschetan77 force-pushed the feat/custom-token-exchange branch from 9141b4c to 7894519 Compare June 12, 2026 11:31

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

🤖 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-server-js/EXAMPLES.md`:
- Around line 43-58: The table of contents contains repeated "Passing
`StoreOptions`" anchors whose numeric suffixes no longer match the actual
heading IDs after the "Custom Token Exchange" insertion; update each duplicated
anchor (the "Passing `StoreOptions`" links that follow "Custom Token Exchange",
and every subsequent occurrence) to use the correct unique fragment identifiers
that correspond to their target headings (e.g., adjust the
`#passing-storeoptions-3`, `#passing-storeoptions-4`, etc. suffixes so each TOC
entry points to the correct heading), ensuring the "Custom Token Exchange" entry
itself uses its proper anchor and all following anchors are shifted to match the
real headings like "Retrieving the logged-in User", "Retrieving the Session
Data", "Retrieving an Access Token", "Logout", and "Handle Backchannel Logout".
- Around line 944-960: The example incorrectly asserts that tokenResponse.act is
populated; update either the example or the parser: preferred fix—modify the
example in the delegation section to not reference tokenResponse.act?.sub
(remove or replace the console.log line) so it doesn't claim the ActClaim is
present, and add a note that act will be available only if the TokenResponse
parser (TokenResponse.fromTokenEndpointResponse) maps the 'act' claim into the
TokenResponse.act field; alternatively, if you choose to implement parsing
instead, update TokenResponse.fromTokenEndpointResponse to extract and assign
the 'act' claim into TokenResponse.act so tokenResponse.act?.sub is valid
(change the example only if you implement the parser change).

In `@packages/auth0-server-js/src/server-client.ts`:
- Around line 731-749: The loginWithCustomTokenExchange path can persist an
access-token-only exchange and merge it into an existing session because it
never guarantees OIDC claims; modify loginWithCustomTokenExchange (around the
authClient.exchangeToken call and before updateStateData / this.#stateStore.set)
to verify the exchange returned OIDC identity (e.g., require
tokenEndpointResponse.id_token or a claims payload) and if absent either
(preferred) reject the exchange by throwing a clear error instructing the caller
to request the openid scope, or (alternative) clear the existing session state
via this.#stateStore.set(this.#stateStoreIdentifier, undefined, true,
storeOptions) before calling updateStateData so you don't bind a previous
user/idToken to a new access token; reference loginWithCustomTokenExchange,
authClient.exchangeToken, updateStateData, and this.#stateStoreIdentifier when
implementing the check.
🪄 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: b124d28a-9035-464d-b6ae-edbaa1808557

📥 Commits

Reviewing files that changed from the base of the PR and between 45839b5 and 7894519.

📒 Files selected for processing (4)
  • packages/auth0-server-js/EXAMPLES.md
  • packages/auth0-server-js/src/server-client.spec.ts
  • packages/auth0-server-js/src/server-client.ts
  • packages/auth0-server-js/src/types.ts

Comment thread packages/auth0-server-js/EXAMPLES.md Outdated
Comment on lines +944 to +960
### Using actor tokens for delegation

When performing a delegation exchange where an intermediate service acts on behalf of a user, provide `actorToken` and `actorTokenType` together. Both are required when using actor tokens:

```ts
const tokenResponse = await serverClient.customTokenExchange({
subjectToken: userToken,
subjectTokenType: 'urn:acme:user-token',
actorToken: serviceAccountToken,
actorTokenType: 'urn:acme:service-token',
audience: 'https://api.example.com',
});

// tokenResponse.act contains the actor claim from the issued token:
// { sub: 'service-account-id', ... }
console.log(tokenResponse.act?.sub);
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n -C3 'act\?:|fromTokenEndpointResponse|authorization_details|issued_token_type' packages/auth0-auth-js/src/types.ts

Repository: auth0/auth0-auth-js

Length of output: 1264


Don’t claim tokenResponse.act is populated in the delegation example
TokenResponse defines act?: ActClaim, but TokenResponse.fromTokenEndpointResponse() (packages/auth0-auth-js/src/types.ts) only assigns authorization_details, tokenType, and issuedTokenType—it doesn’t populate tokenResponse.act. The example should avoid documenting tokenResponse.act?.sub unless/until the parser sets it at runtime.

🤖 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-server-js/EXAMPLES.md` around lines 944 - 960, The example
incorrectly asserts that tokenResponse.act is populated; update either the
example or the parser: preferred fix—modify the example in the delegation
section to not reference tokenResponse.act?.sub (remove or replace the
console.log line) so it doesn't claim the ActClaim is present, and add a note
that act will be available only if the TokenResponse parser
(TokenResponse.fromTokenEndpointResponse) maps the 'act' claim into the
TokenResponse.act field; alternatively, if you choose to implement parsing
instead, update TokenResponse.fromTokenEndpointResponse to extract and assign
the 'act' claim into TokenResponse.act so tokenResponse.act?.sub is valid
(change the example only if you implement the parser change).

Comment on lines +731 to +749
public async loginWithCustomTokenExchange(
options: LoginWithCustomTokenExchangeOptions,
storeOptions?: TStoreOptions
): Promise<LoginWithCustomTokenExchangeResult> {
const domain = await this.#resolveDomain(storeOptions);
const authClient = this.#getAuthClient(domain);
const tokenEndpointResponse = await authClient.exchangeToken(options);

const existingStateData = await this.#stateStore.get(this.#stateStoreIdentifier, storeOptions);
const stateData = updateStateData(
options.audience ?? 'default',
existingStateData,
tokenEndpointResponse,
{ domain }
);

await this.#stateStore.set(this.#stateStoreIdentifier, stateData, true, storeOptions);

return { authorizationDetails: tokenEndpointResponse.authorizationDetails };

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Require identity claims before persisting a token-exchange login.

loginWithCustomTokenExchange() writes directly into the session store without first guaranteeing an ID token / claims payload. Unlike startInteractiveLogin() and loginBackchannel(), this path never adds openid, so an access-token-only exchange can reach updateStateData(). In that case updateStateData() cannot detect a subject change and will merge the new token set into the existing session, leaving the previous user / idToken bound to a different user's exchanged access token. Either force an OIDC-capable scope here and reject exchanges that return no claims, or clear existing session state before persisting when claims are absent.

Suggested direction
   public async loginWithCustomTokenExchange(
     options: LoginWithCustomTokenExchangeOptions,
     storeOptions?: TStoreOptions
   ): Promise<LoginWithCustomTokenExchangeResult> {
     const domain = await this.#resolveDomain(storeOptions);
     const authClient = this.#getAuthClient(domain);
-    const tokenEndpointResponse = await authClient.exchangeToken(options);
+    const tokenEndpointResponse = await authClient.exchangeToken({
+      ...options,
+      scope: ensureOpenIdScope(options.scope),
+    });
+
+    if (!tokenEndpointResponse.idToken || !tokenEndpointResponse.claims) {
+      throw new InvalidConfigurationError(
+        'loginWithCustomTokenExchange requires an ID token response; include the openid scope in the exchange.'
+      );
+    }
🤖 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-server-js/src/server-client.ts` around lines 731 - 749, The
loginWithCustomTokenExchange path can persist an access-token-only exchange and
merge it into an existing session because it never guarantees OIDC claims;
modify loginWithCustomTokenExchange (around the authClient.exchangeToken call
and before updateStateData / this.#stateStore.set) to verify the exchange
returned OIDC identity (e.g., require tokenEndpointResponse.id_token or a claims
payload) and if absent either (preferred) reject the exchange by throwing a
clear error instructing the caller to request the openid scope, or (alternative)
clear the existing session state via
this.#stateStore.set(this.#stateStoreIdentifier, undefined, true, storeOptions)
before calling updateStateData so you don't bind a previous user/idToken to a
new access token; reference loginWithCustomTokenExchange,
authClient.exchangeToken, updateStateData, and this.#stateStoreIdentifier when
implementing the check.

@nandan-bhat nandan-bhat left a comment

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.

Reviewed the custom token exchange methods. The change is clean and the tests pass. I left a few inline notes. The main one is the audience used when we save the token set, since it may not match what getAccessToken looks for later. I also think we should add tests that pass an actor token and check the act claim, since that is the main goal of this feature.


const existingStateData = await this.#stateStore.get(this.#stateStoreIdentifier, storeOptions);
const stateData = updateStateData(
options.audience ?? 'default',

@nandan-bhat nandan-bhat Jun 14, 2026

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.

Here we save the token set using the audience passed in the call (options.audience). But getAccessToken reads the token set using the audience from the client config (this.#options.authorizationParams?.audience). loginBackchannel and completeInteractiveLogin both save using the config audience, not a per call value.

So if someone calls this method with audience 'https://api.example.com' but the client is not configured with that same audience, getAccessToken will look under 'default', not find the saved token, and try to refresh. When an actor token is used there is no refresh token, so it will throw.

The EXAMPLES doc tells people to call getAccessToken after this, so this path can break. Can we either save under the config audience like the other login methods, or make the contract clear ? It would also help to add a test that logs in and then calls getAccessToken to prove it works end to end.

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, we should key the accessToken in TokenSets under primary config audience this.#options.authorizationParams?.audience or the default as we do in loginBackchannel and completeInteractiveLogin.
Update:

  • The access token gets saved under this.#options.authorizationParams?.audience ?? 'default'
  • unit test: loginWithCustomTokenExchange - should allow getAccessToken to return the token after login- end-to-end test that proves loginWithCustomTokenExchange followed by getAccessToken works.

): Promise<LoginWithCustomTokenExchangeResult> {
const domain = await this.#resolveDomain(storeOptions);
const authClient = this.#getAuthClient(domain);
const tokenEndpointResponse = await authClient.exchangeToken(options);

@nandan-bhat nandan-bhat Jun 14, 2026

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.

The other login methods (loginBackchannel, startInteractiveLogin) call ensureOpenIdScope before the exchange. This one passes options straight through.

This is a login method and the session user is taken from the id token claims. The user is only set when an id token comes back. If the caller forgets openid in the scope, Auth0 may not return an id token, and we end up with a logged in session where user is undefined. The tests do not catch this because the mock always returns an id token.

Can we either add openid scope here like the sibling methods, or document that openid is required ? customTokenExchange can stay as is since it does not create a session.

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, I agree, this would create a subtle bug, a session would be silently created without any error, with id token claims undefined.
Update: Added

  • ensureOpenIdScope check in loginWithCustomTokenExchange
  • a note in EXAMPLES.md to clearly call out that SDK requires openid scope for this method, if not provided, SDK will inject appropriate defaults.

spy.mockRestore();
});

test('loginWithCustomTokenExchange - should persist session after successful exchange', async () => {

@nandan-bhat nandan-bhat Jun 14, 2026

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.

None of the new tests pass actorToken or actorTokenType, and none check the act claim. Actor tokens are the main point of this feature.

The spec says the act claim must be saved in the session for loginWithCustomTokenExchange and must be readable on the response for customTokenExchange. The actor tests today only exist at the auth-client level, not for these new server client methods.

Can we add two tests:

  1. loginWithCustomTokenExchange with an actor token, then check the saved session user has act.
  2. customTokenExchange with an actor token, then check result.act.sub.

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.

Added 2 proper unit tests

  • loginWithCustomTokenExchange - should persist act claim on session user when actor token is used
  • customTokenExchange - should return act claim when actor token is used


No StateStore reads or writes occur — `storeOptions` is only used for domain resolution in resolver mode.

### Using actor tokens for delegation

@nandan-bhat nandan-bhat Jun 14, 2026

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.

The spec says we should document that no refresh token is given when an actor token is sent, even if offline_access is in the scope. This section does not mention it.

Without this note, someone using an actor token with loginWithCustomTokenExchange will call getAccessToken after the token expires and hit an error with no warning. Can we add a short line here that says refresh tokens are not issued for actor token exchanges, so the session cannot be refreshed silently ?

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.

Added a important note under this section to clearly call out this.

Comment thread packages/auth0-server-js/EXAMPLES.md Outdated
- [Login using Custom Token Exchange](#login-using-custom-token-exchange)
- [Performing a delegation exchange without a session](#performing-a-delegation-exchange-without-a-session)
- [Using actor tokens for delegation](#using-actor-tokens-for-delegation)
- [Passing `StoreOptions`](#passing-storeoptions-3)

@nandan-bhat nandan-bhat Jun 14, 2026

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.

These StoreOptions anchors do not match the slugs GitHub builds. GitHub numbers repeated headings by their order in the page. This new section is the 6th 'Passing StoreOptions' heading, so the link should be passing-storeoptions-5, not -3. The backchannel one above also looks off.

This numbering was already messy before this PR, but the new section keeps the pattern. Can we fix the anchors so the links actually jump to the right place ?

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.

thanks for pointing this out.
Fixed the TOC.

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

🤖 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-server-js/src/server-client.spec.ts`:
- Around line 2410-2429: The test cases have unguarded mockRestore() calls that
can leak spies if assertions fail before cleanup. In
packages/auth-server-js/src/server-client.spec.ts at lines 2410-2429 (anchor)
and lines 2436-2456 (sibling): wrap the test body (from the exchangeSpy setup
through all assertions) in a try/finally block, moving the
exchangeSpy.mockRestore() call into the finally block to ensure cleanup happens
regardless of whether assertions pass or fail.
- Around line 2420-2425: The test for customTokenExchange with actor token
parameters currently only validates the output/session handling but does not
verify that the actorToken and actorTokenType parameters are actually forwarded
to the underlying exchangeToken call. Add explicit call-argument assertions to
the mock or spy of exchangeToken to verify it receives the actor token and actor
token type parameters in the expected format (likely as actor_token and
actor_token_type). Apply this hardening to all relevant test cases including
those at both the anchor location (2420-2425) and sibling location (2446-2451)
so that regressions in RFC 8693 actor token forwarding are caught.
🪄 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: af52129e-f370-472f-bf42-d743678ef958

📥 Commits

Reviewing files that changed from the base of the PR and between 343c996 and ad51996.

📒 Files selected for processing (2)
  • packages/auth0-server-js/src/server-client.spec.ts
  • packages/auth0-server-js/src/server-client.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/auth0-server-js/src/server-client.ts

Comment thread packages/auth0-server-js/src/server-client.spec.ts
Comment thread packages/auth0-server-js/src/server-client.spec.ts Outdated
@nandan-bhat

Copy link
Copy Markdown
Contributor

LGTM. Thanks for addressing all the review items.

@nandan-bhat nandan-bhat merged commit f505990 into main Jun 15, 2026
16 checks passed
@nandan-bhat nandan-bhat deleted the feat/custom-token-exchange branch June 15, 2026 20:56
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.

2 participants