From 9c40ceeefd240802e37e544cb9f8895a84e4c04b Mon Sep 17 00:00:00 2001 From: Gustavo Valverde Date: Sun, 31 May 2026 17:18:11 +0100 Subject: [PATCH] feat(oauth-provider): dispatch contributed grant types, claims, and discovery The oauth-provider consumes plugin contributions to its host. The token endpoint dispatches plugin-registered custom grant types, the discovery documents advertise contributed grant URIs, metadata, and token-endpoint auth methods, and minted access and ID tokens include plugin-contributed claims. The grant-author helpers (createUserTokens, validateClientCredentials, basicToClientCredentials, getClient, storeToken) and the OAuthContributions contract type are now exported, so grant types like CIBA, OIDC4VCI, and token exchange can ship as external packages. The `grant_type` body parameter accepts any registered grant URI instead of a fixed enum. An unknown grant type returns an `unsupported_grant_type` error response rather than a schema-validation error; the built-in grants are unaffected. --- .changeset/plugin-contribution-protocol.md | 11 ++ .../oauth-provider/src/contributions.test.ts | 150 ++++++++++++++++++ packages/oauth-provider/src/index.ts | 9 ++ packages/oauth-provider/src/metadata.ts | 39 +++++ packages/oauth-provider/src/oauth.ts | 105 ++++++++---- packages/oauth-provider/src/token.ts | 65 +++++++- .../oauth-provider/src/types/contributions.ts | 101 ++++++++++++ packages/oauth-provider/src/types/index.ts | 5 + packages/oauth-provider/src/types/oauth.ts | 10 +- packages/oauth-provider/src/utils/index.ts | 2 +- 10 files changed, 461 insertions(+), 36 deletions(-) create mode 100644 .changeset/plugin-contribution-protocol.md create mode 100644 packages/oauth-provider/src/contributions.test.ts create mode 100644 packages/oauth-provider/src/types/contributions.ts diff --git a/.changeset/plugin-contribution-protocol.md b/.changeset/plugin-contribution-protocol.md new file mode 100644 index 0000000000..a24432ac1e --- /dev/null +++ b/.changeset/plugin-contribution-protocol.md @@ -0,0 +1,11 @@ +--- +"@better-auth/core": minor +"better-auth": minor +"@better-auth/oauth-provider": minor +--- + +Add a plugin contribution protocol so plugins can extend the oauth-provider without changing better-auth. A plugin declares `contributes: { "oauth-provider": { ... } }` (typed via the host-augmented `ContributionContracts` interface) plus an optional `requires` list validated at startup, and the host collects them with `ctx.getContributions(...)` in its init. + +The oauth-provider now consumes those contributions. The token endpoint dispatches plugin-registered custom grant types, the discovery documents advertise contributed grant URIs, metadata, and token-endpoint auth methods, and minted access and ID tokens include plugin-contributed claims. The grant-author helpers (`createUserTokens`, `validateClientCredentials`, `basicToClientCredentials`, `getClient`, `storeToken`) and the `OAuthContributions` contract type are now exported so grant types like CIBA, OIDC4VCI, and token exchange can ship as external packages. + +The token endpoint's `grant_type` parameter now accepts any registered grant URI instead of a fixed enum. An unknown grant type returns an OAuth `unsupported_grant_type` error response rather than a schema-validation error; valid requests for the built-in grants are unaffected. diff --git a/packages/oauth-provider/src/contributions.test.ts b/packages/oauth-provider/src/contributions.test.ts new file mode 100644 index 0000000000..1cbb4dc78d --- /dev/null +++ b/packages/oauth-provider/src/contributions.test.ts @@ -0,0 +1,150 @@ +import { jwt } from "better-auth/plugins/jwt"; +import { getTestInstance } from "better-auth/test"; +import type { BetterAuthPlugin } from "better-auth/types"; +import { decodeJwt } from "jose"; +import { describe, expect, it } from "vitest"; +import { createUserTokens } from "./index"; +import { oauthProvider } from "./oauth"; +import type { OAuthContributions } from "./types/contributions"; + +const BASE_URL = "http://localhost:3000"; +const TOKEN_URL = `${BASE_URL}/api/auth/oauth2/token`; +const METADATA_URL = `${BASE_URL}/api/auth/.well-known/oauth-authorization-server`; +const CUSTOM_GRANT = "urn:better-auth:test:custom-grant"; +const RESOURCE = "https://api.test"; + +/** + * A guest plugin that exercises every OAuthContributions slot: a custom grant + * handler, the grant URI, discovery metadata, an advertised auth method, and an + * access-token claim contributor. + */ +function contributionProbe() { + return { + id: "contribution-probe", + requires: ["oauth-provider"], + contributes: { + "oauth-provider": { + grantTypes: { + // The handler echoes a passthrough body param to prove the open + // schema preserves extension params, then returns a plain response. + [CUSTOM_GRANT]: (ctx) => + ctx.json({ + contributed_grant: true, + echoed: ctx.body.probe, + }), + }, + grantTypeURIs: [CUSTOM_GRANT], + metadata: ({ baseURL }) => ({ + test_probe_endpoint: `${baseURL}/probe`, + }), + tokenEndpointAuthMethods: ["urn:better-auth:test:auth"], + tokenClaims: { + access: () => ({ probe_claim: "present" }), + }, + } satisfies OAuthContributions, + }, + } satisfies BetterAuthPlugin; +} + +async function setup() { + return getTestInstance({ + baseURL: BASE_URL, + plugins: [ + jwt(), + oauthProvider({ + loginPage: "/login", + consentPage: "/consent", + // A valid audience lets client_credentials mint a JWT access token, + // which is where contributed access-token claims are applied. + validAudiences: [RESOURCE], + silenceWarnings: { oauthAuthServerConfig: true, openidConfig: true }, + }), + contributionProbe(), + ], + }); +} + +describe("oauth-provider contribution surface", () => { + it("dispatches a contributed grant type and preserves passthrough body params", async () => { + const { auth } = await setup(); + const response = await auth.handler( + new Request(TOKEN_URL, { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: new URLSearchParams({ + grant_type: CUSTOM_GRANT, + probe: "hello", + }).toString(), + }), + ); + expect(response.status).toBe(200); + const body = (await response.json()) as { + contributed_grant?: boolean; + echoed?: string; + }; + expect(body.contributed_grant).toBe(true); + expect(body.echoed).toBe("hello"); + }); + + it("rejects an unregistered grant type with unsupported_grant_type", async () => { + const { auth } = await setup(); + const response = await auth.handler( + new Request(TOKEN_URL, { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: new URLSearchParams({ + grant_type: "urn:better-auth:test:unregistered", + }).toString(), + }), + ); + expect(response.status).toBe(400); + const body = (await response.json()) as { error?: string }; + expect(body.error).toBe("unsupported_grant_type"); + }); + + it("advertises the contributed grant URI, metadata, and auth method in discovery", async () => { + const { auth } = await setup(); + const response = await auth.handler(new Request(METADATA_URL)); + expect(response.status).toBe(200); + const metadata = (await response.json()) as { + grant_types_supported?: string[]; + token_endpoint_auth_methods_supported?: string[]; + test_probe_endpoint?: string; + }; + expect(metadata.grant_types_supported).toContain(CUSTOM_GRANT); + expect(metadata.token_endpoint_auth_methods_supported).toContain( + "urn:better-auth:test:auth", + ); + expect(metadata.test_probe_endpoint).toBe(`${BASE_URL}/api/auth/probe`); + }); + + it("merges contributed token claims into a minted access token", async () => { + const { auth, signInWithTestUser } = await setup(); + const { headers } = await signInWithTestUser(); + const client = await auth.api.adminCreateOAuthClient({ + headers, + body: { redirect_uris: ["http://localhost:3000/cb"], skip_consent: true }, + }); + const response = await auth.handler( + new Request(TOKEN_URL, { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: new URLSearchParams({ + grant_type: "client_credentials", + client_id: client!.client_id, + client_secret: client!.client_secret!, + resource: RESOURCE, + }).toString(), + }), + ); + expect(response.status).toBe(200); + const body = (await response.json()) as { access_token?: string }; + expect(body.access_token).toBeDefined(); + const claims = decodeJwt(body.access_token!); + expect(claims.probe_claim).toBe("present"); + }); + + it("exports the grant-author token-minting helper", () => { + expect(typeof createUserTokens).toBe("function"); + }); +}); diff --git a/packages/oauth-provider/src/index.ts b/packages/oauth-provider/src/index.ts index 277b36dc10..36b58e3328 100644 --- a/packages/oauth-provider/src/index.ts +++ b/packages/oauth-provider/src/index.ts @@ -15,4 +15,13 @@ export type { OAuthRedirectOnError, } from "./oauth-endpoint"; export { checkOAuthClient, oauthToSchema } from "./register"; +// Grant-author toolkit: helpers a contributed grant handler reuses to mint +// tokens and authenticate the client instead of reimplementing them. +export { createUserTokens } from "./token"; export type * from "./types"; +export { + basicToClientCredentials, + getClient, + storeToken, + validateClientCredentials, +} from "./utils"; diff --git a/packages/oauth-provider/src/metadata.ts b/packages/oauth-provider/src/metadata.ts index ccf64d4ec7..d80bd93675 100644 --- a/packages/oauth-provider/src/metadata.ts +++ b/packages/oauth-provider/src/metadata.ts @@ -15,6 +15,44 @@ import { toClientDiscoveryArray, } from "./utils"; +/** + * Folds plugin-contributed discovery into the metadata document: appends + * contributed token-endpoint auth methods and adds contributed fields. Core + * fields are never overridden — a contributor can only add new keys. + */ +function applyMetadataContributions( + ctx: GenericEndpointContext, + metadata: AuthServerMetadata, +): void { + const extraAuthMethods = (ctx.context as Record) + .oauthExtraAuthMethods as string[] | undefined; + if (extraAuthMethods?.length) { + const methods = metadata.token_endpoint_auth_methods_supported ?? []; + for (const method of extraAuthMethods) { + if (!methods.includes(method as TokenEndpointAuthMethod)) { + methods.push(method as TokenEndpointAuthMethod); + } + } + metadata.token_endpoint_auth_methods_supported = methods; + } + const contributors = (ctx.context as Record) + .oauthMetadataContributors as + | ((c: { baseURL: string }) => Record)[] + | undefined; + if (contributors?.length) { + const target = metadata as Record; + for (const contribute of contributors) { + for (const [key, value] of Object.entries( + contribute({ baseURL: ctx.context.baseURL }), + )) { + if (target[key] == null) { + target[key] = value; + } + } + } + } +} + export function authServerMetadata( ctx: GenericEndpointContext, opts?: JwtOptions, @@ -82,6 +120,7 @@ export function authServerMetadata( code_challenge_methods_supported: ["S256"], authorization_response_iss_parameter_supported: true, }; + applyMetadataContributions(ctx, metadata); return metadata; } diff --git a/packages/oauth-provider/src/oauth.ts b/packages/oauth-provider/src/oauth.ts index 87c6393b06..a64450453a 100644 --- a/packages/oauth-provider/src/oauth.ts +++ b/packages/oauth-provider/src/oauth.ts @@ -31,6 +31,7 @@ import { revokeEndpoint } from "./revoke"; import { schema } from "./schema"; import { tokenEndpoint } from "./token"; import type { OAuthOptions, Scope } from "./types"; +import type { GrantHandler, OAuthContributions } from "./types/contributions"; import { ResourceUriSchema, SafeUrlSchema } from "./types/zod"; import { userInfoEndpoint } from "./userinfo"; import { @@ -271,6 +272,54 @@ export const oauthProvider = >(options: O) => { options: opts as NoInfer, onRequest: handleIssuerMetadataRequest, init: (ctx) => { + // Aggregate contributions from plugins that target this host. Read once + // at init (static, order-independent) and expose on the context so the + // token endpoint, discovery, and minting can consume them at request time. + const contributions = ctx.getContributions("oauth-provider"); + const oauthGrantHandlers: Record = {}; + const extensionGrantURIs: string[] = []; + const oauthMetadataContributors: NonNullable< + OAuthContributions["metadata"] + >[] = []; + const oauthExtraAuthMethods: string[] = []; + const oauthClaimContributors: NonNullable< + OAuthContributions["tokenClaims"] + >[] = []; + for (const contribution of contributions) { + if (contribution.grantTypes) { + Object.assign(oauthGrantHandlers, contribution.grantTypes); + } + if (contribution.grantTypeURIs) { + extensionGrantURIs.push(...contribution.grantTypeURIs); + } + if (contribution.metadata) { + oauthMetadataContributors.push(contribution.metadata); + } + if (contribution.tokenEndpointAuthMethods) { + oauthExtraAuthMethods.push(...contribution.tokenEndpointAuthMethods); + } + if (contribution.tokenClaims) { + oauthClaimContributors.push(contribution.tokenClaims); + } + } + // Allow contributed grant URIs through the token-endpoint allowlist. + if (extensionGrantURIs.length > 0) { + opts.grantTypes = [ + ...(opts.grantTypes ?? [ + "authorization_code", + "client_credentials", + "refresh_token", + ]), + ...extensionGrantURIs, + ]; + } + const pluginContext = { + oauthGrantHandlers, + oauthMetadataContributors, + oauthExtraAuthMethods, + oauthClaimContributors, + }; + // OAuth provider performs adapter-level session lookups by id, so it // currently requires DB-backed sessions whenever secondary storage is enabled. if ( @@ -300,7 +349,7 @@ export const oauthProvider = >(options: O) => { } catch (error) { // baseURL may not be available during init when using dynamic baseURL config if (isDynamicBaseURLInit && issuer === "") { - return; + return { context: pluginContext }; } throw error; } @@ -324,6 +373,7 @@ export const oauthProvider = >(options: O) => { ); } } + return { context: pluginContext }; }, hooks: { before: [ @@ -748,29 +798,28 @@ export const oauthProvider = >(options: O) => { "/oauth2/token", { method: "POST", - body: z.object({ - grant_type: z - .string() - .pipe( - z.enum([ - "authorization_code", - "client_credentials", - "refresh_token", - ]), - ), - client_id: z.string().optional(), - client_secret: z.string().optional(), - client_assertion: z.string().optional(), - client_assertion_type: z.string().optional(), - code: z.string().optional(), - code_verifier: z.string().optional(), - redirect_uri: SafeUrlSchema.optional(), - refresh_token: z.string().optional(), - resource: z - .union([ResourceUriSchema, z.array(ResourceUriSchema).min(1)]) - .optional(), - scope: z.string().optional(), - }), + // Open `grant_type` to any non-empty string so plugin-contributed + // extension grants reach the dispatcher; the allowlist + the token + // endpoint's `default` case still reject unregistered URIs with + // `unsupported_grant_type`. `.passthrough()` keeps extension body + // params (e.g. `subject_token`, `auth_req_id`) for the grant handler. + body: z + .object({ + grant_type: z.string().trim().min(1), + client_id: z.string().optional(), + client_secret: z.string().optional(), + client_assertion: z.string().optional(), + client_assertion_type: z.string().optional(), + code: z.string().optional(), + code_verifier: z.string().optional(), + redirect_uri: SafeUrlSchema.optional(), + refresh_token: z.string().optional(), + resource: z + .union([ResourceUriSchema, z.array(ResourceUriSchema).min(1)]) + .optional(), + scope: z.string().optional(), + }) + .passthrough(), errorCodesByField: { grant_type: { missing: "invalid_request", @@ -791,12 +840,8 @@ export const oauthProvider = >(options: O) => { properties: { grant_type: { type: "string", - enum: [ - "authorization_code", - "client_credentials", - "refresh_token", - ], - description: "OAuth2 grant type", + description: + "OAuth2 grant type. Built-ins: authorization_code, client_credentials, refresh_token. Plugins may register extension grant URIs (e.g. CIBA, token-exchange).", }, client_id: { type: "string", diff --git a/packages/oauth-provider/src/token.ts b/packages/oauth-provider/src/token.ts index 3fdd6419bf..8df533456d 100644 --- a/packages/oauth-provider/src/token.ts +++ b/packages/oauth-provider/src/token.ts @@ -13,6 +13,11 @@ import type { Scope, VerificationValue, } from "./types"; +import type { + GrantHandler, + OAuthContributions, + TokenClaimInfo, +} from "./types/contributions"; import type { GrantType } from "./types/oauth"; import { verificationValueSchema } from "./types/zod"; import { userNormalClaims } from "./userinfo"; @@ -58,11 +63,50 @@ export async function tokenEndpoint( return handleClientCredentialsGrant(ctx, opts); case "refresh_token": return handleRefreshTokenGrant(ctx, opts); + default: { + // Extension grants registered by plugins via OAuthContributions.grantTypes. + // The allowlist check above already rejected unknown grant_type values, so + // reaching here means the URI was allowed; dispatch to its handler. + const handlers = (ctx.context as Record) + .oauthGrantHandlers as Record | undefined; + const handler = handlers?.[grantType]; + if (handler) { + return handler(ctx, opts); + } + throw new APIError("BAD_REQUEST", { + error_description: `unsupported grant_type ${grantType}`, + error: "unsupported_grant_type", + }); + } } } // User Jwt SHALL follow oAuth 2 // NOTE: Requires jwt plugin (assert !opts.disableJwtPlugin) +/** + * Collects extra claims from plugin-contributed `tokenClaims` contributors for + * the given token kind. Returns the merged claims, applied before the + * consumer's `customAccessTokenClaims`/`customIdTokenClaims` and never over the + * pinned security claims set by the caller. + */ +async function collectExtensionClaims( + ctx: GenericEndpointContext, + kind: "access" | "id", + info: TokenClaimInfo, +): Promise> { + const contributors = (ctx.context as Record) + .oauthClaimContributors as + | NonNullable[] + | undefined; + if (!contributors?.length) return {}; + const claims: Record = {}; + for (const contributor of contributors) { + const fn = contributor[kind]; + if (fn) Object.assign(claims, await fn(info)); + } + return claims; +} + async function createJwtAccessToken( ctx: GenericEndpointContext, opts: OAuthOptions, @@ -80,6 +124,14 @@ async function createJwtAccessToken( ) { const iat = overrides?.iat ?? Math.floor(Date.now() / 1000); const exp = overrides?.exp ?? iat + (opts.accessTokenExpiresIn ?? 3600); + const extensionClaims = await collectExtensionClaims(ctx, "access", { + user, + scopes, + client, + referenceId, + sessionId: overrides?.sid, + resources, + }); const customClaims = opts.customAccessTokenClaims ? await opts.customAccessTokenClaims({ user, @@ -96,6 +148,7 @@ async function createJwtAccessToken( return signJWT(ctx, { options: jwtPluginOptions, payload: { + ...extensionClaims, ...customClaims, sub: user?.id, aud: toAudienceClaim(audience), @@ -148,6 +201,7 @@ async function createIdToken( sessionId?: string, authTime?: Date, accessToken?: string, + referenceId?: string, ) { const iat = Math.floor(Date.now() / 1000); const exp = iat + (opts.idTokenExpiresIn ?? 36000); @@ -160,6 +214,13 @@ async function createIdToken( // - silver : mfa const acr = "urn:mace:incommon:iap:bronze"; + const extensionClaims = await collectExtensionClaims(ctx, "id", { + user, + scopes, + client, + sessionId, + referenceId, + }); const customClaims = opts.customIdTokenClaims ? await opts.customIdTokenClaims({ user, @@ -190,6 +251,7 @@ async function createIdToken( ...userClaims, auth_time: authTimeSec, acr, + ...extensionClaims, ...customClaims, at_hash: atHash, iss: jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL, @@ -482,7 +544,7 @@ interface CreateUserTokensParams { originalResources?: string[]; } -async function createUserTokens( +export async function createUserTokens( ctx: GenericEndpointContext, opts: OAuthOptions, params: CreateUserTokensParams, @@ -644,6 +706,7 @@ async function createUserTokens( sessionId, authTime, accessToken, + referenceId, ) : undefined; diff --git a/packages/oauth-provider/src/types/contributions.ts b/packages/oauth-provider/src/types/contributions.ts new file mode 100644 index 0000000000..5e6c8b442e --- /dev/null +++ b/packages/oauth-provider/src/types/contributions.ts @@ -0,0 +1,101 @@ +import type { Awaitable, GenericEndpointContext } from "@better-auth/core"; +import type { User } from "better-auth/types"; +import type { OAuthOptions, SchemaClient, Scope } from "./index"; + +/** + * A custom grant-type handler dispatched by the token endpoint when an incoming + * `grant_type` matches a key a plugin registered via `OAuthContributions.grantTypes`. + * + * It receives the endpoint context and the resolved oauth-provider options and + * returns the full token-endpoint response (the same shape the built-in grant + * handlers produce). Use the exported `createUserTokens` helper to mint tokens. + */ +export type GrantHandler = ( + ctx: GenericEndpointContext, + opts: OAuthOptions, +) => Awaitable; + +/** + * Context passed to a token-claim contributor at minting time. A contributor + * returns extra claims to merge into the access token or ID token. + */ +export interface TokenClaimInfo { + /** The authenticated user, or `undefined` for client_credentials access tokens. */ + user: User | undefined; + /** Scopes granted to the token. */ + scopes: string[]; + /** The OAuth client the token is issued to. */ + client: SchemaClient; + /** Grant reference id (e.g. a CIBA `auth_req_id`), when the grant supplies one. */ + referenceId?: string; + /** Session id bound to the token, when available. */ + sessionId?: string; + /** Requested resource indicators (RFC 8707), when present. */ + resources?: string[]; +} + +/** + * What a plugin can contribute to the `oauth-provider` host through its + * `contributes` field. The host collects these at init via + * `ctx.getContributions("oauth-provider")` and wires them into the token + * endpoint, discovery metadata, and token minting — so a plugin can add custom + * grant types, advertise capabilities, and inject claims without the + * oauth-provider knowing about it. + * + * @example + * ```ts + * import type { OAuthContributions } from "@better-auth/oauth-provider"; + * + * export const ciba = () => ({ + * id: "ciba", + * requires: ["oauth-provider"], + * contributes: { + * "oauth-provider": { + * grantTypes: { "urn:openid:params:grant-type:ciba": cibaGrantHandler }, + * grantTypeURIs: ["urn:openid:params:grant-type:ciba"], + * metadata: ({ baseURL }) => ({ + * backchannel_authentication_endpoint: `${baseURL}/oauth2/bc-authorize`, + * }), + * } satisfies OAuthContributions, + * }, + * }); + * ``` + */ +export interface OAuthContributions { + /** + * Custom grant-type handlers keyed by `grant_type` URI. The token endpoint + * dispatches to the handler when the request's grant_type matches and the URI + * is allowed. Pair every key with a `grantTypeURIs` entry. + */ + grantTypes?: Record; + /** + * Grant-type URIs to add to the token-endpoint allowlist and to + * `grant_types_supported` in discovery. + */ + grantTypeURIs?: string[]; + /** + * Fields merged into the `.well-known` discovery documents (e.g. an endpoint + * or capability a profile adds). + */ + metadata?: (ctx: { baseURL: string }) => Record; + /** + * Token-endpoint client authentication methods to advertise in + * `token_endpoint_auth_methods_supported`. + */ + tokenEndpointAuthMethods?: string[]; + /** + * Claim contributors merged into issued tokens. Extension claims are applied + * before the consumer's `customAccessTokenClaims`/`customIdTokenClaims` and + * never override pinned security claims (`sub`, `iss`, `aud`, `iat`, `exp`). + */ + tokenClaims?: { + access?: (info: TokenClaimInfo) => Awaitable>; + id?: (info: TokenClaimInfo) => Awaitable>; + }; +} + +declare module "@better-auth/core" { + interface ContributionContracts { + "oauth-provider": OAuthContributions; + } +} diff --git a/packages/oauth-provider/src/types/index.ts b/packages/oauth-provider/src/types/index.ts index 94623affda..14ee291657 100644 --- a/packages/oauth-provider/src/types/index.ts +++ b/packages/oauth-provider/src/types/index.ts @@ -5,6 +5,11 @@ import type { schema } from "../schema"; import type { Awaitable } from "./helpers"; import type { GrantType } from "./oauth"; +export type { + GrantHandler, + OAuthContributions, + TokenClaimInfo, +} from "./contributions"; export type { AuthMethod, AuthServerMetadata, diff --git a/packages/oauth-provider/src/types/oauth.ts b/packages/oauth-provider/src/types/oauth.ts index f24e329e98..0b82f64247 100644 --- a/packages/oauth-provider/src/types/oauth.ts +++ b/packages/oauth-provider/src/types/oauth.ts @@ -10,10 +10,12 @@ export type GrantType = // | "implicit" // NEVER SUPPORT - deprecated in oAuth2.1 // | "password" // NEVER SUPPORT - deprecated in oAuth2.1 | "client_credentials" - | "refresh_token"; -// | "urn:ietf:params:oauth:grant-type:device_code" // specified in oAuth2.1 but not yet implemented -// | "urn:ietf:params:oauth:grant-type:jwt-bearer" // unspecified in oAuth2.1 -// | "urn:ietf:params:oauth:grant-type:saml2-bearer" // unspecified in oAuth2.1 + | "refresh_token" + // Extension grant URIs (CIBA, pre-authorized_code, token-exchange, device_code, + // ...) are registered by plugins via `OAuthContributions.grantTypes`. The + // `(string & {})` member accepts those custom URIs while preserving literal + // autocomplete for the built-ins above. + | (string & {}); export type AuthMethod = | "client_secret_basic" // Basic header diff --git a/packages/oauth-provider/src/utils/index.ts b/packages/oauth-provider/src/utils/index.ts index 05d7d7b38a..288536b5e3 100644 --- a/packages/oauth-provider/src/utils/index.ts +++ b/packages/oauth-provider/src/utils/index.ts @@ -487,7 +487,7 @@ export async function getStoredToken( // spaces aren't rejected before reaching the spec-correct decoder. const BASIC_SCHEME_PREFIX = /^Basic +/i; -function basicToClientCredentials(authorization: string) { +export function basicToClientCredentials(authorization: string) { if (!BASIC_SCHEME_PREFIX.test(authorization)) { return undefined; }