strapi-plugin-oidc 1.8.4 → 1.8.5
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/README.md +11 -3
- package/dist/admin/{index-Bb9-aYb4.mjs → index-BlfNZYVU.mjs} +1 -1
- package/dist/admin/{index-Dk6TYtio.js → index-C8nfr95D.js} +1 -1
- package/dist/admin/{index-Bmg4eTYb.js → index-DDCcJt16.js} +183 -179
- package/dist/admin/{index-BqWd-Iiq.mjs → index-DRFXk_MQ.mjs} +183 -179
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/server/index.js +222 -217
- package/dist/server/index.mjs +223 -218
- package/package.json +1 -1
package/dist/server/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { randomBytes, randomUUID, createHash } from "node:crypto";
|
|
2
2
|
import pkceChallenge from "pkce-challenge";
|
|
3
3
|
import { jwtVerify, errors, createRemoteJWKSet } from "jose";
|
|
4
4
|
import { Readable } from "node:stream";
|
|
@@ -339,9 +339,149 @@ function clearAuthCookies(strapi2, ctx) {
|
|
|
339
339
|
ctx.cookies.set(name, "", rootPathOptions);
|
|
340
340
|
}
|
|
341
341
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
342
|
+
class OidcError extends Error {
|
|
343
|
+
kind;
|
|
344
|
+
cause;
|
|
345
|
+
constructor(kind, message, cause) {
|
|
346
|
+
super(message);
|
|
347
|
+
this.name = "OidcError";
|
|
348
|
+
this.kind = kind;
|
|
349
|
+
this.cause = cause;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
const OIDC_ERROR_DISPATCH = {
|
|
353
|
+
nonce_mismatch: { action: "nonce_mismatch", code: errorCodes.NONCE_MISMATCH },
|
|
354
|
+
token_exchange_failed: {
|
|
355
|
+
action: "token_exchange_failed",
|
|
356
|
+
code: errorCodes.TOKEN_EXCHANGE_FAILED
|
|
357
|
+
},
|
|
358
|
+
id_token_parse_failed: {
|
|
359
|
+
action: "login_failure",
|
|
360
|
+
code: errorCodes.ID_TOKEN_PARSE_FAILED,
|
|
361
|
+
key: "id_token_parse_failed"
|
|
362
|
+
},
|
|
363
|
+
userinfo_fetch_failed: {
|
|
364
|
+
action: "login_failure",
|
|
365
|
+
code: errorCodes.USERINFO_FETCH_FAILED,
|
|
366
|
+
key: "userinfo_fetch_failed"
|
|
367
|
+
},
|
|
368
|
+
user_creation_failed: {
|
|
369
|
+
action: "login_failure",
|
|
370
|
+
code: errorCodes.USER_CREATION_FAILED,
|
|
371
|
+
key: "user_creation_failed"
|
|
372
|
+
},
|
|
373
|
+
whitelist_rejected: {
|
|
374
|
+
action: "whitelist_rejected",
|
|
375
|
+
code: errorCodes.WHITELIST_CHECK_FAILED,
|
|
376
|
+
key: "whitelist_rejected"
|
|
377
|
+
},
|
|
378
|
+
invalid_email: {
|
|
379
|
+
action: "login_failure",
|
|
380
|
+
code: errorCodes.TOKEN_EXCHANGE_FAILED,
|
|
381
|
+
key: "sign_in_unknown"
|
|
382
|
+
},
|
|
383
|
+
email_not_verified: {
|
|
384
|
+
action: "email_not_verified",
|
|
385
|
+
code: errorCodes.EMAIL_NOT_VERIFIED,
|
|
386
|
+
key: "email_not_verified"
|
|
387
|
+
},
|
|
388
|
+
id_token_invalid: {
|
|
389
|
+
action: "id_token_invalid",
|
|
390
|
+
code: errorCodes.ID_TOKEN_INVALID,
|
|
391
|
+
key: "id_token_invalid"
|
|
392
|
+
},
|
|
393
|
+
unknown: {
|
|
394
|
+
action: "login_failure",
|
|
395
|
+
code: errorCodes.TOKEN_EXCHANGE_FAILED,
|
|
396
|
+
key: "sign_in_unknown"
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
function toMessage(e) {
|
|
400
|
+
return e instanceof Error ? e.message : String(e);
|
|
401
|
+
}
|
|
402
|
+
const REQUIRED_CONFIG_KEYS = [
|
|
403
|
+
"OIDC_DISCOVERY_URL",
|
|
404
|
+
"OIDC_CLIENT_ID",
|
|
405
|
+
"OIDC_CLIENT_SECRET",
|
|
406
|
+
"OIDC_REDIRECT_URI",
|
|
407
|
+
"OIDC_SCOPE",
|
|
408
|
+
"OIDC_FAMILY_NAME_FIELD",
|
|
409
|
+
"OIDC_GIVEN_NAME_FIELD",
|
|
410
|
+
// Populated at bootstrap from OIDC_DISCOVERY_URL — checked here as a runtime safety net
|
|
411
|
+
"OIDC_TOKEN_ENDPOINT",
|
|
412
|
+
"OIDC_USERINFO_ENDPOINT",
|
|
413
|
+
"OIDC_AUTHORIZATION_ENDPOINT"
|
|
414
|
+
];
|
|
415
|
+
const jwksCache = /* @__PURE__ */ new Map();
|
|
416
|
+
let jwksDisabledWarned = false;
|
|
417
|
+
function getJwks(uri) {
|
|
418
|
+
let jwks = jwksCache.get(uri);
|
|
419
|
+
if (!jwks) {
|
|
420
|
+
jwks = createRemoteJWKSet(new URL(uri));
|
|
421
|
+
jwksCache.set(uri, jwks);
|
|
422
|
+
}
|
|
423
|
+
return jwks;
|
|
424
|
+
}
|
|
425
|
+
async function verifyIdToken(idToken, config2) {
|
|
426
|
+
const jwksUri = config2.OIDC_JWKS_URI;
|
|
427
|
+
const issuer = config2.OIDC_ISSUER;
|
|
428
|
+
if (!jwksUri) {
|
|
429
|
+
if (!jwksDisabledWarned) {
|
|
430
|
+
jwksDisabledWarned = true;
|
|
431
|
+
strapi.log.warn(errorMessages.JWKS_URI_NOT_CONFIGURED);
|
|
432
|
+
}
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
435
|
+
try {
|
|
436
|
+
const jwks = getJwks(jwksUri);
|
|
437
|
+
const { payload } = await jwtVerify(idToken, jwks, {
|
|
438
|
+
issuer: issuer || void 0,
|
|
439
|
+
audience: config2.OIDC_CLIENT_ID
|
|
440
|
+
});
|
|
441
|
+
return payload;
|
|
442
|
+
} catch (e) {
|
|
443
|
+
if (e instanceof errors.JWTClaimValidationFailed || e instanceof errors.JWSSignatureVerificationFailed || e instanceof errors.JWTExpired || e instanceof errors.JWTInvalid || e instanceof errors.JWSInvalid) {
|
|
444
|
+
const msg = toMessage(e);
|
|
445
|
+
throw new OidcError("id_token_invalid", msg, e);
|
|
446
|
+
}
|
|
447
|
+
throw e;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
function configValidation() {
|
|
451
|
+
const config2 = strapi.config.get("plugin::strapi-plugin-oidc");
|
|
452
|
+
const missing = REQUIRED_CONFIG_KEYS.filter((key) => !config2[key]);
|
|
453
|
+
if (missing.length === 0) {
|
|
454
|
+
return config2;
|
|
455
|
+
}
|
|
456
|
+
throw new Error(errorMessages.MISSING_CONFIG(missing.join(", ")));
|
|
457
|
+
}
|
|
458
|
+
async function oidcSignIn(ctx) {
|
|
459
|
+
const { OIDC_CLIENT_ID, OIDC_REDIRECT_URI, OIDC_SCOPE, OIDC_AUTHORIZATION_ENDPOINT } = configValidation();
|
|
460
|
+
const { code_verifier: codeVerifier, code_challenge: codeChallenge } = await pkceChallenge();
|
|
461
|
+
const state = randomBytes(32).toString("base64url");
|
|
462
|
+
const nonce = randomBytes(32).toString("base64url");
|
|
463
|
+
const cookieOptions = {
|
|
464
|
+
httpOnly: true,
|
|
465
|
+
maxAge: 6e5,
|
|
466
|
+
secure: shouldMarkSecure(strapi, ctx),
|
|
467
|
+
sameSite: "lax"
|
|
468
|
+
};
|
|
469
|
+
ctx.cookies.set("oidc_code_verifier", codeVerifier, cookieOptions);
|
|
470
|
+
ctx.cookies.set("oidc_state", state, cookieOptions);
|
|
471
|
+
ctx.cookies.set("oidc_nonce", nonce, cookieOptions);
|
|
472
|
+
const params = new URLSearchParams({
|
|
473
|
+
response_type: "code",
|
|
474
|
+
client_id: OIDC_CLIENT_ID,
|
|
475
|
+
redirect_uri: OIDC_REDIRECT_URI,
|
|
476
|
+
scope: OIDC_SCOPE,
|
|
477
|
+
code_challenge: codeChallenge,
|
|
478
|
+
code_challenge_method: "S256",
|
|
479
|
+
state,
|
|
480
|
+
nonce
|
|
481
|
+
});
|
|
482
|
+
const authorizationUrl = `${OIDC_AUTHORIZATION_ENDPOINT}?${params.toString()}`;
|
|
483
|
+
ctx.set("Location", authorizationUrl);
|
|
484
|
+
return ctx.send({}, 302);
|
|
345
485
|
}
|
|
346
486
|
const en = {
|
|
347
487
|
"global.plugins.strapi-plugin-oidc": "OIDC Plugin",
|
|
@@ -504,63 +644,6 @@ const authPageMessages = (locale) => ({
|
|
|
504
644
|
errorTitle: t(locale, "auth.page.error.title", "Authentication Failed"),
|
|
505
645
|
returnToLogin: t(locale, "auth.page.error.returnToLogin", "Return to Login")
|
|
506
646
|
});
|
|
507
|
-
class OidcError extends Error {
|
|
508
|
-
kind;
|
|
509
|
-
cause;
|
|
510
|
-
constructor(kind, message, cause) {
|
|
511
|
-
super(message);
|
|
512
|
-
this.name = "OidcError";
|
|
513
|
-
this.kind = kind;
|
|
514
|
-
this.cause = cause;
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
const OIDC_ERROR_DISPATCH = {
|
|
518
|
-
nonce_mismatch: { action: "nonce_mismatch", code: errorCodes.NONCE_MISMATCH },
|
|
519
|
-
token_exchange_failed: {
|
|
520
|
-
action: "token_exchange_failed",
|
|
521
|
-
code: errorCodes.TOKEN_EXCHANGE_FAILED
|
|
522
|
-
},
|
|
523
|
-
id_token_parse_failed: {
|
|
524
|
-
action: "login_failure",
|
|
525
|
-
code: errorCodes.ID_TOKEN_PARSE_FAILED,
|
|
526
|
-
key: "id_token_parse_failed"
|
|
527
|
-
},
|
|
528
|
-
userinfo_fetch_failed: {
|
|
529
|
-
action: "login_failure",
|
|
530
|
-
code: errorCodes.USERINFO_FETCH_FAILED,
|
|
531
|
-
key: "userinfo_fetch_failed"
|
|
532
|
-
},
|
|
533
|
-
user_creation_failed: {
|
|
534
|
-
action: "login_failure",
|
|
535
|
-
code: errorCodes.USER_CREATION_FAILED,
|
|
536
|
-
key: "user_creation_failed"
|
|
537
|
-
},
|
|
538
|
-
whitelist_rejected: {
|
|
539
|
-
action: "whitelist_rejected",
|
|
540
|
-
code: errorCodes.WHITELIST_CHECK_FAILED,
|
|
541
|
-
key: "whitelist_rejected"
|
|
542
|
-
},
|
|
543
|
-
invalid_email: {
|
|
544
|
-
action: "login_failure",
|
|
545
|
-
code: errorCodes.TOKEN_EXCHANGE_FAILED,
|
|
546
|
-
key: "sign_in_unknown"
|
|
547
|
-
},
|
|
548
|
-
email_not_verified: {
|
|
549
|
-
action: "email_not_verified",
|
|
550
|
-
code: errorCodes.EMAIL_NOT_VERIFIED,
|
|
551
|
-
key: "email_not_verified"
|
|
552
|
-
},
|
|
553
|
-
id_token_invalid: {
|
|
554
|
-
action: "id_token_invalid",
|
|
555
|
-
code: errorCodes.ID_TOKEN_INVALID,
|
|
556
|
-
key: "id_token_invalid"
|
|
557
|
-
},
|
|
558
|
-
unknown: {
|
|
559
|
-
action: "login_failure",
|
|
560
|
-
code: errorCodes.TOKEN_EXCHANGE_FAILED,
|
|
561
|
-
key: "sign_in_unknown"
|
|
562
|
-
}
|
|
563
|
-
};
|
|
564
647
|
const TRUSTED_IP_HEADERS = /* @__PURE__ */ new Set([
|
|
565
648
|
"cf-connecting-ip",
|
|
566
649
|
"true-client-ip",
|
|
@@ -591,128 +674,9 @@ function getClientIp(ctx) {
|
|
|
591
674
|
}
|
|
592
675
|
return ctx.ip;
|
|
593
676
|
}
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
const REQUIRED_CONFIG_KEYS = [
|
|
598
|
-
"OIDC_DISCOVERY_URL",
|
|
599
|
-
"OIDC_CLIENT_ID",
|
|
600
|
-
"OIDC_CLIENT_SECRET",
|
|
601
|
-
"OIDC_REDIRECT_URI",
|
|
602
|
-
"OIDC_SCOPE",
|
|
603
|
-
"OIDC_FAMILY_NAME_FIELD",
|
|
604
|
-
"OIDC_GIVEN_NAME_FIELD",
|
|
605
|
-
// Populated at bootstrap from OIDC_DISCOVERY_URL — checked here as a runtime safety net
|
|
606
|
-
"OIDC_TOKEN_ENDPOINT",
|
|
607
|
-
"OIDC_USERINFO_ENDPOINT",
|
|
608
|
-
"OIDC_AUTHORIZATION_ENDPOINT"
|
|
609
|
-
];
|
|
610
|
-
const LOGOUT_USERINFO_TIMEOUT_MS = 1500;
|
|
611
|
-
const jwksCache = /* @__PURE__ */ new Map();
|
|
612
|
-
let jwksDisabledWarned = false;
|
|
613
|
-
function getJwks(uri) {
|
|
614
|
-
let jwks = jwksCache.get(uri);
|
|
615
|
-
if (!jwks) {
|
|
616
|
-
jwks = createRemoteJWKSet(new URL(uri));
|
|
617
|
-
jwksCache.set(uri, jwks);
|
|
618
|
-
}
|
|
619
|
-
return jwks;
|
|
620
|
-
}
|
|
621
|
-
async function verifyIdToken(idToken, config2) {
|
|
622
|
-
const jwksUri = config2.OIDC_JWKS_URI;
|
|
623
|
-
const issuer = config2.OIDC_ISSUER;
|
|
624
|
-
if (!jwksUri) {
|
|
625
|
-
if (!jwksDisabledWarned) {
|
|
626
|
-
jwksDisabledWarned = true;
|
|
627
|
-
strapi.log.warn(errorMessages.JWKS_URI_NOT_CONFIGURED);
|
|
628
|
-
}
|
|
629
|
-
return null;
|
|
630
|
-
}
|
|
631
|
-
try {
|
|
632
|
-
const jwks = getJwks(jwksUri);
|
|
633
|
-
const { payload } = await jwtVerify(idToken, jwks, {
|
|
634
|
-
issuer: issuer || void 0,
|
|
635
|
-
audience: config2.OIDC_CLIENT_ID
|
|
636
|
-
});
|
|
637
|
-
return payload;
|
|
638
|
-
} catch (e) {
|
|
639
|
-
if (e instanceof errors.JWTClaimValidationFailed || e instanceof errors.JWSSignatureVerificationFailed || e instanceof errors.JWTExpired || e instanceof errors.JWTInvalid || e instanceof errors.JWSInvalid) {
|
|
640
|
-
const msg = toMessage(e);
|
|
641
|
-
throw new OidcError("id_token_invalid", msg, e);
|
|
642
|
-
}
|
|
643
|
-
throw e;
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
function configValidation() {
|
|
647
|
-
const config2 = strapi.config.get("plugin::strapi-plugin-oidc");
|
|
648
|
-
const missing = REQUIRED_CONFIG_KEYS.filter((key) => !config2[key]);
|
|
649
|
-
if (missing.length === 0) {
|
|
650
|
-
return config2;
|
|
651
|
-
}
|
|
652
|
-
throw new Error(errorMessages.MISSING_CONFIG(missing.join(", ")));
|
|
653
|
-
}
|
|
654
|
-
async function oidcSignIn(ctx) {
|
|
655
|
-
const { OIDC_CLIENT_ID, OIDC_REDIRECT_URI, OIDC_SCOPE, OIDC_AUTHORIZATION_ENDPOINT } = configValidation();
|
|
656
|
-
const { code_verifier: codeVerifier, code_challenge: codeChallenge } = await pkceChallenge();
|
|
657
|
-
const state = randomBytes(32).toString("base64url");
|
|
658
|
-
const nonce = randomBytes(32).toString("base64url");
|
|
659
|
-
const cookieOptions = {
|
|
660
|
-
httpOnly: true,
|
|
661
|
-
maxAge: 6e5,
|
|
662
|
-
secure: shouldMarkSecure(strapi, ctx),
|
|
663
|
-
sameSite: "lax"
|
|
664
|
-
};
|
|
665
|
-
ctx.cookies.set("oidc_code_verifier", codeVerifier, cookieOptions);
|
|
666
|
-
ctx.cookies.set("oidc_state", state, cookieOptions);
|
|
667
|
-
ctx.cookies.set("oidc_nonce", nonce, cookieOptions);
|
|
668
|
-
const params = new URLSearchParams({
|
|
669
|
-
response_type: "code",
|
|
670
|
-
client_id: OIDC_CLIENT_ID,
|
|
671
|
-
redirect_uri: OIDC_REDIRECT_URI,
|
|
672
|
-
scope: OIDC_SCOPE,
|
|
673
|
-
code_challenge: codeChallenge,
|
|
674
|
-
code_challenge_method: "S256",
|
|
675
|
-
state,
|
|
676
|
-
nonce
|
|
677
|
-
});
|
|
678
|
-
const authorizationUrl = `${OIDC_AUTHORIZATION_ENDPOINT}?${params.toString()}`;
|
|
679
|
-
ctx.set("Location", authorizationUrl);
|
|
680
|
-
return ctx.send({}, 302);
|
|
681
|
-
}
|
|
682
|
-
async function exchangeTokenAndFetchUserInfo(config2, params, expectedNonce) {
|
|
683
|
-
const response = await fetch(config2.OIDC_TOKEN_ENDPOINT, {
|
|
684
|
-
method: "POST",
|
|
685
|
-
body: params,
|
|
686
|
-
headers: {
|
|
687
|
-
"Content-Type": "application/x-www-form-urlencoded"
|
|
688
|
-
}
|
|
689
|
-
});
|
|
690
|
-
if (!response.ok) {
|
|
691
|
-
throw new OidcError("token_exchange_failed", errorMessages.TOKEN_EXCHANGE_FAILED);
|
|
692
|
-
}
|
|
693
|
-
const tokenData = await response.json();
|
|
694
|
-
if (tokenData.id_token) {
|
|
695
|
-
const verifiedPayload = await verifyIdToken(tokenData.id_token, config2);
|
|
696
|
-
try {
|
|
697
|
-
const idTokenPayload = verifiedPayload ?? JSON.parse(
|
|
698
|
-
Buffer.from(tokenData.id_token.split(".")[1], "base64url").toString("utf8")
|
|
699
|
-
);
|
|
700
|
-
if (idTokenPayload.nonce !== expectedNonce) {
|
|
701
|
-
throw new OidcError("nonce_mismatch", errorMessages.NONCE_MISMATCH);
|
|
702
|
-
}
|
|
703
|
-
} catch (e) {
|
|
704
|
-
if (e instanceof OidcError) throw e;
|
|
705
|
-
throw new OidcError("id_token_parse_failed", errorMessages.ID_TOKEN_PARSE_FAILED, e);
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
const userResponse = await fetch(config2.OIDC_USERINFO_ENDPOINT, {
|
|
709
|
-
headers: { Authorization: `Bearer ${tokenData.access_token}` }
|
|
710
|
-
});
|
|
711
|
-
if (!userResponse.ok) {
|
|
712
|
-
throw new OidcError("userinfo_fetch_failed", errorMessages.USERINFO_FETCH_FAILED);
|
|
713
|
-
}
|
|
714
|
-
const userInfo = await userResponse.json();
|
|
715
|
-
return { userInfo, accessToken: tokenData.access_token };
|
|
677
|
+
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
678
|
+
function isValidEmail(email) {
|
|
679
|
+
return EMAIL_REGEX.test(email);
|
|
716
680
|
}
|
|
717
681
|
function collectGroupMapRoleNames(userInfo, config2) {
|
|
718
682
|
const rawGroups = userInfo[config2.OIDC_GROUP_FIELD];
|
|
@@ -892,6 +856,61 @@ function classifyOidcError(e, userInfo) {
|
|
|
892
856
|
params
|
|
893
857
|
};
|
|
894
858
|
}
|
|
859
|
+
async function handleCallbackError(e, userInfo, auditLog2, oauthService2, ctx) {
|
|
860
|
+
const errorInfo = classifyOidcError(e, userInfo);
|
|
861
|
+
const message = toMessage(e);
|
|
862
|
+
await auditLog2.log({
|
|
863
|
+
action: errorInfo.action,
|
|
864
|
+
email: userInfo?.email,
|
|
865
|
+
ip: getClientIp(ctx),
|
|
866
|
+
detailsKey: errorInfo.action,
|
|
867
|
+
detailsParams: errorInfo.action === "login_failure" ? { message } : void 0
|
|
868
|
+
});
|
|
869
|
+
strapi.log.error({
|
|
870
|
+
code: errorInfo.code,
|
|
871
|
+
phase: "oidc_callback",
|
|
872
|
+
message: e instanceof Error ? e.message : "Unknown sign-in error",
|
|
873
|
+
detail: errorInfo.key ? getErrorDetail(errorInfo.key, errorInfo.params) : void 0,
|
|
874
|
+
email: userInfo?.email
|
|
875
|
+
});
|
|
876
|
+
const locale = negotiateLocale(ctx.request.headers["accept-language"]);
|
|
877
|
+
ctx.send(oauthService2.renderSignUpError(userFacingMessages(locale).signInError, locale));
|
|
878
|
+
}
|
|
879
|
+
async function exchangeTokenAndFetchUserInfo(config2, params, expectedNonce) {
|
|
880
|
+
const response = await fetch(config2.OIDC_TOKEN_ENDPOINT, {
|
|
881
|
+
method: "POST",
|
|
882
|
+
body: params,
|
|
883
|
+
headers: {
|
|
884
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
885
|
+
}
|
|
886
|
+
});
|
|
887
|
+
if (!response.ok) {
|
|
888
|
+
throw new OidcError("token_exchange_failed", errorMessages.TOKEN_EXCHANGE_FAILED);
|
|
889
|
+
}
|
|
890
|
+
const tokenData = await response.json();
|
|
891
|
+
if (tokenData.id_token) {
|
|
892
|
+
const verifiedPayload = await verifyIdToken(tokenData.id_token, config2);
|
|
893
|
+
try {
|
|
894
|
+
const idTokenPayload = verifiedPayload ?? JSON.parse(
|
|
895
|
+
Buffer.from(tokenData.id_token.split(".")[1], "base64url").toString("utf8")
|
|
896
|
+
);
|
|
897
|
+
if (idTokenPayload.nonce !== expectedNonce) {
|
|
898
|
+
throw new OidcError("nonce_mismatch", errorMessages.NONCE_MISMATCH);
|
|
899
|
+
}
|
|
900
|
+
} catch (e) {
|
|
901
|
+
if (e instanceof OidcError) throw e;
|
|
902
|
+
throw new OidcError("id_token_parse_failed", errorMessages.ID_TOKEN_PARSE_FAILED, e);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
const userResponse = await fetch(config2.OIDC_USERINFO_ENDPOINT, {
|
|
906
|
+
headers: { Authorization: `Bearer ${tokenData.access_token}` }
|
|
907
|
+
});
|
|
908
|
+
if (!userResponse.ok) {
|
|
909
|
+
throw new OidcError("userinfo_fetch_failed", errorMessages.USERINFO_FETCH_FAILED);
|
|
910
|
+
}
|
|
911
|
+
const userInfo = await userResponse.json();
|
|
912
|
+
return { userInfo, accessToken: tokenData.access_token };
|
|
913
|
+
}
|
|
895
914
|
function readAndClearPkceCookies(ctx) {
|
|
896
915
|
const oidcState = ctx.cookies.get("oidc_state");
|
|
897
916
|
const codeVerifier = ctx.cookies.get("oidc_code_verifier");
|
|
@@ -925,26 +944,6 @@ async function logSuccessfulAuth(auditLog2, ctx, user, userCreated, rolesUpdated
|
|
|
925
944
|
}
|
|
926
945
|
await Promise.all(entries);
|
|
927
946
|
}
|
|
928
|
-
async function handleCallbackError(e, userInfo, auditLog2, oauthService2, ctx) {
|
|
929
|
-
const errorInfo = classifyOidcError(e, userInfo);
|
|
930
|
-
const message = toMessage(e);
|
|
931
|
-
await auditLog2.log({
|
|
932
|
-
action: errorInfo.action,
|
|
933
|
-
email: userInfo?.email,
|
|
934
|
-
ip: getClientIp(ctx),
|
|
935
|
-
detailsKey: errorInfo.action,
|
|
936
|
-
detailsParams: errorInfo.action === "login_failure" ? { message } : void 0
|
|
937
|
-
});
|
|
938
|
-
strapi.log.error({
|
|
939
|
-
code: errorInfo.code,
|
|
940
|
-
phase: "oidc_callback",
|
|
941
|
-
message: e instanceof Error ? e.message : "Unknown sign-in error",
|
|
942
|
-
detail: errorInfo.key ? getErrorDetail(errorInfo.key, errorInfo.params) : void 0,
|
|
943
|
-
email: userInfo?.email
|
|
944
|
-
});
|
|
945
|
-
const locale = negotiateLocale(ctx.request.headers["accept-language"]);
|
|
946
|
-
ctx.send(oauthService2.renderSignUpError(userFacingMessages(locale).signInError, locale));
|
|
947
|
-
}
|
|
948
947
|
async function oidcSignInCallback(ctx) {
|
|
949
948
|
const config2 = configValidation();
|
|
950
949
|
const oauthService2 = getOauthService();
|
|
@@ -1012,6 +1011,7 @@ async function oidcSignInCallback(ctx) {
|
|
|
1012
1011
|
await handleCallbackError(e, userInfo, auditLog2, oauthService2, ctx);
|
|
1013
1012
|
}
|
|
1014
1013
|
}
|
|
1014
|
+
const LOGOUT_USERINFO_TIMEOUT_MS = 1500;
|
|
1015
1015
|
async function isProviderSessionExpired(userinfoEndpoint, accessToken) {
|
|
1016
1016
|
try {
|
|
1017
1017
|
const response = await fetch(userinfoEndpoint, {
|
|
@@ -2055,6 +2055,7 @@ function translateDetails(key, params) {
|
|
|
2055
2055
|
if (!translation) return null;
|
|
2056
2056
|
return interpolate(translation, params);
|
|
2057
2057
|
}
|
|
2058
|
+
const DAY_MS = 864e5;
|
|
2058
2059
|
const STRING_OP_MAP = {
|
|
2059
2060
|
$eq: (v) => v,
|
|
2060
2061
|
$contains: (v) => ({ $containsi: v }),
|
|
@@ -2067,9 +2068,11 @@ const DATE_OP_MAP = {
|
|
|
2067
2068
|
$lt: (v) => ({ $lt: v }),
|
|
2068
2069
|
$lte: (v) => ({ $lte: v }),
|
|
2069
2070
|
$between: (v) => ({ $between: v })
|
|
2070
|
-
// $in is handled separately: each ISO day-start is expanded to a [day, day+1) range.
|
|
2071
2071
|
};
|
|
2072
|
-
const
|
|
2072
|
+
const ACTION_OP_MAP = {
|
|
2073
|
+
$eq: (v) => v,
|
|
2074
|
+
$in: (v) => ({ $in: v })
|
|
2075
|
+
};
|
|
2073
2076
|
function nextDayIso(iso) {
|
|
2074
2077
|
return new Date(new Date(iso).getTime() + DAY_MS).toISOString();
|
|
2075
2078
|
}
|
|
@@ -2077,10 +2080,6 @@ function expandCreatedAtInToDayRanges(days) {
|
|
|
2077
2080
|
const ranges = days.map((d) => ({ createdAt: { $gte: d, $lt: nextDayIso(d) } }));
|
|
2078
2081
|
return ranges.length === 1 ? ranges[0] : { $or: ranges };
|
|
2079
2082
|
}
|
|
2080
|
-
const ACTION_OP_MAP = {
|
|
2081
|
-
$eq: (v) => v,
|
|
2082
|
-
$in: (v) => ({ $in: v })
|
|
2083
|
-
};
|
|
2084
2083
|
function mapFieldFilter(conditions, field, filter, opMap) {
|
|
2085
2084
|
for (const [op, value] of Object.entries(filter)) {
|
|
2086
2085
|
const transform = opMap[op];
|
|
@@ -2089,20 +2088,26 @@ function mapFieldFilter(conditions, field, filter, opMap) {
|
|
|
2089
2088
|
if (result !== void 0) conditions.push({ [field]: result });
|
|
2090
2089
|
}
|
|
2091
2090
|
}
|
|
2091
|
+
function buildDateConditions(conditions, createdAt) {
|
|
2092
|
+
if (!createdAt) return;
|
|
2093
|
+
const { $in: inDays, ...rest } = createdAt;
|
|
2094
|
+
if (Array.isArray(inDays) && inDays.length > 0) {
|
|
2095
|
+
conditions.push(expandCreatedAtInToDayRanges(inDays));
|
|
2096
|
+
}
|
|
2097
|
+
for (const [op, value] of Object.entries(rest)) {
|
|
2098
|
+
const transform = DATE_OP_MAP[op];
|
|
2099
|
+
if (transform) {
|
|
2100
|
+
const result = transform(value);
|
|
2101
|
+
if (result !== void 0) conditions.push({ createdAt: result });
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2092
2105
|
function buildWhereClause(filters) {
|
|
2093
2106
|
const conditions = [];
|
|
2094
2107
|
if (filters.action) mapFieldFilter(conditions, "action", filters.action, ACTION_OP_MAP);
|
|
2095
2108
|
if (filters.email) mapFieldFilter(conditions, "email", filters.email, STRING_OP_MAP);
|
|
2096
2109
|
if (filters.ip) mapFieldFilter(conditions, "ip", filters.ip, STRING_OP_MAP);
|
|
2097
|
-
if (filters.createdAt)
|
|
2098
|
-
const { $in: inDays, ...rest } = filters.createdAt;
|
|
2099
|
-
if (Array.isArray(inDays) && inDays.length > 0) {
|
|
2100
|
-
conditions.push(expandCreatedAtInToDayRanges(inDays));
|
|
2101
|
-
}
|
|
2102
|
-
if (Object.keys(rest).length > 0) {
|
|
2103
|
-
mapFieldFilter(conditions, "createdAt", rest, DATE_OP_MAP);
|
|
2104
|
-
}
|
|
2105
|
-
}
|
|
2110
|
+
if (filters.createdAt) buildDateConditions(conditions, filters.createdAt);
|
|
2106
2111
|
if (conditions.length === 0) return {};
|
|
2107
2112
|
if (conditions.length === 1) return conditions[0];
|
|
2108
2113
|
return { $and: conditions };
|
package/package.json
CHANGED