From 3ef3cfecfa48e5bae6f2153be2339f619920ad2b Mon Sep 17 00:00:00 2001 From: Gustavo Valverde Date: Sun, 14 Jun 2026 20:46:40 -0700 Subject: [PATCH] fix(oauth-provider)!: bind client authentication to the issuing grant (#10063) --- .../oauth-provider-extension-surface.md | 2 +- docs/content/docs/plugins/oauth-provider.mdx | 14 +- .../oauth-provider/src/extensions.test.ts | 148 +++++++++++++----- packages/oauth-provider/src/introspect.ts | 6 +- packages/oauth-provider/src/revoke.ts | 4 +- packages/oauth-provider/src/token.ts | 33 ++-- packages/oauth-provider/src/types/index.ts | 24 ++- .../src/utils/client-assertion.ts | 4 +- packages/oauth-provider/src/utils/index.ts | 23 ++- 9 files changed, 164 insertions(+), 94 deletions(-) diff --git a/.changeset/oauth-provider-extension-surface.md b/.changeset/oauth-provider-extension-surface.md index 560e1e0671..9d03458d3a 100644 --- a/.changeset/oauth-provider-extension-surface.md +++ b/.changeset/oauth-provider-extension-surface.md @@ -4,7 +4,7 @@ Add an OAuth Provider extension surface so companion plugins can register token grants, assertion-based client authentication methods, additive discovery metadata, access-token/ID-token/UserInfo claim contributors, and client-id discovery sources without changing provider core for each OAuth RFC. Register through `extendOAuthProvider(ctx, extension)` in a plugin `init()` hook. -Contributions are guarded: grant types, auth methods, and assertion types must be disjoint across extensions; metadata and claims are additive and never override authorization-server core. Assertion-based client authentication methods can reuse the exported `consumeClientAssertion` helper for the RFC 7523 audience, lifetime, and `jti` replay checks. +Contributions are guarded: grant types, auth methods, and assertion types must be disjoint across extensions; metadata and claims are additive and never override authorization-server core. Assertion-based client authentication methods can reuse the exported `consumeClientAssertion` helper for the RFC 7523 audience, lifetime, and `jti` replay checks. A client authentication strategy proves only which client id the caller controls. The authorization server resolves and authorizes the client record itself, bound to the grant being issued, so a strategy cannot influence a client's grants, scopes, or enabled state, and a handler cannot authorize one grant while minting another. Plugins reach provider capabilities through one surface. A grant handler receives a `provider` (resolve a client, issue tokens, hash or look up a token, verify a token), and a plugin's own endpoints obtain the same object with `getOAuthProviderApi(ctx, opts, grantType?)`. An issued token can be sender-constrained by passing a `confirmation` (RFC 7800 `cnf`) to `issueTokens` or returning it from a client-authentication strategy; the authorization server owns `cnf`, so a claim contributor cannot set it. diff --git a/docs/content/docs/plugins/oauth-provider.mdx b/docs/content/docs/plugins/oauth-provider.mdx index 475ed431fd..03aabd4a7b 100644 --- a/docs/content/docs/plugins/oauth-provider.mdx +++ b/docs/content/docs/plugins/oauth-provider.mdx @@ -1417,10 +1417,9 @@ export const customOAuthExtension = () => extendOAuthProvider(ctx, { grants: { "urn:example:params:oauth:grant-type:custom": async ({ - grantType, provider, }) => { - const { client } = await provider.authenticateClient({ grantType }); + const { client } = await provider.authenticateClient(); return provider.issueTokens({ client, scopes: ["openid"], @@ -1450,11 +1449,11 @@ A claims contributor can add new claim names but never replaces an identity, aut A grant handler receives a `provider` capability surface (`getClient`, `authenticateClient`, `issueTokens`, `hashToken`, `validateAccessToken`). A plugin that needs those from its own endpoints (a back-channel authorization endpoint, a polling endpoint) obtains the same object with `getOAuthProviderApi(ctx, opts, grantType?)`, so it can resolve a client or verify a token without reaching into provider internals. Pass the grant type to mint tokens away from the token endpoint; omit it for read-only use, in which case `issueTokens` throws rather than mint an unlabeled grant. -To sender-constrain an issued token (RFC 7800 `cnf`), pass `confirmation` to `issueTokens`, or set it on the authenticated client a `clientAuthentication` strategy returns. The provider stamps it as the access token's `cnf` and marks the response `token_type` accordingly. `cnf` is authorization-server-owned and cannot be set through a claim contributor. +To sender-constrain an issued token (RFC 7800 `cnf`), pass `confirmation` to `issueTokens`, or return it from a `clientAuthentication` strategy. The provider stamps it as the access token's `cnf` and marks the response `token_type` accordingly. `cnf` is authorization-server-owned and cannot be set through a claim contributor. #### Client authentication obligations -A `clientAuthentication` strategy verifies the assertion against its own key source and returns the registered client. After verifying the signature it must enforce the same assertion hygiene the built-in `private_key_jwt` method enforces, or the provider will accept a forged or replayed assertion. Use the exported `consumeClientAssertion` helper to bind the assertion to the endpoint audience, require a bounded lifetime, and reject `jti` replays: +A `clientAuthentication` strategy verifies the assertion against its own key source and returns the client id it proved; the provider resolves and authorizes the client record itself, so a strategy proves identity but never supplies the record. After verifying the signature it must enforce the same assertion hygiene the built-in `private_key_jwt` method enforces, or the provider will accept a forged or replayed assertion. Use the exported `consumeClientAssertion` helper to bind the assertion to the endpoint audience, require a bounded lifetime, and reject `jti` replays: ```ts import { consumeClientAssertion } from "@better-auth/oauth-provider"; @@ -1468,10 +1467,9 @@ authenticate: async ({ ctx, opts, assertion, expectedAudience }) => { payload, expectedAudience: expectedAudience!, }); - return { - clientId: payload.sub as string, - client: await loadRegisteredClient(ctx, opts, payload.sub), - }; + // Return only the proven client id (and an optional `confirmation`). The + // provider resolves and authorizes the client record itself. + return { clientId: payload.sub as string }; }; ``` diff --git a/packages/oauth-provider/src/extensions.test.ts b/packages/oauth-provider/src/extensions.test.ts index 7f270a6629..3d1c913519 100644 --- a/packages/oauth-provider/src/extensions.test.ts +++ b/packages/oauth-provider/src/extensions.test.ts @@ -16,7 +16,6 @@ import type { SchemaClient, Scope, } from "./types"; -import { validateClientCredentials } from "./utils"; import { consumeClientAssertion } from "./utils/client-assertion"; describe("oauth-provider extensions", async () => { @@ -26,6 +25,7 @@ describe("oauth-provider extensions", async () => { const extensionGrant = "urn:better-auth:test:grant"; const extensionOpaqueGrant = "urn:better-auth:test:opaque-grant"; const extensionOpaqueBoundGrant = "urn:better-auth:test:opaque-bound-grant"; + const extensionDriftGrant = "urn:better-auth:test:drift-grant"; const extensionAuthMethod = "test_attestation_jwt"; const extensionAssertionType = "urn:better-auth:test:client-assertion-type:test-attestation"; @@ -47,7 +47,7 @@ describe("oauth-provider extensions", async () => { init(ctx) { extendOAuthProvider(ctx, { grants: { - [extensionGrant]: async ({ ctx, grantType, provider }) => { + [extensionGrant]: async ({ ctx, provider }) => { observedCustomParam = (ctx.body as { custom_param?: string }) .custom_param; if (!grantUser) { @@ -58,7 +58,6 @@ describe("oauth-provider extensions", async () => { } const { client, confirmation } = await provider.authenticateClient({ scopes: ["openid", "email", "vc"], - grantType, }); return provider.issueTokens({ client, @@ -86,7 +85,7 @@ describe("oauth-provider extensions", async () => { // Issues an opaque access token (no resource -> no audience), so // introspection re-derives extension claims through the resolver // instead of returning a signed JWT payload verbatim. - [extensionOpaqueGrant]: async ({ grantType, provider }) => { + [extensionOpaqueGrant]: async ({ provider }) => { if (!grantUser) { throw new APIError("BAD_REQUEST", { error: "invalid_request", @@ -95,7 +94,6 @@ describe("oauth-provider extensions", async () => { } const { client } = await provider.authenticateClient({ scopes: ["openid", "email", "vc"], - grantType, }); return provider.issueTokens({ client, @@ -109,7 +107,7 @@ describe("oauth-provider extensions", async () => { // Sender-constrains an opaque access token (no resource). The // confirmation is persisted on the opaque-token row (RFC 7800 `cnf`) // and surfaced at introspection, not silently dropped. - [extensionOpaqueBoundGrant]: async ({ grantType, provider }) => { + [extensionOpaqueBoundGrant]: async ({ provider }) => { if (!grantUser) { throw new APIError("BAD_REQUEST", { error: "invalid_request", @@ -118,7 +116,6 @@ describe("oauth-provider extensions", async () => { } const { client } = await provider.authenticateClient({ scopes: ["openid", "email", "vc"], - grantType, }); return provider.issueTokens({ client, @@ -127,32 +124,42 @@ describe("oauth-provider extensions", async () => { confirmation: { jkt: "opaque-bound-jkt" }, }); }, + // A stale/buggy handler that still passes a permissive grantType the + // type no longer allows. The provider must ignore it and authorize + // against the dispatched grant. + [extensionDriftGrant]: async ({ provider }) => { + if (!grantUser) { + throw new APIError("BAD_REQUEST", { + error: "invalid_request", + error_description: "test user is not ready", + }); + } + const staleRequest = { + scopes: ["openid", "email", "vc"], + grantType: extensionOpaqueGrant, + } as Parameters[0]; + const { client } = await provider.authenticateClient(staleRequest); + return provider.issueTokens({ + client, + scopes: ["openid", "email", "vc"], + user: grantUser, + }); + }, }, clientAuthentication: { [extensionAuthMethod]: { assertionTypes: [extensionAssertionType], - authenticate: async ({ ctx, assertion, clientId }) => { + authenticate: async ({ assertion, clientId }) => { if (!clientId || assertion !== `assertion:${clientId}`) { throw new APIError("UNAUTHORIZED", { error: "invalid_client", error_description: "invalid test assertion", }); } - const client = await ctx.context.adapter.findOne< - SchemaClient - >({ - model: "oauthClient", - where: [{ field: "clientId", value: clientId }], - }); - if (!client) { - throw new APIError("BAD_REQUEST", { - error: "invalid_client", - error_description: "missing client", - }); - } + // Prove the caller controls clientId; the provider resolves and + // authorizes the client record itself. return { clientId, - client, // A sender-constraint the strategy proved (wallet // attestation); the grant forwards it to issueTokens. An // mTLS-style `x5t#S256` keeps the token bearer-presentable, @@ -373,29 +380,56 @@ describe("oauth-provider extensions", async () => { expect(response.error?.status).toBe(400); }); - it("rejects assertion auth for legacy clients with omitted auth method", async () => { - const legacyClient = { - clientId: "legacy-default-client", - public: false, - } as SchemaClient; - await expect( - validateClientCredentials( - {} as Parameters[0], - {} as Parameters[1], - legacyClient.clientId, - undefined, - undefined, - legacyClient, - undefined, - extensionAuthMethod, - ), - ).rejects.toMatchObject({ - statusCode: 400, + it("rejects assertion auth for clients with omitted (default) auth method", async () => { + // A client with no explicit auth method defaults to client_secret_basic, so + // it cannot authenticate through an extension assertion method. + const legacyClient = await auth.api.adminCreateOAuthClient({ + headers, body: { - error: "invalid_client", - error_description: `client registered for client_secret_basic cannot use ${extensionAuthMethod}`, + grant_types: [extensionGrant], + scope: "openid email vc", + type: "web", }, }); + const response = await client.$fetch("/oauth2/token", { + method: "POST", + body: new URLSearchParams({ + grant_type: extensionGrant, + client_id: legacyClient!.client_id, + client_assertion_type: extensionAssertionType, + client_assertion: `assertion:${legacyClient!.client_id}`, + }), + headers: { + "content-type": "application/x-www-form-urlencoded", + }, + }); + expect(response.error?.status).toBe(400); + expect( + (response.error as { error_description?: string } | undefined) + ?.error_description, + ).toContain("client registered for client_secret_basic cannot use"); + }); + + it("authorizes against the resolved record, not the strategy", async () => { + // The strategy proves a client id; the provider resolves the record. A proven + // id with no registered client is rejected, since a strategy cannot conjure a + // client the authorization server never registered. + const response = await client.$fetch("/oauth2/token", { + method: "POST", + body: new URLSearchParams({ + grant_type: extensionGrant, + client_id: "unregistered-but-proven", + client_assertion_type: extensionAssertionType, + client_assertion: "assertion:unregistered-but-proven", + }), + headers: { + "content-type": "application/x-www-form-urlencoded", + }, + }); + expect(response.error?.status).toBe(400); + expect((response.error as { error?: string } | undefined)?.error).toBe( + "invalid_client", + ); }); it("rejects invalid direct extension options during provider setup", () => { @@ -548,6 +582,38 @@ describe("oauth-provider extensions", async () => { expect(introspection.data?.extension_access_claim).toBe("extension-access"); }); + it("ignores a runtime grantType argument and gates on the dispatched grant", async () => { + // The drift handler smuggles grantType: extensionOpaqueGrant into + // authenticateClient. This client is registered for the opaque grant but not + // the dispatched drift grant. Honoring the smuggled grant would authorize it; + // the bound grant must gate instead. + const oauthClient = await auth.api.adminCreateOAuthClient({ + headers, + body: { + token_endpoint_auth_method: extensionAuthMethod, + grant_types: [extensionOpaqueGrant], + scope: "openid email vc", + type: "web", + }, + }); + const response = await client.$fetch("/oauth2/token", { + method: "POST", + body: new URLSearchParams({ + grant_type: extensionDriftGrant, + client_id: oauthClient!.client_id, + client_assertion_type: extensionAssertionType, + client_assertion: `assertion:${oauthClient!.client_id}`, + }), + headers: { + "content-type": "application/x-www-form-urlencoded", + }, + }); + expect(response.error?.status).toBe(400); + expect((response.error as { error?: string } | undefined)?.error).toBe( + "unauthorized_client", + ); + }); + it("rejects unsupported extension grant types before credential handling", async () => { const response = await client.$fetch("/oauth2/token", { method: "POST", diff --git a/packages/oauth-provider/src/introspect.ts b/packages/oauth-provider/src/introspect.ts index d49ffe7a45..4d8507358d 100644 --- a/packages/oauth-provider/src/introspect.ts +++ b/packages/oauth-provider/src/introspect.ts @@ -595,11 +595,11 @@ export async function introspectEndpoint( const { clientId: client_id, clientSecret: client_secret, - preVerifiedClient, + preVerified, authMethod, } = destructureCredentials(credentials); - if (!client_id || (!client_secret && !preVerifiedClient)) { + if (!client_id || (!client_secret && !preVerified)) { throw new APIError("UNAUTHORIZED", { error_description: "missing required credentials", error: "invalid_client", @@ -624,7 +624,7 @@ export async function introspectEndpoint( client_id, client_secret, undefined, - preVerifiedClient, + preVerified, undefined, authMethod, ); diff --git a/packages/oauth-provider/src/revoke.ts b/packages/oauth-provider/src/revoke.ts index 80392fb1d0..30d40895c8 100644 --- a/packages/oauth-provider/src/revoke.ts +++ b/packages/oauth-provider/src/revoke.ts @@ -314,7 +314,7 @@ export async function revokeEndpoint( const { clientId: client_id, clientSecret: client_secret, - preVerifiedClient, + preVerified, authMethod, } = destructureCredentials(credentials); @@ -343,7 +343,7 @@ export async function revokeEndpoint( client_id, client_secret, undefined, - preVerifiedClient, + preVerified, undefined, authMethod, ); diff --git a/packages/oauth-provider/src/token.ts b/packages/oauth-provider/src/token.ts index d63c173f41..1805bfbecf 100644 --- a/packages/oauth-provider/src/token.ts +++ b/packages/oauth-provider/src/token.ts @@ -139,13 +139,8 @@ export function getOAuthProviderApi( // reject a valid assertion minted for the real endpoint. `${ctx.context.baseURL}${ctx.path ?? "/oauth2/token"}`, ); - const { - clientId, - clientSecret, - preVerifiedClient, - authMethod, - confirmation, - } = destructureCredentials(credentials); + const { clientId, clientSecret, preVerified, authMethod, confirmation } = + destructureCredentials(credentials); if (!clientId) { throw new APIError("BAD_REQUEST", { error_description: "Missing required client_id", @@ -155,7 +150,7 @@ export function getOAuthProviderApi( if ( request?.requireCredentials !== false && !clientSecret && - !preVerifiedClient + !preVerified ) { throw new APIError("BAD_REQUEST", { error_description: "Missing required client credentials", @@ -168,8 +163,8 @@ export function getOAuthProviderApi( clientId, clientSecret, request?.scopes, - preVerifiedClient, - request?.grantType ?? grantType, + preVerified, + grantType, authMethod, ); return { @@ -1101,7 +1096,7 @@ async function handleAuthorizationCodeGrant( const { clientId: client_id, clientSecret: client_secret, - preVerifiedClient, + preVerified, authMethod, } = destructureCredentials(credentials); @@ -1140,7 +1135,7 @@ async function handleAuthorizationCodeGrant( const isAuthCodeWithSecret = client_id && client_secret; const isAuthCodeWithPkce = client_id && code && code_verifier; - if (!isAuthCodeWithSecret && !isAuthCodeWithPkce && !preVerifiedClient) { + if (!isAuthCodeWithSecret && !isAuthCodeWithPkce && !preVerified) { throw new APIError("BAD_REQUEST", { error_description: "Either code_verifier or client_secret is required", error: "invalid_request", @@ -1172,7 +1167,7 @@ async function handleAuthorizationCodeGrant( client_id, client_secret, scopes, - preVerifiedClient, + preVerified, "authorization_code", authMethod, ); @@ -1195,7 +1190,7 @@ async function handleAuthorizationCodeGrant( } } else { // PKCE is optional - must have either PKCE, client_secret, or client_assertion - if (!(isAuthCodeWithPkce || isAuthCodeWithSecret || preVerifiedClient)) { + if (!(isAuthCodeWithPkce || isAuthCodeWithSecret || preVerified)) { throw new APIError("BAD_REQUEST", { error_description: "Either PKCE (code_verifier) or client authentication (client_secret or client_assertion) is required", @@ -1315,7 +1310,7 @@ async function handleClientCredentialsGrant( const { clientId: client_id, clientSecret: client_secret, - preVerifiedClient, + preVerified, authMethod, } = destructureCredentials(credentials); const { scope, resource }: { scope?: string; resource?: string | string[] } = @@ -1328,7 +1323,7 @@ async function handleClientCredentialsGrant( error: "invalid_grant", }); } - if (!client_secret && !preVerifiedClient) { + if (!client_secret && !preVerified) { throw new APIError("BAD_REQUEST", { error_description: "Missing a required client_secret", error: "invalid_grant", @@ -1342,7 +1337,7 @@ async function handleClientCredentialsGrant( client_id, client_secret, undefined, - preVerifiedClient, + preVerified, "client_credentials", authMethod, ); @@ -1402,7 +1397,7 @@ async function handleRefreshTokenGrant( const { clientId: client_id, clientSecret: client_secret, - preVerifiedClient, + preVerified, authMethod, } = destructureCredentials(credentials); @@ -1510,7 +1505,7 @@ async function handleRefreshTokenGrant( client_id, client_secret, // Optional for refresh_grant but required on confidential clients requestedScopes ?? scopes, - preVerifiedClient, + preVerified, "refresh_token", authMethod, ); diff --git a/packages/oauth-provider/src/types/index.ts b/packages/oauth-provider/src/types/index.ts index 6e5c68c8fe..b4fe451fac 100644 --- a/packages/oauth-provider/src/types/index.ts +++ b/packages/oauth-provider/src/types/index.ts @@ -110,10 +110,6 @@ export interface OAuthClientAuthenticationRequest { * Scopes to validate against the registered client. */ scopes?: string[]; - /** - * Grant type to enforce for the registered client. - */ - grantType?: GrantType; /** * Set to `false` for public extension grants that only require client_id. * @@ -252,6 +248,18 @@ export interface OAuthClientAuthenticationInput { expectedAudience?: string; } +export interface OAuthClientAuthenticationResult { + /** The client id the assertion proved the caller controls. */ + clientId: string; + /** + * A sender-constraint the strategy proved (for example a wallet-instance key + * thumbprint). The provider stamps it as the issued token's RFC 7800 `cnf`. + * Set it only after proving possession; the authorization server writes it as + * token material and does not verify it again. + */ + confirmation?: Confirmation; +} + export interface OAuthClientAuthenticationStrategy { /** * Assertion type URIs this strategy consumes from `client_assertion_type`. @@ -261,7 +269,11 @@ export interface OAuthClientAuthenticationStrategy { */ assertionTypes?: string[]; /** - * Verifies the presented assertion and returns the authenticated client. + * Verifies the presented assertion and returns the proven client id (plus any + * sender-constraint it established). The strategy proves the caller controls + * `clientId`; it does not supply the authorization record. The provider + * resolves and authorizes the client itself, so a strategy cannot influence + * the client's grants, scopes, or enabled state. * * The strategy owns the full RFC 7521/7523 verification. After verifying the * signature against its own key source, it MUST enforce the assertion-hygiene @@ -278,7 +290,7 @@ export interface OAuthClientAuthenticationStrategy { */ authenticate: ( input: OAuthClientAuthenticationInput, - ) => Awaitable; + ) => Awaitable; } export interface OAuthMetadataExtensionInput { diff --git a/packages/oauth-provider/src/utils/client-assertion.ts b/packages/oauth-provider/src/utils/client-assertion.ts index 35b2d1e41a..2374a61681 100644 --- a/packages/oauth-provider/src/utils/client-assertion.ts +++ b/packages/oauth-provider/src/utils/client-assertion.ts @@ -311,7 +311,7 @@ export async function verifyClientAssertion( clientAssertionType: string, clientIdHint?: string, expectedAudience?: string, -): Promise<{ clientId: string; client: SchemaClient }> { +): Promise<{ clientId: string }> { if (clientAssertionType !== CLIENT_ASSERTION_TYPE) { throw new APIError("BAD_REQUEST", { error_description: "unsupported client_assertion_type", @@ -453,5 +453,5 @@ export async function verifyClientAssertion( expectedAudience: audience, }); - return { clientId, client }; + return { clientId }; } diff --git a/packages/oauth-provider/src/utils/index.ts b/packages/oauth-provider/src/utils/index.ts index deff758aee..b60d16a06d 100644 --- a/packages/oauth-provider/src/utils/index.ts +++ b/packages/oauth-provider/src/utils/index.ts @@ -522,8 +522,11 @@ export function clientAllowsGrant( } /** - * Validates client credentials failing on mismatches - * and incorrectly provided information + * Resolves the registered client by id and authorizes it: existence, disabled + * state, registered auth method, requested scopes, and grant type. The record is + * always resolved here via `getClient`, so a client-auth strategy proves the + * caller controls `clientId` but never supplies the record. `preVerified` marks + * that an assertion already proved control, so the client-secret check is skipped. * * @internal */ @@ -533,11 +536,11 @@ export async function validateClientCredentials( clientId: string, clientSecret?: string, // optional because required if client is confidential or this value is defined scopes?: string[], // checks requested scopes against allowed scopes - preVerifiedClient?: SchemaClient, + preVerified?: boolean, // an assertion already proved control of clientId; skip the secret check grantType?: GrantType, // if set, enforces the client is registered for this grant type authMethod?: TokenEndpointAuthMethod, ) { - const client = preVerifiedClient ?? (await getClient(ctx, options, clientId)); + const client = await getClient(ctx, options, clientId); if (!client) { throw new APIError("BAD_REQUEST", { error_description: "missing client", @@ -552,7 +555,7 @@ export async function validateClientCredentials( } // Enforce registered auth method for assertion/pre-verified methods. - if (preVerifiedClient && authMethod) { + if (preVerified && authMethod) { const registeredAuthMethod = client.tokenEndpointAuthMethod ?? "client_secret_basic"; if (registeredAuthMethod !== authMethod) { @@ -568,7 +571,7 @@ export async function validateClientCredentials( options, client.tokenEndpointAuthMethod, )) && - !preVerifiedClient + !preVerified ) { throw new APIError("BAD_REQUEST", { error_description: `client registered for ${client.tokenEndpointAuthMethod} must use client_assertion`, @@ -577,7 +580,7 @@ export async function validateClientCredentials( } // Skip secret checks for pre-verified clients (already authenticated via assertion) - if (!preVerifiedClient) { + if (!preVerified) { // Require secret for confidential clients if (!client.public && !clientSecret) { throw new APIError("BAD_REQUEST", { @@ -662,7 +665,6 @@ export type ExtractedCredentials = kind: "pre_verified"; method: TokenEndpointAuthMethod; clientId: string; - client: SchemaClient; /** Sender-constraint the auth strategy proved, forwarded to issuance. */ confirmation?: Confirmation; } @@ -682,8 +684,7 @@ export function destructureCredentials( credentials?.kind === "client_secret" ? credentials.clientSecret : undefined, - preVerifiedClient: - credentials?.kind === "pre_verified" ? credentials.client : undefined, + preVerified: credentials?.kind === "pre_verified", authMethod: credentials?.method, confirmation: credentials?.kind === "pre_verified" @@ -742,7 +743,6 @@ export async function extractClientCredentials( kind: "pre_verified", method: extensionStrategy.method, clientId: result.clientId, - client: result.client, confirmation: result.confirmation, }; } @@ -761,7 +761,6 @@ export async function extractClientCredentials( kind: "pre_verified", method: "private_key_jwt", clientId: result.clientId, - client: result.client, }; }