Skip to content

Prerelease preview: auth0-api-js v1.7.0-beta.0 - #317

Draft
tusharpandey13 wants to merge 37 commits into
mainfrom
prep/api-js-1.7.0-beta
Draft

tusharpandey13 wants to merge 37 commits into
mainfrom
prep/api-js-1.7.0-beta

Conversation

@tusharpandey13

Copy link
Copy Markdown
Contributor

Prerelease preview: auth0-api-js v1.7.0-beta.0

This draft integrates four open PRs into one branch so that @auth0/auth0-api-js can be published as a prerelease before they merge. It is not meant to be merged as is. Once the four PRs land on main, the real release PR replaces this one.

Integrated PRs:

On top of the merges, one follow-up commit reconciles #291 with #290: the organization policy now also applies to claims passed through context.verified, on the trusted copy recorded at verification time and before the cache key is derived, so a token the policy rejects never reaches the store. It also removes a duplicate TokenSet export that the merge of #289 and #290 produces. The merge conflicts were resolved the same way each PR's notes describe.

The last commit bumps the version to 1.7.0-beta.0 and adds the CHANGELOG entry.

Changelog

Added

Publishing

This is for prerelease publishing by the release owner only. The release workflow publishes a version containing beta with npm publish --tag beta and marks the GitHub release as a prerelease, so latest stays on 1.6.1. The workflow runs only for merged PRs whose head branch starts with release/ (or on manual dispatch), so this branch does not publish anything. To publish, the release owner can recreate it as release/auth0-api-js-v1.7.0-beta.0, or run the workflow by hand on this branch.

Order matters: @auth0/auth0-auth-js 1.18.0 has to be published first, because npm install fails on the ^1.18.0 floor until it exists. CI on this PR is red for the same reason.

Testing

Run locally with the workspace auth-js temporarily at 1.18.0:

  • auth0-api-js: 829 tests pass (16 files), build, lint and typecheck clean.
  • auth0-auth-js: 592 tests pass.
  • auth0-server-js: 622 tests pass.

Every test from the four source branches is present here, except one test in #290 that the follow-up replaces (it asserted that context.verified is refused while an organization policy is configured; the follow-up enforces the policy instead).

🤖 Generated with Claude Code

tusharpandey13 and others added 30 commits September 29, 2026 14:04
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add ApiClient.getClientCredentialsToken returning TokenSet {accessToken, expiresAt(seconds)}.
Plumb optional scope through getTokenByClientCredentials in auth0-auth-js.
Caching deferred to SDK-11299 (clean seam, no tokenStore param).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…g [SDK-11361]

Add 13 tests: api-client TC-01..09 (TokenSet shape, epoch-seconds expiresAt, audience
fallback/override, scope variants incl empty-string, MissingClientAuthError guard,
TokenByClientCredentialsError propagation, no token_fingerprint); auth-client TC-A1..A4
(scope append on both grant branches incl fullResponse + empty string).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add Client Credentials (Machine-to-Machine) section to README and EXAMPLES
covering confidential-client setup, audience/scope override, epoch-seconds
expiresAt, and MissingClientAuthError. CHANGELOG entry deferred to release
branch (version-guard hook restricts CHANGELOG to release/*).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…etTokenOnBehalfOf

Introduces optional token caching for the on-behalf-of flow in the API client.
When a caller passes a TokenStore, getTokenOnBehalfOf performs cache-aside:
verify the subject token, derive a namespaced cache key from the internally
verified claims, look up the cache, and on a miss exchange then store. When no
store is passed, behavior is byte-identical to before.

Core contracts only (the MCP layer owns the running store instance, the
covering-lookup index, and eviction):
- TokenStore interface (get/set/delete by opaque key)
- CachedToken record (extends TokenSet; records grantedScopes)
- DownscopedTokenError (thrown when the exchange grants fewer scopes than
  requested; the downscoped token is never cached)

expiresAt values are epoch seconds. Cache key is
{iss}|{clientId}|{sub}|{orgId}|{audience}|{normalizedRequestedScopes}, derived
solely from internally verified claims. Adds 27 tests (302 total passing).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…enError

Adds an EXAMPLES.md section showing an InMemoryTokenStore implementation passed
to getTokenOnBehalfOf, the CachedToken shape (expiresAt in epoch seconds,
grantedScopes), DownscopedTokenError semantics, and a responsibility table for
the core vs. higher-layer (store instance, covering-lookup, index, eviction).
Adds a short OBO caching note to README.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ache hit

S1: buildOboCacheKey joined components with a raw `|`, but Auth0 `sub`
values always contain `|` and audience/scope are caller-controlled, so
distinct requests could collapse to one key and a hit could return a
token minted for the wrong audience. Encode the key with JSON.stringify
over the fixed-length component array so each segment is unambiguously
quoted/escaped. Key composition (iss|clientId|sub|orgId|audience|scopes)
is unchanged — only the encoding is now injective.

S2: CachedToken dropped tokenType/issuedTokenType, so a cache hit
returned a Bearer-looking shape even for DPoP-bound tokens. Persist both
fields on set and return them on hit, matching the miss/no-store shape.

Adds tests: raw-delimiter collision inputs now map to distinct keys (and
a `|`-bearing sub stays intact), and a cache hit returns tokenType/
issuedTokenType identical to the miss path. No-store path untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rification

SR-2: buildOboCacheKey now includes exchangingClientId and exchangingAudience
as the first two slots, preventing cache collisions when two ApiClient
instances with different clientId or configured audience share a TokenStore.

SR-3: verifyAccessToken, #enforceOrganizationPolicy, and the downscope guard
are now unconditional — they run on both the store path and the no-store path.
Added OrganizationPolicy type, MissingOrganizationError, and
OrganizationNotAllowedError to enforce org_id claim policy before exchange.
…-export TokenByClientCredentialsError

Back-port of the verified hardening from the integration branch.

- SR-11: getClientCredentialsToken now surfaces the authorization-server
  granted `scope` and `tokenType` on the returned TokenSet instead of
  silently dropping them, so callers can detect a downscoped grant. Fields
  are omitted when Auth0 omits them (RFC 6749 §5.1: absent scope means the
  full requested scope was granted).
- SR-6: re-export TokenByClientCredentialsError from the package index so
  consumers can catch it by type.

Adds tests for scope/tokenType surfacing, downscoped grant, and the
absent-scope case. vitest 286, tsup, eslint, tsc all green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…lation/scope-key tests

Back-port of the verified hardening from the integration branch.

- SR-8: apply a 5-second clock-skew leeway to the OBO cache-hit expiry
  check, so a near-expiry cached token is treated as a miss and re-exchanged
  rather than served and then rejected downstream.
- SR-9: tests pinning scope-key shape parity (scope order and duplicates
  normalize to the same key; genuinely distinct scope sets get distinct
  keys).
- SR-7: tests proving no cross-entry bleed on a shared store (distinct scope
  keys and distinct exchangingClientId prefixes are isolated), satisfied by
  the 8-slot key binding.
- Subject-isolation e2e: a cached OBO token is keyed injectively across all
  four identity axes (sub, org_id, iss, subjectClientId); varying any one
  produces a distinct cache entry.

Also removes a pre-existing unused OrganizationPolicy import (eslint).
buildPrincipal wiring is deferred until PR #284 (Principal) lands on main;
the cache key derives identity from verified claims in the meantime.

vitest 319, tsup, eslint, tsc all green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add api-client.obo-cache-e2e.spec.ts covering the complete getTokenOnBehalfOf
pipeline without internal spies: JWKS fetched from an MSW-mocked discovery
endpoint, token exchange executed against an MSW-mocked token endpoint, and
a real functional Map-based TokenStore. Tests cover cache miss (exchange runs
and result is stored), cache hit (second call with identical params skips the
endpoint), near-expiry re-exchange (cached token within the 5 s clock-skew
leeway is treated as a miss), DownscopedTokenError (partial grant is rejected
and nothing is stored), and cache-key injectivity across all eight key
dimensions (exchangingClientId, exchangingAudience, iss, subjectClientId, sub,
orgId, downstreamAudience, normalizedScopes).

Test count: 319 before, 325 after (+6).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add five additional MSW-mocked end-to-end scenarios to the organization
policy test suite:

- 'allow' default passes through a token that carries an org_id claim
- organizationPolicy: 'allow' explicit leaves tokens without org_id untouched
- 'required' with a multi-element array allowlist accepts a token whose
  org_id matches the second element
- 'required' with a multi-element array allowlist rejects a token whose
  org_id is absent from the list with OrganizationNotAllowedError
- 'required' treats a whitespace-only org_id as missing and throws
  MissingOrganizationError

Test count increases from 281 to 286 (11 files, all passing).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…t [SDK-11361]

Extend api-client.spec.ts with two additional end-to-end scenarios for
getClientCredentialsToken that were not covered by the initial test pass:

- TC-13: verifies MissingClientAuthError is thrown when a clientId is
  configured without any client authentication method (clientSecret or
  assertion key), exercising the auth-layer guard distinct from the
  no-clientId case covered by TC-07.
- TC-14: captures the form-encoded POST body and asserts that client_id
  and client_secret are forwarded verbatim to the token endpoint,
  confirming credential plumbing through ApiClient -> AuthClient ->
  oauth4webapi end-to-end.

Test count: 286 -> 288 (api-client.spec.ts: 79 -> 81).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ate description

Reject organizationPolicy values other than 'required' or 'allow' and
malformed organizationId values at construction, so misconfiguration
no longer silently disables enforcement or rejects every token.
Drop embedded double quotes from organization error messages (RFC 6750),
document that organizationId takes org IDs, and add coverage for invalid
config, non-string org_id claims, DPoP mode and the challenge header.

Co-Authored-By: Claude <noreply@anthropic.com>
The constructor validated `organizationId` and then stored the caller's
object by reference, so pushing an extra id onto that array, or setting
`organizationPolicy` to `'allow'`, changed enforcement after validation had
passed. Snapshot the policy and the allowlist at construction and enforce
against the snapshot. The allowlist becomes a Set, which also drops the
per-request linear scan.

Reject an `organizationId` entry that carries surrounding whitespace instead
of accepting it. Such a value can never equal an `org_id` claim, so accepting
it silently rejected every token, and a comma-delimited environment variable
is the usual way to produce one. Array entries are now checked through
`Array.from`, so array holes are validated rather than skipped.

Pin the wire contract in tests. Organization failures report
`error="invalid_token"`, which is what RFC 6750 section 3.1 permits for that
parameter, while the specific SDK `code` stays on the error object for
application logic and never reaches the challenge. The challenge carries no
embedded double quote, and neither the rejected organization nor the
allowlist appears in the message or the header. Both error classes and the
`organizationId` option now document that contract.

Every new assertion was checked against a reverted implementation to confirm
it fails when the behaviour it pins is removed.

Co-Authored-By: Claude <noreply@anthropic.com>
The JSDoc on both organization error classes pointed at the equivalent error
in the sibling SDK for another language. That note reaches the published
typings, where it tells a consumer nothing they can act on. Each class now
names its own `code` value instead, which is what application code branches
on, and the paragraph below it already explains that the wire challenge
always reports `invalid_token`.

Comments only. No behaviour change, and the test count is unchanged.

Co-Authored-By: Claude <noreply@anthropic.com>
getTokenByClientCredentials accepted an audience and an organization but had no
way to request a narrowed set of scopes, so a caller always received whatever
the client's full grant allowed. Add an optional scope option and append it to
the token request on both the bare and the full-response code paths.

The option is tested against undefined rather than for truthiness. An empty
string is a meaningful request for zero scopes, so it now reaches the token
endpoint instead of being silently dropped, which keeps it distinct from
omitting the option altogether. Tests cover both paths and pin the empty-string
behaviour.

Co-Authored-By: Claude <noreply@anthropic.com>
…grant

Consolidate TokenSet into a single declaration. It now lives in token-store.ts,
which is also where the token store work will extend it, and types.ts no longer
carries a second copy. index.ts exports it exactly once, so an explicit export
can no longer shadow the star export from types.ts and silently drop the scope
and tokenType fields from the public type.

Move the auth0-auth-js scope plumbing out of this branch and onto its own
branch. No published version of @auth0/auth0-auth-js forwards a scope on the
client credentials grant, and this package accepted ^1.3.0, so every resolvable
version dropped the option and the caller received a token for the client's full
grant. Raise the floor to ^1.18.0, the first release that will carry the
forwarding, rather than leaving the option best effort. This branch therefore
does not install until that release is published, which is deliberate and is why
the two changes are ordered this way.

Correct the documented response behaviour. Auth0 returns scope whenever scopes
were requested or granted, so an absent scope is not a statement that the full
request was honoured, and the previous wording invited callers to read it that
way. Document that tokenType arrives lower-cased by the underlying OAuth client
and assert that value exactly instead of hiding it behind toLowerCase().

Replace five tests that could not fail or that tested the wrong layer. Two
asserted a snake_case key against a camelCase result, so they always passed, and
they now assert the exact key set of the returned object. Three asserted the
scope form field on the wire, which this branch no longer changes, and they now
assert the options handed to the auth client, which is this package's own
responsibility.

Remove the internal tracker identifiers that were reaching the published typings,
along with the note promising a token cache in this method. Caching tokens from
this grant is deferred and is not part of this change.

Co-Authored-By: Claude <noreply@anthropic.com>
…n store

Pass a token store to `getTokenOnBehalfOf` and an exchanged token is reused
instead of being requested again. The token is cached under the scopes the
exchange actually granted, and those scopes are recorded alongside it, so a
later call asking for a narrower set is served from cache. A new
`scopeMatching` option selects exact matching, the default, or covering
matching, which reuses a token whose granted scopes cover the request and
prefers the smallest one. Covering matching searches several entries per
subject, so it requires an `IndexedTokenStore`.

Verification is conditional on the store. The claims are needed to derive a
cache key, so the subject token is verified when a store is supplied and
forwarded untouched otherwise, which is how the released package behaves. That
internal verification runs as a bearer token with no request context, so it
cannot satisfy a required DPoP mode, accept a DPoP-bound token, or give a
domains resolver a URL or headers. For those cases, pass the result of an
earlier `verifyAccessToken` call as `context.verified`: the claims object is
checked by identity against the ones this client verified, the token strings
must match, and expired claims are rejected. The key is derived from a copy
taken at verification time, so editing a claims object afterwards cannot move
the cache key.

Cache keys are versioned, namespaced by matching mode, and fixed length. They
hash the exchanging tenant, client and audience, the subject token's issuer,
client, subject, organization and session, the downstream audience, and the
scope set. Each field is length prefixed and an absent field encodes
distinctly from an empty one, so no two identities share a key, and no claim
value is held in the clear. A token whose key claims are not strings is not
cached at all, rather than keyed on a value the encoding cannot distinguish.

The cache never becomes an availability dependency. A read that throws, a
malformed entry, an entry close to expiry and an unreadable index are all
treated as a miss, and a write that throws still returns the token that was
just issued. Entry fields are read once into a fresh object, so a value cannot
change between validation and use. An index member is a hint: a member whose
key does not match the scopes it claims is skipped, and a candidate's coverage
is re-checked against the stored entry.

A result granted fewer scopes than requested is no longer rejected here. It is
cached under what was granted and returned, with `scope` exactly as the
authorization server sent it and `grantedScopes` carrying the normalized set.
The caller decides whether a narrowed grant is acceptable, because it has the
request context to turn that into the right response. `DownscopedTokenError` is
removed.

Organization policy also moves out of this method. The option and its two error
classes are removed here and the organization-policy change owns them, which
makes it a prerequisite. One guard remains at the pre-verified path as a
marked integration point: it refuses the combination rather than silently
skipping a policy it cannot yet enforce.

Also in this change: an empty granted scope is no longer dropped as falsy, so
it stays distinct from an absent one instead of falling back to the requested
scopes; missing client authentication is reported before any verification or
cache access; and `verifyAccessToken` uses the audience captured at
construction.

Co-Authored-By: Claude <noreply@anthropic.com>
Cover the token store, the two scope-matching modes, the indexed store the
covering mode needs, and the cache-entry shape.

Correct three claims that no longer hold. The subject token is forwarded
untouched only when no store is supplied; with a store it is verified, and that
verification runs as a bearer token, so a required DPoP mode, a DPoP-bound
token or a custom domains resolver needs the token verified at ingress and
handed in. A narrowed grant is cached and returned rather than rejected, and
the caller checks `grantedScopes` to decide what to do about it. An empty
`scope` means nothing was granted and is distinct from the field being absent,
so a truthiness test on it reads an empty grant as a full grant.

State the operational caveats plainly: this package applies no encryption, so a
store on shared or persistent storage is responsible for protecting the tokens
it holds; a store failure costs extra exchanges and never fails a call; entries
are validated for shape but not integrity, and must expose their fields as own
properties; nothing is ever pruned, so eviction belongs to the implementation;
and because the cache key's session component outlives any single access token,
a revoked session does not invalidate an already cached delegated token before
that token expires.

Co-Authored-By: Claude <noreply@anthropic.com>
…redentials-scope

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
… request

Capture the form-encoded body at the HTTP boundary for a set scope, an
omitted scope and an empty string scope, so the tests fail if a
dependency drops the parameter.

Co-Authored-By: Claude <noreply@anthropic.com>
…dentials result

An absent scope means the scopes were granted as requested (RFC 6749
section 5.1). An empty string means nothing was granted. Align the
TokenSet comments with the on-behalf-of token cache branch.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
tusharpandey13 and others added 7 commits October 19, 2026 10:43
Describe how scope is requested and how an omitted scope in the
token response is interpreted (RFC 6749 section 5.1).

Co-Authored-By: Claude <noreply@anthropic.com>
An absent scope in the token response means the scopes were granted as requested (RFC 6749 section 5.1), and an empty scope means nothing was granted. Say so on OnBehalfOfTokenResult.scope and grantedScopes, TokenSet.scope, the getTokenOnBehalfOf description and the README.

Co-Authored-By: Claude <noreply@anthropic.com>
…s-1.7.0-beta

# Conflicts:
#	packages/auth0-api-js/src/api-client.ts
#	packages/auth0-api-js/src/types.ts
…s-1.7.0-beta

# Conflicts:
#	packages/auth0-api-js/README.md
#	packages/auth0-api-js/src/api-client.spec.ts
#	packages/auth0-api-js/src/api-client.ts
#	packages/auth0-api-js/src/token-store.ts
…alf Of claims

Reconcile the organization policy and client credentials changes with the
On Behalf Of token cache.

When the caller passes pre-verified claims, the organization policy is now
applied to the trusted copy of those claims before the cache key is derived,
using the same checks and challenges as verifyAccessToken. A rejected token
never reaches the token store. This replaces the guard that refused the
combination of a pre-verified token and an organization policy.

Also remove the duplicate TokenSet type export from the package entry point.
TokenSet stays in the grouped export.

Co-Authored-By: Claude <noreply@anthropic.com>
Prepare the first beta of auth0-api-js 1.7.0. It adds organization policy
enforcement, the On Behalf Of token store and cache-aside path, and the
client credentials token method. The release workflow publishes versions
containing "beta" under the beta dist-tag.

Co-Authored-By: Claude <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 19, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

This branch has not been deployed

No deployments
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