Repository navigation
Prerelease preview: auth0-api-js v1.7.0-beta.0 - #317
Draft
tusharpandey13 wants to merge 37 commits into
Draft
tusharpandey13 wants to merge 37 commits into
tusharpandey13 wants to merge 37 commits into
Conversation
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>
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
…scope' into prep/api-js-1.7.0-beta
…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>
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: true
Comment |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Prerelease preview: auth0-api-js v1.7.0-beta.0
This draft integrates four open PRs into one branch so that
@auth0/auth0-api-jscan 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:
ApiClient(required or optionalorg_id, with an allowlist).scopeon the client credentials grant in@auth0/auth0-auth-js. This change ships in auth-js 1.18.0 (draft release PR Release auth0-auth-js v1.18.0 #316), and api-js now depends on^1.18.0.getClientCredentialsTokenfor machine-to-machine tokens.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 duplicateTokenSetexport 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.0and adds the CHANGELOG entry.Changelog
Added
Publishing
This is for prerelease publishing by the release owner only. The release workflow publishes a version containing
betawithnpm publish --tag betaand marks the GitHub release as a prerelease, solateststays on 1.6.1. The workflow runs only for merged PRs whose head branch starts withrelease/(or on manual dispatch), so this branch does not publish anything. To publish, the release owner can recreate it asrelease/auth0-api-js-v1.7.0-beta.0, or run the workflow by hand on this branch.Order matters:
@auth0/auth0-auth-js1.18.0 has to be published first, becausenpm installfails on the^1.18.0floor 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:
Every test from the four source branches is present here, except one test in #290 that the follow-up replaces (it asserted that
context.verifiedis refused while an organization policy is configured; the follow-up enforces the policy instead).🤖 Generated with Claude Code