@better-auth/oauth-provider 1.7.6 → 1.7.8
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/{authorize-CLuqtSXQ.mjs → authorize-C95F-9E1.mjs} +126 -61
- package/dist/client-resource.d.mts +1 -1
- package/dist/client-resource.mjs +2 -2
- package/dist/client.d.mts +1 -1
- package/dist/client.mjs +1 -1
- package/dist/index.d.mts +11 -4
- package/dist/index.mjs +4 -4
- package/dist/internal.d.mts +1 -1
- package/dist/internal.mjs +1 -1
- package/dist/{introspect-CbYi2MJT.mjs → introspect-H3uoUiuS.mjs} +257 -264
- package/dist/{oauth-1Ud-hvZY.d.mts → oauth-C_RnL2nt.d.mts} +84 -1
- package/dist/{oauth-CJ85_W0W.d.mts → oauth-pKAoqtlo.d.mts} +3 -1
- package/dist/{utils-CoqjzHuU.mjs → utils-CGIZS-Da.mjs} +6 -1
- package/dist/{version-9KCSx144.mjs → version-BPS0os17.mjs} +1 -1
- package/package.json +8 -8
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-wcPFST8Q.mjs";
|
|
2
|
-
import { A as collectExtensionAccessTokenClaims, C as storeToken, E as validateClientCredentials, F as getExtensionGrantHandler, L as getSupportedGrantTypes, M as collectExtensionUserInfoClaims, R as hasUserInfoClaimExtension, T as toResourceList, a as getClient, b as resolveSubjectIdentifier, c as getStoredToken, h as parseClientMetadata, i as extractClientCredentials, j as collectExtensionIdTokenClaims, l as isPKCERequired, n as decryptStoredClientSecret, o as getJwtPlugin, p as normalizeTimestampValue, r as destructureCredentials, t as clientAllowsGrant, w as toAudienceClaim, y as resolveSessionAuthTime } from "./utils-
|
|
2
|
+
import { A as collectExtensionAccessTokenClaims, C as storeToken, E as validateClientCredentials, F as getExtensionGrantHandler, L as getSupportedGrantTypes, M as collectExtensionUserInfoClaims, R as hasUserInfoClaimExtension, T as toResourceList, a as getClient, b as resolveSubjectIdentifier, c as getStoredToken, h as parseClientMetadata, i as extractClientCredentials, j as collectExtensionIdTokenClaims, l as isPKCERequired, n as decryptStoredClientSecret, o as getJwtPlugin, p as normalizeTimestampValue, r as destructureCredentials, t as clientAllowsGrant, w as toAudienceClaim, y as resolveSessionAuthTime } from "./utils-CGIZS-Da.mjs";
|
|
3
3
|
import { APIError } from "better-auth/api";
|
|
4
4
|
import { generateRandomString, symmetricDecrypt, symmetricEncrypt } from "better-auth/crypto";
|
|
5
5
|
import { APIError as APIError$1 } from "better-call";
|
|
@@ -7,8 +7,8 @@ import { logger } from "@better-auth/core/env";
|
|
|
7
7
|
import * as z from "zod";
|
|
8
8
|
import { createDpopReplayStore, enforceDpopBinding, generateCodeChallenge, getConfirmationJkt, getDpopJktFromPayload, getJwks, isDpopBindingError, isDpopProofError, parseAccessTokenAuthorization, stripAccessTokenAuthorizationScheme, verifyDpopProof } from "better-auth/oauth2";
|
|
9
9
|
import { SignJWT, base64url, createLocalJWKSet, decodeProtectedHeader, jwtVerify } from "jose";
|
|
10
|
-
import { resolveSigningKey, signJWT, toExpJWT } from "better-auth/plugins";
|
|
11
10
|
import { SafeUrlSchema } from "@better-auth/core/utils/redirect-uri";
|
|
11
|
+
import { resolveSigningKey, signJWT, toExpJWT } from "better-auth/plugins";
|
|
12
12
|
//#region src/authentication-context.ts
|
|
13
13
|
const RESERVED_ID_TOKEN_CLAIMS = new Set([
|
|
14
14
|
"iss",
|
|
@@ -265,6 +265,187 @@ async function resolveAccessTokenClaims(input) {
|
|
|
265
265
|
});
|
|
266
266
|
}
|
|
267
267
|
//#endregion
|
|
268
|
+
//#region src/types/zod.ts
|
|
269
|
+
const DANGEROUS_SCHEMES = [
|
|
270
|
+
"javascript:",
|
|
271
|
+
"data:",
|
|
272
|
+
"vbscript:"
|
|
273
|
+
];
|
|
274
|
+
/**
|
|
275
|
+
* Validates an RFC 8707 resource indicator. The value must be an absolute URI
|
|
276
|
+
* with no fragment (RFC 8707 §2). Unlike a redirect URI it is not restricted to
|
|
277
|
+
* HTTPS, because a resource server identifier may use any absolute URI scheme;
|
|
278
|
+
* configured OAuth resources are the authoritative control over
|
|
279
|
+
* which resources a token may target.
|
|
280
|
+
*/
|
|
281
|
+
const ResourceUriSchema = z.string().superRefine((val, ctx) => {
|
|
282
|
+
if (!URL.canParse(val)) {
|
|
283
|
+
ctx.addIssue({
|
|
284
|
+
code: "custom",
|
|
285
|
+
message: "resource must be an absolute URI",
|
|
286
|
+
fatal: true
|
|
287
|
+
});
|
|
288
|
+
return z.NEVER;
|
|
289
|
+
}
|
|
290
|
+
if (val.includes("#")) {
|
|
291
|
+
ctx.addIssue({
|
|
292
|
+
code: "custom",
|
|
293
|
+
message: "resource must not contain a fragment"
|
|
294
|
+
});
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
if (DANGEROUS_SCHEMES.includes(new URL(val).protocol)) ctx.addIssue({
|
|
298
|
+
code: "custom",
|
|
299
|
+
message: "resource cannot use javascript:, data:, or vbscript: scheme"
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
const authorizationPromptTokenSchema = z.enum([
|
|
303
|
+
"none",
|
|
304
|
+
"consent",
|
|
305
|
+
"login",
|
|
306
|
+
"create",
|
|
307
|
+
"select_account"
|
|
308
|
+
]);
|
|
309
|
+
const authorizationPromptSchema = z.string().superRefine((value, ctx) => {
|
|
310
|
+
const promptTokens = value.split(" ").map((token) => token.trim()).filter(Boolean);
|
|
311
|
+
const promptSet = /* @__PURE__ */ new Set();
|
|
312
|
+
if (!promptTokens.length) {
|
|
313
|
+
ctx.addIssue({
|
|
314
|
+
code: "custom",
|
|
315
|
+
message: "prompt must include at least one value"
|
|
316
|
+
});
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
for (const token of promptTokens) {
|
|
320
|
+
const result = authorizationPromptTokenSchema.safeParse(token);
|
|
321
|
+
if (!result.success) {
|
|
322
|
+
ctx.addIssue({
|
|
323
|
+
code: "custom",
|
|
324
|
+
message: `unsupported prompt value: ${token}`
|
|
325
|
+
});
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
promptSet.add(result.data);
|
|
329
|
+
}
|
|
330
|
+
if (promptSet.has("none") && promptSet.size > 1) ctx.addIssue({
|
|
331
|
+
code: "custom",
|
|
332
|
+
message: "prompt=none cannot be combined with other prompt values"
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
/** Canonical RFC 7517 JWK Set metadata for an OAuth client. */
|
|
336
|
+
const clientJwksSchema = z.object({ keys: z.array(z.record(z.string(), z.unknown())).min(1) });
|
|
337
|
+
const maxAgeSchema = z.union([z.number(), z.string().trim().min(1)]).transform((value, ctx) => {
|
|
338
|
+
const maxAge = typeof value === "number" ? value : Number(value);
|
|
339
|
+
if (!Number.isInteger(maxAge) || maxAge < 0) {
|
|
340
|
+
ctx.addIssue({
|
|
341
|
+
code: "custom",
|
|
342
|
+
message: "max_age must be a non-negative integer"
|
|
343
|
+
});
|
|
344
|
+
return z.NEVER;
|
|
345
|
+
}
|
|
346
|
+
return maxAge;
|
|
347
|
+
});
|
|
348
|
+
const dpopJktSchema = z.string().regex(/^[A-Za-z0-9_-]{43}$/, "dpop_jkt must be a base64url-encoded SHA-256 JWK thumbprint");
|
|
349
|
+
/**
|
|
350
|
+
* Runtime schema for OAuthAuthorizationQuery.
|
|
351
|
+
* Uses passthrough to tolerate fields added by future extensions (PAR, FPA, etc.)
|
|
352
|
+
*/
|
|
353
|
+
const authorizationQuerySchema = z.object({
|
|
354
|
+
response_type: z.string().pipe(z.enum(["code"])).optional(),
|
|
355
|
+
request: z.string().optional(),
|
|
356
|
+
request_uri: z.string().optional(),
|
|
357
|
+
redirect_uri: SafeUrlSchema.optional(),
|
|
358
|
+
scope: z.string().optional(),
|
|
359
|
+
state: z.string().optional(),
|
|
360
|
+
client_id: z.string().min(1, "client_id is required"),
|
|
361
|
+
prompt: authorizationPromptSchema.optional(),
|
|
362
|
+
display: z.string().optional(),
|
|
363
|
+
ui_locales: z.string().optional(),
|
|
364
|
+
max_age: maxAgeSchema.optional(),
|
|
365
|
+
acr_values: z.string().optional(),
|
|
366
|
+
login_hint: z.string().optional(),
|
|
367
|
+
id_token_hint: z.string().optional(),
|
|
368
|
+
code_challenge: z.string().optional(),
|
|
369
|
+
code_challenge_method: z.string().pipe(z.enum(["S256"])).optional(),
|
|
370
|
+
nonce: z.string().optional(),
|
|
371
|
+
claims: claimsRequestInputSchema.optional(),
|
|
372
|
+
dpop_jkt: dpopJktSchema.optional(),
|
|
373
|
+
resource: z.union([ResourceUriSchema, z.array(ResourceUriSchema).min(1)]).optional()
|
|
374
|
+
}).passthrough();
|
|
375
|
+
const storedAuthorizationQuerySchema = authorizationQuerySchema.extend({
|
|
376
|
+
redirect_uri: SafeUrlSchema.optional(),
|
|
377
|
+
claims: claimsRequestParameterSchema.optional()
|
|
378
|
+
});
|
|
379
|
+
/**
|
|
380
|
+
* Runtime schema for the authorization code verification value.
|
|
381
|
+
* Validates structure on deserialization from the JSON blob stored in the DB.
|
|
382
|
+
* Uses passthrough so future fields (e.g. from authorization challenge) don't break parsing.
|
|
383
|
+
*/
|
|
384
|
+
const verificationValueSchema = z.object({
|
|
385
|
+
type: z.literal("authorization_code"),
|
|
386
|
+
query: storedAuthorizationQuerySchema,
|
|
387
|
+
sessionId: z.string(),
|
|
388
|
+
userId: z.string(),
|
|
389
|
+
referenceId: z.string().optional(),
|
|
390
|
+
authTime: z.number().optional(),
|
|
391
|
+
resource: z.array(z.string()).optional()
|
|
392
|
+
}).passthrough();
|
|
393
|
+
/**
|
|
394
|
+
* Request body accepted at `POST /oauth2/register` (RFC 7591 §2 client
|
|
395
|
+
* metadata). This is the single source of truth for the registration contract:
|
|
396
|
+
* the endpoint validates against it and {@link ClientRegistrationRequest} is
|
|
397
|
+
* inferred from it, so the type a `validateInitialAccessToken` callback receives
|
|
398
|
+
* always matches what is actually validated. `grant_types` and
|
|
399
|
+
* `token_endpoint_auth_method` are open strings because extensions can register
|
|
400
|
+
* custom values. Server-assigned fields (`client_id`, `client_secret`, the
|
|
401
|
+
* issued/expiry timestamps) and internal state (`disabled`, `reference_id`) are
|
|
402
|
+
* never part of a registration request.
|
|
403
|
+
*
|
|
404
|
+
* @see https://datatracker.ietf.org/doc/html/rfc7591#section-2
|
|
405
|
+
*/
|
|
406
|
+
const clientRegistrationRequestSchema = z.object({
|
|
407
|
+
redirect_uris: z.array(SafeUrlSchema).min(1).optional(),
|
|
408
|
+
scope: z.string().optional(),
|
|
409
|
+
client_name: z.string().refine((value) => value.trim().length > 0, "client_name cannot be empty").optional(),
|
|
410
|
+
client_uri: z.string().optional(),
|
|
411
|
+
logo_uri: z.string().optional(),
|
|
412
|
+
contacts: z.array(z.string().min(1)).min(1).optional(),
|
|
413
|
+
tos_uri: z.string().optional(),
|
|
414
|
+
policy_uri: z.string().optional(),
|
|
415
|
+
software_id: z.string().optional(),
|
|
416
|
+
software_version: z.string().optional(),
|
|
417
|
+
software_statement: z.string().optional(),
|
|
418
|
+
post_logout_redirect_uris: z.array(SafeUrlSchema).min(1).optional(),
|
|
419
|
+
backchannel_logout_uri: SafeUrlSchema.optional(),
|
|
420
|
+
backchannel_logout_session_required: z.boolean().optional(),
|
|
421
|
+
token_endpoint_auth_method: z.string().trim().min(1).optional(),
|
|
422
|
+
jwks: clientJwksSchema.optional(),
|
|
423
|
+
jwks_uri: z.string().optional(),
|
|
424
|
+
grant_types: z.array(z.string().trim().min(1)).min(1).optional(),
|
|
425
|
+
response_types: z.array(z.enum(["code"])).optional(),
|
|
426
|
+
application_type: z.enum(["web", "native"]).optional(),
|
|
427
|
+
subject_type: z.enum(["public", "pairwise"]).optional(),
|
|
428
|
+
dpop_bound_access_tokens: z.boolean().optional(),
|
|
429
|
+
resources: z.array(ResourceUriSchema).optional(),
|
|
430
|
+
skip_consent: z.never({ error: "skip_consent cannot be set during dynamic client registration" }).optional()
|
|
431
|
+
});
|
|
432
|
+
/**
|
|
433
|
+
* Complete OAuth client metadata document schema shared by registration and
|
|
434
|
+
* verified client-discovery integrations.
|
|
435
|
+
*
|
|
436
|
+
* Unlike {@link clientRegistrationRequestSchema}, a metadata document carries
|
|
437
|
+
* its own identifier and may contain the server-assigned secret fields that a
|
|
438
|
+
* discovery profile must explicitly prohibit. Keeping those fields in the
|
|
439
|
+
* parsed shape lets profile validation distinguish a forbidden value from an
|
|
440
|
+
* unknown extension without unsafe casts or ad-hoc allowlists.
|
|
441
|
+
*/
|
|
442
|
+
const oauthClientMetadataSchema = clientRegistrationRequestSchema.omit({ skip_consent: true }).extend({
|
|
443
|
+
client_id: z.string().refine((value) => value.trim().length > 0, "client_id cannot be empty"),
|
|
444
|
+
client_secret: z.string().optional(),
|
|
445
|
+
client_secret_expires_at: z.number().int().nonnegative().optional(),
|
|
446
|
+
client_id_issued_at: z.number().int().nonnegative().optional()
|
|
447
|
+
}).loose();
|
|
448
|
+
//#endregion
|
|
268
449
|
//#region src/resources.ts
|
|
269
450
|
/**
|
|
270
451
|
* Source-of-truth list of asymmetric JWS algorithms supported by the JWT
|
|
@@ -317,18 +498,10 @@ async function checkIdentifier(opts, identifier) {
|
|
|
317
498
|
};
|
|
318
499
|
return { ok: true };
|
|
319
500
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
url = new URL(identifier);
|
|
323
|
-
} catch {
|
|
324
|
-
return {
|
|
325
|
-
ok: false,
|
|
326
|
-
reason: `resource identifier ${identifier} must be an absolute URI (RFC 8707 §2)`
|
|
327
|
-
};
|
|
328
|
-
}
|
|
329
|
-
if (url.hash) return {
|
|
501
|
+
const parsed = ResourceUriSchema.safeParse(identifier);
|
|
502
|
+
if (!parsed.success) return {
|
|
330
503
|
ok: false,
|
|
331
|
-
reason: `resource identifier ${identifier}
|
|
504
|
+
reason: `resource identifier ${identifier} is invalid: ${parsed.error.issues[0]?.message ?? "not an absolute URI"}`
|
|
332
505
|
};
|
|
333
506
|
return { ok: true };
|
|
334
507
|
}
|
|
@@ -654,10 +827,10 @@ function invalidateResourceCache(identifier) {
|
|
|
654
827
|
* Looks up an {@link OAuthResource} by `identifier`, consulting the in-process
|
|
655
828
|
* cache when the identifier is in {@link OAuthOptions.cachedResources}.
|
|
656
829
|
*
|
|
657
|
-
* Triggers the lazy seed via {@link seedResourcesOnce} on first call —
|
|
658
|
-
*
|
|
659
|
-
*
|
|
660
|
-
*
|
|
830
|
+
* Triggers the lazy seed via {@link seedResourcesOnce} on first call — the
|
|
831
|
+
* retry path when the startup seed failed (database unreachable, or
|
|
832
|
+
* migrations run after plugin init). The seed is idempotent and coalesced,
|
|
833
|
+
* so the cost is paid only once per process.
|
|
661
834
|
*
|
|
662
835
|
* Returns a defensive copy so callers can't mutate cached state.
|
|
663
836
|
*
|
|
@@ -748,29 +921,56 @@ function buildSeedUpdate(input, mode, now) {
|
|
|
748
921
|
}
|
|
749
922
|
/**
|
|
750
923
|
* Pattern matched against adapter errors to detect the "table not yet
|
|
751
|
-
* created" case — i.e. migrations haven't
|
|
752
|
-
*
|
|
924
|
+
* created" case — i.e. migrations or `auth generate` haven't run. Only
|
|
925
|
+
* used to keep the expected startup seed failure quiet.
|
|
753
926
|
*
|
|
754
927
|
* Covers SQLite ("no such table"), Postgres ("relation X does not exist"),
|
|
755
|
-
*
|
|
928
|
+
* MySQL ("Table X does not exist" / contracted form), and the Drizzle and
|
|
929
|
+
* Prisma adapters' missing-model errors.
|
|
930
|
+
*/
|
|
931
|
+
const MISSING_TABLE_PATTERN = /no such table|relation.*does not exist|table.*does(?: not|n'?t) exist|model.*(?:does not exist|was not found in the schema)/i;
|
|
932
|
+
/**
|
|
933
|
+
* Logs a failed startup seed. Missing tables are expected before migrations
|
|
934
|
+
* and stay quiet; anything else is an error worth seeing.
|
|
935
|
+
*
|
|
936
|
+
* @internal
|
|
756
937
|
*/
|
|
757
|
-
|
|
938
|
+
function logStartupSeedFailure(ctx, error) {
|
|
939
|
+
let missingTable = false;
|
|
940
|
+
let current = error;
|
|
941
|
+
for (let depth = 0; current && !missingTable && depth < 5; depth++) {
|
|
942
|
+
missingTable = MISSING_TABLE_PATTERN.test(current instanceof Error ? current.message : String(current));
|
|
943
|
+
current = current instanceof Error ? current.cause : void 0;
|
|
944
|
+
}
|
|
945
|
+
if (missingTable) {
|
|
946
|
+
ctx.logger.debug("oauth-provider: oauthResource table not yet created; deferring resource seed to first access.");
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
ctx.logger.error("oauth-provider: could not seed `resources` at startup; retrying on first resource access.", error);
|
|
950
|
+
}
|
|
758
951
|
/**
|
|
759
|
-
*
|
|
760
|
-
* one Better Auth instance suppress seeding for every later instance in the
|
|
761
|
-
* process. Endpoint `AuthContext` objects are not guaranteed to be stable
|
|
762
|
-
* requests, so
|
|
952
|
+
* Lazy-seed state per adapter and plugin options. A module-level boolean would
|
|
953
|
+
* let one Better Auth instance suppress seeding for every later instance in the
|
|
954
|
+
* same process. Endpoint `AuthContext` objects are not guaranteed to be stable
|
|
955
|
+
* across requests, so the adapter identifies the backing store, and the options
|
|
956
|
+
* object separates instances that share an adapter but configure different
|
|
957
|
+
* `resources`.
|
|
763
958
|
*/
|
|
764
959
|
let seedStates = /* @__PURE__ */ new WeakMap();
|
|
765
|
-
function getSeedState(ctx) {
|
|
766
|
-
const
|
|
767
|
-
|
|
960
|
+
function getSeedState(ctx, opts) {
|
|
961
|
+
const adapterKey = ctx.adapter;
|
|
962
|
+
let byOptions = seedStates.get(adapterKey);
|
|
963
|
+
if (!byOptions) {
|
|
964
|
+
byOptions = /* @__PURE__ */ new WeakMap();
|
|
965
|
+
seedStates.set(adapterKey, byOptions);
|
|
966
|
+
}
|
|
967
|
+
const existing = byOptions.get(opts);
|
|
768
968
|
if (existing) return existing;
|
|
769
969
|
const created = {
|
|
770
970
|
completed: false,
|
|
771
971
|
promise: null
|
|
772
972
|
};
|
|
773
|
-
|
|
973
|
+
byOptions.set(opts, created);
|
|
774
974
|
return created;
|
|
775
975
|
}
|
|
776
976
|
/**
|
|
@@ -790,13 +990,11 @@ function getSeedState(ctx) {
|
|
|
790
990
|
*
|
|
791
991
|
* Race-safety: the `identifier` column carries a UNIQUE constraint, so two
|
|
792
992
|
* processes booting simultaneously can each attempt the insert — one wins,
|
|
793
|
-
* the other
|
|
993
|
+
* the other re-reads the row and treats the failed insert as a no-op.
|
|
794
994
|
*
|
|
795
|
-
*
|
|
796
|
-
*
|
|
797
|
-
* after
|
|
798
|
-
* defers — the lazy {@link seedResourcesOnce} path picks up the work on
|
|
799
|
-
* the first resource access.
|
|
995
|
+
* Throws on storage errors, including a missing table. Callers go through
|
|
996
|
+
* {@link seedResourcesOnce}, which resets so the next resource access
|
|
997
|
+
* retries (e.g. after migrations run).
|
|
800
998
|
*
|
|
801
999
|
* Idempotent: safe to call multiple times.
|
|
802
1000
|
*
|
|
@@ -819,69 +1017,45 @@ async function seedResources(ctx, opts) {
|
|
|
819
1017
|
const { signingAlgorithm: _, ...rest } = input;
|
|
820
1018
|
input = rest;
|
|
821
1019
|
}
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
1020
|
+
const where = [{
|
|
1021
|
+
field: "identifier",
|
|
1022
|
+
value: input.identifier
|
|
1023
|
+
}];
|
|
1024
|
+
const existing = await ctx.adapter.findOne({
|
|
1025
|
+
model: modelName,
|
|
1026
|
+
where
|
|
1027
|
+
});
|
|
1028
|
+
const now = /* @__PURE__ */ new Date();
|
|
1029
|
+
if (!existing) try {
|
|
1030
|
+
await ctx.adapter.create({
|
|
825
1031
|
model: modelName,
|
|
826
|
-
|
|
827
|
-
field: "identifier",
|
|
828
|
-
value: input.identifier
|
|
829
|
-
}]
|
|
1032
|
+
data: buildSeedRow(input, now)
|
|
830
1033
|
});
|
|
831
|
-
} catch (err) {
|
|
832
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
833
|
-
if (MISSING_TABLE_PATTERN.test(message)) {
|
|
834
|
-
logger.debug("oauth-provider: oauthResource table not yet created; deferring resource seed to first access.");
|
|
835
|
-
return;
|
|
836
|
-
}
|
|
837
|
-
throw err;
|
|
838
|
-
}
|
|
839
|
-
const now = /* @__PURE__ */ new Date();
|
|
840
|
-
if (!existing) {
|
|
841
|
-
try {
|
|
842
|
-
await ctx.adapter.create({
|
|
843
|
-
model: modelName,
|
|
844
|
-
data: buildSeedRow(input, now)
|
|
845
|
-
});
|
|
846
|
-
} catch (err) {
|
|
847
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
848
|
-
if (/unique|duplicate|UNIQUE/i.test(message)) {
|
|
849
|
-
logger.debug(`oauth-provider: resource ${input.identifier} already inserted by a concurrent process — skipping.`);
|
|
850
|
-
continue;
|
|
851
|
-
}
|
|
852
|
-
if (MISSING_TABLE_PATTERN.test(message)) {
|
|
853
|
-
logger.debug("oauth-provider: oauthResource table not yet created; deferring resource seed to first access.");
|
|
854
|
-
return;
|
|
855
|
-
}
|
|
856
|
-
throw err;
|
|
857
|
-
}
|
|
858
1034
|
continue;
|
|
1035
|
+
} catch (err) {
|
|
1036
|
+
if (!await ctx.adapter.findOne({
|
|
1037
|
+
model: modelName,
|
|
1038
|
+
where
|
|
1039
|
+
}).catch(() => null)) throw err;
|
|
859
1040
|
}
|
|
860
1041
|
if (mode === "insertOnly") continue;
|
|
861
1042
|
await ctx.adapter.update({
|
|
862
1043
|
model: modelName,
|
|
863
|
-
where
|
|
864
|
-
field: "identifier",
|
|
865
|
-
value: input.identifier
|
|
866
|
-
}],
|
|
1044
|
+
where,
|
|
867
1045
|
update: buildSeedUpdate(input, mode, now)
|
|
868
1046
|
});
|
|
869
1047
|
}
|
|
870
1048
|
}
|
|
871
1049
|
/**
|
|
872
|
-
* Idempotent, coalesced wrapper around {@link seedResources}
|
|
873
|
-
*
|
|
874
|
-
*
|
|
875
|
-
*
|
|
876
|
-
*
|
|
877
|
-
* The endpoint context shape (`{ context: AuthContext }`) is what
|
|
878
|
-
* `getResource` receives, so this is a convenience overload that unwraps
|
|
879
|
-
* for callers.
|
|
1050
|
+
* Idempotent, coalesced wrapper around {@link seedResources}, called from
|
|
1051
|
+
* plugin `init` and from every resource lookup. The first call runs the
|
|
1052
|
+
* seed, concurrent calls await the same promise, and calls after a success
|
|
1053
|
+
* are no-ops. A failure resets the state so the next call retries.
|
|
880
1054
|
*
|
|
881
1055
|
* @internal
|
|
882
1056
|
*/
|
|
883
1057
|
async function seedResourcesOnce(ctx, opts) {
|
|
884
|
-
const state = getSeedState(ctx);
|
|
1058
|
+
const state = getSeedState(ctx, opts);
|
|
885
1059
|
if (state.completed === true) return;
|
|
886
1060
|
if (state.promise !== null) return state.promise;
|
|
887
1061
|
state.promise = seedResources(ctx, opts).then(() => {
|
|
@@ -944,187 +1118,6 @@ function validateClientCredentialsScopes(scopes, grantTypes, tokenEndpointAuthMe
|
|
|
944
1118
|
});
|
|
945
1119
|
}
|
|
946
1120
|
//#endregion
|
|
947
|
-
//#region src/types/zod.ts
|
|
948
|
-
const DANGEROUS_SCHEMES = [
|
|
949
|
-
"javascript:",
|
|
950
|
-
"data:",
|
|
951
|
-
"vbscript:"
|
|
952
|
-
];
|
|
953
|
-
/**
|
|
954
|
-
* Validates an RFC 8707 resource indicator. The value must be an absolute URI
|
|
955
|
-
* with no fragment (RFC 8707 §2). Unlike a redirect URI it is not restricted to
|
|
956
|
-
* HTTPS, because a resource server identifier may use any absolute URI scheme;
|
|
957
|
-
* configured OAuth resources are the authoritative control over
|
|
958
|
-
* which resources a token may target.
|
|
959
|
-
*/
|
|
960
|
-
const ResourceUriSchema = z.string().superRefine((val, ctx) => {
|
|
961
|
-
if (!URL.canParse(val)) {
|
|
962
|
-
ctx.addIssue({
|
|
963
|
-
code: "custom",
|
|
964
|
-
message: "resource must be an absolute URI",
|
|
965
|
-
fatal: true
|
|
966
|
-
});
|
|
967
|
-
return z.NEVER;
|
|
968
|
-
}
|
|
969
|
-
if (val.includes("#")) {
|
|
970
|
-
ctx.addIssue({
|
|
971
|
-
code: "custom",
|
|
972
|
-
message: "resource must not contain a fragment"
|
|
973
|
-
});
|
|
974
|
-
return;
|
|
975
|
-
}
|
|
976
|
-
if (DANGEROUS_SCHEMES.includes(new URL(val).protocol)) ctx.addIssue({
|
|
977
|
-
code: "custom",
|
|
978
|
-
message: "resource cannot use javascript:, data:, or vbscript: scheme"
|
|
979
|
-
});
|
|
980
|
-
});
|
|
981
|
-
const authorizationPromptTokenSchema = z.enum([
|
|
982
|
-
"none",
|
|
983
|
-
"consent",
|
|
984
|
-
"login",
|
|
985
|
-
"create",
|
|
986
|
-
"select_account"
|
|
987
|
-
]);
|
|
988
|
-
const authorizationPromptSchema = z.string().superRefine((value, ctx) => {
|
|
989
|
-
const promptTokens = value.split(" ").map((token) => token.trim()).filter(Boolean);
|
|
990
|
-
const promptSet = /* @__PURE__ */ new Set();
|
|
991
|
-
if (!promptTokens.length) {
|
|
992
|
-
ctx.addIssue({
|
|
993
|
-
code: "custom",
|
|
994
|
-
message: "prompt must include at least one value"
|
|
995
|
-
});
|
|
996
|
-
return;
|
|
997
|
-
}
|
|
998
|
-
for (const token of promptTokens) {
|
|
999
|
-
const result = authorizationPromptTokenSchema.safeParse(token);
|
|
1000
|
-
if (!result.success) {
|
|
1001
|
-
ctx.addIssue({
|
|
1002
|
-
code: "custom",
|
|
1003
|
-
message: `unsupported prompt value: ${token}`
|
|
1004
|
-
});
|
|
1005
|
-
continue;
|
|
1006
|
-
}
|
|
1007
|
-
promptSet.add(result.data);
|
|
1008
|
-
}
|
|
1009
|
-
if (promptSet.has("none") && promptSet.size > 1) ctx.addIssue({
|
|
1010
|
-
code: "custom",
|
|
1011
|
-
message: "prompt=none cannot be combined with other prompt values"
|
|
1012
|
-
});
|
|
1013
|
-
});
|
|
1014
|
-
/** Canonical RFC 7517 JWK Set metadata for an OAuth client. */
|
|
1015
|
-
const clientJwksSchema = z.object({ keys: z.array(z.record(z.string(), z.unknown())).min(1) });
|
|
1016
|
-
const maxAgeSchema = z.union([z.number(), z.string().trim().min(1)]).transform((value, ctx) => {
|
|
1017
|
-
const maxAge = typeof value === "number" ? value : Number(value);
|
|
1018
|
-
if (!Number.isInteger(maxAge) || maxAge < 0) {
|
|
1019
|
-
ctx.addIssue({
|
|
1020
|
-
code: "custom",
|
|
1021
|
-
message: "max_age must be a non-negative integer"
|
|
1022
|
-
});
|
|
1023
|
-
return z.NEVER;
|
|
1024
|
-
}
|
|
1025
|
-
return maxAge;
|
|
1026
|
-
});
|
|
1027
|
-
const dpopJktSchema = z.string().regex(/^[A-Za-z0-9_-]{43}$/, "dpop_jkt must be a base64url-encoded SHA-256 JWK thumbprint");
|
|
1028
|
-
/**
|
|
1029
|
-
* Runtime schema for OAuthAuthorizationQuery.
|
|
1030
|
-
* Uses passthrough to tolerate fields added by future extensions (PAR, FPA, etc.)
|
|
1031
|
-
*/
|
|
1032
|
-
const authorizationQuerySchema = z.object({
|
|
1033
|
-
response_type: z.string().pipe(z.enum(["code"])).optional(),
|
|
1034
|
-
request: z.string().optional(),
|
|
1035
|
-
request_uri: z.string().optional(),
|
|
1036
|
-
redirect_uri: SafeUrlSchema.optional(),
|
|
1037
|
-
scope: z.string().optional(),
|
|
1038
|
-
state: z.string().optional(),
|
|
1039
|
-
client_id: z.string().min(1, "client_id is required"),
|
|
1040
|
-
prompt: authorizationPromptSchema.optional(),
|
|
1041
|
-
display: z.string().optional(),
|
|
1042
|
-
ui_locales: z.string().optional(),
|
|
1043
|
-
max_age: maxAgeSchema.optional(),
|
|
1044
|
-
acr_values: z.string().optional(),
|
|
1045
|
-
login_hint: z.string().optional(),
|
|
1046
|
-
id_token_hint: z.string().optional(),
|
|
1047
|
-
code_challenge: z.string().optional(),
|
|
1048
|
-
code_challenge_method: z.string().pipe(z.enum(["S256"])).optional(),
|
|
1049
|
-
nonce: z.string().optional(),
|
|
1050
|
-
claims: claimsRequestInputSchema.optional(),
|
|
1051
|
-
dpop_jkt: dpopJktSchema.optional(),
|
|
1052
|
-
resource: z.union([ResourceUriSchema, z.array(ResourceUriSchema).min(1)]).optional()
|
|
1053
|
-
}).passthrough();
|
|
1054
|
-
const storedAuthorizationQuerySchema = authorizationQuerySchema.extend({
|
|
1055
|
-
redirect_uri: SafeUrlSchema.optional(),
|
|
1056
|
-
claims: claimsRequestParameterSchema.optional()
|
|
1057
|
-
});
|
|
1058
|
-
/**
|
|
1059
|
-
* Runtime schema for the authorization code verification value.
|
|
1060
|
-
* Validates structure on deserialization from the JSON blob stored in the DB.
|
|
1061
|
-
* Uses passthrough so future fields (e.g. from authorization challenge) don't break parsing.
|
|
1062
|
-
*/
|
|
1063
|
-
const verificationValueSchema = z.object({
|
|
1064
|
-
type: z.literal("authorization_code"),
|
|
1065
|
-
query: storedAuthorizationQuerySchema,
|
|
1066
|
-
sessionId: z.string(),
|
|
1067
|
-
userId: z.string(),
|
|
1068
|
-
referenceId: z.string().optional(),
|
|
1069
|
-
authTime: z.number().optional(),
|
|
1070
|
-
resource: z.array(z.string()).optional()
|
|
1071
|
-
}).passthrough();
|
|
1072
|
-
/**
|
|
1073
|
-
* Request body accepted at `POST /oauth2/register` (RFC 7591 §2 client
|
|
1074
|
-
* metadata). This is the single source of truth for the registration contract:
|
|
1075
|
-
* the endpoint validates against it and {@link ClientRegistrationRequest} is
|
|
1076
|
-
* inferred from it, so the type a `validateInitialAccessToken` callback receives
|
|
1077
|
-
* always matches what is actually validated. `grant_types` and
|
|
1078
|
-
* `token_endpoint_auth_method` are open strings because extensions can register
|
|
1079
|
-
* custom values. Server-assigned fields (`client_id`, `client_secret`, the
|
|
1080
|
-
* issued/expiry timestamps) and internal state (`disabled`, `reference_id`) are
|
|
1081
|
-
* never part of a registration request.
|
|
1082
|
-
*
|
|
1083
|
-
* @see https://datatracker.ietf.org/doc/html/rfc7591#section-2
|
|
1084
|
-
*/
|
|
1085
|
-
const clientRegistrationRequestSchema = z.object({
|
|
1086
|
-
redirect_uris: z.array(SafeUrlSchema).min(1).optional(),
|
|
1087
|
-
scope: z.string().optional(),
|
|
1088
|
-
client_name: z.string().refine((value) => value.trim().length > 0, "client_name cannot be empty").optional(),
|
|
1089
|
-
client_uri: z.string().optional(),
|
|
1090
|
-
logo_uri: z.string().optional(),
|
|
1091
|
-
contacts: z.array(z.string().min(1)).min(1).optional(),
|
|
1092
|
-
tos_uri: z.string().optional(),
|
|
1093
|
-
policy_uri: z.string().optional(),
|
|
1094
|
-
software_id: z.string().optional(),
|
|
1095
|
-
software_version: z.string().optional(),
|
|
1096
|
-
software_statement: z.string().optional(),
|
|
1097
|
-
post_logout_redirect_uris: z.array(SafeUrlSchema).min(1).optional(),
|
|
1098
|
-
backchannel_logout_uri: SafeUrlSchema.optional(),
|
|
1099
|
-
backchannel_logout_session_required: z.boolean().optional(),
|
|
1100
|
-
token_endpoint_auth_method: z.string().trim().min(1).optional(),
|
|
1101
|
-
jwks: clientJwksSchema.optional(),
|
|
1102
|
-
jwks_uri: z.string().optional(),
|
|
1103
|
-
grant_types: z.array(z.string().trim().min(1)).min(1).optional(),
|
|
1104
|
-
response_types: z.array(z.enum(["code"])).optional(),
|
|
1105
|
-
application_type: z.enum(["web", "native"]).optional(),
|
|
1106
|
-
subject_type: z.enum(["public", "pairwise"]).optional(),
|
|
1107
|
-
dpop_bound_access_tokens: z.boolean().optional(),
|
|
1108
|
-
resources: z.array(ResourceUriSchema).optional(),
|
|
1109
|
-
skip_consent: z.never({ error: "skip_consent cannot be set during dynamic client registration" }).optional()
|
|
1110
|
-
});
|
|
1111
|
-
/**
|
|
1112
|
-
* Complete OAuth client metadata document schema shared by registration and
|
|
1113
|
-
* verified client-discovery integrations.
|
|
1114
|
-
*
|
|
1115
|
-
* Unlike {@link clientRegistrationRequestSchema}, a metadata document carries
|
|
1116
|
-
* its own identifier and may contain the server-assigned secret fields that a
|
|
1117
|
-
* discovery profile must explicitly prohibit. Keeping those fields in the
|
|
1118
|
-
* parsed shape lets profile validation distinguish a forbidden value from an
|
|
1119
|
-
* unknown extension without unsafe casts or ad-hoc allowlists.
|
|
1120
|
-
*/
|
|
1121
|
-
const oauthClientMetadataSchema = clientRegistrationRequestSchema.omit({ skip_consent: true }).extend({
|
|
1122
|
-
client_id: z.string().refine((value) => value.trim().length > 0, "client_id cannot be empty"),
|
|
1123
|
-
client_secret: z.string().optional(),
|
|
1124
|
-
client_secret_expires_at: z.number().int().nonnegative().optional(),
|
|
1125
|
-
client_id_issued_at: z.number().int().nonnegative().optional()
|
|
1126
|
-
}).loose();
|
|
1127
|
-
//#endregion
|
|
1128
1121
|
//#region src/userinfo.ts
|
|
1129
1122
|
/**
|
|
1130
1123
|
* Builds the standard OIDC claims (OIDC Core §5.1) for the UserInfo response.
|
|
@@ -2548,4 +2541,4 @@ async function introspectEndpoint(ctx, opts) {
|
|
|
2548
2541
|
}
|
|
2549
2542
|
}
|
|
2550
2543
|
//#endregion
|
|
2551
|
-
export {
|
|
2544
|
+
export { claimsRequestParameterSchema as A, clientJwksSchema as C, STANDARD_CLAIM_NAMES as D, STANDARD_CLAIMS as E, getRequestedUserInfoClaims as M, isValidOidcClaimsRequest as N, getSupportedClaims as O, authorizationQuerySchema as S, oauthClientMetadataSchema as T, logStartupSeedFailure as _, invalidateRefreshFamily as a, ResourceUriSchema as b, normalizeClientCredentialsScopes as c, assertIdentifierValid as d, extractRepeatedResourceFromForm as f, logEnforcePerClientResourcesResolution as g, isAudienceClaimAllowed as h, getOAuthProviderApi as i, filterClaimsRequestUserInfoClaims as j, canSatisfyEssentialAcrRequest as k, validateClientCredentialsScopes as l, invalidateResourceCache as m, introspect_exports as n, tokenEndpoint as o, getResource as p, decodeRefreshToken as r, userInfoEndpoint as s, introspectEndpoint as t, JWS_ALGORITHMS as u, resolveResourcePolicy as v, clientRegistrationRequestSchema as w, SafeUrlSchema as x, seedResourcesOnce as y };
|