strapi-plugin-oidc 1.8.3 → 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 +22 -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 +231 -219
- package/dist/server/index.mjs +232 -220
- 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,70 +650,20 @@ 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
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
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
|
-
const TRUSTED_IP_HEADER = "cf-connecting-ip";
|
|
653
|
+
const TRUSTED_IP_HEADERS = /* @__PURE__ */ new Set([
|
|
654
|
+
"cf-connecting-ip",
|
|
655
|
+
"true-client-ip",
|
|
656
|
+
"x-real-ip",
|
|
657
|
+
"fastly-client-ip",
|
|
658
|
+
"fly-client-ip",
|
|
659
|
+
"x-nf-client-connection-ip"
|
|
660
|
+
]);
|
|
571
661
|
function getTrustedHeaderName() {
|
|
572
662
|
const config2 = strapi.config.get("plugin::strapi-plugin-oidc") ?? {};
|
|
573
663
|
const raw = config2.OIDC_TRUSTED_IP_HEADER;
|
|
574
664
|
if (typeof raw !== "string" || !raw) return void 0;
|
|
575
665
|
const normalized = raw.trim().toLowerCase();
|
|
576
|
-
return normalized
|
|
666
|
+
return TRUSTED_IP_HEADERS.has(normalized) ? normalized : void 0;
|
|
577
667
|
}
|
|
578
668
|
function getClientIp(ctx) {
|
|
579
669
|
const proxyTrusted = ctx.app?.proxy === true;
|
|
@@ -590,128 +680,9 @@ function getClientIp(ctx) {
|
|
|
590
680
|
}
|
|
591
681
|
return ctx.ip;
|
|
592
682
|
}
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
const REQUIRED_CONFIG_KEYS = [
|
|
597
|
-
"OIDC_DISCOVERY_URL",
|
|
598
|
-
"OIDC_CLIENT_ID",
|
|
599
|
-
"OIDC_CLIENT_SECRET",
|
|
600
|
-
"OIDC_REDIRECT_URI",
|
|
601
|
-
"OIDC_SCOPE",
|
|
602
|
-
"OIDC_FAMILY_NAME_FIELD",
|
|
603
|
-
"OIDC_GIVEN_NAME_FIELD",
|
|
604
|
-
// Populated at bootstrap from OIDC_DISCOVERY_URL — checked here as a runtime safety net
|
|
605
|
-
"OIDC_TOKEN_ENDPOINT",
|
|
606
|
-
"OIDC_USERINFO_ENDPOINT",
|
|
607
|
-
"OIDC_AUTHORIZATION_ENDPOINT"
|
|
608
|
-
];
|
|
609
|
-
const LOGOUT_USERINFO_TIMEOUT_MS = 1500;
|
|
610
|
-
const jwksCache = /* @__PURE__ */ new Map();
|
|
611
|
-
let jwksDisabledWarned = false;
|
|
612
|
-
function getJwks(uri) {
|
|
613
|
-
let jwks = jwksCache.get(uri);
|
|
614
|
-
if (!jwks) {
|
|
615
|
-
jwks = jose.createRemoteJWKSet(new URL(uri));
|
|
616
|
-
jwksCache.set(uri, jwks);
|
|
617
|
-
}
|
|
618
|
-
return jwks;
|
|
619
|
-
}
|
|
620
|
-
async function verifyIdToken(idToken, config2) {
|
|
621
|
-
const jwksUri = config2.OIDC_JWKS_URI;
|
|
622
|
-
const issuer = config2.OIDC_ISSUER;
|
|
623
|
-
if (!jwksUri) {
|
|
624
|
-
if (!jwksDisabledWarned) {
|
|
625
|
-
jwksDisabledWarned = true;
|
|
626
|
-
strapi.log.warn(errorMessages.JWKS_URI_NOT_CONFIGURED);
|
|
627
|
-
}
|
|
628
|
-
return null;
|
|
629
|
-
}
|
|
630
|
-
try {
|
|
631
|
-
const jwks = getJwks(jwksUri);
|
|
632
|
-
const { payload } = await jose.jwtVerify(idToken, jwks, {
|
|
633
|
-
issuer: issuer || void 0,
|
|
634
|
-
audience: config2.OIDC_CLIENT_ID
|
|
635
|
-
});
|
|
636
|
-
return payload;
|
|
637
|
-
} catch (e) {
|
|
638
|
-
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) {
|
|
639
|
-
const msg = toMessage(e);
|
|
640
|
-
throw new OidcError("id_token_invalid", msg, e);
|
|
641
|
-
}
|
|
642
|
-
throw e;
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
function configValidation() {
|
|
646
|
-
const config2 = strapi.config.get("plugin::strapi-plugin-oidc");
|
|
647
|
-
const missing = REQUIRED_CONFIG_KEYS.filter((key) => !config2[key]);
|
|
648
|
-
if (missing.length === 0) {
|
|
649
|
-
return config2;
|
|
650
|
-
}
|
|
651
|
-
throw new Error(errorMessages.MISSING_CONFIG(missing.join(", ")));
|
|
652
|
-
}
|
|
653
|
-
async function oidcSignIn(ctx) {
|
|
654
|
-
const { OIDC_CLIENT_ID, OIDC_REDIRECT_URI, OIDC_SCOPE, OIDC_AUTHORIZATION_ENDPOINT } = configValidation();
|
|
655
|
-
const { code_verifier: codeVerifier, code_challenge: codeChallenge } = await pkceChallenge__default.default();
|
|
656
|
-
const state = node_crypto.randomBytes(32).toString("base64url");
|
|
657
|
-
const nonce = node_crypto.randomBytes(32).toString("base64url");
|
|
658
|
-
const cookieOptions = {
|
|
659
|
-
httpOnly: true,
|
|
660
|
-
maxAge: 6e5,
|
|
661
|
-
secure: shouldMarkSecure(strapi, ctx),
|
|
662
|
-
sameSite: "lax"
|
|
663
|
-
};
|
|
664
|
-
ctx.cookies.set("oidc_code_verifier", codeVerifier, cookieOptions);
|
|
665
|
-
ctx.cookies.set("oidc_state", state, cookieOptions);
|
|
666
|
-
ctx.cookies.set("oidc_nonce", nonce, cookieOptions);
|
|
667
|
-
const params = new URLSearchParams({
|
|
668
|
-
response_type: "code",
|
|
669
|
-
client_id: OIDC_CLIENT_ID,
|
|
670
|
-
redirect_uri: OIDC_REDIRECT_URI,
|
|
671
|
-
scope: OIDC_SCOPE,
|
|
672
|
-
code_challenge: codeChallenge,
|
|
673
|
-
code_challenge_method: "S256",
|
|
674
|
-
state,
|
|
675
|
-
nonce
|
|
676
|
-
});
|
|
677
|
-
const authorizationUrl = `${OIDC_AUTHORIZATION_ENDPOINT}?${params.toString()}`;
|
|
678
|
-
ctx.set("Location", authorizationUrl);
|
|
679
|
-
return ctx.send({}, 302);
|
|
680
|
-
}
|
|
681
|
-
async function exchangeTokenAndFetchUserInfo(config2, params, expectedNonce) {
|
|
682
|
-
const response = await fetch(config2.OIDC_TOKEN_ENDPOINT, {
|
|
683
|
-
method: "POST",
|
|
684
|
-
body: params,
|
|
685
|
-
headers: {
|
|
686
|
-
"Content-Type": "application/x-www-form-urlencoded"
|
|
687
|
-
}
|
|
688
|
-
});
|
|
689
|
-
if (!response.ok) {
|
|
690
|
-
throw new OidcError("token_exchange_failed", errorMessages.TOKEN_EXCHANGE_FAILED);
|
|
691
|
-
}
|
|
692
|
-
const tokenData = await response.json();
|
|
693
|
-
if (tokenData.id_token) {
|
|
694
|
-
const verifiedPayload = await verifyIdToken(tokenData.id_token, config2);
|
|
695
|
-
try {
|
|
696
|
-
const idTokenPayload = verifiedPayload ?? JSON.parse(
|
|
697
|
-
Buffer.from(tokenData.id_token.split(".")[1], "base64url").toString("utf8")
|
|
698
|
-
);
|
|
699
|
-
if (idTokenPayload.nonce !== expectedNonce) {
|
|
700
|
-
throw new OidcError("nonce_mismatch", errorMessages.NONCE_MISMATCH);
|
|
701
|
-
}
|
|
702
|
-
} catch (e) {
|
|
703
|
-
if (e instanceof OidcError) throw e;
|
|
704
|
-
throw new OidcError("id_token_parse_failed", errorMessages.ID_TOKEN_PARSE_FAILED, e);
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
const userResponse = await fetch(config2.OIDC_USERINFO_ENDPOINT, {
|
|
708
|
-
headers: { Authorization: `Bearer ${tokenData.access_token}` }
|
|
709
|
-
});
|
|
710
|
-
if (!userResponse.ok) {
|
|
711
|
-
throw new OidcError("userinfo_fetch_failed", errorMessages.USERINFO_FETCH_FAILED);
|
|
712
|
-
}
|
|
713
|
-
const userInfo = await userResponse.json();
|
|
714
|
-
return { userInfo, accessToken: tokenData.access_token };
|
|
683
|
+
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
684
|
+
function isValidEmail(email) {
|
|
685
|
+
return EMAIL_REGEX.test(email);
|
|
715
686
|
}
|
|
716
687
|
function collectGroupMapRoleNames(userInfo, config2) {
|
|
717
688
|
const rawGroups = userInfo[config2.OIDC_GROUP_FIELD];
|
|
@@ -891,6 +862,61 @@ function classifyOidcError(e, userInfo) {
|
|
|
891
862
|
params
|
|
892
863
|
};
|
|
893
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
|
+
}
|
|
894
920
|
function readAndClearPkceCookies(ctx) {
|
|
895
921
|
const oidcState = ctx.cookies.get("oidc_state");
|
|
896
922
|
const codeVerifier = ctx.cookies.get("oidc_code_verifier");
|
|
@@ -924,26 +950,6 @@ async function logSuccessfulAuth(auditLog2, ctx, user, userCreated, rolesUpdated
|
|
|
924
950
|
}
|
|
925
951
|
await Promise.all(entries);
|
|
926
952
|
}
|
|
927
|
-
async function handleCallbackError(e, userInfo, auditLog2, oauthService2, ctx) {
|
|
928
|
-
const errorInfo = classifyOidcError(e, userInfo);
|
|
929
|
-
const message = toMessage(e);
|
|
930
|
-
await auditLog2.log({
|
|
931
|
-
action: errorInfo.action,
|
|
932
|
-
email: userInfo?.email,
|
|
933
|
-
ip: getClientIp(ctx),
|
|
934
|
-
detailsKey: errorInfo.action,
|
|
935
|
-
detailsParams: errorInfo.action === "login_failure" ? { message } : void 0
|
|
936
|
-
});
|
|
937
|
-
strapi.log.error({
|
|
938
|
-
code: errorInfo.code,
|
|
939
|
-
phase: "oidc_callback",
|
|
940
|
-
message: e instanceof Error ? e.message : "Unknown sign-in error",
|
|
941
|
-
detail: errorInfo.key ? getErrorDetail(errorInfo.key, errorInfo.params) : void 0,
|
|
942
|
-
email: userInfo?.email
|
|
943
|
-
});
|
|
944
|
-
const locale = negotiateLocale(ctx.request.headers["accept-language"]);
|
|
945
|
-
ctx.send(oauthService2.renderSignUpError(userFacingMessages(locale).signInError, locale));
|
|
946
|
-
}
|
|
947
953
|
async function oidcSignInCallback(ctx) {
|
|
948
954
|
const config2 = configValidation();
|
|
949
955
|
const oauthService2 = getOauthService();
|
|
@@ -1011,6 +1017,7 @@ async function oidcSignInCallback(ctx) {
|
|
|
1011
1017
|
await handleCallbackError(e, userInfo, auditLog2, oauthService2, ctx);
|
|
1012
1018
|
}
|
|
1013
1019
|
}
|
|
1020
|
+
const LOGOUT_USERINFO_TIMEOUT_MS = 1500;
|
|
1014
1021
|
async function isProviderSessionExpired(userinfoEndpoint, accessToken) {
|
|
1015
1022
|
try {
|
|
1016
1023
|
const response = await fetch(userinfoEndpoint, {
|
|
@@ -2054,6 +2061,7 @@ function translateDetails(key, params) {
|
|
|
2054
2061
|
if (!translation) return null;
|
|
2055
2062
|
return interpolate(translation, params);
|
|
2056
2063
|
}
|
|
2064
|
+
const DAY_MS = 864e5;
|
|
2057
2065
|
const STRING_OP_MAP = {
|
|
2058
2066
|
$eq: (v) => v,
|
|
2059
2067
|
$contains: (v) => ({ $containsi: v }),
|
|
@@ -2066,9 +2074,11 @@ const DATE_OP_MAP = {
|
|
|
2066
2074
|
$lt: (v) => ({ $lt: v }),
|
|
2067
2075
|
$lte: (v) => ({ $lte: v }),
|
|
2068
2076
|
$between: (v) => ({ $between: v })
|
|
2069
|
-
// $in is handled separately: each ISO day-start is expanded to a [day, day+1) range.
|
|
2070
2077
|
};
|
|
2071
|
-
const
|
|
2078
|
+
const ACTION_OP_MAP = {
|
|
2079
|
+
$eq: (v) => v,
|
|
2080
|
+
$in: (v) => ({ $in: v })
|
|
2081
|
+
};
|
|
2072
2082
|
function nextDayIso(iso) {
|
|
2073
2083
|
return new Date(new Date(iso).getTime() + DAY_MS).toISOString();
|
|
2074
2084
|
}
|
|
@@ -2076,10 +2086,6 @@ function expandCreatedAtInToDayRanges(days) {
|
|
|
2076
2086
|
const ranges = days.map((d) => ({ createdAt: { $gte: d, $lt: nextDayIso(d) } }));
|
|
2077
2087
|
return ranges.length === 1 ? ranges[0] : { $or: ranges };
|
|
2078
2088
|
}
|
|
2079
|
-
const ACTION_OP_MAP = {
|
|
2080
|
-
$eq: (v) => v,
|
|
2081
|
-
$in: (v) => ({ $in: v })
|
|
2082
|
-
};
|
|
2083
2089
|
function mapFieldFilter(conditions, field, filter, opMap) {
|
|
2084
2090
|
for (const [op, value] of Object.entries(filter)) {
|
|
2085
2091
|
const transform = opMap[op];
|
|
@@ -2088,20 +2094,26 @@ function mapFieldFilter(conditions, field, filter, opMap) {
|
|
|
2088
2094
|
if (result !== void 0) conditions.push({ [field]: result });
|
|
2089
2095
|
}
|
|
2090
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
|
+
}
|
|
2091
2111
|
function buildWhereClause(filters) {
|
|
2092
2112
|
const conditions = [];
|
|
2093
2113
|
if (filters.action) mapFieldFilter(conditions, "action", filters.action, ACTION_OP_MAP);
|
|
2094
2114
|
if (filters.email) mapFieldFilter(conditions, "email", filters.email, STRING_OP_MAP);
|
|
2095
2115
|
if (filters.ip) mapFieldFilter(conditions, "ip", filters.ip, STRING_OP_MAP);
|
|
2096
|
-
if (filters.createdAt)
|
|
2097
|
-
const { $in: inDays, ...rest } = filters.createdAt;
|
|
2098
|
-
if (Array.isArray(inDays) && inDays.length > 0) {
|
|
2099
|
-
conditions.push(expandCreatedAtInToDayRanges(inDays));
|
|
2100
|
-
}
|
|
2101
|
-
if (Object.keys(rest).length > 0) {
|
|
2102
|
-
mapFieldFilter(conditions, "createdAt", rest, DATE_OP_MAP);
|
|
2103
|
-
}
|
|
2104
|
-
}
|
|
2116
|
+
if (filters.createdAt) buildDateConditions(conditions, filters.createdAt);
|
|
2105
2117
|
if (conditions.length === 0) return {};
|
|
2106
2118
|
if (conditions.length === 1) return conditions[0];
|
|
2107
2119
|
return { $and: conditions };
|