@better-auth/oauth-provider 1.6.2 → 1.7.0-beta.0
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.
- package/dist/client-assertion-DZqo-L5j.mjs +272 -0
- package/dist/client-resource.d.mts +1 -1
- package/dist/client-resource.mjs +3 -2
- package/dist/client.d.mts +1 -1
- package/dist/client.mjs +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +132 -64
- package/dist/mcp-CYnz-MXn.mjs +56 -0
- package/dist/{oauth-Cc0nzj5Q.d.mts → oauth-C8aTlaAC.d.mts} +30 -7
- package/dist/{oauth-CetEXi_Z.d.mts → oauth-Dh4YXCXY.d.mts} +30 -1
- package/dist/{utils-B9Pj9EPf.mjs → utils-CIbcUsZ5.mjs} +77 -74
- package/dist/{version-Usf6Oz_M.mjs → version-BGWhjYBb.mjs} +1 -1
- package/package.json +5 -5
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { a as getClient } from "./utils-CIbcUsZ5.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) {
|
|
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 (!ctx.context.isTrustedOrigin(parsed.href)) throw new APIError("BAD_REQUEST", {
|
|
76
|
+
error_description: "client jwks_uri is not trusted",
|
|
77
|
+
error: "invalid_client"
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
async function fetchJwksFromUri(jwksUri) {
|
|
81
|
+
const controller = new AbortController();
|
|
82
|
+
const timeout = setTimeout(() => controller.abort(), JWKS_FETCH_TIMEOUT_MS);
|
|
83
|
+
try {
|
|
84
|
+
const response = await fetch(jwksUri, {
|
|
85
|
+
signal: controller.signal,
|
|
86
|
+
headers: { accept: "application/json" },
|
|
87
|
+
redirect: "error"
|
|
88
|
+
});
|
|
89
|
+
if (!response.ok) throw new Error(`JWKS fetch returned ${response.status}`);
|
|
90
|
+
const jwks = await response.json();
|
|
91
|
+
if (!jwks.keys || !Array.isArray(jwks.keys)) throw new Error("JWKS response missing keys array");
|
|
92
|
+
return jwks;
|
|
93
|
+
} finally {
|
|
94
|
+
clearTimeout(timeout);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
async function fetchClientJwks(ctx, client) {
|
|
98
|
+
if (client.jwks) return JSON.parse(client.jwks);
|
|
99
|
+
if (!client.jwksUri) throw new APIError("BAD_REQUEST", {
|
|
100
|
+
error_description: "client has no JWKS configured",
|
|
101
|
+
error: "invalid_client"
|
|
102
|
+
});
|
|
103
|
+
validateJwksUri(ctx, client.jwksUri);
|
|
104
|
+
const now = Date.now();
|
|
105
|
+
const cached = jwksCache.get(client.jwksUri);
|
|
106
|
+
if (cached && now - cached.fetchedAt < JWKS_CACHE_TTL_MS) return cached.jwks;
|
|
107
|
+
try {
|
|
108
|
+
const jwks = await fetchJwksFromUri(client.jwksUri);
|
|
109
|
+
setJwksCache(client.jwksUri, jwks, now);
|
|
110
|
+
return jwks;
|
|
111
|
+
} catch {
|
|
112
|
+
const staleLimitMs = JWKS_CACHE_TTL_MS * 2;
|
|
113
|
+
if (cached && now - cached.fetchedAt < staleLimitMs) return cached.jwks;
|
|
114
|
+
throw new APIError("BAD_REQUEST", {
|
|
115
|
+
error_description: "failed to fetch client JWKS",
|
|
116
|
+
error: "invalid_client"
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Refetch JWKS from jwks_uri when signature verification fails with cached keys.
|
|
122
|
+
* Handles key rotation: the client may have published a new key that isn't in our cache yet.
|
|
123
|
+
*/
|
|
124
|
+
async function refetchClientJwks(client) {
|
|
125
|
+
if (!client.jwksUri) return null;
|
|
126
|
+
try {
|
|
127
|
+
const jwks = await fetchJwksFromUri(client.jwksUri);
|
|
128
|
+
setJwksCache(client.jwksUri, jwks, Date.now());
|
|
129
|
+
return jwks;
|
|
130
|
+
} catch {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Verifies a client assertion JWT for `private_key_jwt` authentication.
|
|
136
|
+
*
|
|
137
|
+
* Validates: signature, iss=client_id, sub=client_id, aud=token_endpoint,
|
|
138
|
+
* exp, assertion max lifetime, jti uniqueness (replay prevention).
|
|
139
|
+
*/
|
|
140
|
+
async function verifyClientAssertion(ctx, opts, clientAssertion, clientAssertionType, clientIdHint, expectedAudience) {
|
|
141
|
+
if (clientAssertionType !== CLIENT_ASSERTION_TYPE) throw new APIError("BAD_REQUEST", {
|
|
142
|
+
error_description: "unsupported client_assertion_type",
|
|
143
|
+
error: "invalid_client"
|
|
144
|
+
});
|
|
145
|
+
let header;
|
|
146
|
+
try {
|
|
147
|
+
header = decodeProtectedHeader(clientAssertion);
|
|
148
|
+
} catch {
|
|
149
|
+
throw new APIError("BAD_REQUEST", {
|
|
150
|
+
error_description: "malformed client assertion: invalid JWT header",
|
|
151
|
+
error: "invalid_client"
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
if (!header.alg || !ASSERTION_SIGNING_ALGORITHMS.includes(header.alg)) throw new APIError("BAD_REQUEST", {
|
|
155
|
+
error_description: `unsupported assertion signing algorithm: ${header.alg}`,
|
|
156
|
+
error: "invalid_client"
|
|
157
|
+
});
|
|
158
|
+
let unverified;
|
|
159
|
+
try {
|
|
160
|
+
unverified = decodeJwt(clientAssertion);
|
|
161
|
+
} catch {
|
|
162
|
+
throw new APIError("BAD_REQUEST", {
|
|
163
|
+
error_description: "malformed client assertion: invalid JWT payload",
|
|
164
|
+
error: "invalid_client"
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
const clientId = unverified.sub ?? unverified.iss;
|
|
168
|
+
if (!clientId) throw new APIError("BAD_REQUEST", {
|
|
169
|
+
error_description: "client assertion must contain sub or iss claim identifying the client",
|
|
170
|
+
error: "invalid_client"
|
|
171
|
+
});
|
|
172
|
+
if (clientIdHint && clientIdHint !== clientId) throw new APIError("BAD_REQUEST", {
|
|
173
|
+
error_description: "client_id in body does not match assertion sub/iss",
|
|
174
|
+
error: "invalid_client"
|
|
175
|
+
});
|
|
176
|
+
const client = await getClient(ctx, opts, clientId);
|
|
177
|
+
if (!client) throw new APIError("BAD_REQUEST", {
|
|
178
|
+
error_description: "unknown client",
|
|
179
|
+
error: "invalid_client"
|
|
180
|
+
});
|
|
181
|
+
if (client.disabled) throw new APIError("BAD_REQUEST", {
|
|
182
|
+
error_description: "client is disabled",
|
|
183
|
+
error: "invalid_client"
|
|
184
|
+
});
|
|
185
|
+
if (client.tokenEndpointAuthMethod !== "private_key_jwt") throw new APIError("BAD_REQUEST", {
|
|
186
|
+
error_description: "client is not registered for private_key_jwt authentication",
|
|
187
|
+
error: "invalid_client"
|
|
188
|
+
});
|
|
189
|
+
const jwks = await fetchClientJwks(ctx, client);
|
|
190
|
+
const audience = expectedAudience ?? `${ctx.context.baseURL}/oauth2/token`;
|
|
191
|
+
const maxLifetime = opts.assertionMaxLifetime ?? 300;
|
|
192
|
+
const verifyOpts = {
|
|
193
|
+
issuer: clientId,
|
|
194
|
+
subject: clientId,
|
|
195
|
+
audience,
|
|
196
|
+
algorithms: ALGORITHMS_LIST
|
|
197
|
+
};
|
|
198
|
+
let payload;
|
|
199
|
+
try {
|
|
200
|
+
({payload} = await jwtVerify(clientAssertion, createLocalJWKSet(jwks), verifyOpts));
|
|
201
|
+
} catch (verifyErr) {
|
|
202
|
+
if (verifyErr instanceof Error && /no matching key|no applicable key/i.test(verifyErr.message)) {
|
|
203
|
+
const refreshed = await refetchClientJwks(client);
|
|
204
|
+
if (refreshed) try {
|
|
205
|
+
({payload} = await jwtVerify(clientAssertion, createLocalJWKSet(refreshed), verifyOpts));
|
|
206
|
+
} catch {
|
|
207
|
+
throw new APIError("UNAUTHORIZED", {
|
|
208
|
+
error_description: "client assertion signature verification failed",
|
|
209
|
+
error: "invalid_client"
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
else throw new APIError("UNAUTHORIZED", {
|
|
213
|
+
error_description: "client assertion signature verification failed",
|
|
214
|
+
error: "invalid_client"
|
|
215
|
+
});
|
|
216
|
+
} else throw new APIError("UNAUTHORIZED", {
|
|
217
|
+
error_description: "client assertion signature verification failed",
|
|
218
|
+
error: "invalid_client"
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
222
|
+
if (typeof payload.exp !== "number") throw new APIError("BAD_REQUEST", {
|
|
223
|
+
error_description: "client assertion must include exp claim",
|
|
224
|
+
error: "invalid_client"
|
|
225
|
+
});
|
|
226
|
+
if (payload.exp - now > maxLifetime) throw new APIError("BAD_REQUEST", {
|
|
227
|
+
error_description: `client assertion exp is too far in the future (max ${maxLifetime}s)`,
|
|
228
|
+
error: "invalid_client"
|
|
229
|
+
});
|
|
230
|
+
if (typeof payload.iat === "number" && now - payload.iat > maxLifetime) throw new APIError("BAD_REQUEST", {
|
|
231
|
+
error_description: `client assertion iat is too far in the past (max ${maxLifetime}s)`,
|
|
232
|
+
error: "invalid_client"
|
|
233
|
+
});
|
|
234
|
+
if (!payload.jti) throw new APIError("BAD_REQUEST", {
|
|
235
|
+
error_description: "client assertion must include jti claim",
|
|
236
|
+
error: "invalid_client"
|
|
237
|
+
});
|
|
238
|
+
const jtiIdentifier = `private_key_jwt:${clientId}:${payload.jti}`;
|
|
239
|
+
if (pendingAssertionIds.has(jtiIdentifier)) throw new APIError("BAD_REQUEST", {
|
|
240
|
+
error_description: "client assertion jti has already been used",
|
|
241
|
+
error: "invalid_client"
|
|
242
|
+
});
|
|
243
|
+
pendingAssertionIds.add(jtiIdentifier);
|
|
244
|
+
try {
|
|
245
|
+
if (await ctx.context.internalAdapter.findVerificationValue(jtiIdentifier)) throw new APIError("BAD_REQUEST", {
|
|
246
|
+
error_description: "client assertion jti has already been used",
|
|
247
|
+
error: "invalid_client"
|
|
248
|
+
});
|
|
249
|
+
const jtiExpiry = /* @__PURE__ */ new Date(payload.exp * 1e3);
|
|
250
|
+
try {
|
|
251
|
+
await ctx.context.internalAdapter.createVerificationValue({
|
|
252
|
+
identifier: jtiIdentifier,
|
|
253
|
+
value: clientId,
|
|
254
|
+
expiresAt: jtiExpiry
|
|
255
|
+
});
|
|
256
|
+
} catch (createErr) {
|
|
257
|
+
if (await ctx.context.internalAdapter.findVerificationValue(jtiIdentifier)) throw new APIError("BAD_REQUEST", {
|
|
258
|
+
error_description: "client assertion jti has already been used",
|
|
259
|
+
error: "invalid_client"
|
|
260
|
+
});
|
|
261
|
+
throw createErr;
|
|
262
|
+
}
|
|
263
|
+
} finally {
|
|
264
|
+
pendingAssertionIds.delete(jtiIdentifier);
|
|
265
|
+
}
|
|
266
|
+
return {
|
|
267
|
+
clientId,
|
|
268
|
+
client
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
//#endregion
|
|
272
|
+
export { isPrivateHostname as n, client_assertion_exports as t };
|
package/dist/client-resource.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { t as handleMcpErrors } from "./mcp-CYnz-MXn.mjs";
|
|
2
|
+
import { o as getJwtPlugin, s as getOAuthProviderPlugin } from "./utils-CIbcUsZ5.mjs";
|
|
3
|
+
import { t as PACKAGE_VERSION } from "./version-BGWhjYBb.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
package/dist/client.mjs
CHANGED
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
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-
|
|
2
|
-
import { n as oauthProvider, t as getOAuthProviderState } from "./oauth-
|
|
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-C8aTlaAC.mjs";
|
|
2
|
+
import { n as oauthProvider, t as getOAuthProviderState } from "./oauth-Dh4YXCXY.mjs";
|
|
3
3
|
import { verifyAccessToken } from "better-auth/oauth2";
|
|
4
4
|
import { JWSAlgorithms, JwtOptions } from "better-auth/plugins";
|
|
5
5
|
import { JWTPayload } from "jose";
|
package/dist/index.mjs
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { n as isPrivateHostname } from "./client-assertion-DZqo-L5j.mjs";
|
|
2
|
+
import { n as mcpHandler } from "./mcp-CYnz-MXn.mjs";
|
|
3
|
+
import { _ as storeToken, a as getClient, c as getStoredToken, d as parseClientMetadata, f as parsePrompt, g as storeClientSecret, h as searchParamsToQuery, i as extractClientCredentials, l as isPKCERequired, m as resolveSubjectIdentifier, n as deleteFromPrompt, o as getJwtPlugin, p as resolveSessionAuthTime, r as destructureCredentials, t as decryptStoredClientSecret, u as normalizeTimestampValue, v as validateClientCredentials, y as verifyOAuthQueryParams } from "./utils-CIbcUsZ5.mjs";
|
|
4
|
+
import { t as PACKAGE_VERSION } from "./version-BGWhjYBb.mjs";
|
|
3
5
|
import { APIError, createAuthEndpoint, createAuthMiddleware, getOAuthState, getSessionFromCtx, sessionMiddleware } from "better-auth/api";
|
|
4
6
|
import { generateCodeChallenge, getJwks, verifyJwsAccessToken } from "better-auth/oauth2";
|
|
5
7
|
import { APIError as APIError$1 } from "better-call";
|
|
8
|
+
import { ASSERTION_SIGNING_ALGORITHMS } from "@better-auth/core/oauth2";
|
|
6
9
|
import { isBrowserFetchRequest } from "@better-auth/core/utils/fetch-metadata";
|
|
7
10
|
import { generateRandomString, makeSignature } from "better-auth/crypto";
|
|
8
11
|
import { defineRequestState } from "@better-auth/core/context";
|
|
@@ -483,13 +486,8 @@ async function checkVerificationValue(ctx, opts, code, client_id, redirect_uri)
|
|
|
483
486
|
* Obtains new Session Jwt and Refresh Tokens using a code
|
|
484
487
|
*/
|
|
485
488
|
async function handleAuthorizationCodeGrant(ctx, opts) {
|
|
486
|
-
|
|
487
|
-
const
|
|
488
|
-
if (authorization?.startsWith("Basic ")) {
|
|
489
|
-
const res = basicToClientCredentials(authorization);
|
|
490
|
-
client_id = res?.client_id;
|
|
491
|
-
client_secret = res?.client_secret;
|
|
492
|
-
}
|
|
489
|
+
const { clientId: client_id, clientSecret: client_secret, preVerifiedClient } = destructureCredentials(await extractClientCredentials(ctx, opts, `${ctx.context.baseURL}/oauth2/token`));
|
|
490
|
+
const { code, code_verifier, redirect_uri } = ctx.body;
|
|
493
491
|
if (!client_id) throw new APIError("BAD_REQUEST", {
|
|
494
492
|
error_description: "client_id is required",
|
|
495
493
|
error: "invalid_request"
|
|
@@ -504,7 +502,7 @@ async function handleAuthorizationCodeGrant(ctx, opts) {
|
|
|
504
502
|
});
|
|
505
503
|
const isAuthCodeWithSecret = client_id && client_secret;
|
|
506
504
|
const isAuthCodeWithPkce = client_id && code && code_verifier;
|
|
507
|
-
if (!isAuthCodeWithSecret && !isAuthCodeWithPkce) throw new APIError("BAD_REQUEST", {
|
|
505
|
+
if (!isAuthCodeWithSecret && !isAuthCodeWithPkce && !preVerifiedClient) throw new APIError("BAD_REQUEST", {
|
|
508
506
|
error_description: "Either code_verifier or client_secret is required",
|
|
509
507
|
error: "invalid_request"
|
|
510
508
|
});
|
|
@@ -516,14 +514,14 @@ async function handleAuthorizationCodeGrant(ctx, opts) {
|
|
|
516
514
|
error: "invalid_scope"
|
|
517
515
|
});
|
|
518
516
|
/** Verify Client */
|
|
519
|
-
const client = await validateClientCredentials(ctx, opts, client_id, client_secret, scopes);
|
|
517
|
+
const client = await validateClientCredentials(ctx, opts, client_id, client_secret, scopes, preVerifiedClient);
|
|
520
518
|
if (isPKCERequired(client, (verificationValue.query?.scope)?.split(" ") || [])) {
|
|
521
519
|
if (!isAuthCodeWithPkce) throw new APIError("BAD_REQUEST", {
|
|
522
520
|
error_description: "PKCE is required for this client",
|
|
523
521
|
error: "invalid_request"
|
|
524
522
|
});
|
|
525
|
-
} else if (!(isAuthCodeWithPkce || isAuthCodeWithSecret)) throw new APIError("BAD_REQUEST", {
|
|
526
|
-
error_description: "Either PKCE (code_verifier) or client authentication (client_secret) is required",
|
|
523
|
+
} else if (!(isAuthCodeWithPkce || isAuthCodeWithSecret || preVerifiedClient)) throw new APIError("BAD_REQUEST", {
|
|
524
|
+
error_description: "Either PKCE (code_verifier) or client authentication (client_secret or client_assertion) is required",
|
|
527
525
|
error: "invalid_request"
|
|
528
526
|
});
|
|
529
527
|
/** Check PKCE challenge if verifier is provided */
|
|
@@ -574,22 +572,17 @@ async function handleAuthorizationCodeGrant(ctx, opts) {
|
|
|
574
572
|
* MUST follow https://datatracker.ietf.org/doc/html/rfc6749#section-4.4
|
|
575
573
|
*/
|
|
576
574
|
async function handleClientCredentialsGrant(ctx, opts) {
|
|
577
|
-
|
|
578
|
-
const
|
|
579
|
-
if (authorization?.startsWith("Basic ")) {
|
|
580
|
-
const res = basicToClientCredentials(authorization);
|
|
581
|
-
client_id = res?.client_id;
|
|
582
|
-
client_secret = res?.client_secret;
|
|
583
|
-
}
|
|
575
|
+
const { clientId: client_id, clientSecret: client_secret, preVerifiedClient } = destructureCredentials(await extractClientCredentials(ctx, opts, `${ctx.context.baseURL}/oauth2/token`));
|
|
576
|
+
const { scope } = ctx.body;
|
|
584
577
|
if (!client_id) throw new APIError("BAD_REQUEST", {
|
|
585
578
|
error_description: "Missing required client_id",
|
|
586
579
|
error: "invalid_grant"
|
|
587
580
|
});
|
|
588
|
-
if (!client_secret) throw new APIError("BAD_REQUEST", {
|
|
581
|
+
if (!client_secret && !preVerifiedClient) throw new APIError("BAD_REQUEST", {
|
|
589
582
|
error_description: "Missing a required client_secret",
|
|
590
583
|
error: "invalid_grant"
|
|
591
584
|
});
|
|
592
|
-
const client = await validateClientCredentials(ctx, opts, client_id, client_secret);
|
|
585
|
+
const client = await validateClientCredentials(ctx, opts, client_id, client_secret, void 0, preVerifiedClient);
|
|
593
586
|
let requestedScopes = scope?.split(" ");
|
|
594
587
|
if (requestedScopes) {
|
|
595
588
|
const validScopes = new Set(client.scopes ?? opts.scopes);
|
|
@@ -653,13 +646,8 @@ async function handleClientCredentialsGrant(ctx, opts) {
|
|
|
653
646
|
* To add scopes, you must restart the authorize process again.
|
|
654
647
|
*/
|
|
655
648
|
async function handleRefreshTokenGrant(ctx, opts) {
|
|
656
|
-
|
|
657
|
-
const
|
|
658
|
-
if (authorization?.startsWith("Basic ")) {
|
|
659
|
-
const res = basicToClientCredentials(authorization);
|
|
660
|
-
client_id = res?.client_id;
|
|
661
|
-
client_secret = res?.client_secret;
|
|
662
|
-
}
|
|
649
|
+
const { clientId: client_id, clientSecret: client_secret, preVerifiedClient } = destructureCredentials(await extractClientCredentials(ctx, opts, `${ctx.context.baseURL}/oauth2/token`));
|
|
650
|
+
const { refresh_token, scope } = ctx.body;
|
|
663
651
|
if (!client_id) throw new APIError("BAD_REQUEST", {
|
|
664
652
|
error_description: "Missing required client_id",
|
|
665
653
|
error: "invalid_grant"
|
|
@@ -713,7 +701,7 @@ async function handleRefreshTokenGrant(ctx, opts) {
|
|
|
713
701
|
error: "invalid_scope"
|
|
714
702
|
});
|
|
715
703
|
}
|
|
716
|
-
const client = await validateClientCredentials(ctx, opts, client_id, client_secret, requestedScopes ?? scopes);
|
|
704
|
+
const client = await validateClientCredentials(ctx, opts, client_id, client_secret, requestedScopes ?? scopes, preVerifiedClient);
|
|
717
705
|
const user = await ctx.context.internalAdapter.findUserById(refreshToken.userId);
|
|
718
706
|
if (!user) throw new APIError("BAD_REQUEST", {
|
|
719
707
|
error_description: "user not found",
|
|
@@ -931,14 +919,9 @@ async function resolveIntrospectionSub(opts, payload, client) {
|
|
|
931
919
|
return payload;
|
|
932
920
|
}
|
|
933
921
|
async function introspectEndpoint(ctx, opts) {
|
|
934
|
-
let {
|
|
935
|
-
const
|
|
936
|
-
if (
|
|
937
|
-
const res = basicToClientCredentials(authorization);
|
|
938
|
-
client_id = res?.client_id;
|
|
939
|
-
client_secret = res?.client_secret;
|
|
940
|
-
}
|
|
941
|
-
if (!client_id || !client_secret) throw new APIError$1("UNAUTHORIZED", {
|
|
922
|
+
let { token, token_type_hint } = ctx.body;
|
|
923
|
+
const { clientId: client_id, clientSecret: client_secret, preVerifiedClient } = destructureCredentials(await extractClientCredentials(ctx, opts, `${ctx.context.baseURL}/oauth2/introspect`));
|
|
924
|
+
if (!client_id || !client_secret && !preVerifiedClient) throw new APIError$1("UNAUTHORIZED", {
|
|
942
925
|
error_description: "missing required credentials",
|
|
943
926
|
error: "invalid_client"
|
|
944
927
|
});
|
|
@@ -947,7 +930,7 @@ async function introspectEndpoint(ctx, opts) {
|
|
|
947
930
|
error_description: "missing a required token for introspection",
|
|
948
931
|
error: "invalid_request"
|
|
949
932
|
});
|
|
950
|
-
const client = await validateClientCredentials(ctx, opts, client_id, client_secret);
|
|
933
|
+
const client = await validateClientCredentials(ctx, opts, client_id, client_secret, void 0, preVerifiedClient);
|
|
951
934
|
try {
|
|
952
935
|
if (token_type_hint === void 0 || token_type_hint === "access_token") try {
|
|
953
936
|
return resolveIntrospectionSub(opts, await validateAccessToken(ctx, opts, token, client.clientId), client);
|
|
@@ -1172,14 +1155,59 @@ async function checkOAuthClient(client, opts, settings) {
|
|
|
1172
1155
|
error: "invalid_client_metadata",
|
|
1173
1156
|
error_description: `pkce is required for registered clients.`
|
|
1174
1157
|
});
|
|
1158
|
+
if (client.token_endpoint_auth_method === "private_key_jwt") {
|
|
1159
|
+
if (client.jwks && client.jwks_uri) throw new APIError("BAD_REQUEST", {
|
|
1160
|
+
error: "invalid_client_metadata",
|
|
1161
|
+
error_description: "jwks and jwks_uri are mutually exclusive"
|
|
1162
|
+
});
|
|
1163
|
+
if (!client.jwks && !client.jwks_uri) throw new APIError("BAD_REQUEST", {
|
|
1164
|
+
error: "invalid_client_metadata",
|
|
1165
|
+
error_description: "private_key_jwt requires either jwks or jwks_uri"
|
|
1166
|
+
});
|
|
1167
|
+
if (client.jwks_uri) try {
|
|
1168
|
+
const uri = new URL(client.jwks_uri);
|
|
1169
|
+
if (uri.protocol !== "https:") throw new APIError("BAD_REQUEST", {
|
|
1170
|
+
error: "invalid_client_metadata",
|
|
1171
|
+
error_description: "jwks_uri must use HTTPS"
|
|
1172
|
+
});
|
|
1173
|
+
if (isPrivateHostname(uri.hostname)) throw new APIError("BAD_REQUEST", {
|
|
1174
|
+
error: "invalid_client_metadata",
|
|
1175
|
+
error_description: "jwks_uri must not point to a private or reserved address"
|
|
1176
|
+
});
|
|
1177
|
+
if (settings?.ctx && !settings.ctx.context.isTrustedOrigin(uri.href)) throw new APIError("BAD_REQUEST", {
|
|
1178
|
+
error: "invalid_client_metadata",
|
|
1179
|
+
error_description: "jwks_uri must belong to a trusted origin"
|
|
1180
|
+
});
|
|
1181
|
+
} catch (e) {
|
|
1182
|
+
if (e instanceof APIError) throw e;
|
|
1183
|
+
throw new APIError("BAD_REQUEST", {
|
|
1184
|
+
error: "invalid_client_metadata",
|
|
1185
|
+
error_description: "jwks_uri must be a valid URL"
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
1188
|
+
if (client.jwks) {
|
|
1189
|
+
const keys = Array.isArray(client.jwks) ? client.jwks : client.jwks.keys;
|
|
1190
|
+
if (!Array.isArray(keys) || keys.length === 0) throw new APIError("BAD_REQUEST", {
|
|
1191
|
+
error: "invalid_client_metadata",
|
|
1192
|
+
error_description: "jwks must be a non-empty array of JWK objects or a JWKS document {keys:[...]}"
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
} else if (client.jwks || client.jwks_uri) throw new APIError("BAD_REQUEST", {
|
|
1196
|
+
error: "invalid_client_metadata",
|
|
1197
|
+
error_description: "jwks and jwks_uri are only allowed with private_key_jwt authentication"
|
|
1198
|
+
});
|
|
1175
1199
|
}
|
|
1176
1200
|
async function createOAuthClientEndpoint(ctx, opts, settings) {
|
|
1177
1201
|
const body = ctx.body;
|
|
1178
1202
|
const session = await getSessionFromCtx(ctx);
|
|
1179
1203
|
const isPublic = body.token_endpoint_auth_method === "none";
|
|
1180
|
-
|
|
1204
|
+
const isPrivateKeyJwt = body.token_endpoint_auth_method === "private_key_jwt";
|
|
1205
|
+
await checkOAuthClient(ctx.body, opts, {
|
|
1206
|
+
...settings,
|
|
1207
|
+
ctx
|
|
1208
|
+
});
|
|
1181
1209
|
const clientId = opts.generateClientId?.() || generateRandomString(32, "a-z", "A-Z");
|
|
1182
|
-
const clientSecret = isPublic ? void 0 : opts.generateClientSecret?.() || generateRandomString(32, "a-z", "A-Z");
|
|
1210
|
+
const clientSecret = isPublic || isPrivateKeyJwt ? void 0 : opts.generateClientSecret?.() || generateRandomString(32, "a-z", "A-Z");
|
|
1183
1211
|
const storedClientSecret = clientSecret ? await storeClientSecret(ctx, opts, clientSecret) : void 0;
|
|
1184
1212
|
const iat = Math.floor(Date.now() / 1e3);
|
|
1185
1213
|
const referenceId = opts.clientReference ? await opts.clientReference({
|
|
@@ -1189,8 +1217,6 @@ async function createOAuthClientEndpoint(ctx, opts, settings) {
|
|
|
1189
1217
|
const schema = oauthToSchema({
|
|
1190
1218
|
...body ?? {},
|
|
1191
1219
|
disabled: void 0,
|
|
1192
|
-
jwks: void 0,
|
|
1193
|
-
jwks_uri: void 0,
|
|
1194
1220
|
client_secret_expires_at: storedClientSecret ? settings.isRegister && opts?.clientRegistrationClientSecretExpiration ? toExpJWT(opts.clientRegistrationClientSecretExpiration, iat) : 0 : void 0,
|
|
1195
1221
|
client_id: clientId,
|
|
1196
1222
|
client_secret: storedClientSecret,
|
|
@@ -1225,7 +1251,7 @@ async function createOAuthClientEndpoint(ctx, opts, settings) {
|
|
|
1225
1251
|
* @returns
|
|
1226
1252
|
*/
|
|
1227
1253
|
function oauthToSchema(input) {
|
|
1228
|
-
const { client_id: clientId, client_secret: clientSecret, client_secret_expires_at: _expiresAt, scope: _scope, user_id: userId, client_id_issued_at: _createdAt, client_name: name, client_uri: uri, logo_uri: icon, contacts, tos_uri: tos, policy_uri: policy, jwks:
|
|
1254
|
+
const { client_id: clientId, client_secret: clientSecret, client_secret_expires_at: _expiresAt, scope: _scope, user_id: userId, client_id_issued_at: _createdAt, client_name: name, client_uri: uri, logo_uri: icon, contacts, tos_uri: tos, policy_uri: policy, jwks: inputJwks, jwks_uri: jwksUri, software_id: softwareId, software_version: softwareVersion, software_statement: softwareStatement, redirect_uris: redirectUris, post_logout_redirect_uris: postLogoutRedirectUris, token_endpoint_auth_method: tokenEndpointAuthMethod, grant_types: grantTypes, response_types: responseTypes, public: _public, type, disabled, skip_consent: skipConsent, enable_end_session: enableEndSession, require_pkce: requirePKCE, subject_type: subjectType, reference_id: referenceId, metadata: inputMetadata, ...rest } = input;
|
|
1229
1255
|
const expiresAt = _expiresAt ? /* @__PURE__ */ new Date(_expiresAt * 1e3) : void 0;
|
|
1230
1256
|
const createdAt = _createdAt ? /* @__PURE__ */ new Date(_createdAt * 1e3) : void 0;
|
|
1231
1257
|
const scopes = _scope?.split(" ");
|
|
@@ -1233,6 +1259,7 @@ function oauthToSchema(input) {
|
|
|
1233
1259
|
...rest && Object.keys(rest).length ? rest : {},
|
|
1234
1260
|
...inputMetadata && typeof inputMetadata === "object" ? inputMetadata : {}
|
|
1235
1261
|
};
|
|
1262
|
+
const metadata = Object.keys(metadataObj).length ? JSON.stringify(metadataObj) : void 0;
|
|
1236
1263
|
return {
|
|
1237
1264
|
clientId,
|
|
1238
1265
|
clientSecret,
|
|
@@ -1255,6 +1282,8 @@ function oauthToSchema(input) {
|
|
|
1255
1282
|
tokenEndpointAuthMethod,
|
|
1256
1283
|
grantTypes,
|
|
1257
1284
|
responseTypes,
|
|
1285
|
+
jwks: inputJwks ? JSON.stringify({ keys: Array.isArray(inputJwks) ? inputJwks : inputJwks.keys }) : void 0,
|
|
1286
|
+
jwksUri,
|
|
1258
1287
|
public: _public,
|
|
1259
1288
|
type,
|
|
1260
1289
|
skipConsent,
|
|
@@ -1262,7 +1291,7 @@ function oauthToSchema(input) {
|
|
|
1262
1291
|
requirePKCE,
|
|
1263
1292
|
subjectType,
|
|
1264
1293
|
referenceId,
|
|
1265
|
-
metadata
|
|
1294
|
+
metadata
|
|
1266
1295
|
};
|
|
1267
1296
|
}
|
|
1268
1297
|
/**
|
|
@@ -1272,7 +1301,7 @@ function oauthToSchema(input) {
|
|
|
1272
1301
|
* @returns
|
|
1273
1302
|
*/
|
|
1274
1303
|
function schemaToOAuth(input) {
|
|
1275
|
-
const { clientId, clientSecret, disabled, scopes, userId, createdAt, updatedAt: _updatedAt, expiresAt, name, uri, icon, contacts, tos, policy, softwareId, softwareVersion, softwareStatement, redirectUris, postLogoutRedirectUris, tokenEndpointAuthMethod, grantTypes, responseTypes, public: _public, type, skipConsent, enableEndSession, requirePKCE, subjectType, referenceId, metadata } = input;
|
|
1304
|
+
const { clientId, clientSecret, disabled, scopes, userId, createdAt, updatedAt: _updatedAt, expiresAt, name, uri, icon, contacts, tos, policy, softwareId, softwareVersion, softwareStatement, redirectUris, postLogoutRedirectUris, tokenEndpointAuthMethod, grantTypes, responseTypes, public: _public, type, jwks, jwksUri, skipConsent, enableEndSession, requirePKCE, subjectType, referenceId, metadata } = input;
|
|
1276
1305
|
const _expiresAt = expiresAt ? Math.round(new Date(expiresAt).getTime() / 1e3) : void 0;
|
|
1277
1306
|
const _createdAt = createdAt ? Math.round(new Date(createdAt).getTime() / 1e3) : void 0;
|
|
1278
1307
|
const _scopes = scopes?.join(" ");
|
|
@@ -1290,6 +1319,8 @@ function schemaToOAuth(input) {
|
|
|
1290
1319
|
contacts: contacts ?? void 0,
|
|
1291
1320
|
tos_uri: tos ?? void 0,
|
|
1292
1321
|
policy_uri: policy ?? void 0,
|
|
1322
|
+
jwks: jwks ? JSON.parse(jwks).keys : void 0,
|
|
1323
|
+
jwks_uri: jwksUri ?? void 0,
|
|
1293
1324
|
software_id: softwareId ?? void 0,
|
|
1294
1325
|
software_version: softwareVersion ?? void 0,
|
|
1295
1326
|
software_statement: softwareStatement ?? void 0,
|
|
@@ -1506,7 +1537,13 @@ async function updateClientEndpoint(ctx, opts) {
|
|
|
1506
1537
|
await checkOAuthClient({
|
|
1507
1538
|
...schemaToOAuth(client),
|
|
1508
1539
|
...updates
|
|
1509
|
-
}, opts);
|
|
1540
|
+
}, opts, { ctx });
|
|
1541
|
+
const schemaUpdates = { ...oauthToSchema(updates) };
|
|
1542
|
+
if (updates.token_endpoint_auth_method) if (updates.token_endpoint_auth_method === "private_key_jwt") schemaUpdates.clientSecret = null;
|
|
1543
|
+
else {
|
|
1544
|
+
schemaUpdates.jwks = null;
|
|
1545
|
+
schemaUpdates.jwksUri = null;
|
|
1546
|
+
}
|
|
1510
1547
|
const updatedClient = await ctx.context.adapter.update({
|
|
1511
1548
|
model: "oauthClient",
|
|
1512
1549
|
where: [{
|
|
@@ -1514,7 +1551,7 @@ async function updateClientEndpoint(ctx, opts) {
|
|
|
1514
1551
|
value: clientId
|
|
1515
1552
|
}],
|
|
1516
1553
|
update: {
|
|
1517
|
-
...
|
|
1554
|
+
...schemaUpdates,
|
|
1518
1555
|
updatedAt: /* @__PURE__ */ new Date(Math.floor(Date.now() / 1e3) * 1e3)
|
|
1519
1556
|
}
|
|
1520
1557
|
});
|
|
@@ -1552,7 +1589,7 @@ async function rotateClientSecretEndpoint(ctx, opts) {
|
|
|
1552
1589
|
if (client.referenceId !== await opts.clientReference(session)) throw new APIError("UNAUTHORIZED");
|
|
1553
1590
|
} else throw new APIError("UNAUTHORIZED");
|
|
1554
1591
|
if (client.public || !client.clientSecret) throw new APIError("BAD_REQUEST", {
|
|
1555
|
-
error_description: "
|
|
1592
|
+
error_description: "secret rotation is only available for clients using client_secret authentication",
|
|
1556
1593
|
error: "invalid_client"
|
|
1557
1594
|
});
|
|
1558
1595
|
const clientSecret = opts.generateClientSecret?.() || generateRandomString(32, "a-z", "A-Z");
|
|
@@ -1597,8 +1634,11 @@ const adminCreateOAuthClient = (opts) => createAuthEndpoint("/admin/oauth2/creat
|
|
|
1597
1634
|
token_endpoint_auth_method: z.enum([
|
|
1598
1635
|
"none",
|
|
1599
1636
|
"client_secret_basic",
|
|
1600
|
-
"client_secret_post"
|
|
1637
|
+
"client_secret_post",
|
|
1638
|
+
"private_key_jwt"
|
|
1601
1639
|
]).default("client_secret_basic").optional(),
|
|
1640
|
+
jwks: z.union([z.array(z.record(z.string(), z.unknown())), z.object({ keys: z.array(z.record(z.string(), z.unknown())) })]).optional(),
|
|
1641
|
+
jwks_uri: z.string().optional(),
|
|
1602
1642
|
grant_types: z.array(z.enum([
|
|
1603
1643
|
"authorization_code",
|
|
1604
1644
|
"client_credentials",
|
|
@@ -1780,8 +1820,11 @@ const createOAuthClient = (opts) => createAuthEndpoint("/oauth2/create-client",
|
|
|
1780
1820
|
token_endpoint_auth_method: z.enum([
|
|
1781
1821
|
"none",
|
|
1782
1822
|
"client_secret_basic",
|
|
1783
|
-
"client_secret_post"
|
|
1823
|
+
"client_secret_post",
|
|
1824
|
+
"private_key_jwt"
|
|
1784
1825
|
]).default("client_secret_basic").optional(),
|
|
1826
|
+
jwks: z.union([z.array(z.record(z.string(), z.unknown())), z.object({ keys: z.array(z.record(z.string(), z.unknown())) })]).optional(),
|
|
1827
|
+
jwks_uri: z.string().optional(),
|
|
1785
1828
|
grant_types: z.array(z.enum([
|
|
1786
1829
|
"authorization_code",
|
|
1787
1830
|
"client_credentials",
|
|
@@ -2339,13 +2382,8 @@ async function revokeAccessToken(ctx, opts, clientId, token) {
|
|
|
2339
2382
|
});
|
|
2340
2383
|
}
|
|
2341
2384
|
async function revokeEndpoint(ctx, opts) {
|
|
2342
|
-
let {
|
|
2343
|
-
const
|
|
2344
|
-
if (authorization?.startsWith("Basic ")) {
|
|
2345
|
-
const res = basicToClientCredentials(authorization);
|
|
2346
|
-
client_id = res?.client_id;
|
|
2347
|
-
client_secret = res?.client_secret;
|
|
2348
|
-
}
|
|
2385
|
+
let { token, token_type_hint } = ctx.body;
|
|
2386
|
+
const { clientId: client_id, clientSecret: client_secret, preVerifiedClient } = destructureCredentials(await extractClientCredentials(ctx, opts, `${ctx.context.baseURL}/oauth2/revoke`));
|
|
2349
2387
|
if (!client_id) throw new APIError$1("UNAUTHORIZED", {
|
|
2350
2388
|
error_description: "missing required credentials",
|
|
2351
2389
|
error: "invalid_client"
|
|
@@ -2355,7 +2393,7 @@ async function revokeEndpoint(ctx, opts) {
|
|
|
2355
2393
|
error_description: "missing a required token for introspection",
|
|
2356
2394
|
error: "invalid_request"
|
|
2357
2395
|
});
|
|
2358
|
-
const client = await validateClientCredentials(ctx, opts, client_id, client_secret);
|
|
2396
|
+
const client = await validateClientCredentials(ctx, opts, client_id, client_secret, void 0, preVerifiedClient);
|
|
2359
2397
|
try {
|
|
2360
2398
|
if (token_type_hint === void 0 || token_type_hint === "access_token") try {
|
|
2361
2399
|
return await revokeAccessToken(ctx, opts, client.clientId, token);
|
|
@@ -2490,6 +2528,14 @@ const schema = {
|
|
|
2490
2528
|
type: "string",
|
|
2491
2529
|
required: false
|
|
2492
2530
|
},
|
|
2531
|
+
jwks: {
|
|
2532
|
+
type: "string",
|
|
2533
|
+
required: false
|
|
2534
|
+
},
|
|
2535
|
+
jwksUri: {
|
|
2536
|
+
type: "string",
|
|
2537
|
+
required: false
|
|
2538
|
+
},
|
|
2493
2539
|
grantTypes: {
|
|
2494
2540
|
type: "string[]",
|
|
2495
2541
|
required: false
|
|
@@ -2992,6 +3038,8 @@ const oauthProvider = (options) => {
|
|
|
2992
3038
|
]),
|
|
2993
3039
|
client_id: z.string().optional(),
|
|
2994
3040
|
client_secret: z.string().optional(),
|
|
3041
|
+
client_assertion: z.string().optional(),
|
|
3042
|
+
client_assertion_type: z.string().optional(),
|
|
2995
3043
|
code: z.string().optional(),
|
|
2996
3044
|
code_verifier: z.string().optional(),
|
|
2997
3045
|
redirect_uri: SafeUrlSchema.optional(),
|
|
@@ -3116,6 +3164,8 @@ const oauthProvider = (options) => {
|
|
|
3116
3164
|
body: z.object({
|
|
3117
3165
|
client_id: z.string().optional(),
|
|
3118
3166
|
client_secret: z.string().optional(),
|
|
3167
|
+
client_assertion: z.string().optional(),
|
|
3168
|
+
client_assertion_type: z.string().optional(),
|
|
3119
3169
|
token: z.string(),
|
|
3120
3170
|
token_type_hint: z.enum(["access_token", "refresh_token"]).optional()
|
|
3121
3171
|
}),
|
|
@@ -3234,6 +3284,8 @@ const oauthProvider = (options) => {
|
|
|
3234
3284
|
body: z.object({
|
|
3235
3285
|
client_id: z.string().optional(),
|
|
3236
3286
|
client_secret: z.string().optional(),
|
|
3287
|
+
client_assertion: z.string().optional(),
|
|
3288
|
+
client_assertion_type: z.string().optional(),
|
|
3237
3289
|
token: z.string(),
|
|
3238
3290
|
token_type_hint: z.enum(["access_token", "refresh_token"]).optional()
|
|
3239
3291
|
}),
|
|
@@ -3431,8 +3483,11 @@ const oauthProvider = (options) => {
|
|
|
3431
3483
|
token_endpoint_auth_method: z.enum([
|
|
3432
3484
|
"none",
|
|
3433
3485
|
"client_secret_basic",
|
|
3434
|
-
"client_secret_post"
|
|
3486
|
+
"client_secret_post",
|
|
3487
|
+
"private_key_jwt"
|
|
3435
3488
|
]).default("client_secret_basic").optional(),
|
|
3489
|
+
jwks: z.union([z.array(z.record(z.string(), z.unknown())), z.object({ keys: z.array(z.record(z.string(), z.unknown())) })]).optional(),
|
|
3490
|
+
jwks_uri: z.string().optional(),
|
|
3436
3491
|
grant_types: z.array(z.enum([
|
|
3437
3492
|
"authorization_code",
|
|
3438
3493
|
"client_credentials",
|
|
@@ -3537,7 +3592,8 @@ const oauthProvider = (options) => {
|
|
|
3537
3592
|
enum: [
|
|
3538
3593
|
"none",
|
|
3539
3594
|
"client_secret_basic",
|
|
3540
|
-
"client_secret_post"
|
|
3595
|
+
"client_secret_post",
|
|
3596
|
+
"private_key_jwt"
|
|
3541
3597
|
]
|
|
3542
3598
|
},
|
|
3543
3599
|
grant_types: {
|
|
@@ -3922,10 +3978,22 @@ function authServerMetadata(ctx, opts, overrides) {
|
|
|
3922
3978
|
token_endpoint_auth_methods_supported: [
|
|
3923
3979
|
...overrides?.public_client_supported ? ["none"] : [],
|
|
3924
3980
|
"client_secret_basic",
|
|
3925
|
-
"client_secret_post"
|
|
3981
|
+
"client_secret_post",
|
|
3982
|
+
"private_key_jwt"
|
|
3983
|
+
],
|
|
3984
|
+
token_endpoint_auth_signing_alg_values_supported: [...ASSERTION_SIGNING_ALGORITHMS],
|
|
3985
|
+
introspection_endpoint_auth_methods_supported: [
|
|
3986
|
+
"client_secret_basic",
|
|
3987
|
+
"client_secret_post",
|
|
3988
|
+
"private_key_jwt"
|
|
3989
|
+
],
|
|
3990
|
+
introspection_endpoint_auth_signing_alg_values_supported: [...ASSERTION_SIGNING_ALGORITHMS],
|
|
3991
|
+
revocation_endpoint_auth_methods_supported: [
|
|
3992
|
+
"client_secret_basic",
|
|
3993
|
+
"client_secret_post",
|
|
3994
|
+
"private_key_jwt"
|
|
3926
3995
|
],
|
|
3927
|
-
|
|
3928
|
-
revocation_endpoint_auth_methods_supported: ["client_secret_basic", "client_secret_post"],
|
|
3996
|
+
revocation_endpoint_auth_signing_alg_values_supported: [...ASSERTION_SIGNING_ALGORITHMS],
|
|
3929
3997
|
code_challenge_methods_supported: ["S256"],
|
|
3930
3998
|
authorization_response_iss_parameter_supported: true
|
|
3931
3999
|
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { isAPIError } from "better-auth/api";
|
|
2
|
+
import { verifyAccessToken } from "better-auth/oauth2";
|
|
3
|
+
import { APIError as APIError$1 } from "better-call";
|
|
4
|
+
//#region src/mcp.ts
|
|
5
|
+
/**
|
|
6
|
+
* A request middleware handler that checks and responds with
|
|
7
|
+
* a WWW-Authenticate header for unauthenticated responses.
|
|
8
|
+
*
|
|
9
|
+
* @external
|
|
10
|
+
*/
|
|
11
|
+
const mcpHandler = (verifyOptions, handler, opts) => {
|
|
12
|
+
return async (req) => {
|
|
13
|
+
const authorization = req.headers?.get("authorization") ?? void 0;
|
|
14
|
+
const accessToken = authorization?.startsWith("Bearer ") ? authorization.replace("Bearer ", "") : authorization;
|
|
15
|
+
try {
|
|
16
|
+
if (!accessToken?.length) throw new APIError$1("UNAUTHORIZED", { message: "missing authorization header" });
|
|
17
|
+
return handler(req, await verifyAccessToken(accessToken, verifyOptions));
|
|
18
|
+
} catch (error) {
|
|
19
|
+
try {
|
|
20
|
+
handleMcpErrors(error, verifyOptions.verifyOptions.audience, opts);
|
|
21
|
+
} catch (err) {
|
|
22
|
+
if (err instanceof APIError$1) return new Response(err.message, {
|
|
23
|
+
...err,
|
|
24
|
+
status: err.statusCode
|
|
25
|
+
});
|
|
26
|
+
throw new Error(String(err));
|
|
27
|
+
}
|
|
28
|
+
throw new Error(String(error));
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* The following handles all MCP errors and API errors
|
|
34
|
+
*
|
|
35
|
+
* @internal
|
|
36
|
+
*/
|
|
37
|
+
function handleMcpErrors(error, resource, opts) {
|
|
38
|
+
if (isAPIError(error) && error.status === "UNAUTHORIZED") {
|
|
39
|
+
const wwwAuthenticateValue = (Array.isArray(resource) ? resource : [resource]).map((v) => {
|
|
40
|
+
let audiencePath;
|
|
41
|
+
if (URL.canParse?.(v)) {
|
|
42
|
+
const url = new URL(v);
|
|
43
|
+
audiencePath = url.pathname.endsWith("/") ? url.pathname.slice(0, -1) : url.pathname;
|
|
44
|
+
return `Bearer resource_metadata="${url.origin}/.well-known/oauth-protected-resource${audiencePath}"`;
|
|
45
|
+
} else {
|
|
46
|
+
const resourceMetadata = opts?.resourceMetadataMappings?.[v];
|
|
47
|
+
if (!resourceMetadata) throw new APIError$1("INTERNAL_SERVER_ERROR", { message: `missing resource_metadata mapping for ${v}` });
|
|
48
|
+
return `Bearer resource_metadata=${resourceMetadata}`;
|
|
49
|
+
}
|
|
50
|
+
}).join(", ");
|
|
51
|
+
throw new APIError$1("UNAUTHORIZED", { message: error.message }, { "WWW-Authenticate": wwwAuthenticateValue });
|
|
52
|
+
} else if (error instanceof Error) throw error;
|
|
53
|
+
else throw new Error(error);
|
|
54
|
+
}
|
|
55
|
+
//#endregion
|
|
56
|
+
export { mcpHandler as n, handleMcpErrors as t };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AssertionSigningAlgorithm } from "@better-auth/core/oauth2";
|
|
1
2
|
import { JWSAlgorithms } from "better-auth/plugins";
|
|
2
3
|
import { JWTPayload } from "jose";
|
|
3
4
|
import { InferOptionSchema, Session, User } from "better-auth/types";
|
|
@@ -102,6 +103,14 @@ declare const schema: {
|
|
|
102
103
|
type: "string";
|
|
103
104
|
required: false;
|
|
104
105
|
};
|
|
106
|
+
jwks: {
|
|
107
|
+
type: "string";
|
|
108
|
+
required: false;
|
|
109
|
+
};
|
|
110
|
+
jwksUri: {
|
|
111
|
+
type: "string";
|
|
112
|
+
required: false;
|
|
113
|
+
};
|
|
105
114
|
grantTypes: {
|
|
106
115
|
type: "string[]";
|
|
107
116
|
required: false;
|
|
@@ -388,6 +397,13 @@ interface OAuthOptions<Scopes extends readonly Scope[] = InternallySupportedScop
|
|
|
388
397
|
* { "write:payments": "5m", "read:payments": "30m" }
|
|
389
398
|
*/
|
|
390
399
|
scopeExpirations?: { [K in Scopes[number]]?: number | string | Date };
|
|
400
|
+
/**
|
|
401
|
+
* Maximum lifetime in seconds for client assertion JWTs
|
|
402
|
+
* used with `private_key_jwt` authentication.
|
|
403
|
+
*
|
|
404
|
+
* @default 300 (5 minutes)
|
|
405
|
+
*/
|
|
406
|
+
assertionMaxLifetime?: number;
|
|
391
407
|
/**
|
|
392
408
|
* Allows /oauth2/public-client-prelogin endpoint to be
|
|
393
409
|
* requestable prior to login via a valid oauth_query.
|
|
@@ -1158,9 +1174,13 @@ interface SchemaClient<Scopes extends readonly Scope[] = InternallySupportedScop
|
|
|
1158
1174
|
* For example, `https://example.com/logout/callback`
|
|
1159
1175
|
*/
|
|
1160
1176
|
postLogoutRedirectUris?: string[];
|
|
1161
|
-
tokenEndpointAuthMethod?: "none" | "client_secret_basic" | "client_secret_post";
|
|
1177
|
+
tokenEndpointAuthMethod?: "none" | "client_secret_basic" | "client_secret_post" | "private_key_jwt";
|
|
1162
1178
|
grantTypes?: GrantType[];
|
|
1163
1179
|
responseTypes?: "code"[];
|
|
1180
|
+
/** Client's JSON Web Key Set for `private_key_jwt` authentication. Mutually exclusive with `jwksUri`. */
|
|
1181
|
+
jwks?: string;
|
|
1182
|
+
/** URI for the client's JSON Web Key Set. Mutually exclusive with `jwks`. Must be HTTPS. */
|
|
1183
|
+
jwksUri?: string;
|
|
1164
1184
|
/**
|
|
1165
1185
|
* Indicates whether the client is public or confidential.
|
|
1166
1186
|
* If public, refreshing tokens doesn't require
|
|
@@ -1297,7 +1317,7 @@ type OAuthConsent<Scopes extends readonly Scope[] = InternallySupportedScopes[]>
|
|
|
1297
1317
|
* Supported grant types of the token endpoint
|
|
1298
1318
|
*/
|
|
1299
1319
|
type GrantType = "authorization_code" | "client_credentials" | "refresh_token";
|
|
1300
|
-
type AuthMethod = "client_secret_basic" | "client_secret_post";
|
|
1320
|
+
type AuthMethod = "client_secret_basic" | "client_secret_post" | "private_key_jwt";
|
|
1301
1321
|
type TokenEndpointAuthMethod = AuthMethod | "none";
|
|
1302
1322
|
type BearerMethodsSupported = "header" | "body";
|
|
1303
1323
|
/**
|
|
@@ -1374,7 +1394,7 @@ interface AuthServerMetadata {
|
|
|
1374
1394
|
* token endpoint for the "private_key_jwt" and "client_secret_jwt"
|
|
1375
1395
|
* authentication methods (see field token_endpoint_auth_methods_supported).
|
|
1376
1396
|
*/
|
|
1377
|
-
token_endpoint_auth_signing_alg_values_supported?:
|
|
1397
|
+
token_endpoint_auth_signing_alg_values_supported?: AssertionSigningAlgorithm[];
|
|
1378
1398
|
/**
|
|
1379
1399
|
* URL of a page containing human-readable information
|
|
1380
1400
|
* that developers might want or need to know when using the
|
|
@@ -1420,7 +1440,7 @@ interface AuthServerMetadata {
|
|
|
1420
1440
|
* token endpoint for the "private_key_jwt" and "client_secret_jwt"
|
|
1421
1441
|
* authentication methods (see field revocation_endpoint_auth_methods_supported).
|
|
1422
1442
|
*/
|
|
1423
|
-
revocation_endpoint_auth_signing_alg_values_supported?:
|
|
1443
|
+
revocation_endpoint_auth_signing_alg_values_supported?: AssertionSigningAlgorithm[];
|
|
1424
1444
|
/**
|
|
1425
1445
|
* URL of the authorization server's OAuth 2.0
|
|
1426
1446
|
* introspection endpoint [RFC7662](https://datatracker.ietf.org/doc/html/rfc7662)
|
|
@@ -1441,7 +1461,7 @@ interface AuthServerMetadata {
|
|
|
1441
1461
|
* the "private_key_jwt" and "client_secret_jwt" authentication methods
|
|
1442
1462
|
* (see field introspection_endpoint_auth_methods_supported).
|
|
1443
1463
|
*/
|
|
1444
|
-
introspection_endpoint_auth_signing_alg_values_supported?:
|
|
1464
|
+
introspection_endpoint_auth_signing_alg_values_supported?: AssertionSigningAlgorithm[];
|
|
1445
1465
|
/**
|
|
1446
1466
|
* Supported code challenge methods.
|
|
1447
1467
|
*
|
|
@@ -1548,14 +1568,17 @@ interface OAuthClient {
|
|
|
1548
1568
|
contacts?: string[];
|
|
1549
1569
|
tos_uri?: string;
|
|
1550
1570
|
policy_uri?: string;
|
|
1551
|
-
|
|
1571
|
+
/** JWK Set — accepts either a bare key array or an RFC 7517 JWKS object `{"keys":[...]}` */
|
|
1572
|
+
jwks?: Record<string, unknown>[] | {
|
|
1573
|
+
keys: Record<string, unknown>[];
|
|
1574
|
+
};
|
|
1552
1575
|
jwks_uri?: string;
|
|
1553
1576
|
software_id?: string;
|
|
1554
1577
|
software_version?: string;
|
|
1555
1578
|
software_statement?: string;
|
|
1556
1579
|
redirect_uris: string[];
|
|
1557
1580
|
post_logout_redirect_uris?: string[];
|
|
1558
|
-
token_endpoint_auth_method?: "none" | "client_secret_basic" | "client_secret_post";
|
|
1581
|
+
token_endpoint_auth_method?: "none" | "client_secret_basic" | "client_secret_post" | "private_key_jwt";
|
|
1559
1582
|
grant_types?: GrantType[];
|
|
1560
1583
|
response_types?: "code"[];
|
|
1561
1584
|
public?: boolean;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { c as OAuthConsent, i as OIDCMetadata, m as Scope, r as OAuthClient, t as AuthServerMetadata, u as OAuthOptions } from "./oauth-
|
|
1
|
+
import { c as OAuthConsent, i as OIDCMetadata, m as Scope, r as OAuthClient, t as AuthServerMetadata, u as OAuthOptions } from "./oauth-C8aTlaAC.mjs";
|
|
2
2
|
import * as better_call0 from "better-call";
|
|
3
3
|
import * as z from "zod";
|
|
4
4
|
import * as better_auth_plugins0 from "better-auth/plugins";
|
|
@@ -298,6 +298,8 @@ declare const oauthProvider: <O extends OAuthOptions<Scope[]>>(options: O) => {
|
|
|
298
298
|
}>;
|
|
299
299
|
client_id: z.ZodOptional<z.ZodString>;
|
|
300
300
|
client_secret: z.ZodOptional<z.ZodString>;
|
|
301
|
+
client_assertion: z.ZodOptional<z.ZodString>;
|
|
302
|
+
client_assertion_type: z.ZodOptional<z.ZodString>;
|
|
301
303
|
code: z.ZodOptional<z.ZodString>;
|
|
302
304
|
code_verifier: z.ZodOptional<z.ZodString>;
|
|
303
305
|
redirect_uri: z.ZodOptional<z.ZodURL>;
|
|
@@ -436,6 +438,8 @@ declare const oauthProvider: <O extends OAuthOptions<Scope[]>>(options: O) => {
|
|
|
436
438
|
body: z.ZodObject<{
|
|
437
439
|
client_id: z.ZodOptional<z.ZodString>;
|
|
438
440
|
client_secret: z.ZodOptional<z.ZodString>;
|
|
441
|
+
client_assertion: z.ZodOptional<z.ZodString>;
|
|
442
|
+
client_assertion_type: z.ZodOptional<z.ZodString>;
|
|
439
443
|
token: z.ZodString;
|
|
440
444
|
token_type_hint: z.ZodOptional<z.ZodEnum<{
|
|
441
445
|
refresh_token: "refresh_token";
|
|
@@ -573,6 +577,8 @@ declare const oauthProvider: <O extends OAuthOptions<Scope[]>>(options: O) => {
|
|
|
573
577
|
body: z.ZodObject<{
|
|
574
578
|
client_id: z.ZodOptional<z.ZodString>;
|
|
575
579
|
client_secret: z.ZodOptional<z.ZodString>;
|
|
580
|
+
client_assertion: z.ZodOptional<z.ZodString>;
|
|
581
|
+
client_assertion_type: z.ZodOptional<z.ZodString>;
|
|
576
582
|
token: z.ZodString;
|
|
577
583
|
token_type_hint: z.ZodOptional<z.ZodEnum<{
|
|
578
584
|
refresh_token: "refresh_token";
|
|
@@ -831,7 +837,12 @@ declare const oauthProvider: <O extends OAuthOptions<Scope[]>>(options: O) => {
|
|
|
831
837
|
none: "none";
|
|
832
838
|
client_secret_basic: "client_secret_basic";
|
|
833
839
|
client_secret_post: "client_secret_post";
|
|
840
|
+
private_key_jwt: "private_key_jwt";
|
|
834
841
|
}>>>;
|
|
842
|
+
jwks: z.ZodOptional<z.ZodUnion<readonly [z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>>, z.ZodObject<{
|
|
843
|
+
keys: z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
844
|
+
}, z.core.$strip>]>>;
|
|
845
|
+
jwks_uri: z.ZodOptional<z.ZodString>;
|
|
835
846
|
grant_types: z.ZodOptional<z.ZodDefault<z.ZodArray<z.ZodEnum<{
|
|
836
847
|
authorization_code: "authorization_code";
|
|
837
848
|
client_credentials: "client_credentials";
|
|
@@ -1004,7 +1015,12 @@ declare const oauthProvider: <O extends OAuthOptions<Scope[]>>(options: O) => {
|
|
|
1004
1015
|
none: "none";
|
|
1005
1016
|
client_secret_basic: "client_secret_basic";
|
|
1006
1017
|
client_secret_post: "client_secret_post";
|
|
1018
|
+
private_key_jwt: "private_key_jwt";
|
|
1007
1019
|
}>>>;
|
|
1020
|
+
jwks: z.ZodOptional<z.ZodUnion<readonly [z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>>, z.ZodObject<{
|
|
1021
|
+
keys: z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
1022
|
+
}, z.core.$strip>]>>;
|
|
1023
|
+
jwks_uri: z.ZodOptional<z.ZodString>;
|
|
1008
1024
|
grant_types: z.ZodOptional<z.ZodDefault<z.ZodArray<z.ZodEnum<{
|
|
1009
1025
|
authorization_code: "authorization_code";
|
|
1010
1026
|
client_credentials: "client_credentials";
|
|
@@ -1208,7 +1224,12 @@ declare const oauthProvider: <O extends OAuthOptions<Scope[]>>(options: O) => {
|
|
|
1208
1224
|
none: "none";
|
|
1209
1225
|
client_secret_basic: "client_secret_basic";
|
|
1210
1226
|
client_secret_post: "client_secret_post";
|
|
1227
|
+
private_key_jwt: "private_key_jwt";
|
|
1211
1228
|
}>>>;
|
|
1229
|
+
jwks: z.ZodOptional<z.ZodUnion<readonly [z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>>, z.ZodObject<{
|
|
1230
|
+
keys: z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
1231
|
+
}, z.core.$strip>]>>;
|
|
1232
|
+
jwks_uri: z.ZodOptional<z.ZodString>;
|
|
1212
1233
|
grant_types: z.ZodOptional<z.ZodDefault<z.ZodArray<z.ZodEnum<{
|
|
1213
1234
|
authorization_code: "authorization_code";
|
|
1214
1235
|
client_credentials: "client_credentials";
|
|
@@ -1874,6 +1895,14 @@ declare const oauthProvider: <O extends OAuthOptions<Scope[]>>(options: O) => {
|
|
|
1874
1895
|
type: "string";
|
|
1875
1896
|
required: false;
|
|
1876
1897
|
};
|
|
1898
|
+
jwks: {
|
|
1899
|
+
type: "string";
|
|
1900
|
+
required: false;
|
|
1901
|
+
};
|
|
1902
|
+
jwksUri: {
|
|
1903
|
+
type: "string";
|
|
1904
|
+
required: false;
|
|
1905
|
+
};
|
|
1877
1906
|
grantTypes: {
|
|
1878
1907
|
type: "string[]";
|
|
1879
1908
|
required: false;
|
|
@@ -1,62 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { verifyAccessToken } from "better-auth/oauth2";
|
|
3
|
-
import { APIError as APIError$1 } from "better-call";
|
|
1
|
+
import { APIError } from "better-call";
|
|
4
2
|
import { constantTimeEqual, makeSignature, symmetricDecrypt, symmetricEncrypt } from "better-auth/crypto";
|
|
5
3
|
import { BetterAuthError } from "@better-auth/core/error";
|
|
6
4
|
import { base64, base64Url } from "@better-auth/utils/base64";
|
|
7
5
|
import { createHash } from "@better-auth/utils/hash";
|
|
8
|
-
//#region src/mcp.ts
|
|
9
|
-
/**
|
|
10
|
-
* A request middleware handler that checks and responds with
|
|
11
|
-
* a WWW-Authenticate header for unauthenticated responses.
|
|
12
|
-
*
|
|
13
|
-
* @external
|
|
14
|
-
*/
|
|
15
|
-
const mcpHandler = (verifyOptions, handler, opts) => {
|
|
16
|
-
return async (req) => {
|
|
17
|
-
const authorization = req.headers?.get("authorization") ?? void 0;
|
|
18
|
-
const accessToken = authorization?.startsWith("Bearer ") ? authorization.replace("Bearer ", "") : authorization;
|
|
19
|
-
try {
|
|
20
|
-
if (!accessToken?.length) throw new APIError$1("UNAUTHORIZED", { message: "missing authorization header" });
|
|
21
|
-
return handler(req, await verifyAccessToken(accessToken, verifyOptions));
|
|
22
|
-
} catch (error) {
|
|
23
|
-
try {
|
|
24
|
-
handleMcpErrors(error, verifyOptions.verifyOptions.audience, opts);
|
|
25
|
-
} catch (err) {
|
|
26
|
-
if (err instanceof APIError$1) return new Response(err.message, {
|
|
27
|
-
...err,
|
|
28
|
-
status: err.statusCode
|
|
29
|
-
});
|
|
30
|
-
throw new Error(String(err));
|
|
31
|
-
}
|
|
32
|
-
throw new Error(String(error));
|
|
33
|
-
}
|
|
34
|
-
};
|
|
35
|
-
};
|
|
36
|
-
/**
|
|
37
|
-
* The following handles all MCP errors and API errors
|
|
38
|
-
*
|
|
39
|
-
* @internal
|
|
40
|
-
*/
|
|
41
|
-
function handleMcpErrors(error, resource, opts) {
|
|
42
|
-
if (isAPIError(error) && error.status === "UNAUTHORIZED") {
|
|
43
|
-
const wwwAuthenticateValue = (Array.isArray(resource) ? resource : [resource]).map((v) => {
|
|
44
|
-
let audiencePath;
|
|
45
|
-
if (URL.canParse?.(v)) {
|
|
46
|
-
const url = new URL(v);
|
|
47
|
-
audiencePath = url.pathname.endsWith("/") ? url.pathname.slice(0, -1) : url.pathname;
|
|
48
|
-
return `Bearer resource_metadata="${url.origin}/.well-known/oauth-protected-resource${audiencePath}"`;
|
|
49
|
-
} else {
|
|
50
|
-
const resourceMetadata = opts?.resourceMetadataMappings?.[v];
|
|
51
|
-
if (!resourceMetadata) throw new APIError$1("INTERNAL_SERVER_ERROR", { message: `missing resource_metadata mapping for ${v}` });
|
|
52
|
-
return `Bearer resource_metadata=${resourceMetadata}`;
|
|
53
|
-
}
|
|
54
|
-
}).join(", ");
|
|
55
|
-
throw new APIError$1("UNAUTHORIZED", { message: error.message }, { "WWW-Authenticate": wwwAuthenticateValue });
|
|
56
|
-
} else if (error instanceof Error) throw error;
|
|
57
|
-
else throw new Error(error);
|
|
58
|
-
}
|
|
59
|
-
//#endregion
|
|
60
6
|
//#region src/utils/index.ts
|
|
61
7
|
var TTLCache = class {
|
|
62
8
|
cache = /* @__PURE__ */ new Map();
|
|
@@ -183,7 +129,7 @@ async function decryptStoredClientSecret(ctx, storageMethod, storedClientSecret)
|
|
|
183
129
|
async function verifyStoredClientSecret(ctx, opts, storedClientSecret, clientSecret) {
|
|
184
130
|
const storageMethod = opts.storeClientSecret ?? (opts.disableJwtPlugin ? "encrypted" : "hashed");
|
|
185
131
|
if (clientSecret && opts.prefix?.clientSecret) if (clientSecret.startsWith(opts.prefix?.clientSecret)) clientSecret = clientSecret.replace(opts.prefix.clientSecret, "");
|
|
186
|
-
else throw new APIError
|
|
132
|
+
else throw new APIError("UNAUTHORIZED", {
|
|
187
133
|
error_description: "invalid client_secret",
|
|
188
134
|
error: "invalid_client"
|
|
189
135
|
});
|
|
@@ -254,12 +200,12 @@ function basicToClientCredentials(authorization) {
|
|
|
254
200
|
if (authorization.startsWith("Basic ")) {
|
|
255
201
|
const encoded = authorization.replace("Basic ", "");
|
|
256
202
|
const decoded = new TextDecoder().decode(base64.decode(encoded));
|
|
257
|
-
if (!decoded.includes(":")) throw new APIError
|
|
203
|
+
if (!decoded.includes(":")) throw new APIError("BAD_REQUEST", {
|
|
258
204
|
error_description: "invalid authorization header format",
|
|
259
205
|
error: "invalid_client"
|
|
260
206
|
});
|
|
261
207
|
const [id, secret] = decoded.split(":", 2);
|
|
262
|
-
if (!id || !secret) throw new APIError
|
|
208
|
+
if (!id || !secret) throw new APIError("BAD_REQUEST", {
|
|
263
209
|
error_description: "invalid authorization header format",
|
|
264
210
|
error: "invalid_client"
|
|
265
211
|
});
|
|
@@ -275,31 +221,37 @@ function basicToClientCredentials(authorization) {
|
|
|
275
221
|
*
|
|
276
222
|
* @internal
|
|
277
223
|
*/
|
|
278
|
-
async function validateClientCredentials(ctx, options, clientId, clientSecret, scopes) {
|
|
279
|
-
const client = await getClient(ctx, options, clientId);
|
|
280
|
-
if (!client) throw new APIError
|
|
224
|
+
async function validateClientCredentials(ctx, options, clientId, clientSecret, scopes, preVerifiedClient) {
|
|
225
|
+
const client = preVerifiedClient ?? await getClient(ctx, options, clientId);
|
|
226
|
+
if (!client) throw new APIError("BAD_REQUEST", {
|
|
281
227
|
error_description: "missing client",
|
|
282
228
|
error: "invalid_client"
|
|
283
229
|
});
|
|
284
|
-
if (client.disabled) throw new APIError
|
|
230
|
+
if (client.disabled) throw new APIError("BAD_REQUEST", {
|
|
285
231
|
error_description: "client is disabled",
|
|
286
232
|
error: "invalid_client"
|
|
287
233
|
});
|
|
288
|
-
if (
|
|
289
|
-
error_description: "client
|
|
290
|
-
error: "invalid_client"
|
|
291
|
-
});
|
|
292
|
-
if (clientSecret && !client.clientSecret) throw new APIError$1("BAD_REQUEST", {
|
|
293
|
-
error_description: "public client, client secret should not be received",
|
|
294
|
-
error: "invalid_client"
|
|
295
|
-
});
|
|
296
|
-
if (clientSecret && !await verifyStoredClientSecret(ctx, options, client.clientSecret, clientSecret)) throw new APIError$1("UNAUTHORIZED", {
|
|
297
|
-
error_description: "invalid client_secret",
|
|
234
|
+
if (client.tokenEndpointAuthMethod === "private_key_jwt" && !preVerifiedClient) throw new APIError("BAD_REQUEST", {
|
|
235
|
+
error_description: "client registered for private_key_jwt must use client_assertion",
|
|
298
236
|
error: "invalid_client"
|
|
299
237
|
});
|
|
238
|
+
if (!preVerifiedClient) {
|
|
239
|
+
if (!client.public && !clientSecret) throw new APIError("BAD_REQUEST", {
|
|
240
|
+
error_description: "client secret must be provided",
|
|
241
|
+
error: "invalid_client"
|
|
242
|
+
});
|
|
243
|
+
if (clientSecret && !client.clientSecret) throw new APIError("BAD_REQUEST", {
|
|
244
|
+
error_description: "public client, client secret should not be received",
|
|
245
|
+
error: "invalid_client"
|
|
246
|
+
});
|
|
247
|
+
if (clientSecret && !await verifyStoredClientSecret(ctx, options, client.clientSecret, clientSecret)) throw new APIError("UNAUTHORIZED", {
|
|
248
|
+
error_description: "invalid client_secret",
|
|
249
|
+
error: "invalid_client"
|
|
250
|
+
});
|
|
251
|
+
}
|
|
300
252
|
if (scopes && client.scopes) {
|
|
301
253
|
const validScopes = new Set(client.scopes);
|
|
302
|
-
for (const sc of scopes) if (!validScopes.has(sc)) throw new APIError
|
|
254
|
+
for (const sc of scopes) if (!validScopes.has(sc)) throw new APIError("BAD_REQUEST", {
|
|
303
255
|
error_description: `client does not allow scope ${sc}`,
|
|
304
256
|
error: "invalid_scope"
|
|
305
257
|
});
|
|
@@ -316,6 +268,57 @@ function parseClientMetadata(metadata) {
|
|
|
316
268
|
if (!metadata) return void 0;
|
|
317
269
|
return typeof metadata === "string" ? JSON.parse(metadata) : metadata;
|
|
318
270
|
}
|
|
271
|
+
/** Unwraps ExtractedCredentials into the fields each grant handler needs. */
|
|
272
|
+
function destructureCredentials(credentials) {
|
|
273
|
+
return {
|
|
274
|
+
clientId: credentials?.clientId,
|
|
275
|
+
clientSecret: credentials?.method === "client_secret_basic" || credentials?.method === "client_secret_post" ? credentials.clientSecret : void 0,
|
|
276
|
+
preVerifiedClient: credentials?.method === "private_key_jwt" ? credentials.client : void 0
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Extracts and resolves client credentials from the request.
|
|
281
|
+
* Supports: client_secret_basic, client_secret_post, private_key_jwt, and none (public).
|
|
282
|
+
*/
|
|
283
|
+
async function extractClientCredentials(ctx, opts, expectedAudience) {
|
|
284
|
+
const body = ctx.body ?? {};
|
|
285
|
+
const authorization = ctx.request?.headers.get("authorization") ?? void 0;
|
|
286
|
+
if (body.client_assertion_type || body.client_assertion) {
|
|
287
|
+
if (!body.client_assertion || !body.client_assertion_type) throw new APIError("BAD_REQUEST", {
|
|
288
|
+
error_description: "client_assertion and client_assertion_type must both be provided",
|
|
289
|
+
error: "invalid_client"
|
|
290
|
+
});
|
|
291
|
+
if (body.client_secret || authorization?.startsWith("Basic ")) throw new APIError("BAD_REQUEST", {
|
|
292
|
+
error_description: "client_assertion cannot be combined with client_secret or Basic auth",
|
|
293
|
+
error: "invalid_client"
|
|
294
|
+
});
|
|
295
|
+
const { verifyClientAssertion: verify } = await import("./client-assertion-DZqo-L5j.mjs").then((n) => n.t);
|
|
296
|
+
const result = await verify(ctx, opts, body.client_assertion, body.client_assertion_type, body.client_id, expectedAudience);
|
|
297
|
+
return {
|
|
298
|
+
method: "private_key_jwt",
|
|
299
|
+
clientId: result.clientId,
|
|
300
|
+
client: result.client
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
if (authorization?.startsWith("Basic ")) {
|
|
304
|
+
const res = basicToClientCredentials(authorization);
|
|
305
|
+
if (res) return {
|
|
306
|
+
method: "client_secret_basic",
|
|
307
|
+
clientId: res.client_id,
|
|
308
|
+
clientSecret: res.client_secret
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
if (body.client_id && body.client_secret) return {
|
|
312
|
+
method: "client_secret_post",
|
|
313
|
+
clientId: body.client_id,
|
|
314
|
+
clientSecret: body.client_secret
|
|
315
|
+
};
|
|
316
|
+
if (body.client_id) return {
|
|
317
|
+
method: "none",
|
|
318
|
+
clientId: body.client_id
|
|
319
|
+
};
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
319
322
|
/**
|
|
320
323
|
* Parse space-separated prompt string into a set of prompts
|
|
321
324
|
*
|
|
@@ -411,4 +414,4 @@ function isPKCERequired(client, requestedScopes) {
|
|
|
411
414
|
return false;
|
|
412
415
|
}
|
|
413
416
|
//#endregion
|
|
414
|
-
export {
|
|
417
|
+
export { storeToken as _, getClient as a, getStoredToken as c, parseClientMetadata as d, parsePrompt as f, storeClientSecret as g, searchParamsToQuery as h, extractClientCredentials as i, isPKCERequired as l, resolveSubjectIdentifier as m, deleteFromPrompt as n, getJwtPlugin as o, resolveSessionAuthTime as p, destructureCredentials as r, getOAuthProviderPlugin as s, decryptStoredClientSecret as t, normalizeTimestampValue as u, validateClientCredentials as v, verifyOAuthQueryParams as y };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@better-auth/oauth-provider",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0-beta.0",
|
|
4
4
|
"description": "An oauth provider plugin for Better Auth",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -64,15 +64,15 @@
|
|
|
64
64
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
65
65
|
"listhen": "^1.9.0",
|
|
66
66
|
"tsdown": "0.21.1",
|
|
67
|
-
"@better-auth/core": "1.
|
|
68
|
-
"better-auth": "1.
|
|
67
|
+
"@better-auth/core": "1.7.0-beta.0",
|
|
68
|
+
"better-auth": "1.7.0-beta.0"
|
|
69
69
|
},
|
|
70
70
|
"peerDependencies": {
|
|
71
71
|
"@better-auth/utils": "0.4.0",
|
|
72
72
|
"@better-fetch/fetch": "1.1.21",
|
|
73
73
|
"better-call": "1.3.5",
|
|
74
|
-
"@better-auth/core": "^1.
|
|
75
|
-
"better-auth": "^1.
|
|
74
|
+
"@better-auth/core": "^1.7.0-beta.0",
|
|
75
|
+
"better-auth": "^1.7.0-beta.0"
|
|
76
76
|
},
|
|
77
77
|
"scripts": {
|
|
78
78
|
"build": "tsdown",
|