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