@absolutejs/auth 0.78.0 → 0.80.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/README.md +48 -0
- package/changelog.json +44 -0
- package/dist/apikeys/config.d.ts +1 -1
- package/dist/apikeys/tokenRoutes.d.ts +7 -0
- package/dist/cli/migrate.js +46 -1
- package/dist/cli/migrate.js.map +6 -5
- package/dist/cli/setup.d.ts +12 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +665 -605
- package/dist/index.js.map +13 -11
- package/dist/linkedProviders/credentialError.d.ts +7 -0
- package/dist/linkedProviders/index.d.ts +1 -0
- package/dist/linkedProviders/index.js +57 -10
- package/dist/linkedProviders/index.js.map +6 -5
- package/dist/manifest.js +13 -17
- package/dist/manifest.js.map +3 -3
- package/dist/manifest.json +9 -14
- package/dist/server.js +664 -605
- package/dist/server.js.map +13 -11
- package/dist/session/neonStore.d.ts +4 -0
- package/dist/types.d.ts +1 -1
- package/package.json +11 -7
package/dist/index.js
CHANGED
|
@@ -3027,7 +3027,7 @@ import { Elysia, t } from "elysia";
|
|
|
3027
3027
|
// src/apikeys/config.ts
|
|
3028
3028
|
init_constants();
|
|
3029
3029
|
init_crypto();
|
|
3030
|
-
var DEFAULT_TOKEN_ROUTE = "/
|
|
3030
|
+
var DEFAULT_TOKEN_ROUTE = "/auth/api/token";
|
|
3031
3031
|
var ACCESS_TOKEN_PREFIX = "at_";
|
|
3032
3032
|
var API_KEY_PREFIX = "sk_";
|
|
3033
3033
|
var BEARER_PREFIX = "Bearer ";
|
|
@@ -3239,6 +3239,602 @@ var apiKeysRoutes = ({
|
|
|
3239
3239
|
});
|
|
3240
3240
|
};
|
|
3241
3241
|
|
|
3242
|
+
// src/oidc/config.ts
|
|
3243
|
+
init_constants();
|
|
3244
|
+
init_crypto();
|
|
3245
|
+
|
|
3246
|
+
// src/oidc/keys.ts
|
|
3247
|
+
var ENCODER = new TextEncoder;
|
|
3248
|
+
var ES256 = { hash: "SHA-256", name: "ECDSA" };
|
|
3249
|
+
var KEY_PARAMS = { name: "ECDSA", namedCurve: "P-256" };
|
|
3250
|
+
var ES256_JOSE_SIGNATURE_BYTES = 64;
|
|
3251
|
+
var toBase64Url = (bytes) => Buffer.from(bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes)).toString("base64url");
|
|
3252
|
+
var fromBase64Url = (value) => new Uint8Array(Buffer.from(value, "base64url"));
|
|
3253
|
+
var encodeSegment = (value) => Buffer.from(JSON.stringify(value)).toString("base64url");
|
|
3254
|
+
var decodeSegment = (segment) => {
|
|
3255
|
+
try {
|
|
3256
|
+
const value = JSON.parse(Buffer.from(segment, "base64url").toString("utf8"));
|
|
3257
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
3258
|
+
return;
|
|
3259
|
+
}
|
|
3260
|
+
return Object.fromEntries(Object.entries(value));
|
|
3261
|
+
} catch {
|
|
3262
|
+
return;
|
|
3263
|
+
}
|
|
3264
|
+
};
|
|
3265
|
+
var generateSigningKey = async () => {
|
|
3266
|
+
const pair = await crypto.subtle.generateKey(KEY_PARAMS, true, [
|
|
3267
|
+
"sign",
|
|
3268
|
+
"verify"
|
|
3269
|
+
]);
|
|
3270
|
+
const privateJwk = await crypto.subtle.exportKey("jwk", pair.privateKey);
|
|
3271
|
+
const publicJwk = await crypto.subtle.exportKey("jwk", pair.publicKey);
|
|
3272
|
+
return { kid: await jwkThumbprint(publicJwk), privateJwk, publicJwk };
|
|
3273
|
+
};
|
|
3274
|
+
var jwkThumbprint = async (jwk) => {
|
|
3275
|
+
const canonical = JSON.stringify({
|
|
3276
|
+
crv: jwk.crv,
|
|
3277
|
+
kty: jwk.kty,
|
|
3278
|
+
x: jwk.x,
|
|
3279
|
+
y: jwk.y
|
|
3280
|
+
});
|
|
3281
|
+
return toBase64Url(await crypto.subtle.digest("SHA-256", ENCODER.encode(canonical)));
|
|
3282
|
+
};
|
|
3283
|
+
var signJwt = async (payload, signing, typ = "JWT") => {
|
|
3284
|
+
const input = `${encodeSegment({ alg: "ES256", kid: signing.kid, typ })}.${encodeSegment(payload)}`;
|
|
3285
|
+
const encoded = ENCODER.encode(input);
|
|
3286
|
+
let signature;
|
|
3287
|
+
if (signing.sign !== undefined) {
|
|
3288
|
+
signature = await signing.sign(encoded);
|
|
3289
|
+
} else {
|
|
3290
|
+
const key = await crypto.subtle.importKey("jwk", signing.privateJwk, KEY_PARAMS, false, ["sign"]);
|
|
3291
|
+
signature = await crypto.subtle.sign(ES256, key, encoded);
|
|
3292
|
+
}
|
|
3293
|
+
if (signature.byteLength !== ES256_JOSE_SIGNATURE_BYTES) {
|
|
3294
|
+
throw new Error("ES256 signer must return a 64-byte JOSE signature");
|
|
3295
|
+
}
|
|
3296
|
+
return `${input}.${toBase64Url(signature)}`;
|
|
3297
|
+
};
|
|
3298
|
+
var toPublicJwk = (key) => ({
|
|
3299
|
+
alg: "ES256",
|
|
3300
|
+
crv: key.publicJwk.crv,
|
|
3301
|
+
kid: key.kid,
|
|
3302
|
+
kty: key.publicJwk.kty,
|
|
3303
|
+
use: "sig",
|
|
3304
|
+
x: key.publicJwk.x,
|
|
3305
|
+
y: key.publicJwk.y
|
|
3306
|
+
});
|
|
3307
|
+
var verifyJwt = async (token, publicJwk) => {
|
|
3308
|
+
const [headerSegment, payloadSegment, signatureSegment] = token.split(".");
|
|
3309
|
+
if (headerSegment === undefined || payloadSegment === undefined || signatureSegment === undefined) {
|
|
3310
|
+
return;
|
|
3311
|
+
}
|
|
3312
|
+
const key = await crypto.subtle.importKey("jwk", publicJwk, KEY_PARAMS, false, ["verify"]);
|
|
3313
|
+
const valid = await crypto.subtle.verify(ES256, key, fromBase64Url(signatureSegment), ENCODER.encode(`${headerSegment}.${payloadSegment}`));
|
|
3314
|
+
if (!valid)
|
|
3315
|
+
return;
|
|
3316
|
+
const header = decodeSegment(headerSegment);
|
|
3317
|
+
const payload = decodeSegment(payloadSegment);
|
|
3318
|
+
if (header === undefined || payload === undefined)
|
|
3319
|
+
return;
|
|
3320
|
+
return {
|
|
3321
|
+
header,
|
|
3322
|
+
payload
|
|
3323
|
+
};
|
|
3324
|
+
};
|
|
3325
|
+
var signingVerificationKeys = (active, previous = []) => {
|
|
3326
|
+
const keys = [active, ...previous];
|
|
3327
|
+
const keyIds = new Set(keys.map(({ kid }) => kid));
|
|
3328
|
+
if (keyIds.size !== keys.length)
|
|
3329
|
+
throw new Error("OIDC signing key IDs must be unique");
|
|
3330
|
+
return keys;
|
|
3331
|
+
};
|
|
3332
|
+
var verifyJwtWithKeys = async (token, keys) => {
|
|
3333
|
+
const [headerSegment] = token.split(".");
|
|
3334
|
+
if (!headerSegment)
|
|
3335
|
+
return;
|
|
3336
|
+
const header = decodeSegment(headerSegment);
|
|
3337
|
+
const kid = header?.kid;
|
|
3338
|
+
if (typeof kid !== "string" || kid.length === 0)
|
|
3339
|
+
return;
|
|
3340
|
+
const key = keys.find((candidate) => candidate.kid === kid);
|
|
3341
|
+
if (!key)
|
|
3342
|
+
return;
|
|
3343
|
+
return verifyJwt(token, key.publicJwk);
|
|
3344
|
+
};
|
|
3345
|
+
|
|
3346
|
+
// src/oidc/config.ts
|
|
3347
|
+
var DEFAULT_OIDC_ROUTE = "/oauth2";
|
|
3348
|
+
var MS_PER_SECOND = 1000;
|
|
3349
|
+
var TOKEN_BYTES2 = 32;
|
|
3350
|
+
var REFRESH_TTL_DAYS = 30;
|
|
3351
|
+
var DEFAULT_ACCESS_TOKEN_TTL_MS2 = MILLISECONDS_IN_AN_HOUR;
|
|
3352
|
+
var DEFAULT_ID_TOKEN_TTL_MS = MILLISECONDS_IN_AN_HOUR;
|
|
3353
|
+
var DEFAULT_REFRESH_TOKEN_TTL_MS = MILLISECONDS_IN_A_DAY * REFRESH_TTL_DAYS;
|
|
3354
|
+
var resolveAccessTtl = (ttl, scopes) => {
|
|
3355
|
+
if (typeof ttl === "function")
|
|
3356
|
+
return ttl({ scopes });
|
|
3357
|
+
return ttl ?? DEFAULT_ACCESS_TOKEN_TTL_MS2;
|
|
3358
|
+
};
|
|
3359
|
+
var nowSeconds = (milliseconds) => Math.floor(milliseconds / MS_PER_SECOND);
|
|
3360
|
+
var narrowScopes = (available, requested) => requested === undefined || requested.length === 0 ? available : requested.filter((scope) => available.includes(scope));
|
|
3361
|
+
var RESERVED_ACCESS_CLAIMS = new Set([
|
|
3362
|
+
"act",
|
|
3363
|
+
"aud",
|
|
3364
|
+
"client_id",
|
|
3365
|
+
"cnf",
|
|
3366
|
+
"exp",
|
|
3367
|
+
"iat",
|
|
3368
|
+
"iss",
|
|
3369
|
+
"jti",
|
|
3370
|
+
"scope",
|
|
3371
|
+
"sub",
|
|
3372
|
+
"token_use"
|
|
3373
|
+
]);
|
|
3374
|
+
var buildAccessClaims = ({
|
|
3375
|
+
act,
|
|
3376
|
+
audience,
|
|
3377
|
+
clientCertThumbprint,
|
|
3378
|
+
clientId,
|
|
3379
|
+
dpopJkt,
|
|
3380
|
+
extraClaims,
|
|
3381
|
+
issuer,
|
|
3382
|
+
now,
|
|
3383
|
+
scopes,
|
|
3384
|
+
sub,
|
|
3385
|
+
ttl
|
|
3386
|
+
}) => {
|
|
3387
|
+
const safeExtra = extraClaims === undefined ? {} : Object.fromEntries(Object.entries(extraClaims).filter(([key]) => !RESERVED_ACCESS_CLAIMS.has(key)));
|
|
3388
|
+
const claims = {
|
|
3389
|
+
...safeExtra,
|
|
3390
|
+
aud: audience ?? clientId,
|
|
3391
|
+
client_id: clientId,
|
|
3392
|
+
exp: nowSeconds(now + ttl),
|
|
3393
|
+
iat: nowSeconds(now),
|
|
3394
|
+
iss: issuer,
|
|
3395
|
+
jti: crypto.randomUUID(),
|
|
3396
|
+
scope: scopes.join(" "),
|
|
3397
|
+
sub,
|
|
3398
|
+
token_use: "access"
|
|
3399
|
+
};
|
|
3400
|
+
if (act !== undefined)
|
|
3401
|
+
claims.act = act;
|
|
3402
|
+
const cnf = {
|
|
3403
|
+
...dpopJkt === undefined ? {} : { jkt: dpopJkt },
|
|
3404
|
+
...clientCertThumbprint === undefined ? {} : { "x5t#S256": clientCertThumbprint }
|
|
3405
|
+
};
|
|
3406
|
+
if (Object.keys(cnf).length > 0) {
|
|
3407
|
+
claims.cnf = cnf;
|
|
3408
|
+
}
|
|
3409
|
+
return claims;
|
|
3410
|
+
};
|
|
3411
|
+
var exchangeToken = async ({
|
|
3412
|
+
actorClientId,
|
|
3413
|
+
audience,
|
|
3414
|
+
config,
|
|
3415
|
+
dpopJkt,
|
|
3416
|
+
now = Date.now(),
|
|
3417
|
+
requestedScopes,
|
|
3418
|
+
subjectToken
|
|
3419
|
+
}) => {
|
|
3420
|
+
const verified = await verifyJwtWithKeys(subjectToken, signingVerificationKeys(config.signingKey, config.previousSigningKeys));
|
|
3421
|
+
const payload = verified?.payload;
|
|
3422
|
+
if (payload === undefined || typeof payload.sub !== "string" || typeof payload.exp !== "number" || payload.exp <= nowSeconds(now)) {
|
|
3423
|
+
return { error: "invalid_grant", ok: false };
|
|
3424
|
+
}
|
|
3425
|
+
const available = typeof payload.scope === "string" ? payload.scope.split(" ") : [];
|
|
3426
|
+
if (requestedScopes?.some((scope) => !available.includes(scope)) === true) {
|
|
3427
|
+
return { error: "invalid_scope", ok: false };
|
|
3428
|
+
}
|
|
3429
|
+
const scopes = narrowScopes(available, requestedScopes);
|
|
3430
|
+
const ttl = resolveAccessTtl(config.accessTokenTtlMs, scopes);
|
|
3431
|
+
const extraClaims = await config.getAccessTokenClaims?.({
|
|
3432
|
+
audience,
|
|
3433
|
+
clientId: actorClientId,
|
|
3434
|
+
scopes,
|
|
3435
|
+
sub: payload.sub
|
|
3436
|
+
});
|
|
3437
|
+
return {
|
|
3438
|
+
accessToken: await signJwt(buildAccessClaims({
|
|
3439
|
+
act: { sub: actorClientId },
|
|
3440
|
+
audience,
|
|
3441
|
+
clientId: actorClientId,
|
|
3442
|
+
dpopJkt,
|
|
3443
|
+
extraClaims,
|
|
3444
|
+
issuer: config.issuer,
|
|
3445
|
+
now,
|
|
3446
|
+
scopes,
|
|
3447
|
+
sub: payload.sub,
|
|
3448
|
+
ttl
|
|
3449
|
+
}), config.signingKey),
|
|
3450
|
+
expiresIn: Math.floor(ttl / MS_PER_SECOND),
|
|
3451
|
+
ok: true,
|
|
3452
|
+
scope: scopes.join(" ")
|
|
3453
|
+
};
|
|
3454
|
+
};
|
|
3455
|
+
var issueTokenSet = async ({
|
|
3456
|
+
acr,
|
|
3457
|
+
audience,
|
|
3458
|
+
claims,
|
|
3459
|
+
clientCertThumbprint,
|
|
3460
|
+
clientId,
|
|
3461
|
+
config,
|
|
3462
|
+
dpopJkt,
|
|
3463
|
+
familyId,
|
|
3464
|
+
nonce,
|
|
3465
|
+
now = Date.now(),
|
|
3466
|
+
persistRefreshToken,
|
|
3467
|
+
scopes,
|
|
3468
|
+
sub
|
|
3469
|
+
}) => {
|
|
3470
|
+
const accessTtl = resolveAccessTtl(config.accessTokenTtlMs, scopes);
|
|
3471
|
+
const idTtl = config.idTokenTtlMs ?? DEFAULT_ID_TOKEN_TTL_MS;
|
|
3472
|
+
const refreshTtl = config.refreshTokenTtlMs ?? DEFAULT_REFRESH_TOKEN_TTL_MS;
|
|
3473
|
+
const accessExtra = await config.getAccessTokenClaims?.({
|
|
3474
|
+
audience,
|
|
3475
|
+
clientId,
|
|
3476
|
+
scopes,
|
|
3477
|
+
sub
|
|
3478
|
+
});
|
|
3479
|
+
const accessPayload = buildAccessClaims({
|
|
3480
|
+
audience,
|
|
3481
|
+
clientCertThumbprint,
|
|
3482
|
+
clientId,
|
|
3483
|
+
dpopJkt,
|
|
3484
|
+
extraClaims: { ...accessExtra, ...acr === undefined ? {} : { acr } },
|
|
3485
|
+
issuer: config.issuer,
|
|
3486
|
+
now,
|
|
3487
|
+
scopes,
|
|
3488
|
+
sub,
|
|
3489
|
+
ttl: accessTtl
|
|
3490
|
+
});
|
|
3491
|
+
const idPayload = {
|
|
3492
|
+
...claims,
|
|
3493
|
+
aud: clientId,
|
|
3494
|
+
exp: nowSeconds(now + idTtl),
|
|
3495
|
+
iat: nowSeconds(now),
|
|
3496
|
+
iss: config.issuer,
|
|
3497
|
+
sub
|
|
3498
|
+
};
|
|
3499
|
+
if (nonce !== undefined)
|
|
3500
|
+
idPayload.nonce = nonce;
|
|
3501
|
+
if (acr !== undefined)
|
|
3502
|
+
idPayload.acr = acr;
|
|
3503
|
+
const refreshToken = generateSecureToken(TOKEN_BYTES2);
|
|
3504
|
+
const refreshRecord = {
|
|
3505
|
+
acr,
|
|
3506
|
+
audience,
|
|
3507
|
+
claims,
|
|
3508
|
+
clientId,
|
|
3509
|
+
createdAt: now,
|
|
3510
|
+
dpopJkt,
|
|
3511
|
+
expiresAt: now + refreshTtl,
|
|
3512
|
+
familyId: familyId ?? crypto.randomUUID(),
|
|
3513
|
+
scopes,
|
|
3514
|
+
tokenHash: await hashToken(refreshToken),
|
|
3515
|
+
userId: sub
|
|
3516
|
+
};
|
|
3517
|
+
if (persistRefreshToken)
|
|
3518
|
+
await persistRefreshToken(refreshRecord);
|
|
3519
|
+
else
|
|
3520
|
+
await config.refreshTokenStore.saveToken(refreshRecord);
|
|
3521
|
+
return {
|
|
3522
|
+
access_token: await signJwt(accessPayload, config.signingKey),
|
|
3523
|
+
expires_in: Math.floor(accessTtl / MS_PER_SECOND),
|
|
3524
|
+
id_token: await signJwt(idPayload, config.signingKey),
|
|
3525
|
+
refresh_token: refreshToken,
|
|
3526
|
+
scope: scopes.join(" "),
|
|
3527
|
+
token_type: dpopJkt === undefined ? "Bearer" : "DPoP"
|
|
3528
|
+
};
|
|
3529
|
+
};
|
|
3530
|
+
var mcpProtectedResourceMetadata = ({
|
|
3531
|
+
issuer,
|
|
3532
|
+
resource,
|
|
3533
|
+
scopes
|
|
3534
|
+
}) => ({
|
|
3535
|
+
authorization_servers: [issuer],
|
|
3536
|
+
resource,
|
|
3537
|
+
scopes_supported: scopes ?? []
|
|
3538
|
+
});
|
|
3539
|
+
var verifyPkce = async (codeVerifier, codeChallenge) => await hashToken(codeVerifier) === codeChallenge;
|
|
3540
|
+
var inactive = { active: false };
|
|
3541
|
+
var introspectToken = async ({
|
|
3542
|
+
config,
|
|
3543
|
+
hint,
|
|
3544
|
+
now = Date.now(),
|
|
3545
|
+
token
|
|
3546
|
+
}) => {
|
|
3547
|
+
if (hint !== "refresh_token") {
|
|
3548
|
+
const verified = await verifyJwtWithKeys(token, signingVerificationKeys(config.signingKey, config.previousSigningKeys));
|
|
3549
|
+
const payload = verified?.payload;
|
|
3550
|
+
if (payload !== undefined && typeof payload.sub === "string" && typeof payload.exp === "number" && payload.exp > nowSeconds(now)) {
|
|
3551
|
+
return {
|
|
3552
|
+
active: true,
|
|
3553
|
+
client_id: typeof payload.client_id === "string" ? payload.client_id : "",
|
|
3554
|
+
exp: payload.exp,
|
|
3555
|
+
iat: typeof payload.iat === "number" ? payload.iat : 0,
|
|
3556
|
+
scope: typeof payload.scope === "string" ? payload.scope : "",
|
|
3557
|
+
sub: payload.sub,
|
|
3558
|
+
token_type: "access_token"
|
|
3559
|
+
};
|
|
3560
|
+
}
|
|
3561
|
+
}
|
|
3562
|
+
if (hint !== "access_token") {
|
|
3563
|
+
const refresh = await config.refreshTokenStore.getToken(await hashToken(token));
|
|
3564
|
+
if (refresh && refresh.expiresAt > now) {
|
|
3565
|
+
return {
|
|
3566
|
+
active: true,
|
|
3567
|
+
client_id: refresh.clientId,
|
|
3568
|
+
exp: nowSeconds(refresh.expiresAt),
|
|
3569
|
+
iat: nowSeconds(refresh.createdAt),
|
|
3570
|
+
scope: refresh.scopes.join(" "),
|
|
3571
|
+
sub: refresh.userId,
|
|
3572
|
+
token_type: "refresh_token"
|
|
3573
|
+
};
|
|
3574
|
+
}
|
|
3575
|
+
}
|
|
3576
|
+
return inactive;
|
|
3577
|
+
};
|
|
3578
|
+
var revokeRefreshToken = async (config, token) => {
|
|
3579
|
+
const consumed = await config.refreshTokenStore.consumeToken(await hashToken(token));
|
|
3580
|
+
return consumed !== undefined;
|
|
3581
|
+
};
|
|
3582
|
+
var DEVICE_CODE_BYTES = 32;
|
|
3583
|
+
var USER_CODE_HALF_LENGTH = 4;
|
|
3584
|
+
var USER_CODE_ALPHABET = "BCDFGHJKLMNPQRSTVWXZ23456789";
|
|
3585
|
+
var DEFAULT_DEVICE_CODE_TTL_MINUTES = 15;
|
|
3586
|
+
var DEFAULT_DEVICE_CODE_TTL_MS = MILLISECONDS_IN_A_MINUTE * DEFAULT_DEVICE_CODE_TTL_MINUTES;
|
|
3587
|
+
var DEFAULT_DEVICE_POLL_INTERVAL_SECONDS = 5;
|
|
3588
|
+
var generateUserCode = () => {
|
|
3589
|
+
const length = USER_CODE_HALF_LENGTH * 2;
|
|
3590
|
+
const random = crypto.getRandomValues(new Uint8Array(length));
|
|
3591
|
+
let code = "";
|
|
3592
|
+
for (const byte of random) {
|
|
3593
|
+
code += USER_CODE_ALPHABET[byte % USER_CODE_ALPHABET.length];
|
|
3594
|
+
}
|
|
3595
|
+
return `${code.slice(0, USER_CODE_HALF_LENGTH)}-${code.slice(USER_CODE_HALF_LENGTH)}`;
|
|
3596
|
+
};
|
|
3597
|
+
var issueDeviceAuthorization = async ({
|
|
3598
|
+
audience,
|
|
3599
|
+
clientId,
|
|
3600
|
+
config,
|
|
3601
|
+
now = Date.now(),
|
|
3602
|
+
requestedScopes
|
|
3603
|
+
}) => {
|
|
3604
|
+
if (!config.deviceAuthorizationStore) {
|
|
3605
|
+
throw new Error("oidc.deviceAuthorizationStore is not configured \u2014 cannot start a device flow");
|
|
3606
|
+
}
|
|
3607
|
+
const deviceCode = generateSecureToken(DEVICE_CODE_BYTES);
|
|
3608
|
+
const userCode = generateUserCode();
|
|
3609
|
+
const ttl = config.deviceCodeTtlMs ?? DEFAULT_DEVICE_CODE_TTL_MS;
|
|
3610
|
+
const interval = config.devicePollIntervalSeconds ?? DEFAULT_DEVICE_POLL_INTERVAL_SECONDS;
|
|
3611
|
+
await config.deviceAuthorizationStore.saveDeviceAuthorization({
|
|
3612
|
+
audience,
|
|
3613
|
+
clientId,
|
|
3614
|
+
createdAt: now,
|
|
3615
|
+
deviceCodeHash: await hashToken(deviceCode),
|
|
3616
|
+
expiresAt: now + ttl,
|
|
3617
|
+
intervalSeconds: interval,
|
|
3618
|
+
scopes: requestedScopes,
|
|
3619
|
+
status: "pending",
|
|
3620
|
+
userCode
|
|
3621
|
+
});
|
|
3622
|
+
const verificationUri = `${config.issuer}${config.oidcRoute ?? DEFAULT_OIDC_ROUTE}/device`;
|
|
3623
|
+
return {
|
|
3624
|
+
device_code: deviceCode,
|
|
3625
|
+
expires_in: Math.floor(ttl / MS_PER_SECOND),
|
|
3626
|
+
interval,
|
|
3627
|
+
user_code: userCode,
|
|
3628
|
+
verification_uri: verificationUri,
|
|
3629
|
+
verification_uri_complete: `${verificationUri}?user_code=${encodeURIComponent(userCode)}`
|
|
3630
|
+
};
|
|
3631
|
+
};
|
|
3632
|
+
var decideDeviceAuthorization = async (config, userCode, approval) => {
|
|
3633
|
+
if (!config.deviceAuthorizationStore) {
|
|
3634
|
+
return { error: "not_configured", ok: false };
|
|
3635
|
+
}
|
|
3636
|
+
const record = await config.deviceAuthorizationStore.findByUserCode(userCode);
|
|
3637
|
+
if (!record)
|
|
3638
|
+
return { error: "invalid_user_code", ok: false };
|
|
3639
|
+
if (record.expiresAt < Date.now()) {
|
|
3640
|
+
return { error: "expired_token", ok: false };
|
|
3641
|
+
}
|
|
3642
|
+
if (record.status !== "pending") {
|
|
3643
|
+
return { error: "already_decided", ok: false };
|
|
3644
|
+
}
|
|
3645
|
+
await config.deviceAuthorizationStore.updateStatus(record.deviceCodeHash, approval.status, approval.userSub);
|
|
3646
|
+
if (approval.status === "approved" && approval.userSub !== undefined) {
|
|
3647
|
+
await config.onDeviceAuthorizationApproved?.({
|
|
3648
|
+
clientId: record.clientId,
|
|
3649
|
+
scopes: record.scopes,
|
|
3650
|
+
userSub: approval.userSub
|
|
3651
|
+
});
|
|
3652
|
+
}
|
|
3653
|
+
return { ok: true };
|
|
3654
|
+
};
|
|
3655
|
+
var approveDeviceAuthorization = async ({
|
|
3656
|
+
config,
|
|
3657
|
+
userCode,
|
|
3658
|
+
userSub
|
|
3659
|
+
}) => decideDeviceAuthorization(config, userCode, {
|
|
3660
|
+
status: "approved",
|
|
3661
|
+
userSub
|
|
3662
|
+
});
|
|
3663
|
+
var denyDeviceAuthorization = async ({
|
|
3664
|
+
config,
|
|
3665
|
+
userCode
|
|
3666
|
+
}) => decideDeviceAuthorization(config, userCode, { status: "denied" });
|
|
3667
|
+
var exchangeDeviceCode = async ({
|
|
3668
|
+
clientId,
|
|
3669
|
+
config,
|
|
3670
|
+
deviceCode,
|
|
3671
|
+
dpopJkt,
|
|
3672
|
+
now = Date.now()
|
|
3673
|
+
}) => {
|
|
3674
|
+
if (!config.deviceAuthorizationStore) {
|
|
3675
|
+
return { error: "invalid_grant", ok: false };
|
|
3676
|
+
}
|
|
3677
|
+
const deviceCodeHash = await hashToken(deviceCode);
|
|
3678
|
+
const record = await config.deviceAuthorizationStore.findByDeviceCodeHash(deviceCodeHash);
|
|
3679
|
+
if (!record || record.clientId !== clientId) {
|
|
3680
|
+
return { error: "invalid_grant", ok: false };
|
|
3681
|
+
}
|
|
3682
|
+
if (record.expiresAt < now) {
|
|
3683
|
+
await config.deviceAuthorizationStore.deleteByDeviceCodeHash(deviceCodeHash);
|
|
3684
|
+
return { error: "expired_token", ok: false };
|
|
3685
|
+
}
|
|
3686
|
+
if (record.status === "pending") {
|
|
3687
|
+
return { error: "authorization_pending", ok: false };
|
|
3688
|
+
}
|
|
3689
|
+
if (record.status === "denied" || record.userSub === undefined) {
|
|
3690
|
+
return { error: "access_denied", ok: false };
|
|
3691
|
+
}
|
|
3692
|
+
await config.deviceAuthorizationStore.deleteByDeviceCodeHash(deviceCodeHash);
|
|
3693
|
+
const tokenSet = await issueTokenSet({
|
|
3694
|
+
audience: record.audience,
|
|
3695
|
+
clientId,
|
|
3696
|
+
config,
|
|
3697
|
+
dpopJkt,
|
|
3698
|
+
now,
|
|
3699
|
+
scopes: record.scopes,
|
|
3700
|
+
sub: record.userSub
|
|
3701
|
+
});
|
|
3702
|
+
return { ...tokenSet, ok: true };
|
|
3703
|
+
};
|
|
3704
|
+
var AUTH_REQ_ID_BYTES = 32;
|
|
3705
|
+
var DEFAULT_BACKCHANNEL_TTL_MINUTES = 10;
|
|
3706
|
+
var DEFAULT_BACKCHANNEL_TTL_MS = MILLISECONDS_IN_A_MINUTE * DEFAULT_BACKCHANNEL_TTL_MINUTES;
|
|
3707
|
+
var DEFAULT_BACKCHANNEL_POLL_INTERVAL_SECONDS = 5;
|
|
3708
|
+
var CIBA_GRANT_TYPE = "urn:openid:params:grant-type:ciba";
|
|
3709
|
+
var issueBackchannelAuth = async ({
|
|
3710
|
+
clientId,
|
|
3711
|
+
config,
|
|
3712
|
+
loginHint,
|
|
3713
|
+
bindingMessage,
|
|
3714
|
+
now = Date.now(),
|
|
3715
|
+
requestedScopes
|
|
3716
|
+
}) => {
|
|
3717
|
+
if (!config.backchannelAuthStore || !config.resolveBackchannelUser) {
|
|
3718
|
+
return { error: "invalid_request", ok: false };
|
|
3719
|
+
}
|
|
3720
|
+
const client = await config.clientStore.findClient(clientId) ?? await config.resolveClientIdMetadata?.(clientId);
|
|
3721
|
+
if (!client)
|
|
3722
|
+
return { error: "invalid_client", ok: false };
|
|
3723
|
+
const resolved = await config.resolveBackchannelUser({
|
|
3724
|
+
client,
|
|
3725
|
+
loginHint
|
|
3726
|
+
});
|
|
3727
|
+
if (!resolved)
|
|
3728
|
+
return { error: "unknown_user_id", ok: false };
|
|
3729
|
+
const authReqId = generateSecureToken(AUTH_REQ_ID_BYTES);
|
|
3730
|
+
const ttl = config.backchannelAuthTtlMs ?? DEFAULT_BACKCHANNEL_TTL_MS;
|
|
3731
|
+
const interval = config.backchannelPollIntervalSeconds ?? DEFAULT_BACKCHANNEL_POLL_INTERVAL_SECONDS;
|
|
3732
|
+
await config.backchannelAuthStore.saveBackchannelAuth({
|
|
3733
|
+
authReqId,
|
|
3734
|
+
bindingMessage,
|
|
3735
|
+
clientId,
|
|
3736
|
+
createdAt: now,
|
|
3737
|
+
expiresAt: now + ttl,
|
|
3738
|
+
intervalSeconds: interval,
|
|
3739
|
+
scopes: requestedScopes,
|
|
3740
|
+
status: "pending",
|
|
3741
|
+
userSub: resolved.sub
|
|
3742
|
+
});
|
|
3743
|
+
await config.onBackchannelAuthRequest?.({
|
|
3744
|
+
authReqId,
|
|
3745
|
+
bindingMessage,
|
|
3746
|
+
clientId,
|
|
3747
|
+
scopes: requestedScopes,
|
|
3748
|
+
userSub: resolved.sub
|
|
3749
|
+
});
|
|
3750
|
+
return {
|
|
3751
|
+
auth_req_id: authReqId,
|
|
3752
|
+
expires_in: Math.floor(ttl / MS_PER_SECOND),
|
|
3753
|
+
interval,
|
|
3754
|
+
ok: true
|
|
3755
|
+
};
|
|
3756
|
+
};
|
|
3757
|
+
var decideBackchannel = async (config, authReqId, approval) => {
|
|
3758
|
+
if (!config.backchannelAuthStore) {
|
|
3759
|
+
return { error: "not_configured", ok: false };
|
|
3760
|
+
}
|
|
3761
|
+
const record = await config.backchannelAuthStore.findByAuthReqId(authReqId);
|
|
3762
|
+
if (!record)
|
|
3763
|
+
return { error: "invalid_auth_req_id", ok: false };
|
|
3764
|
+
if (record.expiresAt < Date.now()) {
|
|
3765
|
+
return { error: "expired_token", ok: false };
|
|
3766
|
+
}
|
|
3767
|
+
if (record.status !== "pending") {
|
|
3768
|
+
return { error: "already_decided", ok: false };
|
|
3769
|
+
}
|
|
3770
|
+
await config.backchannelAuthStore.updateStatus(authReqId, approval.status, approval.userSub ?? record.userSub);
|
|
3771
|
+
return { ok: true };
|
|
3772
|
+
};
|
|
3773
|
+
var approveBackchannelAuth = async ({
|
|
3774
|
+
authReqId,
|
|
3775
|
+
config,
|
|
3776
|
+
userSub
|
|
3777
|
+
}) => decideBackchannel(config, authReqId, {
|
|
3778
|
+
status: "approved",
|
|
3779
|
+
userSub
|
|
3780
|
+
});
|
|
3781
|
+
var denyBackchannelAuth = async ({
|
|
3782
|
+
authReqId,
|
|
3783
|
+
config
|
|
3784
|
+
}) => decideBackchannel(config, authReqId, { status: "denied" });
|
|
3785
|
+
var exchangeBackchannelAuth = async ({
|
|
3786
|
+
authReqId,
|
|
3787
|
+
clientCertThumbprint,
|
|
3788
|
+
clientId,
|
|
3789
|
+
config,
|
|
3790
|
+
dpopJkt,
|
|
3791
|
+
now = Date.now()
|
|
3792
|
+
}) => {
|
|
3793
|
+
if (!config.backchannelAuthStore) {
|
|
3794
|
+
return { error: "invalid_grant", ok: false };
|
|
3795
|
+
}
|
|
3796
|
+
const record = await config.backchannelAuthStore.findByAuthReqId(authReqId);
|
|
3797
|
+
if (!record || record.clientId !== clientId) {
|
|
3798
|
+
return { error: "invalid_grant", ok: false };
|
|
3799
|
+
}
|
|
3800
|
+
if (record.expiresAt < now) {
|
|
3801
|
+
await config.backchannelAuthStore.deleteByAuthReqId(authReqId);
|
|
3802
|
+
return { error: "expired_token", ok: false };
|
|
3803
|
+
}
|
|
3804
|
+
if (record.lastPolledAt !== undefined && now - record.lastPolledAt < record.intervalSeconds * MS_PER_SECOND) {
|
|
3805
|
+
return { error: "slow_down", ok: false };
|
|
3806
|
+
}
|
|
3807
|
+
await config.backchannelAuthStore.recordPoll(authReqId, now);
|
|
3808
|
+
if (record.status === "pending") {
|
|
3809
|
+
return { error: "authorization_pending", ok: false };
|
|
3810
|
+
}
|
|
3811
|
+
if (record.status === "denied" || record.userSub === undefined) {
|
|
3812
|
+
return { error: "access_denied", ok: false };
|
|
3813
|
+
}
|
|
3814
|
+
await config.backchannelAuthStore.deleteByAuthReqId(authReqId);
|
|
3815
|
+
const tokenSet = await issueTokenSet({
|
|
3816
|
+
clientCertThumbprint,
|
|
3817
|
+
clientId,
|
|
3818
|
+
config,
|
|
3819
|
+
dpopJkt,
|
|
3820
|
+
now,
|
|
3821
|
+
scopes: record.scopes,
|
|
3822
|
+
sub: record.userSub
|
|
3823
|
+
});
|
|
3824
|
+
return { ...tokenSet, ok: true };
|
|
3825
|
+
};
|
|
3826
|
+
|
|
3827
|
+
// src/apikeys/tokenRoutes.ts
|
|
3828
|
+
var assertTokenRouteConfiguration = (apikeys, oidc) => {
|
|
3829
|
+
if (apikeys?.apiClientStore === undefined || apikeys.accessTokenStore === undefined || oidc === undefined)
|
|
3830
|
+
return;
|
|
3831
|
+
const apiTokenRoute = apikeys.tokenRoute ?? DEFAULT_TOKEN_ROUTE;
|
|
3832
|
+
const oidcTokenRoute = `${oidc.oidcRoute ?? DEFAULT_OIDC_ROUTE}/token`;
|
|
3833
|
+
if (apiTokenRoute.replace(/\/$/u, "") === oidcTokenRoute) {
|
|
3834
|
+
throw new Error(`Conflicting auth token routes: POST ${apiTokenRoute} is configured for both API client credentials and OIDC. Set apikeys.tokenRoute to a separate path (default: ${DEFAULT_TOKEN_ROUTE}).`);
|
|
3835
|
+
}
|
|
3836
|
+
};
|
|
3837
|
+
|
|
3242
3838
|
// src/agents/routes.ts
|
|
3243
3839
|
import { Elysia as Elysia3 } from "elysia";
|
|
3244
3840
|
|
|
@@ -3495,108 +4091,6 @@ one resource or transport is not authority for another.
|
|
|
3495
4091
|
// src/agents/registration.ts
|
|
3496
4092
|
init_constants();
|
|
3497
4093
|
init_crypto();
|
|
3498
|
-
|
|
3499
|
-
// src/oidc/keys.ts
|
|
3500
|
-
var ENCODER = new TextEncoder;
|
|
3501
|
-
var ES256 = { hash: "SHA-256", name: "ECDSA" };
|
|
3502
|
-
var KEY_PARAMS = { name: "ECDSA", namedCurve: "P-256" };
|
|
3503
|
-
var ES256_JOSE_SIGNATURE_BYTES = 64;
|
|
3504
|
-
var toBase64Url = (bytes) => Buffer.from(bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes)).toString("base64url");
|
|
3505
|
-
var fromBase64Url = (value) => new Uint8Array(Buffer.from(value, "base64url"));
|
|
3506
|
-
var encodeSegment = (value) => Buffer.from(JSON.stringify(value)).toString("base64url");
|
|
3507
|
-
var decodeSegment = (segment) => {
|
|
3508
|
-
try {
|
|
3509
|
-
const value = JSON.parse(Buffer.from(segment, "base64url").toString("utf8"));
|
|
3510
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
3511
|
-
return;
|
|
3512
|
-
}
|
|
3513
|
-
return Object.fromEntries(Object.entries(value));
|
|
3514
|
-
} catch {
|
|
3515
|
-
return;
|
|
3516
|
-
}
|
|
3517
|
-
};
|
|
3518
|
-
var generateSigningKey = async () => {
|
|
3519
|
-
const pair = await crypto.subtle.generateKey(KEY_PARAMS, true, [
|
|
3520
|
-
"sign",
|
|
3521
|
-
"verify"
|
|
3522
|
-
]);
|
|
3523
|
-
const privateJwk = await crypto.subtle.exportKey("jwk", pair.privateKey);
|
|
3524
|
-
const publicJwk = await crypto.subtle.exportKey("jwk", pair.publicKey);
|
|
3525
|
-
return { kid: await jwkThumbprint(publicJwk), privateJwk, publicJwk };
|
|
3526
|
-
};
|
|
3527
|
-
var jwkThumbprint = async (jwk) => {
|
|
3528
|
-
const canonical = JSON.stringify({
|
|
3529
|
-
crv: jwk.crv,
|
|
3530
|
-
kty: jwk.kty,
|
|
3531
|
-
x: jwk.x,
|
|
3532
|
-
y: jwk.y
|
|
3533
|
-
});
|
|
3534
|
-
return toBase64Url(await crypto.subtle.digest("SHA-256", ENCODER.encode(canonical)));
|
|
3535
|
-
};
|
|
3536
|
-
var signJwt = async (payload, signing, typ = "JWT") => {
|
|
3537
|
-
const input = `${encodeSegment({ alg: "ES256", kid: signing.kid, typ })}.${encodeSegment(payload)}`;
|
|
3538
|
-
const encoded = ENCODER.encode(input);
|
|
3539
|
-
let signature;
|
|
3540
|
-
if (signing.sign !== undefined) {
|
|
3541
|
-
signature = await signing.sign(encoded);
|
|
3542
|
-
} else {
|
|
3543
|
-
const key = await crypto.subtle.importKey("jwk", signing.privateJwk, KEY_PARAMS, false, ["sign"]);
|
|
3544
|
-
signature = await crypto.subtle.sign(ES256, key, encoded);
|
|
3545
|
-
}
|
|
3546
|
-
if (signature.byteLength !== ES256_JOSE_SIGNATURE_BYTES) {
|
|
3547
|
-
throw new Error("ES256 signer must return a 64-byte JOSE signature");
|
|
3548
|
-
}
|
|
3549
|
-
return `${input}.${toBase64Url(signature)}`;
|
|
3550
|
-
};
|
|
3551
|
-
var toPublicJwk = (key) => ({
|
|
3552
|
-
alg: "ES256",
|
|
3553
|
-
crv: key.publicJwk.crv,
|
|
3554
|
-
kid: key.kid,
|
|
3555
|
-
kty: key.publicJwk.kty,
|
|
3556
|
-
use: "sig",
|
|
3557
|
-
x: key.publicJwk.x,
|
|
3558
|
-
y: key.publicJwk.y
|
|
3559
|
-
});
|
|
3560
|
-
var verifyJwt = async (token, publicJwk) => {
|
|
3561
|
-
const [headerSegment, payloadSegment, signatureSegment] = token.split(".");
|
|
3562
|
-
if (headerSegment === undefined || payloadSegment === undefined || signatureSegment === undefined) {
|
|
3563
|
-
return;
|
|
3564
|
-
}
|
|
3565
|
-
const key = await crypto.subtle.importKey("jwk", publicJwk, KEY_PARAMS, false, ["verify"]);
|
|
3566
|
-
const valid = await crypto.subtle.verify(ES256, key, fromBase64Url(signatureSegment), ENCODER.encode(`${headerSegment}.${payloadSegment}`));
|
|
3567
|
-
if (!valid)
|
|
3568
|
-
return;
|
|
3569
|
-
const header = decodeSegment(headerSegment);
|
|
3570
|
-
const payload = decodeSegment(payloadSegment);
|
|
3571
|
-
if (header === undefined || payload === undefined)
|
|
3572
|
-
return;
|
|
3573
|
-
return {
|
|
3574
|
-
header,
|
|
3575
|
-
payload
|
|
3576
|
-
};
|
|
3577
|
-
};
|
|
3578
|
-
var signingVerificationKeys = (active, previous = []) => {
|
|
3579
|
-
const keys = [active, ...previous];
|
|
3580
|
-
const keyIds = new Set(keys.map(({ kid }) => kid));
|
|
3581
|
-
if (keyIds.size !== keys.length)
|
|
3582
|
-
throw new Error("OIDC signing key IDs must be unique");
|
|
3583
|
-
return keys;
|
|
3584
|
-
};
|
|
3585
|
-
var verifyJwtWithKeys = async (token, keys) => {
|
|
3586
|
-
const [headerSegment] = token.split(".");
|
|
3587
|
-
if (!headerSegment)
|
|
3588
|
-
return;
|
|
3589
|
-
const header = decodeSegment(headerSegment);
|
|
3590
|
-
const kid = header?.kid;
|
|
3591
|
-
if (typeof kid !== "string" || kid.length === 0)
|
|
3592
|
-
return;
|
|
3593
|
-
const key = keys.find((candidate) => candidate.kid === kid);
|
|
3594
|
-
if (!key)
|
|
3595
|
-
return;
|
|
3596
|
-
return verifyJwt(token, key.publicJwk);
|
|
3597
|
-
};
|
|
3598
|
-
|
|
3599
|
-
// src/agents/registration.ts
|
|
3600
4094
|
var AGENT_CLAIM_GRANT_TYPE = "urn:workos:agent-auth:grant-type:claim";
|
|
3601
4095
|
var AGENT_IDENTITY_ASSERTION_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:jwt-bearer";
|
|
3602
4096
|
var AGENT_IDENTITY_ASSERTION_TYPE = "urn:ietf:params:oauth:token-type:id-jag";
|
|
@@ -3607,12 +4101,12 @@ var DEFAULT_GUIDE_ROUTE2 = "/auth.md";
|
|
|
3607
4101
|
var DEFAULT_CLAIM_TTL_MS = 24 * 60 * MILLISECONDS_IN_A_MINUTE;
|
|
3608
4102
|
var DEFAULT_ATTEMPT_TTL_MS = 10 * MILLISECONDS_IN_A_MINUTE;
|
|
3609
4103
|
var DEFAULT_ASSERTION_TTL_MS = 60 * MILLISECONDS_IN_A_MINUTE;
|
|
3610
|
-
var
|
|
4104
|
+
var DEFAULT_ACCESS_TOKEN_TTL_MS3 = 15 * MILLISECONDS_IN_A_MINUTE;
|
|
3611
4105
|
var DEFAULT_MAX_AUTH_AGE_MS = 60 * MILLISECONDS_IN_A_MINUTE;
|
|
3612
4106
|
var DEFAULT_POLL_INTERVAL_SECONDS = 5;
|
|
3613
4107
|
var DEFAULT_MAX_CODE_ATTEMPTS = 5;
|
|
3614
4108
|
var MAX_CONCURRENT_UPDATE_RETRIES = 5;
|
|
3615
|
-
var
|
|
4109
|
+
var TOKEN_BYTES3 = 32;
|
|
3616
4110
|
var agentRegistrationDiscoveryMetadata = (config) => {
|
|
3617
4111
|
const registration = requiredRegistration(config);
|
|
3618
4112
|
const endpoints = agentRegistrationEndpoints(config);
|
|
@@ -3728,7 +4222,7 @@ var randomCode = () => {
|
|
|
3728
4222
|
const value = new DataView(bytes.buffer).getUint32(0) % 1e6;
|
|
3729
4223
|
return value.toString().padStart(6, "0");
|
|
3730
4224
|
};
|
|
3731
|
-
var makeSecret = (prefix) => `${prefix}_${generateSecureToken(
|
|
4225
|
+
var makeSecret = (prefix) => `${prefix}_${generateSecureToken(TOKEN_BYTES3)}`;
|
|
3732
4226
|
var makeAttempt = async ({
|
|
3733
4227
|
email,
|
|
3734
4228
|
now,
|
|
@@ -4057,9 +4551,9 @@ var completeAgentClaim = async (config, input, now = Date.now()) => {
|
|
|
4057
4551
|
};
|
|
4058
4552
|
var issueAgentAccessToken = async (config, flow, now) => {
|
|
4059
4553
|
const registration = requiredRegistration(config);
|
|
4060
|
-
const accessToken = `at_${generateSecureToken(
|
|
4554
|
+
const accessToken = `at_${generateSecureToken(TOKEN_BYTES3)}`;
|
|
4061
4555
|
const scopes = (flow.status === "claimed" ? registration.postClaimScopes : registration.preClaimScopes ?? []).filter((scope) => config.scopes.includes(scope));
|
|
4062
|
-
const expiresAt = now + (registration.tokenTtlMs ??
|
|
4556
|
+
const expiresAt = now + (registration.tokenTtlMs ?? DEFAULT_ACCESS_TOKEN_TTL_MS3);
|
|
4063
4557
|
await registration.accessTokenStore.saveToken({
|
|
4064
4558
|
clientId: flow.agentId,
|
|
4065
4559
|
createdAt: now,
|
|
@@ -7291,489 +7785,6 @@ init_constants();
|
|
|
7291
7785
|
init_crypto();
|
|
7292
7786
|
import { Elysia as Elysia24, t as t17 } from "elysia";
|
|
7293
7787
|
|
|
7294
|
-
// src/oidc/config.ts
|
|
7295
|
-
init_constants();
|
|
7296
|
-
init_crypto();
|
|
7297
|
-
var DEFAULT_OIDC_ROUTE = "/oauth2";
|
|
7298
|
-
var MS_PER_SECOND = 1000;
|
|
7299
|
-
var TOKEN_BYTES3 = 32;
|
|
7300
|
-
var REFRESH_TTL_DAYS = 30;
|
|
7301
|
-
var DEFAULT_ACCESS_TOKEN_TTL_MS3 = MILLISECONDS_IN_AN_HOUR;
|
|
7302
|
-
var DEFAULT_ID_TOKEN_TTL_MS = MILLISECONDS_IN_AN_HOUR;
|
|
7303
|
-
var DEFAULT_REFRESH_TOKEN_TTL_MS = MILLISECONDS_IN_A_DAY * REFRESH_TTL_DAYS;
|
|
7304
|
-
var resolveAccessTtl = (ttl, scopes) => {
|
|
7305
|
-
if (typeof ttl === "function")
|
|
7306
|
-
return ttl({ scopes });
|
|
7307
|
-
return ttl ?? DEFAULT_ACCESS_TOKEN_TTL_MS3;
|
|
7308
|
-
};
|
|
7309
|
-
var nowSeconds = (milliseconds) => Math.floor(milliseconds / MS_PER_SECOND);
|
|
7310
|
-
var narrowScopes = (available, requested) => requested === undefined || requested.length === 0 ? available : requested.filter((scope) => available.includes(scope));
|
|
7311
|
-
var RESERVED_ACCESS_CLAIMS = new Set([
|
|
7312
|
-
"act",
|
|
7313
|
-
"aud",
|
|
7314
|
-
"client_id",
|
|
7315
|
-
"cnf",
|
|
7316
|
-
"exp",
|
|
7317
|
-
"iat",
|
|
7318
|
-
"iss",
|
|
7319
|
-
"jti",
|
|
7320
|
-
"scope",
|
|
7321
|
-
"sub",
|
|
7322
|
-
"token_use"
|
|
7323
|
-
]);
|
|
7324
|
-
var buildAccessClaims = ({
|
|
7325
|
-
act,
|
|
7326
|
-
audience,
|
|
7327
|
-
clientCertThumbprint,
|
|
7328
|
-
clientId,
|
|
7329
|
-
dpopJkt,
|
|
7330
|
-
extraClaims,
|
|
7331
|
-
issuer,
|
|
7332
|
-
now,
|
|
7333
|
-
scopes,
|
|
7334
|
-
sub,
|
|
7335
|
-
ttl
|
|
7336
|
-
}) => {
|
|
7337
|
-
const safeExtra = extraClaims === undefined ? {} : Object.fromEntries(Object.entries(extraClaims).filter(([key]) => !RESERVED_ACCESS_CLAIMS.has(key)));
|
|
7338
|
-
const claims = {
|
|
7339
|
-
...safeExtra,
|
|
7340
|
-
aud: audience ?? clientId,
|
|
7341
|
-
client_id: clientId,
|
|
7342
|
-
exp: nowSeconds(now + ttl),
|
|
7343
|
-
iat: nowSeconds(now),
|
|
7344
|
-
iss: issuer,
|
|
7345
|
-
jti: crypto.randomUUID(),
|
|
7346
|
-
scope: scopes.join(" "),
|
|
7347
|
-
sub,
|
|
7348
|
-
token_use: "access"
|
|
7349
|
-
};
|
|
7350
|
-
if (act !== undefined)
|
|
7351
|
-
claims.act = act;
|
|
7352
|
-
const cnf = {
|
|
7353
|
-
...dpopJkt === undefined ? {} : { jkt: dpopJkt },
|
|
7354
|
-
...clientCertThumbprint === undefined ? {} : { "x5t#S256": clientCertThumbprint }
|
|
7355
|
-
};
|
|
7356
|
-
if (Object.keys(cnf).length > 0) {
|
|
7357
|
-
claims.cnf = cnf;
|
|
7358
|
-
}
|
|
7359
|
-
return claims;
|
|
7360
|
-
};
|
|
7361
|
-
var exchangeToken = async ({
|
|
7362
|
-
actorClientId,
|
|
7363
|
-
audience,
|
|
7364
|
-
config,
|
|
7365
|
-
dpopJkt,
|
|
7366
|
-
now = Date.now(),
|
|
7367
|
-
requestedScopes,
|
|
7368
|
-
subjectToken
|
|
7369
|
-
}) => {
|
|
7370
|
-
const verified = await verifyJwtWithKeys(subjectToken, signingVerificationKeys(config.signingKey, config.previousSigningKeys));
|
|
7371
|
-
const payload = verified?.payload;
|
|
7372
|
-
if (payload === undefined || typeof payload.sub !== "string" || typeof payload.exp !== "number" || payload.exp <= nowSeconds(now)) {
|
|
7373
|
-
return { error: "invalid_grant", ok: false };
|
|
7374
|
-
}
|
|
7375
|
-
const available = typeof payload.scope === "string" ? payload.scope.split(" ") : [];
|
|
7376
|
-
if (requestedScopes?.some((scope) => !available.includes(scope)) === true) {
|
|
7377
|
-
return { error: "invalid_scope", ok: false };
|
|
7378
|
-
}
|
|
7379
|
-
const scopes = narrowScopes(available, requestedScopes);
|
|
7380
|
-
const ttl = resolveAccessTtl(config.accessTokenTtlMs, scopes);
|
|
7381
|
-
const extraClaims = await config.getAccessTokenClaims?.({
|
|
7382
|
-
audience,
|
|
7383
|
-
clientId: actorClientId,
|
|
7384
|
-
scopes,
|
|
7385
|
-
sub: payload.sub
|
|
7386
|
-
});
|
|
7387
|
-
return {
|
|
7388
|
-
accessToken: await signJwt(buildAccessClaims({
|
|
7389
|
-
act: { sub: actorClientId },
|
|
7390
|
-
audience,
|
|
7391
|
-
clientId: actorClientId,
|
|
7392
|
-
dpopJkt,
|
|
7393
|
-
extraClaims,
|
|
7394
|
-
issuer: config.issuer,
|
|
7395
|
-
now,
|
|
7396
|
-
scopes,
|
|
7397
|
-
sub: payload.sub,
|
|
7398
|
-
ttl
|
|
7399
|
-
}), config.signingKey),
|
|
7400
|
-
expiresIn: Math.floor(ttl / MS_PER_SECOND),
|
|
7401
|
-
ok: true,
|
|
7402
|
-
scope: scopes.join(" ")
|
|
7403
|
-
};
|
|
7404
|
-
};
|
|
7405
|
-
var issueTokenSet = async ({
|
|
7406
|
-
acr,
|
|
7407
|
-
audience,
|
|
7408
|
-
claims,
|
|
7409
|
-
clientCertThumbprint,
|
|
7410
|
-
clientId,
|
|
7411
|
-
config,
|
|
7412
|
-
dpopJkt,
|
|
7413
|
-
familyId,
|
|
7414
|
-
nonce,
|
|
7415
|
-
now = Date.now(),
|
|
7416
|
-
persistRefreshToken,
|
|
7417
|
-
scopes,
|
|
7418
|
-
sub
|
|
7419
|
-
}) => {
|
|
7420
|
-
const accessTtl = resolveAccessTtl(config.accessTokenTtlMs, scopes);
|
|
7421
|
-
const idTtl = config.idTokenTtlMs ?? DEFAULT_ID_TOKEN_TTL_MS;
|
|
7422
|
-
const refreshTtl = config.refreshTokenTtlMs ?? DEFAULT_REFRESH_TOKEN_TTL_MS;
|
|
7423
|
-
const accessExtra = await config.getAccessTokenClaims?.({
|
|
7424
|
-
audience,
|
|
7425
|
-
clientId,
|
|
7426
|
-
scopes,
|
|
7427
|
-
sub
|
|
7428
|
-
});
|
|
7429
|
-
const accessPayload = buildAccessClaims({
|
|
7430
|
-
audience,
|
|
7431
|
-
clientCertThumbprint,
|
|
7432
|
-
clientId,
|
|
7433
|
-
dpopJkt,
|
|
7434
|
-
extraClaims: { ...accessExtra, ...acr === undefined ? {} : { acr } },
|
|
7435
|
-
issuer: config.issuer,
|
|
7436
|
-
now,
|
|
7437
|
-
scopes,
|
|
7438
|
-
sub,
|
|
7439
|
-
ttl: accessTtl
|
|
7440
|
-
});
|
|
7441
|
-
const idPayload = {
|
|
7442
|
-
...claims,
|
|
7443
|
-
aud: clientId,
|
|
7444
|
-
exp: nowSeconds(now + idTtl),
|
|
7445
|
-
iat: nowSeconds(now),
|
|
7446
|
-
iss: config.issuer,
|
|
7447
|
-
sub
|
|
7448
|
-
};
|
|
7449
|
-
if (nonce !== undefined)
|
|
7450
|
-
idPayload.nonce = nonce;
|
|
7451
|
-
if (acr !== undefined)
|
|
7452
|
-
idPayload.acr = acr;
|
|
7453
|
-
const refreshToken = generateSecureToken(TOKEN_BYTES3);
|
|
7454
|
-
const refreshRecord = {
|
|
7455
|
-
acr,
|
|
7456
|
-
audience,
|
|
7457
|
-
claims,
|
|
7458
|
-
clientId,
|
|
7459
|
-
createdAt: now,
|
|
7460
|
-
dpopJkt,
|
|
7461
|
-
expiresAt: now + refreshTtl,
|
|
7462
|
-
familyId: familyId ?? crypto.randomUUID(),
|
|
7463
|
-
scopes,
|
|
7464
|
-
tokenHash: await hashToken(refreshToken),
|
|
7465
|
-
userId: sub
|
|
7466
|
-
};
|
|
7467
|
-
if (persistRefreshToken)
|
|
7468
|
-
await persistRefreshToken(refreshRecord);
|
|
7469
|
-
else
|
|
7470
|
-
await config.refreshTokenStore.saveToken(refreshRecord);
|
|
7471
|
-
return {
|
|
7472
|
-
access_token: await signJwt(accessPayload, config.signingKey),
|
|
7473
|
-
expires_in: Math.floor(accessTtl / MS_PER_SECOND),
|
|
7474
|
-
id_token: await signJwt(idPayload, config.signingKey),
|
|
7475
|
-
refresh_token: refreshToken,
|
|
7476
|
-
scope: scopes.join(" "),
|
|
7477
|
-
token_type: dpopJkt === undefined ? "Bearer" : "DPoP"
|
|
7478
|
-
};
|
|
7479
|
-
};
|
|
7480
|
-
var mcpProtectedResourceMetadata = ({
|
|
7481
|
-
issuer,
|
|
7482
|
-
resource,
|
|
7483
|
-
scopes
|
|
7484
|
-
}) => ({
|
|
7485
|
-
authorization_servers: [issuer],
|
|
7486
|
-
resource,
|
|
7487
|
-
scopes_supported: scopes ?? []
|
|
7488
|
-
});
|
|
7489
|
-
var verifyPkce = async (codeVerifier, codeChallenge) => await hashToken(codeVerifier) === codeChallenge;
|
|
7490
|
-
var inactive = { active: false };
|
|
7491
|
-
var introspectToken = async ({
|
|
7492
|
-
config,
|
|
7493
|
-
hint,
|
|
7494
|
-
now = Date.now(),
|
|
7495
|
-
token
|
|
7496
|
-
}) => {
|
|
7497
|
-
if (hint !== "refresh_token") {
|
|
7498
|
-
const verified = await verifyJwtWithKeys(token, signingVerificationKeys(config.signingKey, config.previousSigningKeys));
|
|
7499
|
-
const payload = verified?.payload;
|
|
7500
|
-
if (payload !== undefined && typeof payload.sub === "string" && typeof payload.exp === "number" && payload.exp > nowSeconds(now)) {
|
|
7501
|
-
return {
|
|
7502
|
-
active: true,
|
|
7503
|
-
client_id: typeof payload.client_id === "string" ? payload.client_id : "",
|
|
7504
|
-
exp: payload.exp,
|
|
7505
|
-
iat: typeof payload.iat === "number" ? payload.iat : 0,
|
|
7506
|
-
scope: typeof payload.scope === "string" ? payload.scope : "",
|
|
7507
|
-
sub: payload.sub,
|
|
7508
|
-
token_type: "access_token"
|
|
7509
|
-
};
|
|
7510
|
-
}
|
|
7511
|
-
}
|
|
7512
|
-
if (hint !== "access_token") {
|
|
7513
|
-
const refresh = await config.refreshTokenStore.getToken(await hashToken(token));
|
|
7514
|
-
if (refresh && refresh.expiresAt > now) {
|
|
7515
|
-
return {
|
|
7516
|
-
active: true,
|
|
7517
|
-
client_id: refresh.clientId,
|
|
7518
|
-
exp: nowSeconds(refresh.expiresAt),
|
|
7519
|
-
iat: nowSeconds(refresh.createdAt),
|
|
7520
|
-
scope: refresh.scopes.join(" "),
|
|
7521
|
-
sub: refresh.userId,
|
|
7522
|
-
token_type: "refresh_token"
|
|
7523
|
-
};
|
|
7524
|
-
}
|
|
7525
|
-
}
|
|
7526
|
-
return inactive;
|
|
7527
|
-
};
|
|
7528
|
-
var revokeRefreshToken = async (config, token) => {
|
|
7529
|
-
const consumed = await config.refreshTokenStore.consumeToken(await hashToken(token));
|
|
7530
|
-
return consumed !== undefined;
|
|
7531
|
-
};
|
|
7532
|
-
var DEVICE_CODE_BYTES = 32;
|
|
7533
|
-
var USER_CODE_HALF_LENGTH = 4;
|
|
7534
|
-
var USER_CODE_ALPHABET = "BCDFGHJKLMNPQRSTVWXZ23456789";
|
|
7535
|
-
var DEFAULT_DEVICE_CODE_TTL_MINUTES = 15;
|
|
7536
|
-
var DEFAULT_DEVICE_CODE_TTL_MS = MILLISECONDS_IN_A_MINUTE * DEFAULT_DEVICE_CODE_TTL_MINUTES;
|
|
7537
|
-
var DEFAULT_DEVICE_POLL_INTERVAL_SECONDS = 5;
|
|
7538
|
-
var generateUserCode = () => {
|
|
7539
|
-
const length = USER_CODE_HALF_LENGTH * 2;
|
|
7540
|
-
const random = crypto.getRandomValues(new Uint8Array(length));
|
|
7541
|
-
let code = "";
|
|
7542
|
-
for (const byte of random) {
|
|
7543
|
-
code += USER_CODE_ALPHABET[byte % USER_CODE_ALPHABET.length];
|
|
7544
|
-
}
|
|
7545
|
-
return `${code.slice(0, USER_CODE_HALF_LENGTH)}-${code.slice(USER_CODE_HALF_LENGTH)}`;
|
|
7546
|
-
};
|
|
7547
|
-
var issueDeviceAuthorization = async ({
|
|
7548
|
-
audience,
|
|
7549
|
-
clientId,
|
|
7550
|
-
config,
|
|
7551
|
-
now = Date.now(),
|
|
7552
|
-
requestedScopes
|
|
7553
|
-
}) => {
|
|
7554
|
-
if (!config.deviceAuthorizationStore) {
|
|
7555
|
-
throw new Error("oidc.deviceAuthorizationStore is not configured \u2014 cannot start a device flow");
|
|
7556
|
-
}
|
|
7557
|
-
const deviceCode = generateSecureToken(DEVICE_CODE_BYTES);
|
|
7558
|
-
const userCode = generateUserCode();
|
|
7559
|
-
const ttl = config.deviceCodeTtlMs ?? DEFAULT_DEVICE_CODE_TTL_MS;
|
|
7560
|
-
const interval = config.devicePollIntervalSeconds ?? DEFAULT_DEVICE_POLL_INTERVAL_SECONDS;
|
|
7561
|
-
await config.deviceAuthorizationStore.saveDeviceAuthorization({
|
|
7562
|
-
audience,
|
|
7563
|
-
clientId,
|
|
7564
|
-
createdAt: now,
|
|
7565
|
-
deviceCodeHash: await hashToken(deviceCode),
|
|
7566
|
-
expiresAt: now + ttl,
|
|
7567
|
-
intervalSeconds: interval,
|
|
7568
|
-
scopes: requestedScopes,
|
|
7569
|
-
status: "pending",
|
|
7570
|
-
userCode
|
|
7571
|
-
});
|
|
7572
|
-
const verificationUri = `${config.issuer}${config.oidcRoute ?? DEFAULT_OIDC_ROUTE}/device`;
|
|
7573
|
-
return {
|
|
7574
|
-
device_code: deviceCode,
|
|
7575
|
-
expires_in: Math.floor(ttl / MS_PER_SECOND),
|
|
7576
|
-
interval,
|
|
7577
|
-
user_code: userCode,
|
|
7578
|
-
verification_uri: verificationUri,
|
|
7579
|
-
verification_uri_complete: `${verificationUri}?user_code=${encodeURIComponent(userCode)}`
|
|
7580
|
-
};
|
|
7581
|
-
};
|
|
7582
|
-
var decideDeviceAuthorization = async (config, userCode, approval) => {
|
|
7583
|
-
if (!config.deviceAuthorizationStore) {
|
|
7584
|
-
return { error: "not_configured", ok: false };
|
|
7585
|
-
}
|
|
7586
|
-
const record = await config.deviceAuthorizationStore.findByUserCode(userCode);
|
|
7587
|
-
if (!record)
|
|
7588
|
-
return { error: "invalid_user_code", ok: false };
|
|
7589
|
-
if (record.expiresAt < Date.now()) {
|
|
7590
|
-
return { error: "expired_token", ok: false };
|
|
7591
|
-
}
|
|
7592
|
-
if (record.status !== "pending") {
|
|
7593
|
-
return { error: "already_decided", ok: false };
|
|
7594
|
-
}
|
|
7595
|
-
await config.deviceAuthorizationStore.updateStatus(record.deviceCodeHash, approval.status, approval.userSub);
|
|
7596
|
-
if (approval.status === "approved" && approval.userSub !== undefined) {
|
|
7597
|
-
await config.onDeviceAuthorizationApproved?.({
|
|
7598
|
-
clientId: record.clientId,
|
|
7599
|
-
scopes: record.scopes,
|
|
7600
|
-
userSub: approval.userSub
|
|
7601
|
-
});
|
|
7602
|
-
}
|
|
7603
|
-
return { ok: true };
|
|
7604
|
-
};
|
|
7605
|
-
var approveDeviceAuthorization = async ({
|
|
7606
|
-
config,
|
|
7607
|
-
userCode,
|
|
7608
|
-
userSub
|
|
7609
|
-
}) => decideDeviceAuthorization(config, userCode, {
|
|
7610
|
-
status: "approved",
|
|
7611
|
-
userSub
|
|
7612
|
-
});
|
|
7613
|
-
var denyDeviceAuthorization = async ({
|
|
7614
|
-
config,
|
|
7615
|
-
userCode
|
|
7616
|
-
}) => decideDeviceAuthorization(config, userCode, { status: "denied" });
|
|
7617
|
-
var exchangeDeviceCode = async ({
|
|
7618
|
-
clientId,
|
|
7619
|
-
config,
|
|
7620
|
-
deviceCode,
|
|
7621
|
-
dpopJkt,
|
|
7622
|
-
now = Date.now()
|
|
7623
|
-
}) => {
|
|
7624
|
-
if (!config.deviceAuthorizationStore) {
|
|
7625
|
-
return { error: "invalid_grant", ok: false };
|
|
7626
|
-
}
|
|
7627
|
-
const deviceCodeHash = await hashToken(deviceCode);
|
|
7628
|
-
const record = await config.deviceAuthorizationStore.findByDeviceCodeHash(deviceCodeHash);
|
|
7629
|
-
if (!record || record.clientId !== clientId) {
|
|
7630
|
-
return { error: "invalid_grant", ok: false };
|
|
7631
|
-
}
|
|
7632
|
-
if (record.expiresAt < now) {
|
|
7633
|
-
await config.deviceAuthorizationStore.deleteByDeviceCodeHash(deviceCodeHash);
|
|
7634
|
-
return { error: "expired_token", ok: false };
|
|
7635
|
-
}
|
|
7636
|
-
if (record.status === "pending") {
|
|
7637
|
-
return { error: "authorization_pending", ok: false };
|
|
7638
|
-
}
|
|
7639
|
-
if (record.status === "denied" || record.userSub === undefined) {
|
|
7640
|
-
return { error: "access_denied", ok: false };
|
|
7641
|
-
}
|
|
7642
|
-
await config.deviceAuthorizationStore.deleteByDeviceCodeHash(deviceCodeHash);
|
|
7643
|
-
const tokenSet = await issueTokenSet({
|
|
7644
|
-
audience: record.audience,
|
|
7645
|
-
clientId,
|
|
7646
|
-
config,
|
|
7647
|
-
dpopJkt,
|
|
7648
|
-
now,
|
|
7649
|
-
scopes: record.scopes,
|
|
7650
|
-
sub: record.userSub
|
|
7651
|
-
});
|
|
7652
|
-
return { ...tokenSet, ok: true };
|
|
7653
|
-
};
|
|
7654
|
-
var AUTH_REQ_ID_BYTES = 32;
|
|
7655
|
-
var DEFAULT_BACKCHANNEL_TTL_MINUTES = 10;
|
|
7656
|
-
var DEFAULT_BACKCHANNEL_TTL_MS = MILLISECONDS_IN_A_MINUTE * DEFAULT_BACKCHANNEL_TTL_MINUTES;
|
|
7657
|
-
var DEFAULT_BACKCHANNEL_POLL_INTERVAL_SECONDS = 5;
|
|
7658
|
-
var CIBA_GRANT_TYPE = "urn:openid:params:grant-type:ciba";
|
|
7659
|
-
var issueBackchannelAuth = async ({
|
|
7660
|
-
clientId,
|
|
7661
|
-
config,
|
|
7662
|
-
loginHint,
|
|
7663
|
-
bindingMessage,
|
|
7664
|
-
now = Date.now(),
|
|
7665
|
-
requestedScopes
|
|
7666
|
-
}) => {
|
|
7667
|
-
if (!config.backchannelAuthStore || !config.resolveBackchannelUser) {
|
|
7668
|
-
return { error: "invalid_request", ok: false };
|
|
7669
|
-
}
|
|
7670
|
-
const client = await config.clientStore.findClient(clientId) ?? await config.resolveClientIdMetadata?.(clientId);
|
|
7671
|
-
if (!client)
|
|
7672
|
-
return { error: "invalid_client", ok: false };
|
|
7673
|
-
const resolved = await config.resolveBackchannelUser({
|
|
7674
|
-
client,
|
|
7675
|
-
loginHint
|
|
7676
|
-
});
|
|
7677
|
-
if (!resolved)
|
|
7678
|
-
return { error: "unknown_user_id", ok: false };
|
|
7679
|
-
const authReqId = generateSecureToken(AUTH_REQ_ID_BYTES);
|
|
7680
|
-
const ttl = config.backchannelAuthTtlMs ?? DEFAULT_BACKCHANNEL_TTL_MS;
|
|
7681
|
-
const interval = config.backchannelPollIntervalSeconds ?? DEFAULT_BACKCHANNEL_POLL_INTERVAL_SECONDS;
|
|
7682
|
-
await config.backchannelAuthStore.saveBackchannelAuth({
|
|
7683
|
-
authReqId,
|
|
7684
|
-
bindingMessage,
|
|
7685
|
-
clientId,
|
|
7686
|
-
createdAt: now,
|
|
7687
|
-
expiresAt: now + ttl,
|
|
7688
|
-
intervalSeconds: interval,
|
|
7689
|
-
scopes: requestedScopes,
|
|
7690
|
-
status: "pending",
|
|
7691
|
-
userSub: resolved.sub
|
|
7692
|
-
});
|
|
7693
|
-
await config.onBackchannelAuthRequest?.({
|
|
7694
|
-
authReqId,
|
|
7695
|
-
bindingMessage,
|
|
7696
|
-
clientId,
|
|
7697
|
-
scopes: requestedScopes,
|
|
7698
|
-
userSub: resolved.sub
|
|
7699
|
-
});
|
|
7700
|
-
return {
|
|
7701
|
-
auth_req_id: authReqId,
|
|
7702
|
-
expires_in: Math.floor(ttl / MS_PER_SECOND),
|
|
7703
|
-
interval,
|
|
7704
|
-
ok: true
|
|
7705
|
-
};
|
|
7706
|
-
};
|
|
7707
|
-
var decideBackchannel = async (config, authReqId, approval) => {
|
|
7708
|
-
if (!config.backchannelAuthStore) {
|
|
7709
|
-
return { error: "not_configured", ok: false };
|
|
7710
|
-
}
|
|
7711
|
-
const record = await config.backchannelAuthStore.findByAuthReqId(authReqId);
|
|
7712
|
-
if (!record)
|
|
7713
|
-
return { error: "invalid_auth_req_id", ok: false };
|
|
7714
|
-
if (record.expiresAt < Date.now()) {
|
|
7715
|
-
return { error: "expired_token", ok: false };
|
|
7716
|
-
}
|
|
7717
|
-
if (record.status !== "pending") {
|
|
7718
|
-
return { error: "already_decided", ok: false };
|
|
7719
|
-
}
|
|
7720
|
-
await config.backchannelAuthStore.updateStatus(authReqId, approval.status, approval.userSub ?? record.userSub);
|
|
7721
|
-
return { ok: true };
|
|
7722
|
-
};
|
|
7723
|
-
var approveBackchannelAuth = async ({
|
|
7724
|
-
authReqId,
|
|
7725
|
-
config,
|
|
7726
|
-
userSub
|
|
7727
|
-
}) => decideBackchannel(config, authReqId, {
|
|
7728
|
-
status: "approved",
|
|
7729
|
-
userSub
|
|
7730
|
-
});
|
|
7731
|
-
var denyBackchannelAuth = async ({
|
|
7732
|
-
authReqId,
|
|
7733
|
-
config
|
|
7734
|
-
}) => decideBackchannel(config, authReqId, { status: "denied" });
|
|
7735
|
-
var exchangeBackchannelAuth = async ({
|
|
7736
|
-
authReqId,
|
|
7737
|
-
clientCertThumbprint,
|
|
7738
|
-
clientId,
|
|
7739
|
-
config,
|
|
7740
|
-
dpopJkt,
|
|
7741
|
-
now = Date.now()
|
|
7742
|
-
}) => {
|
|
7743
|
-
if (!config.backchannelAuthStore) {
|
|
7744
|
-
return { error: "invalid_grant", ok: false };
|
|
7745
|
-
}
|
|
7746
|
-
const record = await config.backchannelAuthStore.findByAuthReqId(authReqId);
|
|
7747
|
-
if (!record || record.clientId !== clientId) {
|
|
7748
|
-
return { error: "invalid_grant", ok: false };
|
|
7749
|
-
}
|
|
7750
|
-
if (record.expiresAt < now) {
|
|
7751
|
-
await config.backchannelAuthStore.deleteByAuthReqId(authReqId);
|
|
7752
|
-
return { error: "expired_token", ok: false };
|
|
7753
|
-
}
|
|
7754
|
-
if (record.lastPolledAt !== undefined && now - record.lastPolledAt < record.intervalSeconds * MS_PER_SECOND) {
|
|
7755
|
-
return { error: "slow_down", ok: false };
|
|
7756
|
-
}
|
|
7757
|
-
await config.backchannelAuthStore.recordPoll(authReqId, now);
|
|
7758
|
-
if (record.status === "pending") {
|
|
7759
|
-
return { error: "authorization_pending", ok: false };
|
|
7760
|
-
}
|
|
7761
|
-
if (record.status === "denied" || record.userSub === undefined) {
|
|
7762
|
-
return { error: "access_denied", ok: false };
|
|
7763
|
-
}
|
|
7764
|
-
await config.backchannelAuthStore.deleteByAuthReqId(authReqId);
|
|
7765
|
-
const tokenSet = await issueTokenSet({
|
|
7766
|
-
clientCertThumbprint,
|
|
7767
|
-
clientId,
|
|
7768
|
-
config,
|
|
7769
|
-
dpopJkt,
|
|
7770
|
-
now,
|
|
7771
|
-
scopes: record.scopes,
|
|
7772
|
-
sub: record.userSub
|
|
7773
|
-
});
|
|
7774
|
-
return { ...tokenSet, ok: true };
|
|
7775
|
-
};
|
|
7776
|
-
|
|
7777
7788
|
// src/oidc/clientAuth.ts
|
|
7778
7789
|
init_constants();
|
|
7779
7790
|
var CLIENT_ASSERTION_TYPE = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
|
|
@@ -25782,10 +25793,9 @@ var toUnregisteredSessionData = (row) => ({
|
|
|
25782
25793
|
sessionInformation: cloneRecord(row.session_information_json ?? undefined),
|
|
25783
25794
|
userIdentity: cloneRecord(row.user_identity_json ?? undefined)
|
|
25784
25795
|
});
|
|
25785
|
-
var createNeonAuthSessionStore = (databaseUrl, decodeUser) => {
|
|
25786
|
-
|
|
25787
|
-
const
|
|
25788
|
-
return {
|
|
25796
|
+
var createNeonAuthSessionStore = (databaseUrl, decodeUser) => createPostgresAuthSessionStore(drizzle({ client: as(databaseUrl) }), decodeUser);
|
|
25797
|
+
var createPostgresAuthSessionStore = (db, decodeUser) => {
|
|
25798
|
+
const store = {
|
|
25789
25799
|
deleteExpired: async () => {
|
|
25790
25800
|
const rows = await db.delete(authSessionsTable).where(lt(authSessionsTable.expires_at_ms, Date.now())).returning({ id: authSessionsTable.id });
|
|
25791
25801
|
return rows.length;
|
|
@@ -25863,6 +25873,7 @@ var createNeonAuthSessionStore = (databaseUrl, decodeUser) => {
|
|
|
25863
25873
|
});
|
|
25864
25874
|
}
|
|
25865
25875
|
};
|
|
25876
|
+
return store;
|
|
25866
25877
|
};
|
|
25867
25878
|
// src/providersFromEnv.ts
|
|
25868
25879
|
var envKey = (provider, suffix) => `${provider.replace(/[^a-zA-Z0-9]/g, "_").toUpperCase()}_${suffix}`;
|
|
@@ -33959,6 +33970,30 @@ var createRedisAuthSessionStore = (redis, decodeUser, keyPrefix = "auth:session:
|
|
|
33959
33970
|
}
|
|
33960
33971
|
};
|
|
33961
33972
|
};
|
|
33973
|
+
// src/linkedProviders/credentialError.ts
|
|
33974
|
+
class LinkedProviderCredentialError extends Error {
|
|
33975
|
+
code;
|
|
33976
|
+
recovery;
|
|
33977
|
+
constructor(code, recovery, message, cause) {
|
|
33978
|
+
super(message, { cause });
|
|
33979
|
+
this.code = code;
|
|
33980
|
+
this.recovery = recovery;
|
|
33981
|
+
this.name = "LinkedProviderCredentialError";
|
|
33982
|
+
}
|
|
33983
|
+
}
|
|
33984
|
+
var credentialRefreshError = (error2) => {
|
|
33985
|
+
if (error2 instanceof LinkedProviderCredentialError)
|
|
33986
|
+
return error2;
|
|
33987
|
+
const message = error2 instanceof Error ? error2.message : "";
|
|
33988
|
+
if (/\binvalid_grant\b/.test(message)) {
|
|
33989
|
+
return new LinkedProviderCredentialError("invalid_grant", "reconnect", "Authorization has expired or was revoked. Reconnect your account.", error2);
|
|
33990
|
+
}
|
|
33991
|
+
if (/\b(invalid_client|unauthorized_client)\b/.test(message)) {
|
|
33992
|
+
return new LinkedProviderCredentialError("invalid_client", "configuration", "The provider connection is misconfigured. Contact support.", error2);
|
|
33993
|
+
}
|
|
33994
|
+
return new LinkedProviderCredentialError("refresh_failed", "retry", "The provider could not refresh the connection. Try again.", error2);
|
|
33995
|
+
};
|
|
33996
|
+
|
|
33962
33997
|
// src/linkedProviders/resolver.ts
|
|
33963
33998
|
var uniqueStrings = (values) => [...new Set(values)];
|
|
33964
33999
|
var getEffectiveScopes = (grant, binding) => {
|
|
@@ -34056,6 +34091,30 @@ var resolveBindingCredential = async (grantStore, binding, input) => {
|
|
|
34056
34091
|
}
|
|
34057
34092
|
return buildResolvedCredential(grant, binding);
|
|
34058
34093
|
};
|
|
34094
|
+
var refreshAndRecord = async (grant, input, refresh2, grantStore, now) => {
|
|
34095
|
+
let refreshed;
|
|
34096
|
+
try {
|
|
34097
|
+
const result = await refresh2(grant, input);
|
|
34098
|
+
if (!result)
|
|
34099
|
+
throw new Error("Linked provider access token refresh failed");
|
|
34100
|
+
refreshed = result;
|
|
34101
|
+
} catch (cause) {
|
|
34102
|
+
const error2 = credentialRefreshError(cause);
|
|
34103
|
+
await grantStore.saveGrant({
|
|
34104
|
+
...grant,
|
|
34105
|
+
lastRefreshError: error2.message,
|
|
34106
|
+
metadata: { ...grant.metadata, credentialFailureCode: error2.code, credentialRecovery: error2.recovery },
|
|
34107
|
+
status: "refresh_required",
|
|
34108
|
+
updatedAt: now()
|
|
34109
|
+
});
|
|
34110
|
+
throw error2;
|
|
34111
|
+
}
|
|
34112
|
+
const metadata = { ...refreshed.grant.metadata };
|
|
34113
|
+
delete metadata.credentialRecovery;
|
|
34114
|
+
delete metadata.credentialFailureCode;
|
|
34115
|
+
await grantStore.saveGrant({ ...refreshed.grant, lastRefreshError: undefined, metadata });
|
|
34116
|
+
return refreshed;
|
|
34117
|
+
};
|
|
34059
34118
|
var createLinkedProviderCredentialResolver = ({
|
|
34060
34119
|
grantStore,
|
|
34061
34120
|
bindingStore,
|
|
@@ -34085,11 +34144,7 @@ var createLinkedProviderCredentialResolver = ({
|
|
|
34085
34144
|
if (!refreshAccessTokenLease) {
|
|
34086
34145
|
throw new Error("Linked provider access token lease requires refresh");
|
|
34087
34146
|
}
|
|
34088
|
-
const refreshed = await
|
|
34089
|
-
if (!refreshed) {
|
|
34090
|
-
throw new Error("Linked provider access token refresh failed");
|
|
34091
|
-
}
|
|
34092
|
-
await grantStore.saveGrant(refreshed.grant);
|
|
34147
|
+
const refreshed = await refreshAndRecord(grant, input, refreshAccessTokenLease, grantStore, now);
|
|
34093
34148
|
({ lease } = refreshed);
|
|
34094
34149
|
}
|
|
34095
34150
|
if (!lease) {
|
|
@@ -34182,8 +34237,11 @@ var createOAuthLinkedProviderCredentialResolver = async ({
|
|
|
34182
34237
|
tokenType: grant.tokenType
|
|
34183
34238
|
} : null,
|
|
34184
34239
|
refreshAccessTokenLease: async (grant) => {
|
|
34185
|
-
if (!
|
|
34186
|
-
|
|
34240
|
+
if (!grant.refreshTokenCiphertext) {
|
|
34241
|
+
throw new LinkedProviderCredentialError("missing_refresh_token", "reconnect", "Authorization cannot renew automatically. Reconnect your account.");
|
|
34242
|
+
}
|
|
34243
|
+
if (!isValidProviderOption(grant.authProviderKey)) {
|
|
34244
|
+
throw new LinkedProviderCredentialError("unsupported_provider", "configuration", "This provider cannot renew the connection. Contact support.");
|
|
34187
34245
|
}
|
|
34188
34246
|
const providerKey = grant.authProviderKey;
|
|
34189
34247
|
const providerClientName = typeof grant.metadata?.providerClient === "string" && grant.metadata.providerClient.trim().length > 0 ? grant.metadata.providerClient.trim() : undefined;
|
|
@@ -34193,11 +34251,11 @@ var createOAuthLinkedProviderCredentialResolver = async ({
|
|
|
34193
34251
|
providersConfiguration
|
|
34194
34252
|
});
|
|
34195
34253
|
if ("error" in resolvedProviderClientConfiguration || !resolvedProviderClientConfiguration.config) {
|
|
34196
|
-
|
|
34254
|
+
throw new LinkedProviderCredentialError("provider_configuration", "configuration", "The provider connection is misconfigured. Contact support.");
|
|
34197
34255
|
}
|
|
34198
34256
|
const providerClient = await createOAuth2Client(providerKey, resolvedProviderClientConfiguration.config.credentials);
|
|
34199
34257
|
if (!providerClient || !isRefreshableOAuth2Client(providerKey, providerClient)) {
|
|
34200
|
-
|
|
34258
|
+
throw new LinkedProviderCredentialError("unsupported_refresh", "configuration", "This provider cannot renew the connection. Contact support.");
|
|
34201
34259
|
}
|
|
34202
34260
|
const tokenResponse2 = await providerClient.refreshAccessToken(grant.refreshTokenCiphertext);
|
|
34203
34261
|
const refreshedAt = Date.now();
|
|
@@ -40603,6 +40661,7 @@ var buildAuthApplications = async (configuration) => {
|
|
|
40603
40661
|
onRevocationError,
|
|
40604
40662
|
onSessionCleanup
|
|
40605
40663
|
} = configuration;
|
|
40664
|
+
assertTokenRouteConfiguration(apikeys, oidc);
|
|
40606
40665
|
if (push && nativePush2)
|
|
40607
40666
|
throw new Error("Configure `push`, not both `push` and `nativePush`");
|
|
40608
40667
|
const pushConfig = push ?? nativePush2;
|
|
@@ -41133,6 +41192,7 @@ export {
|
|
|
41133
41192
|
createPostgresApiClientStore,
|
|
41134
41193
|
createPostgresApiKeyStore,
|
|
41135
41194
|
createPostgresAuditSink,
|
|
41195
|
+
createPostgresAuthSessionStore,
|
|
41136
41196
|
createPostgresAuthorizationCodeStore,
|
|
41137
41197
|
createPostgresBackchannelAuthStore,
|
|
41138
41198
|
createPostgresClientAssertionJtiStore,
|
|
@@ -41445,5 +41505,5 @@ export {
|
|
|
41445
41505
|
writeWarrant
|
|
41446
41506
|
};
|
|
41447
41507
|
|
|
41448
|
-
//# debugId=
|
|
41508
|
+
//# debugId=637E1C97D77705DC64756E2164756E21
|
|
41449
41509
|
//# sourceMappingURL=index.js.map
|