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

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,6 +1,9 @@
1
- import { a as getClient } from "./utils-DKBWQ8fe.mjs";
1
+ import { o as getClient } from "./utils-D2dLqo7f.mjs";
2
2
  import { APIError } from "better-call";
3
3
  import { CLIENT_ASSERTION_TYPE, PRIVATE_KEY_JWT_SIGNING_ALGORITHMS } from "@better-auth/core/oauth2";
4
+ import { isPublicRoutableHost } from "@better-auth/core/utils/host";
5
+ import { base64Url } from "@better-auth/utils/base64";
6
+ import { createHash } from "@better-auth/utils/hash";
4
7
  import { createLocalJWKSet, decodeJwt, decodeProtectedHeader, jwtVerify } from "jose";
5
8
  //#region \0rolldown/runtime.js
6
9
  var __defProp = Object.defineProperty;
@@ -34,33 +37,21 @@ function setJwksCache(uri, jwks, fetchedAt) {
34
37
  }
35
38
  }
36
39
  const ALGORITHMS_LIST = [...PRIVATE_KEY_JWT_SIGNING_ALGORITHMS];
37
- const pendingAssertionIds = /* @__PURE__ */ new Set();
38
40
  /**
39
- * Block SSRF: reject jwks_uri pointing at private/reserved IP ranges.
40
- * Only HTTPS with public hostnames is allowed.
41
+ * SSRF gate for user-supplied server-side fetch targets (`jwks_uri`,
42
+ * `backchannel_logout_uri`): returns true when the host is NOT publicly
43
+ * routable. That covers loopback, RFC 1918 private, link-local (including AWS
44
+ * IMDS `169.254.169.254`), shared-address-space (carrier-grade NAT),
45
+ * IPv4-mapped IPv6, 6to4/NAT64/Teredo tunnels, every other RFC 6890
46
+ * special-purpose range, and cloud-metadata FQDNs.
47
+ *
48
+ * Delegates to the audited single source of truth so this check cannot drift
49
+ * into the kind of encoding bypass that bespoke regexes invite. This is a
50
+ * syntactic check only: it does not resolve DNS, so a public name that
51
+ * resolves to a private address at fetch time is not caught here.
41
52
  */
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
53
  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;
54
+ return !isPublicRoutableHost(hostname);
64
55
  }
65
56
  function validateJwksUri(ctx, jwksUri, clientIdUrlOrigin) {
66
57
  const parsed = new URL(jwksUri);
@@ -244,33 +235,33 @@ async function verifyClientAssertion(ctx, opts, clientAssertion, clientAssertion
244
235
  error_description: "client assertion must include jti claim",
245
236
  error: "invalid_client"
246
237
  });
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);
238
+ const jtiDigest = await createHash("SHA-256").digest(new TextEncoder().encode(`private_key_jwt:${clientId}:${payload.jti}`));
239
+ const jtiId = base64Url.encode(new Uint8Array(jtiDigest).slice(0, 24), { padding: false });
253
240
  try {
254
- if (await ctx.context.internalAdapter.findVerificationValue(jtiIdentifier)) throw new APIError("BAD_REQUEST", {
241
+ await ctx.context.adapter.create({
242
+ model: "oauthClientAssertion",
243
+ data: {
244
+ id: jtiId,
245
+ expiresAt: /* @__PURE__ */ new Date(payload.exp * 1e3)
246
+ },
247
+ forceAllowId: true
248
+ });
249
+ } catch (createErr) {
250
+ let alreadyUsed = false;
251
+ try {
252
+ alreadyUsed = Boolean(await ctx.context.adapter.findOne({
253
+ model: "oauthClientAssertion",
254
+ where: [{
255
+ field: "id",
256
+ value: jtiId
257
+ }]
258
+ }));
259
+ } catch {}
260
+ if (alreadyUsed) throw new APIError("BAD_REQUEST", {
255
261
  error_description: "client assertion jti has already been used",
256
262
  error: "invalid_client"
257
263
  });
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);
264
+ throw createErr;
274
265
  }
275
266
  return {
276
267
  clientId,
@@ -1,4 +1,4 @@
1
- import { s as ResourceServerMetadata } from "./oauth-q7dn10NU.mjs";
1
+ import { s as ResourceServerMetadata } from "./oauth-BXrYl5x6.mjs";
2
2
  import { JWTPayload, JWTVerifyOptions } from "jose";
3
3
  import { BetterAuthOptions } from "better-auth/types";
4
4
 
@@ -49,6 +49,20 @@ interface VerifyAccessTokenRemote {
49
49
  * is also still active.
50
50
  */
51
51
  force?: boolean;
52
+ /**
53
+ * Accept introspection responses that omit the `aud` claim even when a
54
+ * required `audience` is configured in `verifyOptions`.
55
+ *
56
+ * By default verification fails closed: if you configure an `audience` and
57
+ * the introspection response has no `aud` (or a mismatching one), the token
58
+ * is rejected. Some authorization servers legitimately omit `aud` from
59
+ * introspection responses (it is OPTIONAL per RFC 7662 §2.2); only enable
60
+ * this if you trust the issuer to bind the token to this resource through
61
+ * another mechanism, as it skips the audience check in that case.
62
+ *
63
+ * @default false
64
+ */
65
+ allowMissingAudience?: boolean;
52
66
  }
53
67
  type VerifyAccessTokenOutput<T> = T extends undefined ? (token: string | undefined, opts: VerifyAccessTokenNoAuthOpts) => Promise<JWTPayload> : (token: string | undefined, opts?: VerifyAccessTokenAuthOpts) => Promise<JWTPayload>;
54
68
  type VerifyAccessTokenAuthOpts = {
@@ -1,6 +1,6 @@
1
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";
2
+ import { c as getOAuthProviderPlugin, s as getJwtPlugin } from "./utils-D2dLqo7f.mjs";
3
+ import { t as PACKAGE_VERSION } from "./version-B1ZiRmxj.mjs";
4
4
  import { verifyAccessToken } from "better-auth/oauth2";
5
5
  import { APIError } from "better-call";
6
6
  import { logger } from "@better-auth/core/env";
package/dist/client.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { n as oauthProvider } from "./oauth-Vt3lTNHX.mjs";
1
+ import { n as oauthProvider } from "./oauth-DU6NeviY.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,4 +1,4 @@
1
- import { t as PACKAGE_VERSION } from "./version-nFnRm-a3.mjs";
1
+ import { t as PACKAGE_VERSION } from "./version-B1ZiRmxj.mjs";
2
2
  import { safeJSONParse } from "@better-auth/core/utils/json";
3
3
  //#region src/client.ts
4
4
  function parseSignedQuery(search) {
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
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";
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-BXrYl5x6.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-DU6NeviY.mjs";
3
3
  import { verifyAccessToken } from "better-auth/oauth2";
4
4
  import { JWSAlgorithms, JwtOptions } from "better-auth/plugins";
5
5
  import { JWTPayload } from "jose";
@@ -60,6 +60,8 @@ declare function oidcServerMetadata(ctx: GenericEndpointContext, opts: OAuthOpti
60
60
  code_challenge_methods_supported: "S256"[];
61
61
  authorization_response_iss_parameter_supported?: boolean | undefined;
62
62
  client_id_metadata_document_supported?: boolean | undefined;
63
+ backchannel_logout_supported?: boolean | undefined;
64
+ backchannel_logout_session_supported?: boolean | undefined;
63
65
  id_token_signing_alg_values_supported: JWSAlgorithms[] | ["HS256"];
64
66
  };
65
67
  /**