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