@better-auth/oauth-provider 1.7.0-beta.4 → 1.7.0-beta.6

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.
@@ -1,21 +1,14 @@
1
- import { a as getClient } from "./utils-DKBWQ8fe.mjs";
1
+ import { t as __exportAll } from "./rolldown-runtime-wcPFST8Q.mjs";
2
+ import { a as getClient } from "./utils-Baq6atYN.mjs";
3
+ import { isPublicRoutableHost } from "@better-auth/core/utils/host";
2
4
  import { APIError } from "better-call";
3
5
  import { CLIENT_ASSERTION_TYPE, PRIVATE_KEY_JWT_SIGNING_ALGORITHMS } from "@better-auth/core/oauth2";
6
+ import { base64Url } from "@better-auth/utils/base64";
7
+ import { createHash } from "@better-auth/utils/hash";
4
8
  import { createLocalJWKSet, decodeJwt, decodeProtectedHeader, jwtVerify } from "jose";
5
- //#region \0rolldown/runtime.js
6
- var __defProp = Object.defineProperty;
7
- var __exportAll = (all, no_symbols) => {
8
- let target = {};
9
- for (var name in all) __defProp(target, name, {
10
- get: all[name],
11
- enumerable: true
12
- });
13
- if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
14
- return target;
15
- };
16
- //#endregion
17
9
  //#region src/utils/client-assertion.ts
18
10
  var client_assertion_exports = /* @__PURE__ */ __exportAll({
11
+ consumeClientAssertion: () => consumeClientAssertion,
19
12
  isPrivateHostname: () => isPrivateHostname,
20
13
  verifyClientAssertion: () => verifyClientAssertion
21
14
  });
@@ -34,33 +27,21 @@ function setJwksCache(uri, jwks, fetchedAt) {
34
27
  }
35
28
  }
36
29
  const ALGORITHMS_LIST = [...PRIVATE_KEY_JWT_SIGNING_ALGORITHMS];
37
- const pendingAssertionIds = /* @__PURE__ */ new Set();
38
30
  /**
39
- * Block SSRF: reject jwks_uri pointing at private/reserved IP ranges.
40
- * Only HTTPS with public hostnames is allowed.
31
+ * SSRF gate for user-supplied server-side fetch targets (`jwks_uri`,
32
+ * `backchannel_logout_uri`): returns true when the host is NOT publicly
33
+ * routable. That covers loopback, RFC 1918 private, link-local (including AWS
34
+ * IMDS `169.254.169.254`), shared-address-space (carrier-grade NAT),
35
+ * IPv4-mapped IPv6, 6to4/NAT64/Teredo tunnels, every other RFC 6890
36
+ * special-purpose range, and cloud-metadata FQDNs.
37
+ *
38
+ * Delegates to the audited single source of truth so this check cannot drift
39
+ * into the kind of encoding bypass that bespoke regexes invite. This is a
40
+ * syntactic check only: it does not resolve DNS, so a public name that
41
+ * resolves to a private address at fetch time is not caught here.
41
42
  */
42
- function isPrivateIpv4(hostname) {
43
- const parts = hostname.split(".");
44
- if (parts.length !== 4 || parts.some((p) => !/^\d{1,3}$/.test(p))) return false;
45
- const octets = parts.map(Number);
46
- const a = octets[0];
47
- const b = octets[1];
48
- return a === 10 || a === 0 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a === 169 && b === 254 || a === 127;
49
- }
50
43
  function isPrivateHostname(hostname) {
51
- const lower = hostname.toLowerCase();
52
- const host = lower.startsWith("[") && lower.endsWith("]") ? lower.slice(1, -1) : lower;
53
- if (host === "localhost" || host === "::1") return true;
54
- if (isPrivateIpv4(host)) return true;
55
- if (host.includes(":")) {
56
- const v4MappedMatch = host.match(/^(?:0{0,4}:){0,4}:?(?:0{0,4}:)?ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/);
57
- if (v4MappedMatch && isPrivateIpv4(v4MappedMatch[1])) return true;
58
- const isLinkLocal = host.startsWith("fe8") || host.startsWith("fe9") || host.startsWith("fea") || host.startsWith("feb");
59
- const isUniqueLocal = host.startsWith("fc") || host.startsWith("fd");
60
- if (isLinkLocal || isUniqueLocal) return true;
61
- }
62
- if (host === "metadata.google.internal") return true;
63
- return false;
44
+ return !isPublicRoutableHost(hostname);
64
45
  }
65
46
  function validateJwksUri(ctx, jwksUri, clientIdUrlOrigin) {
66
47
  const parsed = new URL(jwksUri);
@@ -141,6 +122,84 @@ async function refetchClientJwks(client) {
141
122
  }
142
123
  }
143
124
  /**
125
+ * Enforces the assertion-hygiene claims every client-assertion authentication
126
+ * method must check, independent of how the signature is verified or where the
127
+ * verification keys come from:
128
+ * - `aud` MUST include `expectedAudience` (RFC 7523 §3 rule 3),
129
+ * - `exp` MUST be present, unexpired, and at most `assertionMaxLifetime`
130
+ * seconds away (RFC 7523 §3 rule 4),
131
+ * - `iat`, when present, MUST be within `assertionMaxLifetime`,
132
+ * - `jti` MUST be present and single-use; this consumes a replay tombstone keyed
133
+ * by `` `${namespace}:${jti}` ``, inserted under the adapter's primary key so a
134
+ * replay across workers fails atomically.
135
+ *
136
+ * A custom {@link OAuthClientAuthenticationStrategy} should call this after
137
+ * verifying the assertion signature, so an extension method inherits the same
138
+ * replay, lifetime, and audience guarantees as the built-in `private_key_jwt`
139
+ * path, which calls it too.
140
+ *
141
+ * @param params.namespace Scopes the replay tombstone to the method and client,
142
+ * e.g. `` `${method}:${clientId}` ``, so the same `jti` can recur across
143
+ * distinct methods or clients but never within one.
144
+ */
145
+ async function consumeClientAssertion(ctx, opts, params) {
146
+ const { namespace, payload, expectedAudience } = params;
147
+ if (!(Array.isArray(payload.aud) ? payload.aud : payload.aud != null ? [payload.aud] : []).includes(expectedAudience)) throw new APIError("BAD_REQUEST", {
148
+ error_description: "client assertion aud does not match the endpoint",
149
+ error: "invalid_client"
150
+ });
151
+ const maxLifetime = opts.assertionMaxLifetime ?? 300;
152
+ const now = Math.floor(Date.now() / 1e3);
153
+ if (typeof payload.exp !== "number") throw new APIError("BAD_REQUEST", {
154
+ error_description: "client assertion must include exp claim",
155
+ error: "invalid_client"
156
+ });
157
+ if (payload.exp <= now) throw new APIError("BAD_REQUEST", {
158
+ error_description: "client assertion has expired",
159
+ error: "invalid_client"
160
+ });
161
+ if (payload.exp - now > maxLifetime) throw new APIError("BAD_REQUEST", {
162
+ error_description: `client assertion exp is too far in the future (max ${maxLifetime}s)`,
163
+ error: "invalid_client"
164
+ });
165
+ if (typeof payload.iat === "number" && now - payload.iat > maxLifetime) throw new APIError("BAD_REQUEST", {
166
+ error_description: `client assertion iat is too far in the past (max ${maxLifetime}s)`,
167
+ error: "invalid_client"
168
+ });
169
+ if (typeof payload.jti !== "string" || payload.jti.length === 0) throw new APIError("BAD_REQUEST", {
170
+ error_description: "client assertion must include jti claim",
171
+ error: "invalid_client"
172
+ });
173
+ const jtiDigest = await createHash("SHA-256").digest(new TextEncoder().encode(`${namespace}:${payload.jti}`));
174
+ const jtiId = base64Url.encode(new Uint8Array(jtiDigest).slice(0, 24), { padding: false });
175
+ try {
176
+ await ctx.context.adapter.create({
177
+ model: "oauthClientAssertion",
178
+ data: {
179
+ id: jtiId,
180
+ expiresAt: /* @__PURE__ */ new Date(payload.exp * 1e3)
181
+ },
182
+ forceAllowId: true
183
+ });
184
+ } catch (createErr) {
185
+ let alreadyUsed = false;
186
+ try {
187
+ alreadyUsed = Boolean(await ctx.context.adapter.findOne({
188
+ model: "oauthClientAssertion",
189
+ where: [{
190
+ field: "id",
191
+ value: jtiId
192
+ }]
193
+ }));
194
+ } catch {}
195
+ if (alreadyUsed) throw new APIError("BAD_REQUEST", {
196
+ error_description: "client assertion jti has already been used",
197
+ error: "invalid_client"
198
+ });
199
+ throw createErr;
200
+ }
201
+ }
202
+ /**
144
203
  * Verifies a client assertion JWT for `private_key_jwt` authentication.
145
204
  *
146
205
  * Validates: signature, iss=client_id, sub=client_id, aud=token_endpoint,
@@ -197,7 +256,6 @@ async function verifyClientAssertion(ctx, opts, clientAssertion, clientAssertion
197
256
  });
198
257
  const jwks = await fetchClientJwks(ctx, client);
199
258
  const audience = expectedAudience ?? `${ctx.context.baseURL}/oauth2/token`;
200
- const maxLifetime = opts.assertionMaxLifetime ?? 300;
201
259
  const verifyOpts = {
202
260
  issuer: clientId,
203
261
  subject: clientId,
@@ -227,55 +285,12 @@ async function verifyClientAssertion(ctx, opts, clientAssertion, clientAssertion
227
285
  error: "invalid_client"
228
286
  });
229
287
  }
230
- const now = Math.floor(Date.now() / 1e3);
231
- if (typeof payload.exp !== "number") throw new APIError("BAD_REQUEST", {
232
- error_description: "client assertion must include exp claim",
233
- error: "invalid_client"
234
- });
235
- if (payload.exp - now > maxLifetime) throw new APIError("BAD_REQUEST", {
236
- error_description: `client assertion exp is too far in the future (max ${maxLifetime}s)`,
237
- error: "invalid_client"
238
- });
239
- if (typeof payload.iat === "number" && now - payload.iat > maxLifetime) throw new APIError("BAD_REQUEST", {
240
- error_description: `client assertion iat is too far in the past (max ${maxLifetime}s)`,
241
- error: "invalid_client"
288
+ await consumeClientAssertion(ctx, opts, {
289
+ namespace: `private_key_jwt:${clientId}`,
290
+ payload,
291
+ expectedAudience: audience
242
292
  });
243
- if (!payload.jti) throw new APIError("BAD_REQUEST", {
244
- error_description: "client assertion must include jti claim",
245
- error: "invalid_client"
246
- });
247
- const jtiIdentifier = `private_key_jwt:${clientId}:${payload.jti}`;
248
- if (pendingAssertionIds.has(jtiIdentifier)) throw new APIError("BAD_REQUEST", {
249
- error_description: "client assertion jti has already been used",
250
- error: "invalid_client"
251
- });
252
- pendingAssertionIds.add(jtiIdentifier);
253
- try {
254
- if (await ctx.context.internalAdapter.findVerificationValue(jtiIdentifier)) throw new APIError("BAD_REQUEST", {
255
- error_description: "client assertion jti has already been used",
256
- error: "invalid_client"
257
- });
258
- const jtiExpiry = /* @__PURE__ */ new Date(payload.exp * 1e3);
259
- try {
260
- await ctx.context.internalAdapter.createVerificationValue({
261
- identifier: jtiIdentifier,
262
- value: clientId,
263
- expiresAt: jtiExpiry
264
- });
265
- } catch (createErr) {
266
- if (await ctx.context.internalAdapter.findVerificationValue(jtiIdentifier)) throw new APIError("BAD_REQUEST", {
267
- error_description: "client assertion jti has already been used",
268
- error: "invalid_client"
269
- });
270
- throw createErr;
271
- }
272
- } finally {
273
- pendingAssertionIds.delete(jtiIdentifier);
274
- }
275
- return {
276
- clientId,
277
- client
278
- };
293
+ return { clientId };
279
294
  }
280
295
  //#endregion
281
- export { isPrivateHostname as n, client_assertion_exports as t };
296
+ export { consumeClientAssertion as n, isPrivateHostname as r, client_assertion_exports as t };
@@ -1,4 +1,5 @@
1
- import { s as ResourceServerMetadata } from "./oauth-q7dn10NU.mjs";
1
+ import { c as ResourceServerMetadata } from "./oauth-CaXmZpoL.mjs";
2
+ import { ResourceRequestInput, VerifyAccessTokenRequestOptions } from "better-auth/oauth2";
2
3
  import { JWTPayload, JWTVerifyOptions } from "jose";
3
4
  import { BetterAuthOptions } from "better-auth/types";
4
5
 
@@ -22,7 +23,14 @@ declare const oauthProviderResourceClient: <T extends ResourceClientAuth | undef
22
23
  *
23
24
  * The optional auth parameter can fill known values automatically.
24
25
  */
25
- verifyAccessToken: VerifyAccessTokenOutput<T>;
26
+ verifyBearerToken: VerifyAccessTokenOutput<T>;
27
+ /**
28
+ * Performs verification of a protected-resource request. Use this for
29
+ * new resource-server integrations so sender-constrained DPoP access
30
+ * tokens are enforced with the request method, URL, Authorization
31
+ * scheme, DPoP proof, `ath`, and `cnf.jkt` binding.
32
+ */
33
+ verifyAccessTokenRequest: VerifyAccessTokenRequestOutput<T>;
26
34
  /**
27
35
  * An authorization server does not typically publish
28
36
  * the `/.well-known/oauth-protected-resource` themselves.
@@ -49,8 +57,23 @@ interface VerifyAccessTokenRemote {
49
57
  * is also still active.
50
58
  */
51
59
  force?: boolean;
60
+ /**
61
+ * Accept introspection responses that omit the `aud` claim even when a
62
+ * required `audience` is configured in `verifyOptions`.
63
+ *
64
+ * By default verification fails closed: if you configure an `audience` and
65
+ * the introspection response has no `aud` (or a mismatching one), the token
66
+ * is rejected. Some authorization servers legitimately omit `aud` from
67
+ * introspection responses (it is OPTIONAL per RFC 7662 §2.2); only enable
68
+ * this if you trust the issuer to bind the token to this resource through
69
+ * another mechanism, as it skips the audience check in that case.
70
+ *
71
+ * @default false
72
+ */
73
+ allowMissingAudience?: boolean;
52
74
  }
53
75
  type VerifyAccessTokenOutput<T> = T extends undefined ? (token: string | undefined, opts: VerifyAccessTokenNoAuthOpts) => Promise<JWTPayload> : (token: string | undefined, opts?: VerifyAccessTokenAuthOpts) => Promise<JWTPayload>;
76
+ type VerifyAccessTokenRequestOutput<T> = T extends undefined ? (request: Request | ResourceRequestInput, opts: VerifyAccessTokenRequestNoAuthOpts) => Promise<JWTPayload> : (request: Request | ResourceRequestInput, opts?: VerifyAccessTokenRequestAuthOpts) => Promise<JWTPayload>;
54
77
  type VerifyAccessTokenAuthOpts = {
55
78
  verifyOptions?: JWTVerifyOptions & Required<Pick<JWTVerifyOptions, "audience">>;
56
79
  scopes?: string[];
@@ -58,6 +81,9 @@ type VerifyAccessTokenAuthOpts = {
58
81
  remoteVerify?: VerifyAccessTokenRemote; /** Maps non-url (ie urn, client) resources to resource_metadata */
59
82
  resourceMetadataMappings?: Record<string, string>;
60
83
  };
84
+ type VerifyAccessTokenRequestAuthOpts = VerifyAccessTokenAuthOpts & {
85
+ dpop?: VerifyAccessTokenRequestOptions["dpop"];
86
+ };
61
87
  type VerifyAccessTokenNoAuthOpts = {
62
88
  verifyOptions: JWTVerifyOptions & Required<Pick<JWTVerifyOptions, "audience" | "issuer">>;
63
89
  scopes?: string[];
@@ -71,6 +97,9 @@ type VerifyAccessTokenNoAuthOpts = {
71
97
  remoteVerify: VerifyAccessTokenRemote; /** Maps non-url (ie urn, client) resources to resource_metadata */
72
98
  resourceMetadataMappings?: Record<string, string>;
73
99
  };
100
+ type VerifyAccessTokenRequestNoAuthOpts = VerifyAccessTokenNoAuthOpts & {
101
+ dpop?: VerifyAccessTokenRequestOptions["dpop"];
102
+ };
74
103
  type ProtectedResourceMetadataOutput<T> = T extends undefined ? (overrides: ResourceServerMetadata, opts?: {
75
104
  silenceWarnings?: {
76
105
  oidcScopes?: boolean;
@@ -1,10 +1,10 @@
1
- import { t as handleMcpErrors } from "./mcp-CYnz-MXn.mjs";
2
- import { o as getJwtPlugin, s as getOAuthProviderPlugin } from "./utils-DKBWQ8fe.mjs";
3
- import { t as PACKAGE_VERSION } from "./version-nFnRm-a3.mjs";
4
- import { verifyAccessToken } from "better-auth/oauth2";
1
+ import { o as getJwtPlugin, s as getOAuthProviderPlugin } from "./utils-Baq6atYN.mjs";
2
+ import { t as PACKAGE_VERSION } from "./version-CUu3vBtU.mjs";
3
+ import { t as raiseResourceServerChallenge } from "./resource-challenge-B-cqv4ur.mjs";
5
4
  import { APIError } from "better-call";
6
5
  import { logger } from "@better-auth/core/env";
7
6
  import { BetterAuthError } from "@better-auth/core/error";
7
+ import { DPOP_SIGNING_ALGORITHMS, requestToResourceInput, verifyAccessTokenRequest, verifyBearerToken } from "better-auth/oauth2";
8
8
  //#region src/client-resource.ts
9
9
  const oauthProviderResourceClient = (auth) => {
10
10
  let oauthProviderPlugin;
@@ -22,36 +22,55 @@ const oauthProviderResourceClient = (auth) => {
22
22
  return (await getJwtPluginOptions())?.jwt?.issuer ?? authServerBaseUrl;
23
23
  };
24
24
  const authServerBasePath = auth?.options.basePath;
25
+ const resolveVerifyAccessTokenOptions = async (opts) => {
26
+ const jwtPluginOptions = await getJwtPluginOptions();
27
+ const audience = opts?.verifyOptions?.audience ?? authServerBaseUrl;
28
+ const issuer = opts?.verifyOptions?.issuer ?? jwtPluginOptions?.jwt?.issuer ?? authServerBaseUrl;
29
+ if (!audience) throw Error("please define opts.verifyOptions.audience");
30
+ if (!issuer) throw Error("please define opts.verifyOptions.issuer");
31
+ const jwksUrl = opts?.jwksUrl ?? jwtPluginOptions?.jwks?.remoteUrl ?? (authServerBaseUrl ? `${authServerBaseUrl + (authServerBasePath ?? "")}${jwtPluginOptions?.jwks?.jwksPath ?? "/jwks"}` : void 0);
32
+ const introspectUrl = opts?.remoteVerify?.introspectUrl ?? (authServerBaseUrl ? `${authServerBaseUrl}${authServerBasePath ?? ""}/oauth2/introspect` : void 0);
33
+ return {
34
+ ...opts,
35
+ jwksUrl,
36
+ verifyOptions: {
37
+ ...opts?.verifyOptions,
38
+ audience,
39
+ issuer
40
+ },
41
+ remoteVerify: opts?.remoteVerify && introspectUrl ? {
42
+ ...opts.remoteVerify,
43
+ introspectUrl
44
+ } : void 0
45
+ };
46
+ };
47
+ const toResourceRequestInput = (request) => typeof request.headers?.get === "function" ? requestToResourceInput(request) : request;
25
48
  return {
26
49
  id: "oauth-provider-resource-client",
27
50
  version: PACKAGE_VERSION,
28
51
  getActions() {
29
52
  return {
30
- verifyAccessToken: (async (token, opts) => {
31
- const jwtPluginOptions = await getJwtPluginOptions();
32
- const audience = opts?.verifyOptions?.audience ?? authServerBaseUrl;
33
- const issuer = opts?.verifyOptions?.issuer ?? jwtPluginOptions?.jwt?.issuer ?? authServerBaseUrl;
34
- if (!audience) throw Error("please define opts.verifyOptions.audience");
35
- if (!issuer) throw Error("please define opts.verifyOptions.issuer");
36
- const jwksUrl = opts?.jwksUrl ?? jwtPluginOptions?.jwks?.remoteUrl ?? (authServerBaseUrl ? `${authServerBaseUrl + (authServerBasePath ?? "")}${jwtPluginOptions?.jwks?.jwksPath ?? "/jwks"}` : void 0);
37
- const introspectUrl = opts?.remoteVerify?.introspectUrl ?? (authServerBaseUrl ? `${authServerBaseUrl}${authServerBasePath ?? ""}/oauth2/introspect` : void 0);
53
+ verifyBearerToken: (async (token, opts) => {
54
+ const verifyOptions = await resolveVerifyAccessTokenOptions(opts);
38
55
  try {
39
56
  if (!token?.length) throw new APIError("UNAUTHORIZED", { message: "missing authorization header" });
40
- return await verifyAccessToken(token, {
41
- ...opts,
42
- jwksUrl,
43
- verifyOptions: {
44
- ...opts?.verifyOptions,
45
- audience,
46
- issuer
47
- },
48
- remoteVerify: opts?.remoteVerify && introspectUrl ? {
49
- ...opts.remoteVerify,
50
- introspectUrl
51
- } : void 0
57
+ return await verifyBearerToken(token, verifyOptions);
58
+ } catch (error) {
59
+ raiseResourceServerChallenge(error, verifyOptions.verifyOptions.audience, {
60
+ resourceMetadataMappings: opts?.resourceMetadataMappings,
61
+ dpopSigningAlgorithms: DPOP_SIGNING_ALGORITHMS
52
62
  });
63
+ }
64
+ }),
65
+ verifyAccessTokenRequest: (async (request, opts) => {
66
+ const verifyOptions = await resolveVerifyAccessTokenOptions(opts);
67
+ try {
68
+ return await verifyAccessTokenRequest(toResourceRequestInput(request), verifyOptions);
53
69
  } catch (error) {
54
- throw handleMcpErrors(error, audience, { resourceMetadataMappings: opts?.resourceMetadataMappings });
70
+ raiseResourceServerChallenge(error, verifyOptions.verifyOptions.audience, {
71
+ resourceMetadataMappings: opts?.resourceMetadataMappings,
72
+ dpopSigningAlgorithms: opts?.dpop?.signingAlgorithms ?? DPOP_SIGNING_ALGORITHMS
73
+ });
55
74
  }
56
75
  }),
57
76
  getProtectedResourceMetadata: (async (overrides, opts) => {
@@ -78,6 +97,7 @@ const oauthProviderResourceClient = (auth) => {
78
97
  return {
79
98
  resource,
80
99
  authorization_servers: authorizationServer ? [authorizationServer] : void 0,
100
+ dpop_signing_alg_values_supported: [...oauthProviderOptions?.dpop?.signingAlgorithms ?? DPOP_SIGNING_ALGORITHMS],
81
101
  ...overrides
82
102
  };
83
103
  })
package/dist/client.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { n as oauthProvider } from "./oauth-Vt3lTNHX.mjs";
1
+ import { r as oauthProvider } from "./oauth-CAeemjD7.mjs";
2
2
  import * as _better_fetch_fetch0 from "@better-fetch/fetch";
3
3
 
4
4
  //#region src/client.d.ts
package/dist/client.mjs CHANGED
@@ -1,17 +1,7 @@
1
- import { t as PACKAGE_VERSION } from "./version-nFnRm-a3.mjs";
1
+ import { t as buildSignedOAuthQuery } from "./signed-query-CFv2jNMT.mjs";
2
+ import { t as PACKAGE_VERSION } from "./version-CUu3vBtU.mjs";
2
3
  import { safeJSONParse } from "@better-auth/core/utils/json";
3
4
  //#region src/client.ts
4
- function parseSignedQuery(search) {
5
- const params = new URLSearchParams(search);
6
- if (params.has("sig")) {
7
- const signedParams = new URLSearchParams();
8
- for (const [key, value] of params.entries()) {
9
- signedParams.append(key, value);
10
- if (key === "sig") break;
11
- }
12
- return signedParams.toString();
13
- }
14
- }
15
5
  const oauthProviderClient = () => {
16
6
  return {
17
7
  id: "oauth-provider-client",
@@ -26,7 +16,7 @@ const oauthProviderClient = () => {
26
16
  if (body?.oauth_query) return;
27
17
  if (typeof window !== "undefined" && window?.location?.search && !(ctx.method === "GET" || ctx.method === "DELETE")) ctx.body = JSON.stringify({
28
18
  ...body,
29
- oauth_query: parseSignedQuery(window.location.search)
19
+ oauth_query: buildSignedOAuthQuery(window.location.search)
30
20
  });
31
21
  } }
32
22
  }],
package/dist/index.d.mts CHANGED
@@ -1,23 +1,37 @@
1
- import { _ as SchemaClient, a as OAuthClient, b as VerificationValue, c as TokenEndpointAuthMethod, d as OAuthAuthorizationQuery, f as OAuthConsent, g as Prompt, h as OAuthRefreshToken, i as GrantType, l as AuthorizePrompt, m as OAuthOptions, n as AuthServerMetadata, o as OIDCMetadata, p as OAuthOpaqueAccessToken, r as BearerMethodsSupported, s as ResourceServerMetadata, t as AuthMethod, u as ClientDiscovery, v as Scope, x as Awaitable, y as StoreTokenType } from "./oauth-q7dn10NU.mjs";
2
- import { a as OAuthErrorCode, c as OAuthRedirectOnError, i as OAuthEndpointRedirectContext, n as oauthProvider, o as OAuthFieldErrorCode, r as OAuthEndpointErrorResult, s as OAuthFieldErrorCodeMap, t as getOAuthProviderState } from "./oauth-Vt3lTNHX.mjs";
3
- import { verifyAccessToken } from "better-auth/oauth2";
1
+ import { A as OAuthProviderExtension, B as StoreTokenType, C as OAuthConsent, D as OAuthOpaqueAccessToken, E as OAuthMetadataExtensionInput, F as OAuthTokenResponse, H as ClientRegistrationRequest, I as OAuthUserInfoExtensionInput, L as Prompt, M as OAuthResource, N as OAuthResourceInput, O as OAuthOptions, P as OAuthTokenIssueParams, R as SchemaClient, S as OAuthClientResource, T as OAuthExtensionGrantHandlerInput, U as ResourceUriSchema, V as VerificationValue, _ as OAuthClaimExtensionInput, a as GrantType, b as OAuthClientAuthenticationResult, c as ResourceServerMetadata, d as ActiveAccessTokenPayload, f as AuthorizePrompt, g as OAuthAuthorizationQuery, h as OAuthAuthenticatedClient, i as Confirmation, j as OAuthRefreshToken, k as OAuthProviderApi, l as TokenEndpointAuthMethod, m as InitialAccessTokenAuthorization, n as AuthServerMetadata, o as OAuthClient, p as ClientDiscovery, r as BearerMethodsSupported, s as OIDCMetadata, t as AuthMethod, u as TokenType, v as OAuthClientAuthenticationInput, w as OAuthExtensionGrantHandler, x as OAuthClientAuthenticationStrategy, y as OAuthClientAuthenticationRequest, z as Scope } from "./oauth-CaXmZpoL.mjs";
2
+ import { a as OAuthEndpointErrorResult, c as OAuthFieldErrorCode, i as getIssuer, l as OAuthFieldErrorCodeMap, n as getOAuthProviderState, o as OAuthEndpointRedirectContext, r as oauthProvider, s as OAuthErrorCode, t as DEFAULT_OAUTH_SCOPES, u as OAuthRedirectOnError } from "./oauth-CAeemjD7.mjs";
3
+ import { getSessionFromCtx } from "better-auth/api";
4
4
  import { JWSAlgorithms, JwtOptions } from "better-auth/plugins";
5
- import { JWTPayload } from "jose";
6
- import { GenericEndpointContext } from "@better-auth/core";
5
+ import { AuthContext, GenericEndpointContext } from "@better-auth/core";
7
6
  import * as better_auth0 from "better-auth";
8
7
 
9
- //#region src/mcp.d.ts
8
+ //#region src/extensions.d.ts
10
9
  /**
11
- * A request middleware handler that checks and responds with
12
- * a WWW-Authenticate header for unauthenticated responses.
10
+ * Registers an {@link OAuthProviderExtension} with the OAuth Provider plugin
11
+ * from a companion plugin's `init()` hook. An extension can add token grants,
12
+ * assertion-based client authentication methods, additive discovery metadata,
13
+ * access-token / ID-token / UserInfo claims, and client-id discovery, without
14
+ * forking provider core.
13
15
  *
14
- * @external
16
+ * Call this once, at `init()` time. It is idempotent in the same `extension`
17
+ * object, so re-running a plugin's `init()` (for example when one plugin factory
18
+ * result is shared across two `betterAuth()` instances) does not register it
19
+ * twice. It throws if the oauth-provider plugin is not installed, if a grant
20
+ * type or assertion type is not an absolute URI, if a client authentication
21
+ * method reuses a built-in name, or if the extension registers a grant type,
22
+ * auth method, or assertion type that another extension already registered
23
+ * (contributions must be disjoint).
24
+ *
25
+ * @example
26
+ * ```ts
27
+ * init(ctx) {
28
+ * extendOAuthProvider(ctx, {
29
+ * grants: { "urn:example:grant": async ({ provider }) => provider.issueTokens(...) },
30
+ * });
31
+ * }
32
+ * ```
15
33
  */
16
- declare const mcpHandler: (/** Resource is the same url as the audience */
17
-
18
- verifyOptions: Parameters<typeof verifyAccessToken>[1], handler: (req: Request, jwt: JWTPayload) => Awaitable<Response>, opts?: {
19
- /** Maps non-url (ie urn, client) resources to resource_metadata */resourceMetadataMappings: Record<string, string>;
20
- }) => (req: Request) => Promise<Response>;
34
+ declare function extendOAuthProvider(ctx: AuthContext, extension: OAuthProviderExtension): void;
21
35
  //#endregion
22
36
  //#region src/metadata.d.ts
23
37
  declare function authServerMetadata(ctx: GenericEndpointContext, opts?: JwtOptions, overrides?: {
@@ -25,8 +39,12 @@ declare function authServerMetadata(ctx: GenericEndpointContext, opts?: JwtOptio
25
39
  dynamic_client_registration_supported?: boolean;
26
40
  public_client_supported?: boolean;
27
41
  grant_types_supported?: GrantType[];
42
+ token_endpoint_auth_methods_supported?: TokenEndpointAuthMethod[];
43
+ endpoint_auth_methods_supported?: TokenEndpointAuthMethod[];
28
44
  jwt_disabled?: boolean;
45
+ dpop_signing_alg_values_supported?: JWSAlgorithms[];
29
46
  }): AuthServerMetadata;
47
+ declare function oauthAuthorizationServerMetadata(ctx: GenericEndpointContext, opts: OAuthOptions<Scope[]>): AuthServerMetadata;
30
48
  declare function oidcServerMetadata(ctx: GenericEndpointContext, opts: OAuthOptions<Scope[]> & {
31
49
  claims?: string[];
32
50
  }): {
@@ -52,16 +70,20 @@ declare function oidcServerMetadata(ctx: GenericEndpointContext, opts: OAuthOpti
52
70
  op_policy_uri?: string | undefined;
53
71
  op_tos_uri?: string | undefined;
54
72
  revocation_endpoint?: string | undefined;
55
- revocation_endpoint_auth_methods_supported?: AuthMethod[] | undefined;
73
+ revocation_endpoint_auth_methods_supported?: TokenEndpointAuthMethod[] | undefined;
56
74
  revocation_endpoint_auth_signing_alg_values_supported?: better_auth0.PrivateKeyJwtSigningAlgorithm[] | undefined;
57
75
  introspection_endpoint?: string | undefined;
58
- introspection_endpoint_auth_methods_supported?: AuthMethod[] | undefined;
76
+ introspection_endpoint_auth_methods_supported?: TokenEndpointAuthMethod[] | undefined;
59
77
  introspection_endpoint_auth_signing_alg_values_supported?: better_auth0.PrivateKeyJwtSigningAlgorithm[] | undefined;
60
78
  code_challenge_methods_supported: "S256"[];
61
79
  authorization_response_iss_parameter_supported?: boolean | undefined;
62
80
  client_id_metadata_document_supported?: boolean | undefined;
81
+ backchannel_logout_supported?: boolean | undefined;
82
+ backchannel_logout_session_supported?: boolean | undefined;
83
+ dpop_signing_alg_values_supported?: JWSAlgorithms[] | undefined;
63
84
  id_token_signing_alg_values_supported: JWSAlgorithms[] | ["HS256"];
64
85
  };
86
+ declare function metadataResponse(body: unknown, extraHeaders?: HeadersInit): Response;
65
87
  /**
66
88
  * Provides an exportable `/.well-known/oauth-authorization-server`.
67
89
  *
@@ -106,4 +128,67 @@ declare function checkOAuthClient(client: OAuthClient, opts: OAuthOptions<Scope[
106
128
  */
107
129
  declare function oauthToSchema(input: OAuthClient): SchemaClient<Scope[]>;
108
130
  //#endregion
109
- export { AuthMethod, AuthServerMetadata, AuthorizePrompt, BearerMethodsSupported, ClientDiscovery, GrantType, OAuthAuthorizationQuery, OAuthClient, OAuthConsent, type OAuthEndpointErrorResult, type OAuthEndpointRedirectContext, type OAuthErrorCode, type OAuthFieldErrorCode, type OAuthFieldErrorCodeMap, OAuthOpaqueAccessToken, OAuthOptions, type OAuthRedirectOnError, OAuthRefreshToken, OIDCMetadata, Prompt, ResourceServerMetadata, SchemaClient, Scope, StoreTokenType, TokenEndpointAuthMethod, VerificationValue, authServerMetadata, checkOAuthClient, getOAuthProviderState, mcpHandler, oauthProvider, oauthProviderAuthServerMetadata, oauthProviderOpenIdConfigMetadata, oauthToSchema, oidcServerMetadata };
131
+ //#region src/resource-challenge.d.ts
132
+ /**
133
+ * Raise an OAuth resource-server challenge for a failed access-token request.
134
+ *
135
+ * Missing/invalid bearer credentials are reported with RFC 6750 plus the RFC
136
+ * 9728 `resource_metadata` pointer. DPoP-bound-token failures are reported with
137
+ * RFC 9449's `DPoP` challenge so clients know which proof algorithms to use.
138
+ * Non-URL resources (for example a `urn:` or a client id) resolve their
139
+ * metadata URL through `resourceMetadataMappings`.
140
+ *
141
+ * @internal
142
+ */
143
+ declare function raiseResourceServerChallenge(error: unknown, resource: string | string[], opts?: {
144
+ /** Maps non-URL (urn, client) resources to their resource_metadata URL. */resourceMetadataMappings?: Record<string, string>; /** DPoP JWS algorithms to advertise in RFC 9449 challenges. */
145
+ dpopSigningAlgorithms?: readonly string[]; /** Space-delimited scopes to advertise in RFC 6750 bearer challenges. */
146
+ scope?: string;
147
+ }): never;
148
+ //#endregion
149
+ //#region src/token.d.ts
150
+ /**
151
+ * Returns the OAuth Provider's server-side capability surface bound to `ctx`.
152
+ * The token endpoint passes one (pre-bound to the dispatched grant) to each
153
+ * extension grant handler; a companion plugin's own endpoint calls this directly
154
+ * with its grant type. `grantType` is bound here, not per issuance, so a handler
155
+ * cannot mislabel the grant; omit it for capabilities that do not issue tokens
156
+ * (`getClient`, `validateAccessToken`, `requireActiveAccessToken`), and
157
+ * `issueTokens` then throws.
158
+ */
159
+ declare function getOAuthProviderApi(ctx: GenericEndpointContext, opts: OAuthOptions<Scope[]>, grantType?: GrantType): OAuthProviderApi;
160
+ //#endregion
161
+ //#region src/utils/client-assertion.d.ts
162
+ /**
163
+ * Enforces the assertion-hygiene claims every client-assertion authentication
164
+ * method must check, independent of how the signature is verified or where the
165
+ * verification keys come from:
166
+ * - `aud` MUST include `expectedAudience` (RFC 7523 §3 rule 3),
167
+ * - `exp` MUST be present, unexpired, and at most `assertionMaxLifetime`
168
+ * seconds away (RFC 7523 §3 rule 4),
169
+ * - `iat`, when present, MUST be within `assertionMaxLifetime`,
170
+ * - `jti` MUST be present and single-use; this consumes a replay tombstone keyed
171
+ * by `` `${namespace}:${jti}` ``, inserted under the adapter's primary key so a
172
+ * replay across workers fails atomically.
173
+ *
174
+ * A custom {@link OAuthClientAuthenticationStrategy} should call this after
175
+ * verifying the assertion signature, so an extension method inherits the same
176
+ * replay, lifetime, and audience guarantees as the built-in `private_key_jwt`
177
+ * path, which calls it too.
178
+ *
179
+ * @param params.namespace Scopes the replay tombstone to the method and client,
180
+ * e.g. `` `${method}:${clientId}` ``, so the same `jti` can recur across
181
+ * distinct methods or clients but never within one.
182
+ */
183
+ declare function consumeClientAssertion(ctx: GenericEndpointContext, opts: OAuthOptions<Scope[]>, params: {
184
+ namespace: string;
185
+ payload: {
186
+ aud?: unknown;
187
+ exp?: unknown;
188
+ iat?: unknown;
189
+ jti?: unknown;
190
+ };
191
+ expectedAudience: string;
192
+ }): Promise<void>;
193
+ //#endregion
194
+ export { ActiveAccessTokenPayload, AuthMethod, AuthServerMetadata, AuthorizePrompt, BearerMethodsSupported, ClientDiscovery, ClientRegistrationRequest, Confirmation, DEFAULT_OAUTH_SCOPES, GrantType, InitialAccessTokenAuthorization, OAuthAuthenticatedClient, OAuthAuthorizationQuery, OAuthClaimExtensionInput, type OAuthClient, OAuthClientAuthenticationInput, OAuthClientAuthenticationRequest, OAuthClientAuthenticationResult, OAuthClientAuthenticationStrategy, OAuthClientResource, OAuthConsent, type OAuthEndpointErrorResult, type OAuthEndpointRedirectContext, type OAuthErrorCode, OAuthExtensionGrantHandler, OAuthExtensionGrantHandlerInput, type OAuthFieldErrorCode, type OAuthFieldErrorCodeMap, OAuthMetadataExtensionInput, OAuthOpaqueAccessToken, OAuthOptions, OAuthProviderApi, OAuthProviderExtension, type OAuthRedirectOnError, OAuthRefreshToken, OAuthResource, OAuthResourceInput, OAuthTokenIssueParams, OAuthTokenResponse, OAuthUserInfoExtensionInput, OIDCMetadata, Prompt, type ResourceServerMetadata, ResourceUriSchema, SchemaClient, Scope, StoreTokenType, TokenEndpointAuthMethod, TokenType, VerificationValue, authServerMetadata, checkOAuthClient, consumeClientAssertion, extendOAuthProvider, getIssuer, getOAuthProviderApi, getOAuthProviderState, metadataResponse, oauthAuthorizationServerMetadata, oauthProvider, oauthProviderAuthServerMetadata, oauthProviderOpenIdConfigMetadata, oauthToSchema, oidcServerMetadata, raiseResourceServerChallenge };