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