@better-auth/oauth-provider 1.7.0-beta.5 → 1.7.0-beta.7

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.
@@ -0,0 +1,63 @@
1
+ import { isAPIError } from "better-auth/api";
2
+ import { APIError as APIError$1 } from "better-call";
3
+ import { DPOP_SIGNING_ALGORITHMS } from "better-auth/oauth2";
4
+ //#region src/resource-challenge.ts
5
+ const DPOP_CHALLENGE_ERRORS = new Set(["invalid_dpop_proof"]);
6
+ function quoteAuthParam(value) {
7
+ return value.replace(/[\r\n]+/g, " ").replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
8
+ }
9
+ function extractDpopError(error) {
10
+ const body = error.body;
11
+ return {
12
+ errorCode: typeof body?.error === "string" ? body.error : void 0,
13
+ description: typeof body?.error_description === "string" ? body.error_description : typeof body?.message === "string" ? body.message : error.message
14
+ };
15
+ }
16
+ function isDpopChallengeError(error) {
17
+ const { errorCode, description } = extractDpopError(error);
18
+ return !!errorCode && (DPOP_CHALLENGE_ERRORS.has(errorCode) || errorCode === "invalid_token" && description.includes("DPoP"));
19
+ }
20
+ function buildDpopChallenge(error, opts) {
21
+ const { errorCode, description } = extractDpopError(error);
22
+ const algorithms = opts?.dpopSigningAlgorithms ?? DPOP_SIGNING_ALGORITHMS;
23
+ return [
24
+ `DPoP error="${quoteAuthParam(errorCode ?? "invalid_dpop_proof")}"`,
25
+ `error_description="${quoteAuthParam(description)}"`,
26
+ `algs="${quoteAuthParam(algorithms.join(" "))}"`
27
+ ].join(", ");
28
+ }
29
+ /**
30
+ * Raise an OAuth resource-server challenge for a failed access-token request.
31
+ *
32
+ * Missing/invalid bearer credentials are reported with RFC 6750 plus the RFC
33
+ * 9728 `resource_metadata` pointer. DPoP-bound-token failures are reported with
34
+ * RFC 9449's `DPoP` challenge so clients know which proof algorithms to use.
35
+ * Non-URL resources (for example a `urn:` or a client id) resolve their
36
+ * metadata URL through `resourceMetadataMappings`.
37
+ *
38
+ * @internal
39
+ */
40
+ function raiseResourceServerChallenge(error, resource, opts) {
41
+ if (isAPIError(error) && error.status === "UNAUTHORIZED") {
42
+ if (isDpopChallengeError(error)) throw new APIError$1("UNAUTHORIZED", { message: error.message }, { "WWW-Authenticate": buildDpopChallenge(error, opts) });
43
+ const wwwAuthenticateValue = (Array.isArray(resource) ? resource : [resource]).map((value) => {
44
+ const url = URL.canParse?.(value) ? new URL(value) : null;
45
+ if (url && url.origin !== "null") {
46
+ const resourcePath = url.pathname.endsWith("/") ? url.pathname.slice(0, -1) : url.pathname;
47
+ let challenge = `Bearer resource_metadata="${url.origin}/.well-known/oauth-protected-resource${resourcePath}${url.search}"`;
48
+ if (opts?.scope) challenge += `, scope="${quoteAuthParam(opts.scope)}"`;
49
+ return challenge;
50
+ }
51
+ const resourceMetadata = opts?.resourceMetadataMappings?.[value];
52
+ if (!resourceMetadata) throw new APIError$1("INTERNAL_SERVER_ERROR", { message: `missing resource_metadata mapping for ${value}` });
53
+ let challenge = `Bearer resource_metadata="${resourceMetadata}"`;
54
+ if (opts?.scope) challenge += `, scope="${quoteAuthParam(opts.scope)}"`;
55
+ return challenge;
56
+ }).join(", ");
57
+ throw new APIError$1("UNAUTHORIZED", { message: error.message }, { "WWW-Authenticate": wwwAuthenticateValue });
58
+ }
59
+ if (error instanceof Error) throw error;
60
+ throw new Error(error);
61
+ }
62
+ //#endregion
63
+ export { raiseResourceServerChallenge as t };
@@ -0,0 +1,13 @@
1
+ //#region \0rolldown/runtime.js
2
+ var __defProp = Object.defineProperty;
3
+ var __exportAll = (all, no_symbols) => {
4
+ let target = {};
5
+ for (var name in all) __defProp(target, name, {
6
+ get: all[name],
7
+ enumerable: true
8
+ });
9
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
10
+ return target;
11
+ };
12
+ //#endregion
13
+ export { __exportAll as t };
@@ -0,0 +1,44 @@
1
+ //#region src/signed-query.ts
2
+ const signedQueryIssuedAtParam = "ba_iat";
3
+ const postLoginClearedParam = "ba_pl";
4
+ const signedQueryParameterNameParam = "ba_param";
5
+ function canonicalizeOAuthQueryParams(params) {
6
+ const canonicalParams = new URLSearchParams();
7
+ const entries = [...params.entries()].sort(([keyA, valueA], [keyB, valueB]) => {
8
+ if (keyA < keyB) return -1;
9
+ if (keyA > keyB) return 1;
10
+ if (valueA < valueB) return -1;
11
+ if (valueA > valueB) return 1;
12
+ return 0;
13
+ });
14
+ for (const [key, value] of entries) canonicalParams.append(key, value);
15
+ return canonicalParams;
16
+ }
17
+ function setSignedOAuthQueryParameterNames(params) {
18
+ params.delete(signedQueryParameterNameParam);
19
+ const signedParameterNames = [...new Set([...params.keys(), signedQueryParameterNameParam])].sort();
20
+ for (const parameterName of signedParameterNames) params.append(signedQueryParameterNameParam, parameterName);
21
+ }
22
+ function getSignedOAuthQueryParameterNames(params) {
23
+ const signedParameterNames = params.getAll(signedQueryParameterNameParam);
24
+ if (!signedParameterNames.length) return;
25
+ return new Set(signedParameterNames);
26
+ }
27
+ function buildSignedOAuthQuery(search) {
28
+ const params = new URLSearchParams(search);
29
+ if (!params.has("sig")) return;
30
+ const signedParameterNames = getSignedOAuthQueryParameterNames(params);
31
+ if (!signedParameterNames) return;
32
+ const signedParams = new URLSearchParams();
33
+ for (const [key, value] of params.entries()) if (key === "sig" || key === signedQueryParameterNameParam || signedParameterNames.has(key)) signedParams.append(key, value);
34
+ return signedParams.toString();
35
+ }
36
+ function getSignedQueryIssuedAt(oauthQuery) {
37
+ const raw = new URLSearchParams(oauthQuery).get(signedQueryIssuedAtParam);
38
+ if (!raw) return null;
39
+ const issuedAt = Number(raw);
40
+ if (!Number.isFinite(issuedAt) || issuedAt <= 0) return null;
41
+ return new Date(issuedAt);
42
+ }
43
+ //#endregion
44
+ export { setSignedOAuthQueryParameterNames as a, postLoginClearedParam as i, canonicalizeOAuthQueryParams as n, signedQueryIssuedAtParam as o, getSignedQueryIssuedAt as r, buildSignedOAuthQuery as t };
@@ -1,10 +1,262 @@
1
- import { APIError } from "better-call";
2
- import { decodeBasicCredentials } from "@better-auth/core/oauth2";
1
+ import { n as canonicalizeOAuthQueryParams } from "./signed-query-CFv2jNMT.mjs";
3
2
  import { constantTimeEqual, makeSignature, symmetricDecrypt, symmetricEncrypt } from "better-auth/crypto";
3
+ import { APIError } from "better-call";
4
+ import { logger } from "@better-auth/core/env";
4
5
  import { BetterAuthError } from "@better-auth/core/error";
6
+ import { CLIENT_ASSERTION_TYPE, decodeBasicCredentials } from "@better-auth/core/oauth2";
5
7
  import { base64Url } from "@better-auth/utils/base64";
6
8
  import { createHash } from "@better-auth/utils/hash";
9
+ //#region src/extensions.ts
10
+ const DEFAULT_GRANT_TYPES = [
11
+ "authorization_code",
12
+ "client_credentials",
13
+ "refresh_token"
14
+ ];
15
+ const BUILT_IN_CONFIDENTIAL_AUTH_METHODS = [
16
+ "client_secret_basic",
17
+ "client_secret_post",
18
+ "private_key_jwt"
19
+ ];
20
+ const RESERVED_TOKEN_ENDPOINT_AUTH_METHODS = ["none", ...BUILT_IN_CONFIDENTIAL_AUTH_METHODS];
21
+ const RESERVED_TOKEN_ENDPOINT_AUTH_METHOD_SET = new Set(RESERVED_TOKEN_ENDPOINT_AUTH_METHODS);
22
+ function assertNonEmptyExtensionValue(name, value) {
23
+ if (value.trim().length > 0) return;
24
+ throw new BetterAuthError(`OAuth Provider extension ${name} cannot be empty`);
25
+ }
26
+ function assertAbsoluteUri(name, value) {
27
+ assertNonEmptyExtensionValue(name, value);
28
+ let url;
29
+ try {
30
+ url = new URL(value);
31
+ } catch {
32
+ url = void 0;
33
+ }
34
+ if (url?.protocol) return;
35
+ throw new BetterAuthError(`OAuth Provider extension ${name} must be an absolute URI: ${value}`);
36
+ }
37
+ function assertExtensionGrantType(grantType) {
38
+ assertAbsoluteUri("grant type", grantType);
39
+ }
40
+ function assertExtensionTokenEndpointAuthMethod(method) {
41
+ assertNonEmptyExtensionValue("token_endpoint_auth_method", method);
42
+ if (!RESERVED_TOKEN_ENDPOINT_AUTH_METHOD_SET.has(method)) return;
43
+ throw new BetterAuthError(`OAuth Provider extension token_endpoint_auth_method is reserved: ${method}`);
44
+ }
45
+ function assertExtensionClientAssertionType(assertionType) {
46
+ assertAbsoluteUri("client_assertion_type", assertionType);
47
+ if (assertionType !== CLIENT_ASSERTION_TYPE) return;
48
+ throw new BetterAuthError(`OAuth Provider extension client_assertion_type is reserved: ${assertionType}`);
49
+ }
50
+ /**
51
+ * Validates one extension's dispatched keys (grant types, auth methods,
52
+ * assertion types) and returns them for the cross-extension disjointness check.
53
+ * Throws on a non-absolute grant/assertion URI, a reserved auth-method name, or
54
+ * an empty assertion-type list.
55
+ */
56
+ function collectExtensionKeys(extension) {
57
+ const grantTypes = Object.keys(extension.grants ?? {});
58
+ for (const grantType of grantTypes) assertExtensionGrantType(grantType);
59
+ const authMethods = [];
60
+ const assertionTypes = [];
61
+ for (const [method, strategy] of Object.entries(extension.clientAuthentication ?? {})) {
62
+ assertExtensionTokenEndpointAuthMethod(method);
63
+ authMethods.push(method);
64
+ const methodAssertionTypes = strategy.assertionTypes ?? [method];
65
+ if (methodAssertionTypes.length === 0) throw new BetterAuthError(`OAuth Provider extension client_assertion_type list cannot be empty for ${method}`);
66
+ for (const assertionType of methodAssertionTypes) {
67
+ assertExtensionClientAssertionType(assertionType);
68
+ assertionTypes.push(assertionType);
69
+ }
70
+ }
71
+ return {
72
+ grantTypes,
73
+ authMethods,
74
+ assertionTypes
75
+ };
76
+ }
77
+ function assertNoDuplicateAcrossExtensions(label, values) {
78
+ const seen = /* @__PURE__ */ new Set();
79
+ for (const value of values) {
80
+ if (seen.has(value)) throw new BetterAuthError(`OAuth Provider extensions register ${label} "${value}" more than once. Extension contributions must be disjoint.`);
81
+ seen.add(value);
82
+ }
83
+ }
84
+ /**
85
+ * Validates every extension and rejects two extensions registering the same
86
+ * grant type, auth method, or assertion type: otherwise the first would win and
87
+ * the second be silently unreachable. Runs at setup over the whole list;
88
+ * extensions number in the single digits, so a full re-scan per registration is
89
+ * cheaper than the bookkeeping to cache it.
90
+ */
91
+ function validateOAuthProviderExtensions(extensions) {
92
+ const keys = (extensions ?? []).map(collectExtensionKeys);
93
+ assertNoDuplicateAcrossExtensions("grant type", keys.flatMap((k) => k.grantTypes));
94
+ assertNoDuplicateAcrossExtensions("token_endpoint_auth_method", keys.flatMap((k) => k.authMethods));
95
+ assertNoDuplicateAcrossExtensions("client_assertion_type", keys.flatMap((k) => k.assertionTypes));
96
+ }
97
+ function getOAuthProviderExtensions(opts) {
98
+ return opts.extensions ?? [];
99
+ }
100
+ /**
101
+ * Flattens the client-id discovery sources contributed by every registered
102
+ * extension into a single ordered list. `getClient()` consults them in order;
103
+ * the metadata endpoints merge their `discoveryMetadata`.
104
+ */
105
+ function getClientDiscoveries(opts) {
106
+ return getOAuthProviderExtensions(opts).flatMap((extension) => {
107
+ const discovery = extension.clientDiscovery;
108
+ if (!discovery) return [];
109
+ return Array.isArray(discovery) ? discovery : [discovery];
110
+ });
111
+ }
112
+ /**
113
+ * Registers an {@link OAuthProviderExtension} with the OAuth Provider plugin
114
+ * from a companion plugin's `init()` hook. An extension can add token grants,
115
+ * assertion-based client authentication methods, additive discovery metadata,
116
+ * access-token / ID-token / UserInfo claims, and client-id discovery, without
117
+ * forking provider core.
118
+ *
119
+ * Call this once, at `init()` time. It is idempotent in the same `extension`
120
+ * object, so re-running a plugin's `init()` (for example when one plugin factory
121
+ * result is shared across two `betterAuth()` instances) does not register it
122
+ * twice. It throws if the oauth-provider plugin is not installed, if a grant
123
+ * type or assertion type is not an absolute URI, if a client authentication
124
+ * method reuses a built-in name, or if the extension registers a grant type,
125
+ * auth method, or assertion type that another extension already registered
126
+ * (contributions must be disjoint).
127
+ *
128
+ * @example
129
+ * ```ts
130
+ * init(ctx) {
131
+ * extendOAuthProvider(ctx, {
132
+ * grants: { "urn:example:grant": async ({ provider }) => provider.issueTokens(...) },
133
+ * });
134
+ * }
135
+ * ```
136
+ */
137
+ function extendOAuthProvider(ctx, extension) {
138
+ const provider = ctx.getPlugin("oauth-provider");
139
+ if (!provider) throw new BetterAuthError("extendOAuthProvider requires the oauth-provider plugin.");
140
+ const existing = provider.options.extensions ?? [];
141
+ if (existing.includes(extension)) return;
142
+ const extensions = [...existing, extension];
143
+ validateOAuthProviderExtensions(extensions);
144
+ provider.options.extensions = extensions;
145
+ }
146
+ function getExtensionGrantTypes(opts) {
147
+ return getOAuthProviderExtensions(opts).flatMap((extension) => Object.keys(extension.grants ?? {}));
148
+ }
149
+ function getSupportedGrantTypes(opts) {
150
+ return Array.from(new Set([...opts.grantTypes ?? DEFAULT_GRANT_TYPES, ...getExtensionGrantTypes(opts)]));
151
+ }
152
+ function getExtensionGrantHandler(opts, grantType) {
153
+ for (const extension of getOAuthProviderExtensions(opts)) {
154
+ const handler = extension.grants?.[grantType];
155
+ if (handler) return handler;
156
+ }
157
+ }
158
+ function getExtensionTokenEndpointAuthMethods(opts) {
159
+ return getOAuthProviderExtensions(opts).flatMap((extension) => Object.keys(extension.clientAuthentication ?? {}));
160
+ }
161
+ /**
162
+ * Confidential and extension client-authentication methods the provider
163
+ * supports. Pass `includeNone` to prepend `"none"` for the token endpoint and
164
+ * DCR, where public clients are allowed; the introspection and revocation
165
+ * endpoints, which never accept public clients, omit it (the default).
166
+ */
167
+ function getSupportedAuthMethods(opts, settings) {
168
+ return Array.from(new Set([
169
+ ...settings?.includeNone ? ["none"] : [],
170
+ ...BUILT_IN_CONFIDENTIAL_AUTH_METHODS,
171
+ ...getExtensionTokenEndpointAuthMethods(opts)
172
+ ]));
173
+ }
174
+ function isExtensionTokenEndpointAuthMethod(opts, method) {
175
+ return method ? getExtensionTokenEndpointAuthMethods(opts).includes(method) : false;
176
+ }
177
+ function getExtensionClientAuthenticationStrategy(opts, assertionType) {
178
+ if (assertionType === CLIENT_ASSERTION_TYPE) return void 0;
179
+ for (const extension of getOAuthProviderExtensions(opts)) {
180
+ const strategies = extension.clientAuthentication ?? {};
181
+ for (const [method, strategy] of Object.entries(strategies)) if ((strategy.assertionTypes ?? [method]).includes(assertionType)) return {
182
+ method,
183
+ strategy
184
+ };
185
+ }
186
+ }
187
+ /**
188
+ * Merges each registered extension's `metadata()` contribution into `document`,
189
+ * first-wins: the provider owns every key it already wrote, so an extension can
190
+ * add fields but never override core. Each contributor sees the base `document`,
191
+ * not the running accumulation, so contributions stay order-independent.
192
+ */
193
+ function applyOAuthProviderMetadataExtensions(ctx, opts, type, document) {
194
+ const next = { ...document };
195
+ for (const extension of getOAuthProviderExtensions(opts)) {
196
+ const contribution = extension.metadata?.({
197
+ ctx,
198
+ opts,
199
+ type,
200
+ document
201
+ });
202
+ for (const [key, value] of Object.entries(contribution ?? {})) if (!(key in next)) next[key] = value;
203
+ }
204
+ return next;
205
+ }
206
+ async function collectClaims(opts, run) {
207
+ const claims = {};
208
+ for (const extension of getOAuthProviderExtensions(opts)) {
209
+ const contribution = await run(extension) ?? {};
210
+ for (const [key, value] of Object.entries(contribution)) {
211
+ if (key in claims) {
212
+ logger.warn(`oauth-provider: two extensions contributed the claim "${key}"; keeping the first-registered value.`);
213
+ continue;
214
+ }
215
+ claims[key] = value;
216
+ }
217
+ }
218
+ return claims;
219
+ }
220
+ function collectExtensionAccessTokenClaims(opts, input) {
221
+ return collectClaims(opts, (extension) => extension.claims?.accessToken?.(input));
222
+ }
223
+ function collectExtensionIdTokenClaims(opts, input) {
224
+ return collectClaims(opts, (extension) => extension.claims?.idToken?.(input));
225
+ }
226
+ function collectExtensionUserInfoClaims(opts, input) {
227
+ return collectClaims(opts, (extension) => extension.claims?.userInfo?.(input));
228
+ }
229
+ /**
230
+ * Whether any registered extension contributes UserInfo claims. Lets the
231
+ * UserInfo endpoint skip loading the client when nothing needs it.
232
+ */
233
+ function hasUserInfoClaimExtension(opts) {
234
+ return getOAuthProviderExtensions(opts).some((extension) => extension.claims?.userInfo);
235
+ }
236
+ //#endregion
7
237
  //#region src/utils/index.ts
238
+ /**
239
+ * Extracts the credentials from an `Authorization: Bearer <token>` header.
240
+ *
241
+ * Returns `undefined` when the header is absent or carries a non-Bearer scheme,
242
+ * leaving the caller to decide whether that is an error. Throws an
243
+ * `invalid_request` `APIError` when the Bearer scheme is present but the
244
+ * credentials are missing or the header carries extra parts. The scheme match
245
+ * is case-insensitive and the credentials are the single token after it.
246
+ *
247
+ * @see https://datatracker.ietf.org/doc/html/rfc6750#section-2.1
248
+ * @see https://datatracker.ietf.org/doc/html/rfc7235#section-2.1
249
+ */
250
+ function parseBearerToken(authorization) {
251
+ if (!authorization) return void 0;
252
+ const [scheme, credentials, ...extraParts] = authorization.trim().split(/\s+/);
253
+ if (scheme?.toLowerCase() !== "bearer") return void 0;
254
+ if (!credentials || extraParts.length > 0) throw new APIError("BAD_REQUEST", {
255
+ error: "invalid_request",
256
+ error_description: "Malformed Bearer Authorization header"
257
+ });
258
+ return credentials;
259
+ }
8
260
  var TTLCache = class {
9
261
  cache = /* @__PURE__ */ new Map();
10
262
  constructor() {}
@@ -91,39 +343,15 @@ function toAudienceClaim(audience) {
91
343
  if (!audience?.length) return void 0;
92
344
  return audience.length === 1 ? audience.at(0) : audience;
93
345
  }
94
- /**
95
- * Checks the resource parameter, if provided,
96
- * and returns either a valid audience or a tagged validation error.
97
- */
98
- async function checkResource(ctx, opts, resource, scopes) {
99
- const normalizedResource = toResourceList(resource);
100
- const audience = normalizedResource ? [...normalizedResource] : void 0;
101
- if (audience) {
102
- const hasOpenId = scopes.includes("openid");
103
- const baseUrl = ctx.context.baseURL;
104
- const userInfoEndpoint = `${baseUrl}/oauth2/userinfo`;
105
- if (hasOpenId && !audience.includes(userInfoEndpoint)) audience.push(userInfoEndpoint);
106
- const filteredValidAudiences = opts.validAudiences?.filter((aud) => aud.length);
107
- const validAudiences = new Set(filteredValidAudiences?.length ? filteredValidAudiences : [baseUrl]);
108
- if (hasOpenId) validAudiences.add(userInfoEndpoint);
109
- for (const aud of audience) if (!validAudiences.has(aud)) return {
110
- success: false,
111
- error: "invalid_resource"
112
- };
113
- }
114
- return {
115
- success: true,
116
- audience: toAudienceClaim(audience)
117
- };
118
- }
119
346
  const cachedTrustedClients = new TTLCache();
120
347
  async function verifyOAuthQueryParams(oauth_query, secret) {
121
348
  const queryParams = new URLSearchParams(oauth_query);
122
349
  const sig = queryParams.get("sig");
350
+ const sigs = queryParams.getAll("sig");
123
351
  const exp = Number(queryParams.get("exp"));
124
352
  queryParams.delete("sig");
125
- const verifySig = await makeSignature(queryParams.toString(), secret);
126
- return !!sig && constantTimeEqual(sig, verifySig) && /* @__PURE__ */ new Date(exp * 1e3) >= /* @__PURE__ */ new Date();
353
+ const verifySig = await makeSignature(canonicalizeOAuthQueryParams(queryParams).toString(), secret);
354
+ return sigs.length === 1 && !!sig && constantTimeEqual(sig, verifySig) && /* @__PURE__ */ new Date(exp * 1e3) >= /* @__PURE__ */ new Date();
127
355
  }
128
356
  /**
129
357
  * Get a client by ID, checking trusted clients first, then database
@@ -138,7 +366,7 @@ async function getClient(ctx, options, clientId) {
138
366
  value: clientId
139
367
  }]
140
368
  });
141
- const discoveries = toClientDiscoveryArray(options.clientDiscovery);
369
+ const discoveries = getClientDiscoveries(options);
142
370
  for (const discovery of discoveries) {
143
371
  if (!discovery.matches(clientId)) continue;
144
372
  const resolved = await discovery.resolve(ctx, clientId, dbClient);
@@ -151,24 +379,14 @@ async function getClient(ctx, options, clientId) {
151
379
  return dbClient;
152
380
  }
153
381
  /**
154
- * Normalize the `clientDiscovery` option into an array. Accepts a single
155
- * {@link ClientDiscovery}, an array of them, or `undefined`.
156
- *
157
- * @internal
158
- */
159
- function toClientDiscoveryArray(discovery) {
160
- if (!discovery) return [];
161
- return Array.isArray(discovery) ? discovery : [discovery];
162
- }
163
- /**
164
- * Merge `discoveryMetadata` from every configured {@link ClientDiscovery}
382
+ * Merge `discoveryMetadata` from every contributed {@link ClientDiscovery}
165
383
  * into a single object. Entries are spread in order; later entries override
166
384
  * earlier ones on key collisions.
167
385
  *
168
386
  * @internal
169
387
  */
170
- function mergeDiscoveryMetadata(discovery) {
171
- return toClientDiscoveryArray(discovery).reduce((acc, d) => ({
388
+ function mergeDiscoveryMetadata(discoveries) {
389
+ return discoveries.reduce((acc, d) => ({
172
390
  ...acc,
173
391
  ...d.discoveryMetadata ?? {}
174
392
  }), {});
@@ -302,13 +520,16 @@ function clientAllowsGrant(client, grantType) {
302
520
  return allowedGrants.includes(grantType);
303
521
  }
304
522
  /**
305
- * Validates client credentials failing on mismatches
306
- * and incorrectly provided information
523
+ * Resolves the registered client by id and authorizes it: existence, disabled
524
+ * state, registered auth method, requested scopes, and grant type. The record is
525
+ * always resolved here via `getClient`, so a client-auth strategy proves the
526
+ * caller controls `clientId` but never supplies the record. `preVerified` marks
527
+ * that an assertion already proved control, so the client-secret check is skipped.
307
528
  *
308
529
  * @internal
309
530
  */
310
- async function validateClientCredentials(ctx, options, clientId, clientSecret, scopes, preVerifiedClient, grantType) {
311
- const client = preVerifiedClient ?? await getClient(ctx, options, clientId);
531
+ async function validateClientCredentials(ctx, options, clientId, clientSecret, scopes, preVerified, grantType, authMethod) {
532
+ const client = await getClient(ctx, options, clientId);
312
533
  if (!client) throw new APIError("BAD_REQUEST", {
313
534
  error_description: "missing client",
314
535
  error: "invalid_client"
@@ -317,11 +538,18 @@ async function validateClientCredentials(ctx, options, clientId, clientSecret, s
317
538
  error_description: "client is disabled",
318
539
  error: "invalid_client"
319
540
  });
320
- if (client.tokenEndpointAuthMethod === "private_key_jwt" && !preVerifiedClient) throw new APIError("BAD_REQUEST", {
321
- error_description: "client registered for private_key_jwt must use client_assertion",
541
+ if (preVerified && authMethod) {
542
+ const registeredAuthMethod = client.tokenEndpointAuthMethod ?? "client_secret_basic";
543
+ if (registeredAuthMethod !== authMethod) throw new APIError("BAD_REQUEST", {
544
+ error_description: `client registered for ${registeredAuthMethod} cannot use ${authMethod}`,
545
+ error: "invalid_client"
546
+ });
547
+ }
548
+ if ((client.tokenEndpointAuthMethod === "private_key_jwt" || isExtensionTokenEndpointAuthMethod(options, client.tokenEndpointAuthMethod)) && !preVerified) throw new APIError("BAD_REQUEST", {
549
+ error_description: `client registered for ${client.tokenEndpointAuthMethod} must use client_assertion`,
322
550
  error: "invalid_client"
323
551
  });
324
- if (!preVerifiedClient) {
552
+ if (!preVerified) {
325
553
  if (!client.public && !clientSecret) throw new APIError("BAD_REQUEST", {
326
554
  error_description: "client secret must be provided",
327
555
  error: "invalid_client"
@@ -362,8 +590,10 @@ function parseClientMetadata(metadata) {
362
590
  function destructureCredentials(credentials) {
363
591
  return {
364
592
  clientId: credentials?.clientId,
365
- clientSecret: credentials?.method === "client_secret_basic" || credentials?.method === "client_secret_post" ? credentials.clientSecret : void 0,
366
- preVerifiedClient: credentials?.method === "private_key_jwt" ? credentials.client : void 0
593
+ clientSecret: credentials?.kind === "client_secret" ? credentials.clientSecret : void 0,
594
+ preVerified: credentials?.kind === "pre_verified",
595
+ authMethod: credentials?.method,
596
+ confirmation: credentials?.kind === "pre_verified" ? credentials.confirmation : void 0
367
597
  };
368
598
  }
369
599
  /**
@@ -378,32 +608,53 @@ async function extractClientCredentials(ctx, opts, expectedAudience) {
378
608
  error_description: "client_assertion and client_assertion_type must both be provided",
379
609
  error: "invalid_client"
380
610
  });
381
- if (body.client_secret || authorization?.startsWith("Basic ")) throw new APIError("BAD_REQUEST", {
611
+ if (body.client_secret || authorization && BASIC_SCHEME_PREFIX.test(authorization)) throw new APIError("BAD_REQUEST", {
382
612
  error_description: "client_assertion cannot be combined with client_secret or Basic auth",
383
613
  error: "invalid_client"
384
614
  });
385
- const { verifyClientAssertion: verify } = await import("./client-assertion-DmT1B6_6.mjs").then((n) => n.t);
386
- const result = await verify(ctx, opts, body.client_assertion, body.client_assertion_type, body.client_id, expectedAudience);
615
+ const assertion = body.client_assertion;
616
+ const assertionType = body.client_assertion_type;
617
+ const extensionStrategy = getExtensionClientAuthenticationStrategy(opts, assertionType);
618
+ if (extensionStrategy) {
619
+ const result = await extensionStrategy.strategy.authenticate({
620
+ ctx,
621
+ opts,
622
+ assertion,
623
+ assertionType,
624
+ clientId: body.client_id,
625
+ expectedAudience
626
+ });
627
+ return {
628
+ kind: "pre_verified",
629
+ method: extensionStrategy.method,
630
+ clientId: result.clientId,
631
+ confirmation: result.confirmation
632
+ };
633
+ }
634
+ const { verifyClientAssertion: verify } = await import("./client-assertion-CctbJywV.mjs").then((n) => n.t);
387
635
  return {
636
+ kind: "pre_verified",
388
637
  method: "private_key_jwt",
389
- clientId: result.clientId,
390
- client: result.client
638
+ clientId: (await verify(ctx, opts, assertion, assertionType, body.client_id, expectedAudience)).clientId
391
639
  };
392
640
  }
393
- if (authorization?.startsWith("Basic ")) {
641
+ if (authorization && BASIC_SCHEME_PREFIX.test(authorization)) {
394
642
  const res = basicToClientCredentials(authorization);
395
643
  if (res) return {
644
+ kind: "client_secret",
396
645
  method: "client_secret_basic",
397
646
  clientId: res.client_id,
398
647
  clientSecret: res.client_secret
399
648
  };
400
649
  }
401
650
  if (body.client_id && body.client_secret) return {
651
+ kind: "client_secret",
402
652
  method: "client_secret_post",
403
653
  clientId: body.client_id,
404
654
  clientSecret: body.client_secret
405
655
  };
406
656
  if (body.client_id) return {
657
+ kind: "public",
407
658
  method: "none",
408
659
  clientId: body.client_id
409
660
  };
@@ -462,15 +713,6 @@ function searchParamsToQuery(params) {
462
713
  }
463
714
  return result;
464
715
  }
465
- const signedQueryIssuedAtParam = "ba_iat";
466
- const postLoginClearedParam = "ba_pl";
467
- function getSignedQueryIssuedAt(oauthQuery) {
468
- const raw = new URLSearchParams(oauthQuery).get(signedQueryIssuedAtParam);
469
- if (!raw) return null;
470
- const issuedAt = Number(raw);
471
- if (!Number.isFinite(issuedAt) || issuedAt <= 0) return null;
472
- return new Date(issuedAt);
473
- }
474
716
  function isSessionFreshForSignedQuery(sessionCreatedAt, signedQueryIssuedAt) {
475
717
  if (!signedQueryIssuedAt) return false;
476
718
  const normalized = normalizeTimestampValue(sessionCreatedAt);
@@ -519,4 +761,4 @@ function isPKCERequired(client, requestedScopes) {
519
761
  return false;
520
762
  }
521
763
  //#endregion
522
- export { verifyOAuthQueryParams as A, signedQueryIssuedAtParam as C, toClientDiscoveryArray as D, toAudienceClaim as E, toResourceList as O, searchParamsToQuery as S, storeToken as T, postLoginClearedParam as _, extractClientCredentials as a, resolveSessionAuthTime as b, getOAuthProviderPlugin as c, isPKCERequired as d, isSessionFreshForSignedQuery as f, parsePrompt as g, parseClientMetadata as h, destructureCredentials as i, validateClientCredentials as k, getSignedQueryIssuedAt as l, normalizeTimestampValue as m, clientAllowsGrant as n, getClient as o, mergeDiscoveryMetadata as p, decryptStoredClientSecret as r, getJwtPlugin as s, checkResource as t, getStoredToken as u, removeMaxAgeFromQuery as v, storeClientSecret as w, resolveSubjectIdentifier as x, removePromptFromQuery as y };
764
+ export { collectExtensionUserInfoClaims as A, toAudienceClaim as C, applyOAuthProviderMetadataExtensions as D, verifyOAuthQueryParams as E, getSupportedGrantTypes as F, hasUserInfoClaimExtension as I, isExtensionTokenEndpointAuthMethod as L, getClientDiscoveries as M, getExtensionGrantHandler as N, collectExtensionAccessTokenClaims as O, getSupportedAuthMethods as P, validateOAuthProviderExtensions as R, storeToken as S, validateClientCredentials as T, removePromptFromQuery as _, getClient as a, searchParamsToQuery as b, getStoredToken as c, mergeDiscoveryMetadata as d, normalizeTimestampValue as f, removeMaxAgeFromQuery as g, parsePrompt as h, extractClientCredentials as i, extendOAuthProvider as j, collectExtensionIdTokenClaims as k, isPKCERequired as l, parseClientMetadata as m, decryptStoredClientSecret as n, getJwtPlugin as o, parseBearerToken as p, destructureCredentials as r, getOAuthProviderPlugin as s, clientAllowsGrant as t, isSessionFreshForSignedQuery as u, resolveSessionAuthTime as v, toResourceList as w, storeClientSecret as x, resolveSubjectIdentifier as y };
@@ -1,5 +1,5 @@
1
1
  //#endregion
2
2
  //#region src/version.ts
3
- const PACKAGE_VERSION = "1.7.0-beta.5";
3
+ const PACKAGE_VERSION = "1.7.0-beta.7";
4
4
  //#endregion
5
5
  export { PACKAGE_VERSION as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@better-auth/oauth-provider",
3
- "version": "1.7.0-beta.5",
3
+ "version": "1.7.0-beta.7",
4
4
  "description": "An oauth provider plugin for Better Auth",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -61,18 +61,17 @@
61
61
  "zod": "^4.3.6"
62
62
  },
63
63
  "devDependencies": {
64
- "@modelcontextprotocol/sdk": "^1.27.1",
65
64
  "listhen": "^1.9.0",
66
65
  "tsdown": "0.21.1",
67
- "@better-auth/core": "1.7.0-beta.5",
68
- "better-auth": "1.7.0-beta.5"
66
+ "@better-auth/core": "1.7.0-beta.7",
67
+ "better-auth": "1.7.0-beta.7"
69
68
  },
70
69
  "peerDependencies": {
71
- "@better-auth/utils": "0.4.1",
72
- "@better-fetch/fetch": "1.2.2",
70
+ "@better-auth/utils": "0.4.2",
71
+ "@better-fetch/fetch": "1.3.1",
73
72
  "better-call": "1.3.6",
74
- "@better-auth/core": "^1.7.0-beta.5",
75
- "better-auth": "^1.7.0-beta.5"
73
+ "@better-auth/core": "^1.7.0-beta.7",
74
+ "better-auth": "^1.7.0-beta.7"
76
75
  },
77
76
  "scripts": {
78
77
  "build": "tsdown",