@better-auth/oauth-provider 1.6.3 → 1.7.0-beta.1

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,281 @@
1
+ import { a as getClient } from "./utils-Cx_XnD9i.mjs";
2
+ import { APIError } from "better-call";
3
+ import { ASSERTION_SIGNING_ALGORITHMS, CLIENT_ASSERTION_TYPE } from "@better-auth/core/oauth2";
4
+ 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
+ //#region src/utils/client-assertion.ts
18
+ var client_assertion_exports = /* @__PURE__ */ __exportAll({
19
+ isPrivateHostname: () => isPrivateHostname,
20
+ verifyClientAssertion: () => verifyClientAssertion
21
+ });
22
+ const jwksCache = /* @__PURE__ */ new Map();
23
+ const JWKS_CACHE_TTL_MS = 300 * 1e3;
24
+ const JWKS_CACHE_MAX_ENTRIES = 500;
25
+ const JWKS_FETCH_TIMEOUT_MS = 5e3;
26
+ function setJwksCache(uri, jwks, fetchedAt) {
27
+ jwksCache.set(uri, {
28
+ jwks,
29
+ fetchedAt
30
+ });
31
+ if (jwksCache.size > JWKS_CACHE_MAX_ENTRIES) {
32
+ const oldest = jwksCache.keys().next().value;
33
+ if (oldest !== void 0) jwksCache.delete(oldest);
34
+ }
35
+ }
36
+ const ALGORITHMS_LIST = [...ASSERTION_SIGNING_ALGORITHMS];
37
+ const pendingAssertionIds = /* @__PURE__ */ new Set();
38
+ /**
39
+ * Block SSRF: reject jwks_uri pointing at private/reserved IP ranges.
40
+ * Only HTTPS with public hostnames is allowed.
41
+ */
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
+ 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;
64
+ }
65
+ function validateJwksUri(ctx, jwksUri, clientIdUrlOrigin) {
66
+ const parsed = new URL(jwksUri);
67
+ if (parsed.protocol !== "https:") throw new APIError("BAD_REQUEST", {
68
+ error_description: "jwks_uri must use HTTPS",
69
+ error: "invalid_client"
70
+ });
71
+ if (isPrivateHostname(parsed.hostname)) throw new APIError("BAD_REQUEST", {
72
+ error_description: "jwks_uri must not point to a private or reserved address",
73
+ error: "invalid_client"
74
+ });
75
+ if (clientIdUrlOrigin && parsed.origin === clientIdUrlOrigin) return;
76
+ if (!ctx.context.isTrustedOrigin(parsed.href)) throw new APIError("BAD_REQUEST", {
77
+ error_description: "client jwks_uri is not trusted",
78
+ error: "invalid_client"
79
+ });
80
+ }
81
+ function urlClientIdOrigin(clientId) {
82
+ if (!clientId.startsWith("https://") && !clientId.startsWith("http://")) return;
83
+ try {
84
+ return new URL(clientId).origin;
85
+ } catch {
86
+ return;
87
+ }
88
+ }
89
+ async function fetchJwksFromUri(jwksUri) {
90
+ const controller = new AbortController();
91
+ const timeout = setTimeout(() => controller.abort(), JWKS_FETCH_TIMEOUT_MS);
92
+ try {
93
+ const response = await fetch(jwksUri, {
94
+ signal: controller.signal,
95
+ headers: { accept: "application/json" },
96
+ redirect: "error"
97
+ });
98
+ if (!response.ok) throw new Error(`JWKS fetch returned ${response.status}`);
99
+ const jwks = await response.json();
100
+ if (!jwks.keys || !Array.isArray(jwks.keys)) throw new Error("JWKS response missing keys array");
101
+ return jwks;
102
+ } finally {
103
+ clearTimeout(timeout);
104
+ }
105
+ }
106
+ async function fetchClientJwks(ctx, client) {
107
+ if (client.jwks) return JSON.parse(client.jwks);
108
+ if (!client.jwksUri) throw new APIError("BAD_REQUEST", {
109
+ error_description: "client has no JWKS configured",
110
+ error: "invalid_client"
111
+ });
112
+ validateJwksUri(ctx, client.jwksUri, urlClientIdOrigin(client.clientId));
113
+ const now = Date.now();
114
+ const cached = jwksCache.get(client.jwksUri);
115
+ if (cached && now - cached.fetchedAt < JWKS_CACHE_TTL_MS) return cached.jwks;
116
+ try {
117
+ const jwks = await fetchJwksFromUri(client.jwksUri);
118
+ setJwksCache(client.jwksUri, jwks, now);
119
+ return jwks;
120
+ } catch {
121
+ const staleLimitMs = JWKS_CACHE_TTL_MS * 2;
122
+ if (cached && now - cached.fetchedAt < staleLimitMs) return cached.jwks;
123
+ throw new APIError("BAD_REQUEST", {
124
+ error_description: "failed to fetch client JWKS",
125
+ error: "invalid_client"
126
+ });
127
+ }
128
+ }
129
+ /**
130
+ * Refetch JWKS from jwks_uri when signature verification fails with cached keys.
131
+ * Handles key rotation: the client may have published a new key that isn't in our cache yet.
132
+ */
133
+ async function refetchClientJwks(client) {
134
+ if (!client.jwksUri) return null;
135
+ try {
136
+ const jwks = await fetchJwksFromUri(client.jwksUri);
137
+ setJwksCache(client.jwksUri, jwks, Date.now());
138
+ return jwks;
139
+ } catch {
140
+ return null;
141
+ }
142
+ }
143
+ /**
144
+ * Verifies a client assertion JWT for `private_key_jwt` authentication.
145
+ *
146
+ * Validates: signature, iss=client_id, sub=client_id, aud=token_endpoint,
147
+ * exp, assertion max lifetime, jti uniqueness (replay prevention).
148
+ */
149
+ async function verifyClientAssertion(ctx, opts, clientAssertion, clientAssertionType, clientIdHint, expectedAudience) {
150
+ if (clientAssertionType !== CLIENT_ASSERTION_TYPE) throw new APIError("BAD_REQUEST", {
151
+ error_description: "unsupported client_assertion_type",
152
+ error: "invalid_client"
153
+ });
154
+ let header;
155
+ try {
156
+ header = decodeProtectedHeader(clientAssertion);
157
+ } catch {
158
+ throw new APIError("BAD_REQUEST", {
159
+ error_description: "malformed client assertion: invalid JWT header",
160
+ error: "invalid_client"
161
+ });
162
+ }
163
+ if (!header.alg || !ASSERTION_SIGNING_ALGORITHMS.includes(header.alg)) throw new APIError("BAD_REQUEST", {
164
+ error_description: `unsupported assertion signing algorithm: ${header.alg}`,
165
+ error: "invalid_client"
166
+ });
167
+ let unverified;
168
+ try {
169
+ unverified = decodeJwt(clientAssertion);
170
+ } catch {
171
+ throw new APIError("BAD_REQUEST", {
172
+ error_description: "malformed client assertion: invalid JWT payload",
173
+ error: "invalid_client"
174
+ });
175
+ }
176
+ const clientId = unverified.sub ?? unverified.iss;
177
+ if (!clientId) throw new APIError("BAD_REQUEST", {
178
+ error_description: "client assertion must contain sub or iss claim identifying the client",
179
+ error: "invalid_client"
180
+ });
181
+ if (clientIdHint && clientIdHint !== clientId) throw new APIError("BAD_REQUEST", {
182
+ error_description: "client_id in body does not match assertion sub/iss",
183
+ error: "invalid_client"
184
+ });
185
+ const client = await getClient(ctx, opts, clientId);
186
+ if (!client) throw new APIError("BAD_REQUEST", {
187
+ error_description: "unknown client",
188
+ error: "invalid_client"
189
+ });
190
+ if (client.disabled) throw new APIError("BAD_REQUEST", {
191
+ error_description: "client is disabled",
192
+ error: "invalid_client"
193
+ });
194
+ if (client.tokenEndpointAuthMethod !== "private_key_jwt") throw new APIError("BAD_REQUEST", {
195
+ error_description: "client is not registered for private_key_jwt authentication",
196
+ error: "invalid_client"
197
+ });
198
+ const jwks = await fetchClientJwks(ctx, client);
199
+ const audience = expectedAudience ?? `${ctx.context.baseURL}/oauth2/token`;
200
+ const maxLifetime = opts.assertionMaxLifetime ?? 300;
201
+ const verifyOpts = {
202
+ issuer: clientId,
203
+ subject: clientId,
204
+ audience,
205
+ algorithms: ALGORITHMS_LIST
206
+ };
207
+ let payload;
208
+ try {
209
+ ({payload} = await jwtVerify(clientAssertion, createLocalJWKSet(jwks), verifyOpts));
210
+ } catch (verifyErr) {
211
+ if (verifyErr instanceof Error && /no matching key|no applicable key/i.test(verifyErr.message)) {
212
+ const refreshed = await refetchClientJwks(client);
213
+ if (refreshed) try {
214
+ ({payload} = await jwtVerify(clientAssertion, createLocalJWKSet(refreshed), verifyOpts));
215
+ } catch {
216
+ throw new APIError("UNAUTHORIZED", {
217
+ error_description: "client assertion signature verification failed",
218
+ error: "invalid_client"
219
+ });
220
+ }
221
+ else throw new APIError("UNAUTHORIZED", {
222
+ error_description: "client assertion signature verification failed",
223
+ error: "invalid_client"
224
+ });
225
+ } else throw new APIError("UNAUTHORIZED", {
226
+ error_description: "client assertion signature verification failed",
227
+ error: "invalid_client"
228
+ });
229
+ }
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"
242
+ });
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
+ };
279
+ }
280
+ //#endregion
281
+ export { isPrivateHostname as n, client_assertion_exports as t };
@@ -1,4 +1,4 @@
1
- import { a as ResourceServerMetadata } from "./oauth-IpUqx-_N.mjs";
1
+ import { o as ResourceServerMetadata } from "./oauth-CU79t-eG.mjs";
2
2
  import { JWTPayload, JWTVerifyOptions } from "jose";
3
3
  import { Auth } from "better-auth/types";
4
4
 
@@ -1,5 +1,6 @@
1
- import { a as getJwtPlugin, o as getOAuthProviderPlugin, y as handleMcpErrors } from "./utils-B9Pj9EPf.mjs";
2
- import { t as PACKAGE_VERSION } from "./version-BlcZ64XB.mjs";
1
+ import { t as handleMcpErrors } from "./mcp-CYnz-MXn.mjs";
2
+ import { o as getJwtPlugin, s as getOAuthProviderPlugin } from "./utils-Cx_XnD9i.mjs";
3
+ import { t as PACKAGE_VERSION } from "./version-DIwdpXrQ.mjs";
3
4
  import { verifyAccessToken } from "better-auth/oauth2";
4
5
  import { APIError } from "better-call";
5
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-DH61OMT6.mjs";
1
+ import { n as oauthProvider } from "./oauth-B_qonG53.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-BlcZ64XB.mjs";
1
+ import { t as PACKAGE_VERSION } from "./version-DIwdpXrQ.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,9 +1,10 @@
1
- import { _ as Awaitable, a as ResourceServerMetadata, c as OAuthConsent, d as OAuthRefreshToken, f as Prompt, g as VerificationValue, h as StoreTokenType, i as OIDCMetadata, l as OAuthOpaqueAccessToken, m as Scope, n as GrantType, o as AuthorizePrompt, p as SchemaClient, r as OAuthClient, s as OAuthAuthorizationQuery, t as AuthServerMetadata, u as OAuthOptions } from "./oauth-IpUqx-_N.mjs";
2
- import { n as oauthProvider, t as getOAuthProviderState } from "./oauth-DH61OMT6.mjs";
1
+ import { _ as Scope, a as OIDCMetadata, b as Awaitable, c as AuthorizePrompt, d as OAuthConsent, f as OAuthOpaqueAccessToken, g as SchemaClient, h as Prompt, i as OAuthClient, l as ClientDiscovery, m as OAuthRefreshToken, n as AuthServerMetadata, o as ResourceServerMetadata, p as OAuthOptions, r as GrantType, s as TokenEndpointAuthMethod, t as AuthMethod, u as OAuthAuthorizationQuery, v as StoreTokenType, y as VerificationValue } from "./oauth-CU79t-eG.mjs";
2
+ import { n as oauthProvider, t as getOAuthProviderState } from "./oauth-B_qonG53.mjs";
3
3
  import { verifyAccessToken } from "better-auth/oauth2";
4
4
  import { JWSAlgorithms, JwtOptions } from "better-auth/plugins";
5
5
  import { JWTPayload } from "jose";
6
6
  import { GenericEndpointContext } from "@better-auth/core";
7
+ import * as better_auth0 from "better-auth";
7
8
 
8
9
  //#region src/mcp.d.ts
9
10
  /**
@@ -27,7 +28,37 @@ declare function authServerMetadata(ctx: GenericEndpointContext, opts?: JwtOptio
27
28
  }): AuthServerMetadata;
28
29
  declare function oidcServerMetadata(ctx: GenericEndpointContext, opts: OAuthOptions<Scope[]> & {
29
30
  claims?: string[];
30
- }): Omit<OIDCMetadata, "id_token_signing_alg_values_supported"> & {
31
+ }): {
32
+ jwks_uri?: string | undefined;
33
+ userinfo_endpoint: string;
34
+ acr_values_supported: string[];
35
+ subject_types_supported: ("public" | "pairwise")[];
36
+ claims_supported: string[];
37
+ end_session_endpoint: string;
38
+ prompt_values_supported: Prompt[];
39
+ issuer: string;
40
+ authorization_endpoint: string;
41
+ token_endpoint: string;
42
+ registration_endpoint: string;
43
+ scopes_supported?: string[] | undefined;
44
+ response_types_supported: "code"[];
45
+ response_modes_supported: "query"[];
46
+ grant_types_supported: GrantType[];
47
+ token_endpoint_auth_methods_supported?: TokenEndpointAuthMethod[] | undefined;
48
+ token_endpoint_auth_signing_alg_values_supported?: better_auth0.AssertionSigningAlgorithm[] | undefined;
49
+ service_documentation?: string | undefined;
50
+ ui_locales_supported?: string[] | undefined;
51
+ op_policy_uri?: string | undefined;
52
+ op_tos_uri?: string | undefined;
53
+ revocation_endpoint?: string | undefined;
54
+ revocation_endpoint_auth_methods_supported?: AuthMethod[] | undefined;
55
+ revocation_endpoint_auth_signing_alg_values_supported?: better_auth0.AssertionSigningAlgorithm[] | undefined;
56
+ introspection_endpoint?: string | undefined;
57
+ introspection_endpoint_auth_methods_supported?: AuthMethod[] | undefined;
58
+ introspection_endpoint_auth_signing_alg_values_supported?: better_auth0.AssertionSigningAlgorithm[] | undefined;
59
+ code_challenge_methods_supported: "S256"[];
60
+ authorization_response_iss_parameter_supported?: boolean | undefined;
61
+ client_id_metadata_document_supported?: boolean | undefined;
31
62
  id_token_signing_alg_values_supported: JWSAlgorithms[] | ["HS256"];
32
63
  };
33
64
  /**
@@ -61,4 +92,17 @@ declare const oauthProviderOpenIdConfigMetadata: <Auth extends {
61
92
  headers?: HeadersInit;
62
93
  }) => (request: Request) => Promise<Response>;
63
94
  //#endregion
64
- export { AuthServerMetadata, AuthorizePrompt, OAuthAuthorizationQuery, OAuthClient, OAuthConsent, OAuthOpaqueAccessToken, OAuthOptions, OAuthRefreshToken, OIDCMetadata, Prompt, ResourceServerMetadata, SchemaClient, Scope, StoreTokenType, VerificationValue, authServerMetadata, getOAuthProviderState, mcpHandler, oauthProvider, oauthProviderAuthServerMetadata, oauthProviderOpenIdConfigMetadata, oidcServerMetadata };
95
+ //#region src/register.d.ts
96
+ declare function checkOAuthClient(client: OAuthClient, opts: OAuthOptions<Scope[]>, settings?: {
97
+ isRegister?: boolean;
98
+ ctx?: GenericEndpointContext;
99
+ }): Promise<void>;
100
+ /**
101
+ * Converts an OAuth 2.0 Dynamic Client Schema to a Database Schema
102
+ *
103
+ * @param input
104
+ * @returns
105
+ */
106
+ declare function oauthToSchema(input: OAuthClient): SchemaClient<Scope[]>;
107
+ //#endregion
108
+ export { AuthServerMetadata, AuthorizePrompt, ClientDiscovery, OAuthAuthorizationQuery, OAuthClient, OAuthConsent, OAuthOpaqueAccessToken, OAuthOptions, OAuthRefreshToken, OIDCMetadata, Prompt, ResourceServerMetadata, SchemaClient, Scope, StoreTokenType, VerificationValue, authServerMetadata, checkOAuthClient, getOAuthProviderState, mcpHandler, oauthProvider, oauthProviderAuthServerMetadata, oauthProviderOpenIdConfigMetadata, oauthToSchema, oidcServerMetadata };