@better-auth/oauth-provider 1.7.0-beta.4 → 1.7.0-beta.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{client-assertion-DLMKVgoj.mjs → client-assertion-CctbJywV.mjs} +102 -87
- package/dist/client-resource.d.mts +31 -2
- package/dist/client-resource.mjs +45 -25
- package/dist/client.d.mts +1 -1
- package/dist/client.mjs +3 -13
- package/dist/index.d.mts +102 -17
- package/dist/index.mjs +1747 -1886
- package/dist/introspect-BXqKFUQZ.mjs +2115 -0
- package/dist/{oauth-Vt3lTNHX.d.mts → oauth-CAeemjD7.d.mts} +364 -175
- package/dist/{oauth-q7dn10NU.d.mts → oauth-CaXmZpoL.d.mts} +922 -33
- package/dist/resource-challenge-B-cqv4ur.mjs +63 -0
- package/dist/rolldown-runtime-wcPFST8Q.mjs +13 -0
- package/dist/signed-query-CFv2jNMT.mjs +44 -0
- package/dist/utils-Baq6atYN.mjs +764 -0
- package/dist/{version-nFnRm-a3.mjs → version-CUu3vBtU.mjs} +1 -1
- package/package.json +8 -9
- package/dist/mcp-CYnz-MXn.mjs +0 -56
- package/dist/utils-DKBWQ8fe.mjs +0 -492
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@better-auth/oauth-provider",
|
|
3
|
-
"version": "1.7.0-beta.
|
|
3
|
+
"version": "1.7.0-beta.6",
|
|
4
4
|
"description": "An oauth provider plugin for Better Auth",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -61,18 +61,17 @@
|
|
|
61
61
|
"zod": "^4.3.6"
|
|
62
62
|
},
|
|
63
63
|
"devDependencies": {
|
|
64
|
-
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
65
64
|
"listhen": "^1.9.0",
|
|
66
65
|
"tsdown": "0.21.1",
|
|
67
|
-
"@better-auth/core": "1.7.0-beta.
|
|
68
|
-
"better-auth": "1.7.0-beta.
|
|
66
|
+
"@better-auth/core": "1.7.0-beta.6",
|
|
67
|
+
"better-auth": "1.7.0-beta.6"
|
|
69
68
|
},
|
|
70
69
|
"peerDependencies": {
|
|
71
|
-
"@better-auth/utils": "0.4.
|
|
72
|
-
"@better-fetch/fetch": "1.1
|
|
73
|
-
"better-call": "1.3.
|
|
74
|
-
"@better-auth/core": "^1.7.0-beta.
|
|
75
|
-
"better-auth": "^1.7.0-beta.
|
|
70
|
+
"@better-auth/utils": "0.4.2",
|
|
71
|
+
"@better-fetch/fetch": "1.3.1",
|
|
72
|
+
"better-call": "1.3.6",
|
|
73
|
+
"@better-auth/core": "^1.7.0-beta.6",
|
|
74
|
+
"better-auth": "^1.7.0-beta.6"
|
|
76
75
|
},
|
|
77
76
|
"scripts": {
|
|
78
77
|
"build": "tsdown",
|
package/dist/mcp-CYnz-MXn.mjs
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
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 };
|
package/dist/utils-DKBWQ8fe.mjs
DELETED
|
@@ -1,492 +0,0 @@
|
|
|
1
|
-
import { APIError } from "better-call";
|
|
2
|
-
import { decodeBasicCredentials } from "@better-auth/core/oauth2";
|
|
3
|
-
import { constantTimeEqual, makeSignature, symmetricDecrypt, symmetricEncrypt } from "better-auth/crypto";
|
|
4
|
-
import { BetterAuthError } from "@better-auth/core/error";
|
|
5
|
-
import { base64Url } from "@better-auth/utils/base64";
|
|
6
|
-
import { createHash } from "@better-auth/utils/hash";
|
|
7
|
-
//#region src/utils/index.ts
|
|
8
|
-
var TTLCache = class {
|
|
9
|
-
cache = /* @__PURE__ */ new Map();
|
|
10
|
-
constructor() {}
|
|
11
|
-
set(key, value) {
|
|
12
|
-
this.cache.set(key, value);
|
|
13
|
-
}
|
|
14
|
-
get(key) {
|
|
15
|
-
const entry = this.cache.get(key);
|
|
16
|
-
if (!entry) return void 0;
|
|
17
|
-
if (entry.expiresAt && entry.expiresAt < /* @__PURE__ */ new Date()) {
|
|
18
|
-
this.cache.delete(key);
|
|
19
|
-
return;
|
|
20
|
-
}
|
|
21
|
-
return entry;
|
|
22
|
-
}
|
|
23
|
-
};
|
|
24
|
-
/**
|
|
25
|
-
* Gets the oAuth Provider Plugin
|
|
26
|
-
* @internal
|
|
27
|
-
*/
|
|
28
|
-
const getOAuthProviderPlugin = (ctx) => {
|
|
29
|
-
return ctx.getPlugin("oauth-provider");
|
|
30
|
-
};
|
|
31
|
-
/**
|
|
32
|
-
* Gets the JWT Plugin
|
|
33
|
-
* @internal
|
|
34
|
-
*/
|
|
35
|
-
const getJwtPlugin = (ctx) => {
|
|
36
|
-
const plugin = ctx.getPlugin("jwt");
|
|
37
|
-
if (!plugin) throw new BetterAuthError("jwt_config");
|
|
38
|
-
return plugin;
|
|
39
|
-
};
|
|
40
|
-
/**
|
|
41
|
-
* Normalizes timestamp-like values returned by adapters.
|
|
42
|
-
*
|
|
43
|
-
* Accepts Date instances, epoch milliseconds as numbers, and strings that are
|
|
44
|
-
* either ISO dates or numeric millisecond values such as "1774295570569.0".
|
|
45
|
-
*/
|
|
46
|
-
function normalizeTimestampValue(value) {
|
|
47
|
-
if (value == null) return;
|
|
48
|
-
if (value instanceof Date) return Number.isFinite(value.getTime()) ? value : void 0;
|
|
49
|
-
if (typeof value === "number") {
|
|
50
|
-
if (!Number.isFinite(value)) return;
|
|
51
|
-
const parsed = new Date(value);
|
|
52
|
-
return Number.isFinite(parsed.getTime()) ? parsed : void 0;
|
|
53
|
-
}
|
|
54
|
-
if (typeof value === "string") {
|
|
55
|
-
const trimmed = value.trim();
|
|
56
|
-
if (!trimmed.length) return;
|
|
57
|
-
const numeric = Number(trimmed);
|
|
58
|
-
if (Number.isFinite(numeric)) {
|
|
59
|
-
const parsed = new Date(numeric);
|
|
60
|
-
return Number.isFinite(parsed.getTime()) ? parsed : void 0;
|
|
61
|
-
}
|
|
62
|
-
const parsed = new Date(trimmed);
|
|
63
|
-
return Number.isFinite(parsed.getTime()) ? parsed : void 0;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
/**
|
|
67
|
-
* Resolves a session auth time from common adapter return shapes.
|
|
68
|
-
*/
|
|
69
|
-
function resolveSessionAuthTime(value) {
|
|
70
|
-
if (value instanceof Date) return normalizeTimestampValue(value);
|
|
71
|
-
if (!value || typeof value !== "object") return normalizeTimestampValue(value);
|
|
72
|
-
const direct = normalizeTimestampValue(value.createdAt) ?? normalizeTimestampValue(value.created_at);
|
|
73
|
-
if (direct) return direct;
|
|
74
|
-
const nested = value.session;
|
|
75
|
-
if (!nested || typeof nested !== "object") return;
|
|
76
|
-
return normalizeTimestampValue(nested.createdAt) ?? normalizeTimestampValue(nested.created_at);
|
|
77
|
-
}
|
|
78
|
-
/**
|
|
79
|
-
* Normalizes OAuth resource values into a non-empty string array.
|
|
80
|
-
*/
|
|
81
|
-
function toResourceList(value) {
|
|
82
|
-
if (typeof value === "string") return [value];
|
|
83
|
-
if (!value?.length) return void 0;
|
|
84
|
-
return value;
|
|
85
|
-
}
|
|
86
|
-
/**
|
|
87
|
-
* Normalizes audience values for JWT claims.
|
|
88
|
-
*/
|
|
89
|
-
function toAudienceClaim(audience) {
|
|
90
|
-
if (typeof audience === "string") return audience;
|
|
91
|
-
if (!audience?.length) return void 0;
|
|
92
|
-
return audience.length === 1 ? audience.at(0) : audience;
|
|
93
|
-
}
|
|
94
|
-
/**
|
|
95
|
-
* Checks the resource parameter, if provided,
|
|
96
|
-
* and returns either a valid audience or a tagged validation error.
|
|
97
|
-
*/
|
|
98
|
-
async function checkResource(ctx, opts, resource, scopes) {
|
|
99
|
-
const normalizedResource = toResourceList(resource);
|
|
100
|
-
const audience = normalizedResource ? [...normalizedResource] : void 0;
|
|
101
|
-
if (audience) {
|
|
102
|
-
const hasOpenId = scopes.includes("openid");
|
|
103
|
-
const baseUrl = ctx.context.baseURL;
|
|
104
|
-
const userInfoEndpoint = `${baseUrl}/oauth2/userinfo`;
|
|
105
|
-
if (hasOpenId && !audience.includes(userInfoEndpoint)) audience.push(userInfoEndpoint);
|
|
106
|
-
const filteredValidAudiences = opts.validAudiences?.filter((aud) => aud.length);
|
|
107
|
-
const validAudiences = new Set(filteredValidAudiences?.length ? filteredValidAudiences : [baseUrl]);
|
|
108
|
-
if (hasOpenId) validAudiences.add(userInfoEndpoint);
|
|
109
|
-
for (const aud of audience) if (!validAudiences.has(aud)) return {
|
|
110
|
-
success: false,
|
|
111
|
-
error: "invalid_resource"
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
return {
|
|
115
|
-
success: true,
|
|
116
|
-
audience: toAudienceClaim(audience)
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
const cachedTrustedClients = new TTLCache();
|
|
120
|
-
async function verifyOAuthQueryParams(oauth_query, secret) {
|
|
121
|
-
const queryParams = new URLSearchParams(oauth_query);
|
|
122
|
-
const sig = queryParams.get("sig");
|
|
123
|
-
const exp = Number(queryParams.get("exp"));
|
|
124
|
-
queryParams.delete("sig");
|
|
125
|
-
const verifySig = await makeSignature(queryParams.toString(), secret);
|
|
126
|
-
return !!sig && constantTimeEqual(sig, verifySig) && /* @__PURE__ */ new Date(exp * 1e3) >= /* @__PURE__ */ new Date();
|
|
127
|
-
}
|
|
128
|
-
/**
|
|
129
|
-
* Get a client by ID, checking trusted clients first, then database
|
|
130
|
-
*/
|
|
131
|
-
async function getClient(ctx, options, clientId) {
|
|
132
|
-
const trustedClient = cachedTrustedClients.get(clientId);
|
|
133
|
-
if (trustedClient) return Object.assign({}, trustedClient);
|
|
134
|
-
let dbClient = await ctx.context.adapter.findOne({
|
|
135
|
-
model: options.schema?.oauthClient?.modelName ?? "oauthClient",
|
|
136
|
-
where: [{
|
|
137
|
-
field: "clientId",
|
|
138
|
-
value: clientId
|
|
139
|
-
}]
|
|
140
|
-
});
|
|
141
|
-
const discoveries = toClientDiscoveryArray(options.clientDiscovery);
|
|
142
|
-
for (const discovery of discoveries) {
|
|
143
|
-
if (!discovery.matches(clientId)) continue;
|
|
144
|
-
const resolved = await discovery.resolve(ctx, clientId, dbClient);
|
|
145
|
-
if (resolved) {
|
|
146
|
-
dbClient = resolved;
|
|
147
|
-
break;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
if (dbClient && options.cachedTrustedClients?.has(clientId)) cachedTrustedClients.set(clientId, Object.assign({}, dbClient));
|
|
151
|
-
return dbClient;
|
|
152
|
-
}
|
|
153
|
-
/**
|
|
154
|
-
* Normalize the `clientDiscovery` option into an array. Accepts a single
|
|
155
|
-
* {@link ClientDiscovery}, an array of them, or `undefined`.
|
|
156
|
-
*
|
|
157
|
-
* @internal
|
|
158
|
-
*/
|
|
159
|
-
function toClientDiscoveryArray(discovery) {
|
|
160
|
-
if (!discovery) return [];
|
|
161
|
-
return Array.isArray(discovery) ? discovery : [discovery];
|
|
162
|
-
}
|
|
163
|
-
/**
|
|
164
|
-
* Merge `discoveryMetadata` from every configured {@link ClientDiscovery}
|
|
165
|
-
* into a single object. Entries are spread in order; later entries override
|
|
166
|
-
* earlier ones on key collisions.
|
|
167
|
-
*
|
|
168
|
-
* @internal
|
|
169
|
-
*/
|
|
170
|
-
function mergeDiscoveryMetadata(discovery) {
|
|
171
|
-
return toClientDiscoveryArray(discovery).reduce((acc, d) => ({
|
|
172
|
-
...acc,
|
|
173
|
-
...d.discoveryMetadata ?? {}
|
|
174
|
-
}), {});
|
|
175
|
-
}
|
|
176
|
-
/**
|
|
177
|
-
* Default client secret hasher using SHA-256
|
|
178
|
-
*
|
|
179
|
-
* @internal
|
|
180
|
-
*/
|
|
181
|
-
const defaultHasher = async (value) => {
|
|
182
|
-
const hash = await createHash("SHA-256").digest(new TextEncoder().encode(value));
|
|
183
|
-
return base64Url.encode(new Uint8Array(hash), { padding: false });
|
|
184
|
-
};
|
|
185
|
-
/**
|
|
186
|
-
* Decrypts a storedClientSecret for signing
|
|
187
|
-
*
|
|
188
|
-
* @internal
|
|
189
|
-
*/
|
|
190
|
-
async function decryptStoredClientSecret(ctx, storageMethod, storedClientSecret) {
|
|
191
|
-
if (storageMethod === "encrypted") return await symmetricDecrypt({
|
|
192
|
-
key: ctx.context.secretConfig,
|
|
193
|
-
data: storedClientSecret
|
|
194
|
-
});
|
|
195
|
-
else if (typeof storageMethod === "object" && "decrypt" in storageMethod) return await storageMethod.decrypt(storedClientSecret);
|
|
196
|
-
throw new BetterAuthError(`Unsupported decryption storageMethod type '${storageMethod}'`);
|
|
197
|
-
}
|
|
198
|
-
/**
|
|
199
|
-
* Verify stored client secret against provided client secret
|
|
200
|
-
*
|
|
201
|
-
* @internal
|
|
202
|
-
*/
|
|
203
|
-
async function verifyStoredClientSecret(ctx, opts, storedClientSecret, clientSecret) {
|
|
204
|
-
const storageMethod = opts.storeClientSecret ?? (opts.disableJwtPlugin ? "encrypted" : "hashed");
|
|
205
|
-
if (clientSecret && opts.prefix?.clientSecret) if (clientSecret.startsWith(opts.prefix?.clientSecret)) clientSecret = clientSecret.replace(opts.prefix.clientSecret, "");
|
|
206
|
-
else throw new APIError("UNAUTHORIZED", {
|
|
207
|
-
error_description: "invalid client_secret",
|
|
208
|
-
error: "invalid_client"
|
|
209
|
-
});
|
|
210
|
-
if (storageMethod === "hashed") {
|
|
211
|
-
const hashedClientSecret = clientSecret ? await defaultHasher(clientSecret) : void 0;
|
|
212
|
-
return !!hashedClientSecret && constantTimeEqual(hashedClientSecret, storedClientSecret);
|
|
213
|
-
} else if (typeof storageMethod === "object" && "hash" in storageMethod) if (storageMethod.verify) return !!clientSecret && await storageMethod.verify(clientSecret, storedClientSecret);
|
|
214
|
-
else {
|
|
215
|
-
const hashedClientSecret = clientSecret ? await storageMethod.hash(clientSecret) : void 0;
|
|
216
|
-
return !!hashedClientSecret && constantTimeEqual(hashedClientSecret, storedClientSecret);
|
|
217
|
-
}
|
|
218
|
-
else if (storageMethod === "encrypted") try {
|
|
219
|
-
const decryptedClientSecret = await decryptStoredClientSecret(ctx, storageMethod, storedClientSecret);
|
|
220
|
-
return !!clientSecret && constantTimeEqual(decryptedClientSecret, clientSecret);
|
|
221
|
-
} catch {
|
|
222
|
-
return false;
|
|
223
|
-
}
|
|
224
|
-
else if (typeof storageMethod === "object" && "decrypt" in storageMethod) {
|
|
225
|
-
const decryptedClientSecret = await decryptStoredClientSecret(ctx, storageMethod, storedClientSecret);
|
|
226
|
-
return !!clientSecret && constantTimeEqual(decryptedClientSecret, clientSecret);
|
|
227
|
-
}
|
|
228
|
-
throw new BetterAuthError(`Unsupported verify storageMethod type '${storageMethod}'`);
|
|
229
|
-
}
|
|
230
|
-
/**
|
|
231
|
-
* Store client secret according to the configured storage method
|
|
232
|
-
*
|
|
233
|
-
* @internal
|
|
234
|
-
*/
|
|
235
|
-
async function storeClientSecret(ctx, opts, clientSecret) {
|
|
236
|
-
const storageMethod = opts.storeClientSecret ?? (opts.disableJwtPlugin ? "encrypted" : "hashed");
|
|
237
|
-
if (storageMethod === "encrypted") return await symmetricEncrypt({
|
|
238
|
-
key: ctx.context.secretConfig,
|
|
239
|
-
data: clientSecret
|
|
240
|
-
});
|
|
241
|
-
else if (storageMethod === "hashed") return await defaultHasher(clientSecret);
|
|
242
|
-
else if (typeof storageMethod === "object" && "hash" in storageMethod) return await storageMethod.hash(clientSecret);
|
|
243
|
-
else if (typeof storageMethod === "object" && "encrypt" in storageMethod) return await storageMethod.encrypt(clientSecret);
|
|
244
|
-
throw new BetterAuthError(`Unsupported storeClientSecret type '${storageMethod}'`);
|
|
245
|
-
}
|
|
246
|
-
/**
|
|
247
|
-
* Stores a token value (ie opaque tokens, refresh tokens, transaction tokens, verification codes)
|
|
248
|
-
* on the database in a secure hashed format.
|
|
249
|
-
*
|
|
250
|
-
* @internal
|
|
251
|
-
*/
|
|
252
|
-
async function storeToken(storageMethod = "hashed", token, type) {
|
|
253
|
-
if (storageMethod === "hashed") return await defaultHasher(token);
|
|
254
|
-
else if (typeof storageMethod === "object" && "hash" in storageMethod) return await storageMethod.hash(token, type);
|
|
255
|
-
throw new BetterAuthError(`storeToken: unsupported storageMethod type '${storageMethod}'`);
|
|
256
|
-
}
|
|
257
|
-
/**
|
|
258
|
-
* Gets a hashed token value to find on the database.
|
|
259
|
-
*
|
|
260
|
-
* @internal
|
|
261
|
-
*/
|
|
262
|
-
async function getStoredToken(storageMethod = "hashed", token, type) {
|
|
263
|
-
if (storageMethod === "hashed") return await defaultHasher(token);
|
|
264
|
-
else if (typeof storageMethod === "object" && "hash" in storageMethod) return await storageMethod.hash(token, type);
|
|
265
|
-
throw new BetterAuthError(`getStoredToken: unsupported storageMethod type '${storageMethod}'`);
|
|
266
|
-
}
|
|
267
|
-
/**
|
|
268
|
-
* Converts a BASIC authorization header
|
|
269
|
-
* into its client_id and client_secret representation
|
|
270
|
-
*
|
|
271
|
-
* @internal
|
|
272
|
-
*/
|
|
273
|
-
const BASIC_SCHEME_PREFIX = /^Basic +/i;
|
|
274
|
-
function basicToClientCredentials(authorization) {
|
|
275
|
-
if (!BASIC_SCHEME_PREFIX.test(authorization)) return;
|
|
276
|
-
try {
|
|
277
|
-
const { clientId, clientSecret } = decodeBasicCredentials(authorization);
|
|
278
|
-
return {
|
|
279
|
-
client_id: clientId,
|
|
280
|
-
client_secret: clientSecret
|
|
281
|
-
};
|
|
282
|
-
} catch {
|
|
283
|
-
throw new APIError("BAD_REQUEST", {
|
|
284
|
-
error_description: "invalid authorization header format",
|
|
285
|
-
error: "invalid_client"
|
|
286
|
-
});
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
/**
|
|
290
|
-
* Validates client credentials failing on mismatches
|
|
291
|
-
* and incorrectly provided information
|
|
292
|
-
*
|
|
293
|
-
* @internal
|
|
294
|
-
*/
|
|
295
|
-
async function validateClientCredentials(ctx, options, clientId, clientSecret, scopes, preVerifiedClient) {
|
|
296
|
-
const client = preVerifiedClient ?? await getClient(ctx, options, clientId);
|
|
297
|
-
if (!client) throw new APIError("BAD_REQUEST", {
|
|
298
|
-
error_description: "missing client",
|
|
299
|
-
error: "invalid_client"
|
|
300
|
-
});
|
|
301
|
-
if (client.disabled) throw new APIError("BAD_REQUEST", {
|
|
302
|
-
error_description: "client is disabled",
|
|
303
|
-
error: "invalid_client"
|
|
304
|
-
});
|
|
305
|
-
if (client.tokenEndpointAuthMethod === "private_key_jwt" && !preVerifiedClient) throw new APIError("BAD_REQUEST", {
|
|
306
|
-
error_description: "client registered for private_key_jwt must use client_assertion",
|
|
307
|
-
error: "invalid_client"
|
|
308
|
-
});
|
|
309
|
-
if (!preVerifiedClient) {
|
|
310
|
-
if (!client.public && !clientSecret) throw new APIError("BAD_REQUEST", {
|
|
311
|
-
error_description: "client secret must be provided",
|
|
312
|
-
error: "invalid_client"
|
|
313
|
-
});
|
|
314
|
-
if (clientSecret && !client.clientSecret) throw new APIError("BAD_REQUEST", {
|
|
315
|
-
error_description: "public client, client secret should not be received",
|
|
316
|
-
error: "invalid_client"
|
|
317
|
-
});
|
|
318
|
-
if (clientSecret && !await verifyStoredClientSecret(ctx, options, client.clientSecret, clientSecret)) throw new APIError("UNAUTHORIZED", {
|
|
319
|
-
error_description: "invalid client_secret",
|
|
320
|
-
error: "invalid_client"
|
|
321
|
-
});
|
|
322
|
-
}
|
|
323
|
-
if (scopes && client.scopes) {
|
|
324
|
-
const validScopes = new Set(client.scopes);
|
|
325
|
-
for (const sc of scopes) if (!validScopes.has(sc)) throw new APIError("BAD_REQUEST", {
|
|
326
|
-
error_description: `client does not allow scope ${sc}`,
|
|
327
|
-
error: "invalid_scope"
|
|
328
|
-
});
|
|
329
|
-
}
|
|
330
|
-
return client;
|
|
331
|
-
}
|
|
332
|
-
/**
|
|
333
|
-
* Parse client metadata that may be stored as JSON string or already parsed object.
|
|
334
|
-
* Handles database adapters that auto-parse JSON columns.
|
|
335
|
-
*
|
|
336
|
-
* @internal
|
|
337
|
-
*/
|
|
338
|
-
function parseClientMetadata(metadata) {
|
|
339
|
-
if (!metadata) return void 0;
|
|
340
|
-
return typeof metadata === "string" ? JSON.parse(metadata) : metadata;
|
|
341
|
-
}
|
|
342
|
-
/** Unwraps ExtractedCredentials into the fields each grant handler needs. */
|
|
343
|
-
function destructureCredentials(credentials) {
|
|
344
|
-
return {
|
|
345
|
-
clientId: credentials?.clientId,
|
|
346
|
-
clientSecret: credentials?.method === "client_secret_basic" || credentials?.method === "client_secret_post" ? credentials.clientSecret : void 0,
|
|
347
|
-
preVerifiedClient: credentials?.method === "private_key_jwt" ? credentials.client : void 0
|
|
348
|
-
};
|
|
349
|
-
}
|
|
350
|
-
/**
|
|
351
|
-
* Extracts and resolves client credentials from the request.
|
|
352
|
-
* Supports: client_secret_basic, client_secret_post, private_key_jwt, and none (public).
|
|
353
|
-
*/
|
|
354
|
-
async function extractClientCredentials(ctx, opts, expectedAudience) {
|
|
355
|
-
const body = ctx.body ?? {};
|
|
356
|
-
const authorization = ctx.request?.headers.get("authorization") ?? void 0;
|
|
357
|
-
if (body.client_assertion_type || body.client_assertion) {
|
|
358
|
-
if (!body.client_assertion || !body.client_assertion_type) throw new APIError("BAD_REQUEST", {
|
|
359
|
-
error_description: "client_assertion and client_assertion_type must both be provided",
|
|
360
|
-
error: "invalid_client"
|
|
361
|
-
});
|
|
362
|
-
if (body.client_secret || authorization?.startsWith("Basic ")) throw new APIError("BAD_REQUEST", {
|
|
363
|
-
error_description: "client_assertion cannot be combined with client_secret or Basic auth",
|
|
364
|
-
error: "invalid_client"
|
|
365
|
-
});
|
|
366
|
-
const { verifyClientAssertion: verify } = await import("./client-assertion-DLMKVgoj.mjs").then((n) => n.t);
|
|
367
|
-
const result = await verify(ctx, opts, body.client_assertion, body.client_assertion_type, body.client_id, expectedAudience);
|
|
368
|
-
return {
|
|
369
|
-
method: "private_key_jwt",
|
|
370
|
-
clientId: result.clientId,
|
|
371
|
-
client: result.client
|
|
372
|
-
};
|
|
373
|
-
}
|
|
374
|
-
if (authorization?.startsWith("Basic ")) {
|
|
375
|
-
const res = basicToClientCredentials(authorization);
|
|
376
|
-
if (res) return {
|
|
377
|
-
method: "client_secret_basic",
|
|
378
|
-
clientId: res.client_id,
|
|
379
|
-
clientSecret: res.client_secret
|
|
380
|
-
};
|
|
381
|
-
}
|
|
382
|
-
if (body.client_id && body.client_secret) return {
|
|
383
|
-
method: "client_secret_post",
|
|
384
|
-
clientId: body.client_id,
|
|
385
|
-
clientSecret: body.client_secret
|
|
386
|
-
};
|
|
387
|
-
if (body.client_id) return {
|
|
388
|
-
method: "none",
|
|
389
|
-
clientId: body.client_id
|
|
390
|
-
};
|
|
391
|
-
return null;
|
|
392
|
-
}
|
|
393
|
-
/**
|
|
394
|
-
* Parse space-separated prompt string into a set of prompts
|
|
395
|
-
*
|
|
396
|
-
* @param prompt
|
|
397
|
-
*/
|
|
398
|
-
function parsePrompt(prompt) {
|
|
399
|
-
const prompts = prompt.split(" ").map((p) => p.trim());
|
|
400
|
-
const set = /* @__PURE__ */ new Set();
|
|
401
|
-
for (const p of prompts) if (p === "login" || p === "consent" || p === "create" || p === "select_account" || p === "none") set.add(p);
|
|
402
|
-
return new Set(set);
|
|
403
|
-
}
|
|
404
|
-
/**
|
|
405
|
-
* Extracts the sector identifier (hostname) from a client's first redirect URI.
|
|
406
|
-
*
|
|
407
|
-
* @see https://openid.net/specs/openid-connect-core-1_0.html#PairwiseAlg
|
|
408
|
-
* @internal
|
|
409
|
-
*/
|
|
410
|
-
function getSectorIdentifier(client) {
|
|
411
|
-
const uri = client.redirectUris?.[0];
|
|
412
|
-
if (!uri) throw new BetterAuthError("Client has no redirect URIs for sector identifier");
|
|
413
|
-
return new URL(uri).host;
|
|
414
|
-
}
|
|
415
|
-
/**
|
|
416
|
-
* Computes a pairwise subject identifier using HMAC-SHA256.
|
|
417
|
-
*
|
|
418
|
-
* @see https://openid.net/specs/openid-connect-core-1_0.html#PairwiseAlg
|
|
419
|
-
* @internal
|
|
420
|
-
*/
|
|
421
|
-
async function computePairwiseSub(userId, client, secret) {
|
|
422
|
-
return makeSignature(`${getSectorIdentifier(client)}.${userId}`, secret);
|
|
423
|
-
}
|
|
424
|
-
/**
|
|
425
|
-
* Returns the appropriate subject identifier for a user+client pair.
|
|
426
|
-
* Uses pairwise when the client opts in and the server has a secret configured.
|
|
427
|
-
*
|
|
428
|
-
* @internal
|
|
429
|
-
*/
|
|
430
|
-
async function resolveSubjectIdentifier(userId, client, opts) {
|
|
431
|
-
if (client.subjectType === "pairwise" && opts.pairwiseSecret) return computePairwiseSub(userId, client, opts.pairwiseSecret);
|
|
432
|
-
return userId;
|
|
433
|
-
}
|
|
434
|
-
/**
|
|
435
|
-
* Converts URLSearchParams to a plain object, preserving
|
|
436
|
-
* multi-valued keys as arrays instead of discarding duplicates.
|
|
437
|
-
*/
|
|
438
|
-
function searchParamsToQuery(params) {
|
|
439
|
-
const result = Object.create(null);
|
|
440
|
-
for (const key of new Set(params.keys())) {
|
|
441
|
-
const values = params.getAll(key);
|
|
442
|
-
result[key] = values.length === 1 ? values[0] : values;
|
|
443
|
-
}
|
|
444
|
-
return result;
|
|
445
|
-
}
|
|
446
|
-
const signedQueryIssuedAtParam = "ba_iat";
|
|
447
|
-
const postLoginClearedParam = "ba_pl";
|
|
448
|
-
function getSignedQueryIssuedAt(oauthQuery) {
|
|
449
|
-
const raw = new URLSearchParams(oauthQuery).get(signedQueryIssuedAtParam);
|
|
450
|
-
if (!raw) return null;
|
|
451
|
-
const issuedAt = Number(raw);
|
|
452
|
-
if (!Number.isFinite(issuedAt) || issuedAt <= 0) return null;
|
|
453
|
-
return new Date(issuedAt);
|
|
454
|
-
}
|
|
455
|
-
function removePromptFromQuery(query, prompt) {
|
|
456
|
-
const nextQuery = new URLSearchParams(query);
|
|
457
|
-
const prompts = nextQuery.get("prompt")?.split(" ");
|
|
458
|
-
const foundPrompt = prompts?.findIndex((v) => v === prompt) ?? -1;
|
|
459
|
-
if (foundPrompt >= 0) {
|
|
460
|
-
prompts?.splice(foundPrompt, 1);
|
|
461
|
-
prompts?.length ? nextQuery.set("prompt", prompts.join(" ")) : nextQuery.delete("prompt");
|
|
462
|
-
}
|
|
463
|
-
return nextQuery;
|
|
464
|
-
}
|
|
465
|
-
var PKCERequirementErrors = /* @__PURE__ */ function(PKCERequirementErrors) {
|
|
466
|
-
PKCERequirementErrors["PUBLIC_CLIENT"] = "pkce is required for public clients";
|
|
467
|
-
PKCERequirementErrors["OFFLINE_ACCESS_SCOPE"] = "pkce is required when requesting offline_access scope";
|
|
468
|
-
PKCERequirementErrors["CLIENT_REQUIRE_PKCE"] = "pkce is required for this client";
|
|
469
|
-
return PKCERequirementErrors;
|
|
470
|
-
}(PKCERequirementErrors || {});
|
|
471
|
-
/**
|
|
472
|
-
* Determines if PKCE is required for a given client and scope.
|
|
473
|
-
*
|
|
474
|
-
* PKCE is always required for:
|
|
475
|
-
* 1. Public clients (cannot securely store client_secret)
|
|
476
|
-
* 2. Requests with offline_access scope (refresh token security)
|
|
477
|
-
*
|
|
478
|
-
* For confidential clients without offline_access:
|
|
479
|
-
* - Uses client.requirePKCE if set (defaults to true)
|
|
480
|
-
*
|
|
481
|
-
* Returns false if PKCE is not required, or the reason it is required.
|
|
482
|
-
*
|
|
483
|
-
* @internal
|
|
484
|
-
*/
|
|
485
|
-
function isPKCERequired(client, requestedScopes) {
|
|
486
|
-
if (client.tokenEndpointAuthMethod === "none" || client.type === "native" || client.type === "user-agent-based" || client.public === true) return PKCERequirementErrors.PUBLIC_CLIENT;
|
|
487
|
-
if (requestedScopes?.includes("offline_access")) return PKCERequirementErrors.OFFLINE_ACCESS_SCOPE;
|
|
488
|
-
if (client.requirePKCE ?? true) return PKCERequirementErrors.CLIENT_REQUIRE_PKCE;
|
|
489
|
-
return false;
|
|
490
|
-
}
|
|
491
|
-
//#endregion
|
|
492
|
-
export { toAudienceClaim as C, verifyOAuthQueryParams as D, validateClientCredentials as E, storeToken as S, toResourceList as T, resolveSessionAuthTime as _, getClient as a, signedQueryIssuedAtParam as b, getSignedQueryIssuedAt as c, mergeDiscoveryMetadata as d, normalizeTimestampValue as f, removePromptFromQuery as g, postLoginClearedParam as h, extractClientCredentials as i, getStoredToken as l, parsePrompt as m, decryptStoredClientSecret as n, getJwtPlugin as o, parseClientMetadata as p, destructureCredentials as r, getOAuthProviderPlugin as s, checkResource as t, isPKCERequired as u, resolveSubjectIdentifier as v, toClientDiscoveryArray as w, storeClientSecret as x, searchParamsToQuery as y };
|