@cloudflare/workers-oauth-provider 1.2.1 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -75,11 +75,26 @@ CIMD validation follows [draft-ietf-oauth-client-id-metadata-document-00](https:
75
75
  - Exact authorization-request redirect URI validation, with RFC 8252 loopback port handling.
76
76
  - A 5 KB response size limit and a 10 second timeout covering both headers and body.
77
77
  - Valid UTF-8 JSON object syntax and safe URI schemes for client metadata fields.
78
- - No embedded client secrets or private JWK material.
78
+ - No embedded client secrets or private or symmetric JWK material.
79
79
 
80
80
  Validated documents are cached according to their `Cache-Control` headers, capped at 7 days. Error responses and invalid documents are never cached, and a cached document that stops validating is evicted and re-resolved from origin within the same request.
81
81
 
82
- CIMD token endpoint authentication is negotiated from `token_endpoint_auth_method` and the OpenID RP Metadata Choices field `token_endpoint_auth_methods_supported`. The provider currently implements only `none`: a client may prefer `private_key_jwt` while also offering `none`, in which case the provider selects `none` and applies public-client PKCE requirements. A client that offers only `private_key_jwt` is rejected until assertion validation is implemented.
82
+ CIMD token endpoint authentication is read from `token_endpoint_auth_method` and the OpenID RP Metadata Choices field `token_endpoint_auth_methods_supported`. The provider implements `none` and `private_key_jwt` for CIMD clients. A metadata document gets no registration response, so the client picks its method per request from this server's metadata, and every method both sides support stays usable:
83
+
84
+ - A document that offers only `private_key_jwt` must authenticate every token and revocation request with a signed client assertion.
85
+ - A document that offers `none` and `private_key_jwt` (ChatGPT's does) may use either. It is held to the public-client rules, so PKCE is required, and `lookupClient()` reports `tokenEndpointAuthMethod: 'none'`. An assertion it does send must be valid, and a failed assertion never falls back to `none`.
86
+ - A document that offers only `none` can't send an assertion.
87
+
88
+ With CIMD enabled and the compatibility flag set, the provider advertises `private_key_jwt` in `token_endpoint_auth_methods_supported` and `RS256` and `ES256` in `token_endpoint_auth_signing_alg_values_supported`. Only CIMD clients use it: registration and `createClient()` don't accept `private_key_jwt`.
89
+
90
+ A `private_key_jwt` client publishes its public keys in exactly one of `jwks` or an `https:` `jwks_uri`, and may narrow the algorithm with `token_endpoint_auth_signing_alg` or `token_endpoint_auth_signing_alg_values_supported`. A document whose only method is `private_key_jwt` and whose keys or algorithms are unusable is rejected, including an inline `jwks` with no signing key for an allowed algorithm. One that also offers `none` keeps working with `none`. The token request sends `client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer` and `client_assertion`. `client_id` may be omitted, because the assertion's `iss` identifies the client. The assertion ([RFC 7523](https://www.rfc-editor.org/rfc/rfc7523#section-3)) must:
91
+
92
+ - Be signed with `RS256` or `ES256` by a key in the client's JWKS, chosen by `kid`, or the only matching key when there is no `kid`.
93
+ - Have `iss` and `sub` equal to the `client_id`.
94
+ - Have `aud` set to the authorization server's issuer identifier or its token endpoint URL. An assertion typed `client-authentication+jwt` ([draft-ietf-oauth-rfc7523bis](https://datatracker.ietf.org/doc/draft-ietf-oauth-rfc7523bis/)) must name the issuer identifier alone, as a string.
95
+ - Carry an `exp` no more than an hour away and a `jti`, which makes it single-use. `nbf` and `iat` are optional. A 60 second clock skew applies.
96
+
97
+ A remote JWKS is fetched under the same `global_fetch_strictly_public` protection as the metadata document, with a 64 KB limit and a 10 second timeout. It is cached in memory for 5 minutes, so a key the client removes can verify assertions until the cache expires. An unknown `kid`, or a signature that fails against the cached key, refetches it at most once every 30 seconds. A rotated key is picked up without letting bad assertions make this server hammer the client's host. Replay markers are kept in `OAUTH_KV` under `client-assertion-jti:` (see [storage-schema.md](../storage-schema.md)). Failures return a generic `invalid_client`, and `onError.internal` carries `reason: 'client_assertion_invalid'` with the specific check in `detail.reason`.
83
98
 
84
99
  When a CIMD document cannot be fetched or validated, the token endpoint returns a generic `invalid_client` response and reports diagnostics through `onError.internal`. `OAuthHelpers` methods that resolve a CIMD client throw the exported `CimdFetchError`, allowing applications to distinguish an upstream metadata failure from a client that does not exist. See [Advanced configuration](advanced-configuration.md#cimd-fetch-errors) for an example.
85
100
 
@@ -1,29 +1,29 @@
1
1
  # Migrating to 1.x
2
2
 
3
- This guide takes a Worker from 0.10.x, 1.0 or 1.1 to the latest 1.x release. Each change is tagged with the version that introduced it, so skip the ones older than the version you're on. Stored grants, tokens and clients keep working throughout: there is no KV migration.
3
+ This guide takes a Worker from 0.10.x, 1.0 or 1.1 to the latest 1.x release. Each change is tagged with the version that introduced it, so skip the ones older than the version you're on. There is no KV migration, and stored grants, tokens and clients keep working, provided the resource you configure is the one your existing grants are bound to. Read [Match the resource your existing grants carry](#match-the-resource-your-existing-grants-carry-10) before you deploy: getting it wrong signs every user out.
4
4
 
5
- Coding agents can follow [`skills/migrate-to-1.0/SKILL.md`](../skills/migrate-to-1.0/SKILL.md), which ships in the npm package and points back at the sections below.
5
+ Coding agents can follow [`skills/migrate-to-1.x/SKILL.md`](../skills/migrate-to-1.x/SKILL.md), which ships in the npm package and points back at the sections below.
6
6
 
7
7
  ## Am I affected?
8
8
 
9
9
  Search your code for each of these. Anything you don't use needs no change.
10
10
 
11
- | You use | Since | Change |
12
- | ------------------------------------------------------------------------------------------------------------------------------------ | ----- | ------------------------------------------------------------------------------------------- |
13
- | `new OAuthProvider(` without `resourceMetadata` | 1.0 | [Add the canonical resource](#the-canonical-resource-is-required-10) |
14
- | `resourceMatchOriginOnly` | 1.0 | [Delete it](#removed-resourcematchoriginonly-10) |
15
- | `resolveExternalToken` | 1.0 | [Return `audience`](#resolveexternaltoken-names-its-audience-10) |
16
- | `resourceMetadata.scopes_supported` | 1.2 | [Move it to `requiredScopes`](#requiredscopes-replaces-resourcemetadatascopes_supported-12) |
17
- | `allowImplicitFlow` or `allowPlainPKCE` | 1.2 | [Delete them](#removed-the-implicit-grant-and-plain-pkce-12) |
18
- | Clients with remote `http` or `com.example.app:/` redirect URIs | 1.2 | [Redirect URI policy](#redirect-uris-must-be-https-or-loopback-http-12) |
19
- | `clientRegistrationTTL: 0`, or any lifetime under 60 seconds | 1.2 | [Lifetimes are validated](#lifetimes-are-validated-at-construction-12) |
20
- | User IDs containing `:` | 1.2 | [Encode them](#user-ids-cannot-contain--12) |
21
- | `revokeExistingGrantsBatchSize` | 1.2 | [Delete it](#removed-revokeexistinggrantsbatchsize-12) |
22
- | `resourceMatches`, `validateResourceUri`, `isValidOAuthScopeToken`, `base64UrlToBytes`, `parseJwtJsonPart`, `getJwtCryptoAlgorithms` | 1.2 | [No longer exported](#internal-helpers-are-no-longer-exported-12) |
23
- | `createClient()` or `updateClient()` | 1.2 | [Client helpers validate](#client-helpers-validate-what-they-store-12) |
24
- | `tokenExchangeCallback` | 1.1 | [It can revoke grants, and lifetimes changed](#tokenexchangecallback-11-12) |
25
- | `purgeExpiredData()` on a schedule | 1.2 | [Persist the cursor](#purgeexpireddata-resumes-from-a-cursor-12) |
26
- | `OAuthAuthorizationServer` (1.0 or 1.1) | 1.2 | [Endpoints have defaults](#oauthauthorizationserver-endpoints-have-defaults-12) |
11
+ | You use | Since | Change |
12
+ | ------------------------------------------------------------------------------------------------------------------------------------ | ----- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
13
+ | `new OAuthProvider(` without `resourceMetadata` | 1.0 | [Add the canonical resource](#the-canonical-resource-is-required-10), [matching existing grants](#match-the-resource-your-existing-grants-carry-10) |
14
+ | `resourceMatchOriginOnly` | 1.0 | [Delete it](#removed-resourcematchoriginonly-10), [then match existing grants](#match-the-resource-your-existing-grants-carry-10) |
15
+ | `resolveExternalToken` | 1.0 | [Return `audience`](#resolveexternaltoken-names-its-audience-10) |
16
+ | `resourceMetadata.scopes_supported` | 1.2 | [Move it to `requiredScopes`](#requiredscopes-replaces-resourcemetadatascopes_supported-12) |
17
+ | `allowImplicitFlow` or `allowPlainPKCE` | 1.2 | [Delete them](#removed-the-implicit-grant-and-plain-pkce-12) |
18
+ | Clients with remote `http` or `com.example.app:/` redirect URIs | 1.2 | [Redirect URI policy](#redirect-uris-must-be-https-or-loopback-http-12) |
19
+ | `clientRegistrationTTL: 0`, or any lifetime under 60 seconds | 1.2 | [Lifetimes are validated](#lifetimes-are-validated-at-construction-12) |
20
+ | User IDs containing `:` | 1.2 | [Encode them](#user-ids-cannot-contain--12) |
21
+ | `revokeExistingGrantsBatchSize` | 1.2 | [Delete it](#removed-revokeexistinggrantsbatchsize-12) |
22
+ | `resourceMatches`, `validateResourceUri`, `isValidOAuthScopeToken`, `base64UrlToBytes`, `parseJwtJsonPart`, `getJwtCryptoAlgorithms` | 1.2 | [No longer exported](#internal-helpers-are-no-longer-exported-12) |
23
+ | `createClient()` or `updateClient()` | 1.2 | [Client helpers validate](#client-helpers-validate-what-they-store-12) |
24
+ | `tokenExchangeCallback` | 1.1 | [It can revoke grants, and lifetimes changed](#tokenexchangecallback-11-12) |
25
+ | `purgeExpiredData()` on a schedule | 1.2 | [Persist the cursor](#purgeexpireddata-resumes-from-a-cursor-12) |
26
+ | `OAuthAuthorizationServer` (1.0 or 1.1) | 1.2 | [Endpoints have defaults](#oauthauthorizationserver-endpoints-have-defaults-12) |
27
27
 
28
28
  Construction errors name the rule that failed, so a Worker that starts and passes its tests has cleared most of these. The redirect URI policy and the user ID rule act on stored data and live requests instead: check those by hand.
29
29
 
@@ -49,12 +49,52 @@ Construction validates the rest of the configuration against it:
49
49
  - Every `apiRoute` and `apiHandlers` key must be the resource's path or a path-boundary descendant: `/mcp` covers `/mcp` and `/mcp/tools`, not `/mcp-other`. Absolute routes must be on the resource's origin.
50
50
  - The resource must not sit inside `/.well-known/oauth-protected-resource`.
51
51
 
52
- In 0.x the metadata document was derived per request when `resourceMetadata` was omitted. In 1.x the resource is the identity every token is bound to, so it can't be implicit.
52
+ In 0.x the metadata document was derived per request when `resourceMetadata` was omitted. In 1.x the resource is the identity every token is bound to, so it can't be implicit. Upgrading from 0.x, the value isn't a free choice: it must be the one your users' grants already carry.
53
+
54
+ ### Match the resource your existing grants carry (1.0)
55
+
56
+ Every grant and access token stores the `resource` the client sent when the user signed in, and the client sends that same value again on every refresh. 1.x compares both against the configured resource exactly. Configure a different value, even one that covers the same paths, and every existing session breaks at once:
57
+
58
+ | Request from an existing session | Response |
59
+ | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
60
+ | An API call with its current access token | `401 invalid_token`, internal reason `token_audience_unbound` |
61
+ | A refresh that sends `resource` (most clients) | `400 invalid_target`, "The resource parameter must name exactly one configured protected resource" (`resource_not_configured`) |
62
+ | A refresh without `resource` | `400 invalid_grant`, "The authorization grant is not bound to a configured resource" (`grant_audience_unbound`) |
63
+
64
+ Clients that treat `invalid_target` as temporary retry it forever. Users recover only by signing out and authorizing again. The library won't widen or rewrite a stored resource to match, because RFC 8707 audiences are compared exactly, so the configuration has to match the data.
65
+
66
+ **What your grants carry.** Without `resourceMetadata.resource`, 0.x derived the resource from the metadata URL the client fetched, and its `401` challenge pointed each client at the metadata for the path it called. A client connecting to `https://mcp.example.com/mcp` was told `resource: "https://mcp.example.com/mcp"` and bound its grant to that. Only clients that fetched `/.well-known/oauth-protected-resource` at the root got the bare origin. `resourceMatchOriginOnly: true` changed how audiences were compared, not what was stored, so those grants carry the path too.
67
+
68
+ **Check before you deploy.** On the running 0.x deployment, for every API route:
69
+
70
+ ```sh
71
+ curl -si https://mcp.example.com/mcp | grep -i www-authenticate # note resource_metadata="…"
72
+ curl -s https://mcp.example.com/.well-known/oauth-protected-resource/mcp | jq -r .resource
73
+ ```
74
+
75
+ Then look at real grants. The `resource` field is stored in plain text:
76
+
77
+ ```sh
78
+ npx wrangler kv key list --binding OAUTH_KV --remote --prefix grant: | jq -r '.[0:20][].name' |
79
+ while read -r key; do npx wrangler kv key get --binding OAUTH_KV --remote "$key" | jq -r .resource; done | sort | uniq -c
80
+ ```
81
+
82
+ Configure exactly the value the grants show, including the path, with no added or dropped trailing slash. A Worker served on several hostnames can build the provider per request with `` `${new URL(request.url).origin}/mcp` ``, as long as the result equals what that hostname advertised before. Grants with no `resource` at all, from clients that never sent one, are fine: they bind to the configured resource on their next refresh ([below](#existing-stored-data-nothing-to-do)).
83
+
84
+ **When the grants carry more than one value.** A Worker that served `/mcp` and `/sse` in 0.x advertised a different resource on each, so its grants hold both. One `OAuthProvider` resource can't match both: `https://mcp.example.com/mcp` doesn't cover the `/sse` route, so construction refuses it, and the bare origin covers both routes but matches neither kind of grant. Pick one:
85
+
86
+ - **Drop the second endpoint.** MCP deprecated the HTTP+SSE transport, so if `/sse` sees little traffic, remove the route and configure `https://mcp.example.com/mcp`. Sessions on `/mcp` keep working, and those on `/sse` sign in again.
87
+ - **Declare each resource.** An [`OAuthAuthorizationServer`](resource-servers.md) lists `resources: ['https://mcp.example.com/mcp', 'https://mcp.example.com/sse']`, and each route is hosted by an `OAuthResourceServer` for its own resource. This reproduces what 0.x advertised, so sessions bound to either path keep working. Set `legacyGrantResource` too, so grants with no stored resource have a destination ([below](#existing-stored-data-nothing-to-do)). It is more code: you own the routing.
88
+ - **Accept one sign-in.** Configure the resource you want, tell your users, and make sure your clients start a new authorization on `invalid_target` or `invalid_grant` from a refresh rather than retrying.
89
+
90
+ **Before switching production**, deploy the upgrade to staging with a copy of real sessions, or keep one session from the 0.x deployment, and confirm that a refresh and an API call both succeed. The responses in the table above are what a mismatch looks like, and `onError` reports their internal reasons.
53
91
 
54
92
  ### Removed: `resourceMatchOriginOnly` (1.0)
55
93
 
56
94
  Delete the option. A configuration that still sets it throws `resourceMatchOriginOnly was removed in 1.0`. Audiences are compared exactly against the canonical resource, with two tolerances: ASCII case in the scheme and host is folded, and an empty path equals `/` (RFC 3986 §6.2.3). Port, path, query and trailing slash stay strict.
57
95
 
96
+ Don't replace it with `resource: <origin>`. Origin-only matching hid that grants were stored with the path the client connected to, such as `https://mcp.example.com/mcp`, and an origin resource matches none of them. Configure the resource those grants carry: see [Match the resource your existing grants carry](#match-the-resource-your-existing-grants-carry-10).
97
+
58
98
  ### `resolveExternalToken` names its audience (1.0)
59
99
 
60
100
  `ResolveExternalTokenResult.audience` is required, a single string, and must be the configured resource. A token accepted for any other audience is a `401`.
@@ -256,10 +296,12 @@ You still route the authorization endpoint yourself, before `authorizationServer
256
296
 
257
297
  ## Existing stored data: nothing to do
258
298
 
299
+ This holds once the configured resource matches what your grants carry ([above](#match-the-resource-your-existing-grants-carry-10)).
300
+
259
301
  - An access token stored without an audience keeps working until it expires. It's treated as bound to the migration resource: the sole configured resource, or `legacyGrantResource` on a multi-resource server.
260
302
  - Refresh binds the grant to that resource and returns a bound replacement token.
261
303
  - A stored 0.x audience array resolves to the registered resource it contains.
262
- - A grant bound only to unregistered values fails refresh with `invalid_grant`; conformant clients (Claude, the MCP SDKs) answer by starting a fresh authorization.
304
+ - A grant bound to a resource you didn't configure fails refresh. A refresh that names that resource gets `invalid_target`, and one without `resource` gets `invalid_grant`. Conformant clients (Claude, the MCP SDKs) answer `invalid_grant` by starting a fresh authorization, but some clients retry `invalid_target`. If your grants are bound to a resource, configure exactly that one: see [Match the resource your existing grants carry](#match-the-resource-your-existing-grants-carry-10).
263
305
  - A multi-resource `OAuthAuthorizationServer` without `legacyGrantResource` has no safe destination for unbound records and rejects them. Set it for the migration window and keep it fixed.
264
306
  - Authorization codes issued by 0.x redeem under the same rules, except plain-PKCE ones ([above](#removed-the-implicit-grant-and-plain-pkce-12)).
265
307
  - Grants written before 1.0 have no KV key metadata; each refresh adds it.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudflare/workers-oauth-provider",
3
- "version": "1.2.1",
3
+ "version": "1.2.2",
4
4
  "description": "OAuth provider for Cloudflare Workers",
5
5
  "main": "dist/oauth-provider.js",
6
6
  "types": "dist/oauth-provider.d.ts",
@@ -1,13 +1,13 @@
1
1
  ---
2
- name: workers-oauth-provider-migrate-1.0
2
+ name: workers-oauth-provider-migrate-1.x
3
3
  description: Upgrade a Cloudflare Worker's @cloudflare/workers-oauth-provider from 0.x, 1.0 or 1.1 to the latest 1.x. Use when bumping that dependency, or when OAuthProvider / OAuthAuthorizationServer / OAuthResourceServer construction throws after an upgrade.
4
4
  ---
5
5
 
6
6
  # Upgrade @cloudflare/workers-oauth-provider to the latest 1.x
7
7
 
8
- Reference (read the section for every hit): `node_modules/@cloudflare/workers-oauth-provider/docs/migration-1.0.md`.
8
+ Reference (read the section for every hit): `node_modules/@cloudflare/workers-oauth-provider/docs/migration-1.x.md`.
9
9
  Types and JSDoc: `node_modules/@cloudflare/workers-oauth-provider/dist/oauth-provider.d.ts`.
10
- No KV migration exists or is needed; never edit stored data.
10
+ No KV migration exists or is needed; never edit stored data. Existing sessions survive only if the configured resource equals the `resource` stored on existing grants. A mismatch signs every user out at deploy; see "Existing grants decide the resource" below.
11
11
 
12
12
  ## Procedure
13
13
 
@@ -18,8 +18,8 @@ No KV migration exists or is needed; never edit stored data.
18
18
 
19
19
  | Pattern in the project | Fix | Guide section |
20
20
  | ----------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
21
- | `new OAuthProvider(` without `resourceMetadata` | add `resourceMetadata: { resource }` (ask the user for the value) | The canonical resource is required |
22
- | `resourceMatchOriginOnly` | delete | Removed: `resourceMatchOriginOnly` |
21
+ | `new OAuthProvider(` without `resourceMetadata` | add `resourceMetadata: { resource }` set to the value existing grants carry (see below) | The canonical resource is required |
22
+ | `resourceMatchOriginOnly` | delete; do not substitute the origin as `resource` (see below) | Removed: `resourceMatchOriginOnly` |
23
23
  | `resolveExternalToken` | return `audience: <resource>` | `resolveExternalToken` names its audience |
24
24
  | `scopes_supported` inside `resourceMetadata` | move to top-level `requiredScopes` | `requiredScopes` replaces `resourceMetadata.scopes_supported` |
25
25
  | `allowImplicitFlow`, `allowPlainPKCE` | delete; own clients must use code + S256 | Removed: the implicit grant and plain PKCE |
@@ -34,9 +34,19 @@ No KV migration exists or is needed; never edit stored data.
34
34
  | `new OAuthAuthorizationServer(` | drop `authorizeEndpoint`/`tokenEndpoint` if they equal `${issuer}/authorize`, `${issuer}/oauth/token` | `OAuthAuthorizationServer` endpoints have defaults |
35
35
  | `ExchangeTokenOptions.aud`, `AuthRequest.resource` as arrays | single strings | Type changes |
36
36
 
37
+ ## Existing grants decide the resource
38
+
39
+ The canonical `resource` is not a free choice on a 0.x upgrade. Every stored grant and token carries the `resource` the client sent at sign-in, clients resend it on every refresh, and 1.x compares it exactly. Guide section: "Match the resource your existing grants carry".
40
+
41
+ - 0.x without `resourceMetadata.resource` advertised a resource derived from the path each client called, e.g. `https://host/mcp`, not the origin. `resourceMatchOriginOnly: true` compared by origin but still stored the path.
42
+ - Never pick `new URL(request.url).origin` just because it satisfies the route-coverage check. It matches no path-bound grant.
43
+ - Before changing code, get the value from the live 0.x deployment: `curl -si <api route>` for the `resource_metadata` URL, then `curl <that URL> | jq -r .resource`. If you have access, sample grants with `wrangler kv key list --binding OAUTH_KV --remote --prefix grant:` and `wrangler kv key get … | jq -r .resource`. If you can't check, ask the user and say why it matters.
44
+ - Routes that advertised different resources in 0.x (e.g. `/mcp` and `/sse`) can't share one `OAuthProvider` resource. Stop and give the user the guide's three options (drop the deprecated endpoint, declare each resource on an `OAuthAuthorizationServer`, or accept one sign-in). Don't choose for them.
45
+ - Mismatch symptoms after deploy: `401 invalid_token` (`token_audience_unbound`), refresh `400 invalid_target` "must name exactly one configured protected resource" (`resource_not_configured`), or refresh `400 invalid_grant` (`grant_audience_unbound`).
46
+
37
47
  ## Ask the user, don't guess
38
48
 
39
- - The canonical `resource` (the URL MCP clients connect to; becomes the token audience).
49
+ - The canonical `resource` (the URL MCP clients connect to; becomes the token audience), confirmed against what existing grants carry.
40
50
  - On a multi-resource `OAuthAuthorizationServer`: `legacyGrantResource`, the destination for pre-1.0 grants. Fixed once deployed.
41
51
  - Whether registered clients use remote `http` or custom-scheme redirect URIs (they stop authorizing), and whether any user IDs contain `:`. Neither shows up in a typecheck.
42
52
  - Whether DCR clients registered narrow `grant_types` (now enforced).
@@ -44,6 +54,9 @@ No KV migration exists or is needed; never edit stored data.
44
54
 
45
55
  ## Verify
46
56
 
57
+ - The configured `resource` is byte-for-byte what the 0.x deployment advertised for each API route (scheme, host, path, trailing slash).
58
+ - With a session created before the upgrade (staging, or a kept 0.x session), a refresh and an API call both succeed on the new version.
59
+
47
60
  - `wrangler dev`, then `curl -i http://localhost:8787<api route>`: `401` whose `WWW-Authenticate` has `resource_metadata="…/.well-known/oauth-protected-resource<resource path>"`.
48
61
  - With a loopback dev resource, `curl http://localhost:8787/.well-known/oauth-protected-resource<resource path>`: `200` with the exact `resource`. Production serves it only on the resource's own origin.
49
62
  - `curl http://localhost:8787/.well-known/oauth-authorization-server<issuer path>`: `authorization_endpoint` and `token_endpoint` unchanged from before the upgrade. `OAuthProvider` serves it at the origin root; an `OAuthAuthorizationServer` issuer's path goes after the prefix (RFC 8414 §3.1), e.g. `…/oauth-authorization-server/tenant` for `https://example.com/tenant`.