8000 Tags · zitadel/oidc · GitHub
[go: up one dir, main page]

Skip to content

Tags

Tags

v3.52.0

Toggle v3.52.0's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
feat(op): serve RFC 8414 metadata through a separate MetadataServer (#…

…1035)

# Which Problems Are Solved

- Clients that follow RFC 8414 (OAuth 2.0 Authorization Server
Metadata), such as MCP clients, look up
`/.well-known/oauth-authorization-server`. The `op.Server` API serves
only `/.well-known/openid-configuration`, so these clients get a 404.
- For an issuer with a path, such as `https://h/oauth/p1`, RFC 8414 §3
places the metadata at
`https://h/.well-known/oauth-authorization-server/oauth/p1`, outside the
prefix the OP is mounted under. The mirrored OIDC form from RFC 8414 §5
is outside it too. A router can't route these paths for a dynamic
issuer: chi params never span a `/` and `*` only works at the end of a
pattern, and `http.ServeMux` behaves the same way.
- A multi-tenant OP, with one issuer per path segment, has no built-in
way to get the tenant into `Server` methods or into an
`IssuerFromRequest`.

# How the Problems Are Solved

- **New `MetadataServer` interface**, with `OpenIDConfiguration` and
`AuthorizationServerMetadata`. Implementations embed
`UnimplementedMetadataServer`.
- **`RegisterMetadataServer`** returns a `*MetadataHandler` that serves
only well-known paths. The handler parses the path itself and passes the
issuer path to the server as `MetadataRequest.IssuerPath`:

  | Path | Method | IssuerPath |
  |---|---|---|
| `/.well-known/oauth-authorization-server{P}` |
`AuthorizationServerMetadata` | `P` |
| `/.well-known/openid-configuration{P}` | `OpenIDConfiguration` | `P`
(RFC 8414 §5) |
| `{P}/.well-known/openid-configuration` | `OpenIDConfiguration` | `P`
(OIDC Discovery) |

- Mount it at `/.well-known/` and give the OIDC form its own pattern, or
wrap the whole mux with `Intercept(next)`. `Intercept` also covers
multi-segment issuer paths.
- It answers `GET` and `HEAD`; other methods get a JSON 405 with
`Allow`. Its 404s go through CORS, like the rest of its responses.
- It takes `ServerOption`, so `WithHTTPMiddleware` and
`WithServerCORSOptions` apply. It has no routes of its own: the
middleware runs as a chain around it, and of `WithSetRouter` only the
middleware applies.
- **`WithTenantPrefix(prefix)`**, honoured by both `RegisterServer` and
`RegisterMetadataServer`. Both handlers set the tenant on the context
before user middleware runs, so one `IssuerFromRequest` built on
`TenantFromContext` works for both.
- `RegisterServer` puts a small chi router in front of its own, with
each of its routes registered twice: as `{prefix}/{tenant}{route}` and,
for the empty tenant, as `{prefix}{route}`. chi tries a static segment
before the `{tenant}` parameter, so a known route wins:
`/oauth/authorize` belongs to the empty tenant and `/oauth/p1/authorize`
to tenant `p1`. The empty tenant can serve as a root or platform issuer
at `https://h/oauth`. Any other path under the prefix is forwarded as a
chi `Mount` at `{prefix}/{tenant}` would forward it, so the router's
NotFound handler, mounted routers and middleware still apply. The OP
router gets the rest of the path as the chi route path, the URL is left
as is, and chi's route pattern reads `/oauth/{tenant}/authorize`, as
under a `Mount`. Paths outside the prefix and invalid tenants get a JSON
404 with CORS headers. A tenant `T` loses a route `R` only when `/T`+`R`
is itself a route, which never happens with the default endpoints; the
doc comment spells this out.
- `RegisterMetadataServer` accepts issuer paths `{prefix}` (empty
tenant) and `{prefix}/{tenant}`, and gives anything else a 404 without
calling the server.
- Without the option, `RegisterServer` behaves exactly as before, and
the metadata handler passes every issuer path through without setting a
tenant.
- **Path handling:** both handlers work on the path chi routes on,
including a `RoutePath` set by `Mount`. In the OP handler chi splits the
raw path and the tenant is percent-decoded once; the metadata handler
decodes the path once and then splits it. In both, a tenant with a
`%2F`, a dot segment or an empty segment is a 404. A `RoutePath` that
middleware rebuilt from the decoded path, such as chi's `StripSlashes`,
is not decoded a second time.
- **`TenantFromContext` / `ContextWithTenant`** expose the tenant on the
context.

Wiring with a prefix on a `ServeMux`:

```go
meta := op.RegisterMetadataServer(ms, op.WithTenantPrefix("/oauth"), op.WithHTTPMiddleware(issuerMW))
srv := op.RegisterServer(s, ep, op.WithTenantPrefix("/oauth"), op.WithHTTPMiddleware(issuerMW))
mux.Handle("/.well-known/", meta)
mux.Handle("/oauth/{t}/.well-known/openid-configuration", meta)
mux.Handle("/oauth/.well-known/openid-configuration", meta)
mux.Handle("/oauth/", srv)
```

# Additional Changes

- New constant `oidc.AuthorizationServerMetadataEndpoint`.
- `Server.Discovery` is deprecated in favour of `MetadataServer`.
`RegisterServer` still routes `/.well-known/openid-configuration` to it,
so existing implementations keep working.
- `simpleHandler` now delegates to a generic `serveSimple`, which the
metadata handler shares.
- Table tests on `http.ServeMux` and on chi `Handle`, `Mount` and
`StripSlashes`. They cover:
- both setups end to end: a static prefix on a `ServeMux`, and tenants
directly under the host with `Intercept` in front of the mux;
  - all three URL forms and multi-segment issuer paths;
  - escaped, double-escaped and dot segments;
- the empty tenant, the known-route rule, shadowing and tenants named
like routes;
- chi route patterns and NotFound handlers of routers mounted through
`WithSetRouter`;
  - methods, CORS on 404 and preflights;
  - `RegisterLegacyServer` passing the option through;
- that the `ServeMux` patterns above register without a conflict panic.
- The README features table lists RFC 8414.
- A one-character gofmt fix in a comment in `pkg/oidc/jwt_profile.go`,
which was already on `main`.

# Additional Context

- Related: zitadel/nextgen#2793, which serves one issuer per project
under `/oauth/{project_id}` and maps the empty tenant to the platform
project.
- Supersedes #1034, which routed `Server.Discovery` to the RFC 8414
paths inside `RegisterServer`'s router. That route had to rely on a
router wildcard and precedence workarounds, and it could not serve the
RFC 8414 §5 form.
- `LegacyServer` does not implement `MetadataServer`. `LegacyServer`
users keep the deprecated `Discovery` for now.
- Out of scope: `signed_metadata` (RFC 8414 §2.1), client-side RFC 8414
discovery, and the legacy `Provider` / `CreateRouter`.
- https://www.rfc-editor.org/rfc/rfc8414.html
- https://openid.net/specs/openid-connect-discovery-1_0.html

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

v3.51.17

Toggle v3.51.17's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
fix(op): pass a storage server_error through client authentication (#…

…1000)

# Which Problems Are Solved

- A Storage that fails client authentication with
`oidc.ErrServerError()` (database down, corrupt row) gets a 401
`invalid_client` answer instead of a 500 `server_error`. The client sees
a credentials error for a server fault and retries or rotates secrets
for nothing.
- The cause is the wrap at each client authentication call site.
`oidc.ErrInvalidClient().WithParent(err)` puts a new error on the
outside, and `RequestError` maps the status from the outermost
`*oidc.Error` only, so the typed `server_error` is never seen.

# How the Problems Are Solved

- New unexported helper `clientAuthError(base, err)` in
`pkg/op/client.go`. It returns the Storage's own error when that error
is a `server_error`, and `base.WithParent(err)` for everything else.

- The 8 call sites that wrapped Storage client errors now use it. That
covers `AuthorizeClientCredentialsClient`, `AuthorizeClientIDSecret`,
`AuthorizeCodeClient`, `AuthorizeTokenExchangeClient`,
`ParseTokenRevocationRequest`, `ClientBasicAuth`,
`LegacyServer.VerifyClient` and the legacy server introspection client
check.

- Only `server_error` passes through. A wrong secret or an unknown
client still answers `invalid_client`, so the response does not leak
more than it does today. No exported signature changes.

# Additional Changes

- `pkg/op/client_auth_error_test.go` has unit tests for
`AuthorizeClientCredentialsClient` and `AuthorizeClientIDSecret` and a
token endpoint test through `op.NewOpenIDProvider` with basic auth for
the `client_credentials` and `authorization_code` grants. Each runs one
Storage that returns `oidc.ErrServerError()` and one that returns a
plain error.

```
$ go test -count=1 -run 'StorageErrors' -v ./pkg/op/

main c9e2385 plus the new test file
--- FAIL: TestAuthorizeClientCredentialsClient_StorageErrors/typed_server_error_passes_through
    expected: "server_error"  actual: "invalid_client"
--- FAIL: TestAuthorizeClientIDSecret_StorageErrors/typed_server_error_passes_through
    expected: "server_error"  actual: "invalid_client"
--- FAIL: TestTokenEndpoint_ClientAuthStorageErrors/client_credentials/typed_server_error_passes_through
    expected: 500  actual: 401
--- FAIL: TestTokenEndpoint_ClientAuthStorageErrors/authorization_code/typed_server_error_passes_through
    expected: 500  actual: 401
(4 plain_error_stays_invalid_client subtests PASS)
FAIL	github.com/zitadel/oidc/v3/pkg/op	1.416s

this branch
(8 of 8 subtests PASS)
ok  	github.com/zitadel/oidc/v3/pkg/op	0.986s
```

- `go test -race -count=1 ./pkg/...` (the CI command) gives 9 ok
packages on main and on this branch. `gofmt -l pkg/op` and `go vet
./pkg/op/` are clean. `staticcheck` 2026.2.1 with the tags '', `no_otel`
and `create_regression_data` reports nothing. All with go1.26.4.
- I also checked the complaint over real HTTP with `httptest.NewServer`,
the example storage and a Storage that returns `oidc.ErrServerError()`
from `AuthorizeClientIDSecret` and `ClientCredentials`. A `server_error`
from `GetClientByClientID` gives the same main and branch answers for
the code and revoke rows.

```
                                         main                     this branch
op.NewProvider
  token client_credentials (basic)       401 invalid_client       500 server_error
  token authorization_code (basic/post)  401 invalid_client       500 server_error
  revoke (post)                          401 invalid_client       500 server_error
  device_authorization (basic)           400 unauthorized_client  500 server_error
op.RegisterLegacyServer
  token authorization_code (basic/post)  400 invalid_client       500 server_error
  revoke (post)                          400 invalid_client       500 server_error
  device_authorization (basic)           400 invalid_client       500 server_error
  introspect (basic)                     400 unauthorized_client  500 server_error
plain Storage error, every row above     same answer on main and on this branch
```

- `op.Provider`'s introspection handler writes 401 for every error, on
main and on this branch. I left it as it is.

# Additional Context

- Closes #990
- The paths that already return the Storage error unwrapped (refresh,
device, JWT profile, the legacy server `client_credentials` grant) keep
the behaviour from #865 and are unchanged.

---------

Co-authored-by: Wim Van Laer <wim@wvl.app>

v3.51.16

Toggle v3.51.16's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
fix(op): reject an unsupported code_challenge_method (#1003)

# Which Problems Are Solved

- The authorization endpoint accepted any `code_challenge_method` and
stored it with the auth request. `oidc.VerifyCodeChallenge` only
transforms the `code_verifier` for `S256`, so at the token endpoint an
auth request with an unsupported method (for example `S512`, or a
lower-case `s256` from a misconfigured client) was verified **as if the
method were `plain`**.
- [RFC 7636, section
4.4.1](https://www.rfc-editor.org/rfc/rfc7636#section-4.4.1) requires
the server to answer `invalid_request` when it does not support the
requested transformation.

# How the Problems Are Solved

- Adds `op.ValidateAuthReqCodeChallengeMethod(codeChallenge, method)`,
next to the other `ValidateAuthReq*` helpers. When a `code_challenge` is
sent, it returns `invalid_request` for a method that is not supported.
An absent method is accepted (it defaults to `plain`, section 4.3), and
so is any method sent without a `code_challenge`, since there is nothing
to downgrade.
- Calls it from `ValidateAuthRequestClient` (Provider path) and from
`authorizeWithValidRedirectURI` (op.Server path), after the redirect_uri
is validated, so the error is delivered to the client like the other
request errors.
- A custom `AuthorizeValidator` replaces `ValidateAuthRequestClient`, so
`Authorize` also runs the check in that case only (where it looks the
client up itself), before `CreateAuthRequest`. Since such a validator
may not have verified the redirect_uri, the error is redirected only to
a redirect_uri registered for the client (`redirectURIRegistered`: exact
match, glob, or a native client's loopback redirect on any port, RFC
8252 section 7.3) and rendered directly otherwise.
- The error description does not echo the method.

`plain` and `S256` stay accepted, so behaviour does not change for any
client that sends a supported method.

# Additional Changes

- Adds `oidc.CodeChallengeMethod.IsSupported()` (a `switch` on the two
constants) and uses it in the validator and in the RP's
PKCE-from-discovery check.
- `oidc.VerifyCodeChallenge` switches on the method and returns false
for an unsupported one instead of comparing it as `plain`; the example
storage's `CodeChallengeToOIDC` passes the stored method through instead
of mapping unknown methods to `plain`.
- Tests:
- `TestValidateAuthReqCodeChallengeMethod` and
`TestValidateAuthRequestClientCodeChallengeMethod` (fails if the check
in `ValidateAuthRequestClient` is removed).
- `TestAuthorizeErrorParity`: "unsupported code_challenge_method"
delivers `invalid_request` to the redirect_uri on both paths.
- `TestAuthorizeCustomValidatorCodeChallengeMethod`: a custom validator
that accepts `S512`, with registered, unregistered, native loopback on
another port, and registered native `http` redirect_uris; `state` and
the exact redirect_uri are asserted.
- `TestCodeChallengeMethod_IsSupported` and `TestVerifyCodeChallenge` in
`pkg/oidc`.

# Additional Context

- On Windows, `TestAuthResponseCode`, `TestRoutes` and
`TestServerRoutes` fail on `main` as well (line endings in HTML
fixtures); everything else in `go test ./...` passes locally.

_AI assistance: prepared with Claude Code._

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Wim Van Laer <wim@wvl.app>

v3.51.15

Toggle v3.51.15's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
fix(rp): generate the PKCE code_verifier from 32 random octets (#1004)

# Which Problems Are Solved

- `GenerateAndStoreCodeChallenge` and
`GenerateAndStoreCodeChallengeWithRequest` built the PKCE
`code_verifier` as `base64url(uuid.New().String())`. A version 4 UUID
carries 122 random bits; base64url-encoding its 36-character text form
(fixed dashes, fixed version nibble, hex alphabet only) makes the
verifier 48 characters long without adding any entropy.
- [RFC 7636, section
4.1](https://www.rfc-editor.org/rfc/rfc7636#section-4.1) recommends a
32-octet random sequence (256 bits), base64url-encoded to 43 characters.

# How the Problems Are Solved

- Both functions use `oauth2.GenerateVerifier()` from
`golang.org/x/oauth2` (already a dependency), which produces exactly the
RFC-recommended verifier from `crypto/rand`.
- The now unused `encoding/base64` and `github.com/google/uuid` imports
are removed from `relying_party.go` (the module still uses `uuid`
elsewhere, so `go.mod` is unchanged).

The verifier stays an opaque value stored in the PKCE cookie and sent to
the token endpoint, so nothing else changes for callers or OPs.

# Additional Changes

- `TestGenerateAndStoreCodeChallenge` reads the verifier back from the
cookie and checks it decodes to 32 octets, that the returned challenge
is its S256 hash, and that it does not repeat. It fails before this
change (the decoded UUID text is 36 bytes).

# Additional Context

- Same recommendation as
[`oauth2.GenerateVerifier`](https://pkg.go.dev/golang.org/x/oauth2#GenerateVerifier)
cites.

_AI assistance: prepared with Claude Code._

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Wim Van Laer <wim@wvl.app>

v3.51.14

Toggle v3.51.14's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
fix(http): keep the underlying error when requests to the OP fail (#1002

)

# Which Problems Are Solved

- `HttpRequest` formats body read and unmarshal errors with `%v`, and
the remote key set does the same when fetching the JWKS. The original
error is lost, so callers can't use `errors.Is` / `errors.As` to tell a
network failure, timeout, `*oidc.Error` or `ErrResponseBodyTooLarge`
apart from an invalid signature.

# How the Problems Are Solved

- Wrap those errors with `%w` in `pkg/http/http.go` and
`pkg/client/rp/jwks.go`. Error messages are unchanged.

# Additional Changes

- Tests for `HttpRequest` error wrapping and for
`remoteKeySet.VerifySignature` exposing JWKS fetch errors.

# Additional Context

- Refs #961. This covers the error chain inside the library; passing the
error on from `CodeExchangeHandler` / `UserinfoCallback` needs an API
decision and is left out.

v3.51.13

Toggle v3.51.13's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
fix(oidc): return an error for non-string audience array entries (#998)

# Which Problems Are Solved

- `Audience.UnmarshalJSON` panics when an `aud` array contains a
non-string element, for example `"aud":["client",1]` (`interface
conversion: interface {} is float64, not string`).
- `rp.VerifyIDToken` decodes the claims before it checks the signature,
so an unsigned token can reach this code and panic the relying party.

# How the Problems Are Solved

- Replace the unchecked type assertion with
`gu.AssertInterfaces[string]`, which the `amr` and `locales` decoders in
the same file already use, and return the error with an `oidc aud:`
prefix.

# Additional Changes

- Add test cases to `TestAudience_UnmarshalText`: an array with a number
(now an error, no panic) and a bare number (still ignored, as before).
- The `default` case (number, object, bool, null) is unchanged. It still
leaves `aud` empty, and the audience check rejects the token later.
Changing it to an error would also reject `"aud":null`, so I left it out
of this fix.

# Additional Context

- Reproduced on v3.51.10, v3.51.11 and main.

Co-authored-by: Wim Van Laer <wim@wvl.app>

v3.51.12

Toggle v3.51.12's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
fix(rp): use a single client authentication method for device authori…

…zation (#996)

# Which Problems Are Solved

#858 made `DeviceAccessToken` send exactly one client authentication
method, as [RFC 6749
§2.3](https://datatracker.ietf.org/doc/html/rfc6749#section-2.3)
requires, but `DeviceAuthorization` was not changed and can still send
two:

- **With a signer**, `newDeviceClientCredentialsRequest` sets
`client_assertion` *and* `client_secret`, so the form carries both.
- **`AuthStyleInHeader` is ignored**, so the secret always goes in the
form. A caller who needs `client_secret_basic` passes an `authFn` doing
basic auth, and the request then carries the `Authorization` header
*and* the form secret.

An OP that enforces §2.3 rejects either request. Authelia does, which is
the kind of failure #830 reports for the device flow.

The device flow also signed its assertions with `aud: [rp.Issuer()]`,
which is `[""]` for an OAuth-only RP created with
`NewRelyingPartyOAuth`; the other flows fall back to the token endpoint
since #986/#989.

# How the Problems Are Solved

`DeviceAuthorization` now picks one method first, with a `switch` in the
same order as `DeviceAccessToken`:

1. `AuthStyleInHeader`: basic auth, combined with the caller's
`RequestAuthorization` if there is one (so a tenant or tracing header is
kept);
2. a signer: a signed assertion, signed only on this path;
3. otherwise: the secret in the form.

The caller's `authFn` is never treated as authentication itself. A
`FormAuthorization` (for example the PKCE workaround in #784) can't be
combined with a header, so with one the credentials stay in the form, as
before. `client_id` stays in the form in every case.

Both device-flow assertions now use `jwtProfileAudience`, and
`ClientCredentialsRequest.ClientSecret` is `omitempty`, so a request
authenticated another way carries no empty `client_secret` parameter.

# Additional Changes

- `pkg/client/rp/device_test.go`: an `httptest` OP records the form and
headers and asserts one method per request across each auth style, with
and without a signer, and with header-only, typed-nil and form-only
`authFn`s; a failing signer must not break basic auth; and the assertion
audience is checked for both `DeviceAuthorization` and
`DeviceAccessToken`, with and without an issuer.
- Against `main`, the double-method, header-style, failing-signer and
issuer-less audience cases fail; the default, `AuthStyleInParams` and
form-only cases pass on both.
- `go test ./...` passes except in `pkg/op` (`TestRoutes`,
`TestServerRoutes`, `TestAuthResponseCode`), which fail identically on
`main` for me locally. `go vet` is clean.

# Additional Context

- Follow-up for PR #858
- Related to #830: the token polling part of that report was fixed by
#858 (released in v3.47.9); this covers the device authorization
request.

v3.51.11

Toggle v3.51.11's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
fix(oidc): accept a JSON array for SpaceDelimitedArray (#992)

# Which Problems Are Solved

- JWT access tokens whose `scope` claim is a JSON array of strings (as
issued by Cloudflare Access) cannot be unmarshalled into
`oidc.AccessTokenClaims`, so `op.VerifyAccessToken` rejects them with
`json: cannot unmarshal array into Go struct field atcAlias.scope of
type string`.

# How the Problems Are Solved

- `SpaceDelimitedArray.UnmarshalJSON` uses a type switch, like
`AuthenticationMethodsReferences.UnmarshalJSON`, and handles the array
as a compatibility case with a comment linking #991, similar to the
Auth0 case in `Time.UnmarshalJSON`.
- The only accepted addition is an array that can be written as a
space-delimited string: at least one value, each a non-empty string
without spaces. Every other input behaves exactly as before, including
`null`, `""`, other arrays and the error message.

# Additional Changes

- Adds `TestSpaceDelimitedArray_UnmarshalJSON` (strings, arrays, `null`,
`""`, `[]`, `["a b"]`, `["a",""]`, `["a",null]` and invalid types),
`TestSpaceDelimitedArray_MarshalJSON_FromArray` (an array-parsed value
marshals back to the space-delimited string) and
`TestAccessTokenClaims_UnmarshalJSON_ArrayScope`.

# Additional Context

- Closes #991
- RFC 9068 (section 2.2.3) defines `scope` as a space-delimited string,
so a provider that sends an array is out of spec. As with the existing
exceptions for other out-of-spec providers (e.g. the `exp` claim), this
accepts the array for interoperability.
- `TestRoutes` and `TestServerRoutes` in `pkg/op` fail locally for me on
`main` as well (`expires_in` 300 vs 299), so those failures are
unrelated to this change.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Wim Van Laer <wim@wvl.app>

v3.51.10

Toggle v3.51.10's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
fix(client): keep extra token response fields from the token endpoint (…

…#980)

# Which Problems Are Solved

- `rp.RefreshTokens` drops every token response field except `id_token`.
A provider that returns `refresh_expires_in`, `session_state` or similar
values sends them in the body, but `tokens.Extra(...)` returns nil for
them afterwards. The code exchange path keeps them because it goes
through `golang.org/x/oauth2`, so the two paths behave differently.

# How the Problems Are Solved

- `CallTokenEndpointWithAuthFn` now reads the body once as raw JSON,
decodes it into `AccessTokenResponse` as before for the typed fields,
and attaches the full decoded object with `Token.WithExtra`. `id_token`
stays available under the same key, so `verifyTokenResponse` and
existing callers are unchanged. This also covers the JWT profile grant,
which uses the same function.

# Additional Changes

- `TestCallTokenEndpoint_PreservesExtraFields` checks the typed fields
plus `id_token`, `refresh_expires_in` and `session_state` in the extras.
It fails on `main` with the last two nil.
- `TestCallTokenEndpoint_InvalidResponse` checks that a JSON body that
is not an object still returns an error.
- `go test ./pkg/client/...` passes before and after the change.

# Additional Context

- Closes #907

---------

Co-authored-by: Wim Van Laer <wim@wvl.app>

v3.51.9

Toggle v3.51.9's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
fix(op): isolate provider endpoints and CORS options from package glo…

…bals (#927)

# Which Problems Are Solved

- `Provider.endpoints` was a `*Endpoints` pointing at the package-global
`DefaultEndpoints`, so `WithCustomAuthEndpoint` and the other
`WithCustom*Endpoint` options assigned through the pointer into the
global rather than into the provider. A custom endpoint set on one
provider appeared on every other provider constructed in the same
process, including providers that never asked for it.
- Those writes are unsynchronised, so constructing a provider with
custom endpoints while another provider is already serving requests is a
data race on the shared `Endpoints`. `go test -race` reports it on
`(*Provider).TokenEndpoint()`.
- `Scopes()` and `SupportedClaims()` returned the exported
`DefaultSupportedScopes` and `DefaultSupportedClaims` slices directly,
so a caller that mutates the returned slice edits the package defaults
for the whole process.

# How the Problems Are Solved

- `NewProvider` copies `*DefaultEndpoints` and points
`Provider.endpoints` at that per-provider copy, so each
`WithCustom*Endpoint` assigns into the provider's own `Endpoints`. The
field and `DefaultEndpoints` keep their `*Endpoints` types.
- `NewProvider` copies `defaultCORSOptions` per provider with
`cloneCORSOptions`, which also clones the slice fields so in-place edits
don't reach the shared backing arrays. `CORSOptions()` returns that
pointer to callers, so while the global was shared,
`p.CORSOptions().AllowedOrigins = ...` reconfigured CORS for every
provider in the process.
- `Scopes()` and `SupportedClaims()` return `slices.Clone(...)`.

# Additional Changes

- `RegisterServer` also uses `cloneCORSOptions` instead of taking the
address of the global. There is no reachable mutation path today,
because `WithServerCORSOptions` replaces the pointer and `webServer`
exposes no accessor. It removes the last alias to the global and keeps
both constructors consistent.
- Two regression tests, both of which fail on `main`:
- `TestNewProviderDoesNotMutateDefaultEndpoints` fails with `sibling
provider inherited a customisation it did not request`.
  - `TestNewProviderEndpointsConcurrentRace` trips the race detector.

# Additional Context

- Setting `op.DefaultEndpoints` before constructing a provider still
works as a process-wide default. It no longer retroactively mutates
providers that already exist, which is the racy behaviour being removed.
- Endpoint customisations no longer propagate between providers. Anyone
relying on that will see a change in behaviour.

---------

Co-authored-by: Wim Van Laer <wim@wvl.app>
0