strapi-plugin-oidc 1.4.3 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -5
- package/dist/server/index.js +114 -14
- package/dist/server/index.mjs +114 -14
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -34,17 +34,31 @@ module.exports = ({ env }) => ({
|
|
|
34
34
|
OIDC_REDIRECT_URI: env('OIDC_REDIRECT_URI'), // https://your-strapi.com/strapi-plugin-oidc/oidc/callback
|
|
35
35
|
OIDC_AUTHORIZATION_ENDPOINT: env('OIDC_AUTHORIZATION_ENDPOINT'),
|
|
36
36
|
OIDC_TOKEN_ENDPOINT: env('OIDC_TOKEN_ENDPOINT'),
|
|
37
|
-
|
|
37
|
+
OIDC_USERINFO_ENDPOINT: env('OIDC_USERINFO_ENDPOINT'),
|
|
38
38
|
|
|
39
39
|
// Optional — defaults shown
|
|
40
|
-
|
|
40
|
+
OIDC_SCOPE: 'openid profile email',
|
|
41
41
|
OIDC_GRANT_TYPE: 'authorization_code',
|
|
42
|
-
OIDC_FAMILY_NAME_FIELD: 'family_name',
|
|
43
|
-
OIDC_GIVEN_NAME_FIELD: 'given_name',
|
|
44
|
-
OIDC_LOGOUT_URL: '', // Provider logout URL; omit to redirect to Strapi login
|
|
42
|
+
OIDC_FAMILY_NAME_FIELD: 'family_name', // claim name for the user's last name
|
|
43
|
+
OIDC_GIVEN_NAME_FIELD: 'given_name', // claim name for the user's first name
|
|
45
44
|
OIDC_SSO_BUTTON_TEXT: 'Login via SSO',
|
|
46
45
|
OIDC_ENFORCE: null, // null = use Admin UI toggle; true/false = override in config
|
|
47
46
|
REMEMBER_ME: false, // Persist session across browser restarts
|
|
47
|
+
|
|
48
|
+
// Optional — RP-Initiated Logout (GET /strapi-plugin-oidc/logout)
|
|
49
|
+
// Set OIDC_END_SESSION_ENDPOINT to redirect OIDC sessions to your provider's
|
|
50
|
+
// end-session page on logout. Without it, logout only clears the local session.
|
|
51
|
+
// Find this URL in your provider's /.well-known/openid-configuration as end_session_endpoint.
|
|
52
|
+
OIDC_END_SESSION_ENDPOINT: env('OIDC_END_SESSION_ENDPOINT', ''),
|
|
53
|
+
OIDC_POST_LOGOUT_REDIRECT_URI: env('OIDC_POST_LOGOUT_REDIRECT_URI', ''), // where to land after the provider logs the user out
|
|
54
|
+
|
|
55
|
+
// Optional — Backchannel Logout (POST /strapi-plugin-oidc/logout)
|
|
56
|
+
// When configured, your provider can notify Strapi when a user logs out elsewhere
|
|
57
|
+
// (e.g. from another app or directly from the provider UI), revoking their Strapi session.
|
|
58
|
+
// Set the logout URI in your provider to: https://your-strapi.com/strapi-plugin-oidc/logout
|
|
59
|
+
// Both values are required together — find them in your provider's /.well-known/openid-configuration.
|
|
60
|
+
OIDC_ISSUER: env('OIDC_ISSUER', ''), // validates the iss claim; required for backchannel logout
|
|
61
|
+
OIDC_JWKS_URI: env('OIDC_JWKS_URI', ''), // verifies logout token signatures; required for backchannel logout
|
|
48
62
|
},
|
|
49
63
|
},
|
|
50
64
|
});
|
package/dist/server/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toStringTag]: { value: "Module" } });
|
|
3
3
|
const node_crypto = require("node:crypto");
|
|
4
|
+
const jose = require("jose");
|
|
4
5
|
const pkceChallenge = require("pkce-challenge");
|
|
5
6
|
const strapiUtils = require("@strapi/utils");
|
|
6
7
|
const generator = require("generate-password");
|
|
@@ -161,14 +162,20 @@ const config = {
|
|
|
161
162
|
OIDC_REDIRECT_URI: "http://localhost:1337/strapi-plugin-oidc/oidc/callback",
|
|
162
163
|
OIDC_CLIENT_ID: "",
|
|
163
164
|
OIDC_CLIENT_SECRET: "",
|
|
164
|
-
|
|
165
|
+
OIDC_SCOPE: "openid profile email",
|
|
165
166
|
OIDC_AUTHORIZATION_ENDPOINT: "",
|
|
166
167
|
OIDC_TOKEN_ENDPOINT: "",
|
|
167
|
-
|
|
168
|
+
OIDC_USERINFO_ENDPOINT: "",
|
|
168
169
|
OIDC_GRANT_TYPE: "authorization_code",
|
|
169
170
|
OIDC_FAMILY_NAME_FIELD: "family_name",
|
|
170
171
|
OIDC_GIVEN_NAME_FIELD: "given_name",
|
|
171
|
-
|
|
172
|
+
OIDC_END_SESSION_ENDPOINT: "",
|
|
173
|
+
OIDC_POST_LOGOUT_REDIRECT_URI: "",
|
|
174
|
+
// Where to land after the provider has logged the user out (RP-Initiated Logout)
|
|
175
|
+
OIDC_ISSUER: "",
|
|
176
|
+
// Provider issuer URL — used to validate iss claim in backchannel logout tokens
|
|
177
|
+
OIDC_JWKS_URI: "",
|
|
178
|
+
// Provider JWKS endpoint — required for backchannel logout token signature verification
|
|
172
179
|
OIDC_SSO_BUTTON_TEXT: "Login via SSO",
|
|
173
180
|
OIDC_ENFORCE: null
|
|
174
181
|
// null = use DB setting; true/false = override DB (useful for lockout recovery)
|
|
@@ -221,15 +228,17 @@ function getExpiredCookieOptions(strapi2, ctx) {
|
|
|
221
228
|
function clearAuthCookies(strapi2, ctx) {
|
|
222
229
|
const options2 = getExpiredCookieOptions(strapi2, ctx);
|
|
223
230
|
ctx.cookies.set("strapi_admin_refresh", "", options2);
|
|
224
|
-
|
|
231
|
+
const rootPathOptions = { ...options2, path: "/" };
|
|
232
|
+
ctx.cookies.set("oidc_authenticated", "", rootPathOptions);
|
|
233
|
+
ctx.cookies.set("oidc_id_token", "", rootPathOptions);
|
|
225
234
|
}
|
|
226
235
|
const REQUIRED_CONFIG_KEYS = [
|
|
227
236
|
"OIDC_CLIENT_ID",
|
|
228
237
|
"OIDC_CLIENT_SECRET",
|
|
229
238
|
"OIDC_REDIRECT_URI",
|
|
230
|
-
"
|
|
239
|
+
"OIDC_SCOPE",
|
|
231
240
|
"OIDC_TOKEN_ENDPOINT",
|
|
232
|
-
"
|
|
241
|
+
"OIDC_USERINFO_ENDPOINT",
|
|
233
242
|
"OIDC_GRANT_TYPE",
|
|
234
243
|
"OIDC_FAMILY_NAME_FIELD",
|
|
235
244
|
"OIDC_GIVEN_NAME_FIELD",
|
|
@@ -245,7 +254,7 @@ function configValidation() {
|
|
|
245
254
|
);
|
|
246
255
|
}
|
|
247
256
|
async function oidcSignIn(ctx) {
|
|
248
|
-
const { OIDC_CLIENT_ID, OIDC_REDIRECT_URI,
|
|
257
|
+
const { OIDC_CLIENT_ID, OIDC_REDIRECT_URI, OIDC_SCOPE, OIDC_AUTHORIZATION_ENDPOINT } = configValidation();
|
|
249
258
|
const { code_verifier: codeVerifier, code_challenge: codeChallenge } = await pkceChallenge__default.default();
|
|
250
259
|
const state = node_crypto.randomBytes(32).toString("base64url");
|
|
251
260
|
const nonce = node_crypto.randomBytes(32).toString("base64url");
|
|
@@ -264,7 +273,7 @@ async function oidcSignIn(ctx) {
|
|
|
264
273
|
params.append("response_type", "code");
|
|
265
274
|
params.append("client_id", OIDC_CLIENT_ID);
|
|
266
275
|
params.append("redirect_uri", OIDC_REDIRECT_URI);
|
|
267
|
-
params.append("scope",
|
|
276
|
+
params.append("scope", OIDC_SCOPE);
|
|
268
277
|
params.append("code_challenge", codeChallenge);
|
|
269
278
|
params.append("code_challenge_method", "S256");
|
|
270
279
|
params.append("state", state);
|
|
@@ -297,13 +306,14 @@ async function exchangeTokenAndFetchUserInfo(config2, params, expectedNonce) {
|
|
|
297
306
|
throw new Error("Failed to parse ID token");
|
|
298
307
|
}
|
|
299
308
|
}
|
|
300
|
-
const userResponse = await fetch(config2.
|
|
309
|
+
const userResponse = await fetch(config2.OIDC_USERINFO_ENDPOINT, {
|
|
301
310
|
headers: { Authorization: `Bearer ${tokenData.access_token}` }
|
|
302
311
|
});
|
|
303
312
|
if (!userResponse.ok) {
|
|
304
313
|
throw new Error("Failed to fetch user info");
|
|
305
314
|
}
|
|
306
|
-
|
|
315
|
+
const userInfo = await userResponse.json();
|
|
316
|
+
return { userInfo, idToken: tokenData.id_token };
|
|
307
317
|
}
|
|
308
318
|
async function registerNewUser(userService, oauthService2, roleService2, email, userResponseData, whitelistUser, config2, ctx) {
|
|
309
319
|
let roles2 = [];
|
|
@@ -369,7 +379,11 @@ async function oidcSignInCallback(ctx) {
|
|
|
369
379
|
params.append("grant_type", config2.OIDC_GRANT_TYPE);
|
|
370
380
|
params.append("code_verifier", codeVerifier ?? "");
|
|
371
381
|
try {
|
|
372
|
-
const userResponseData = await exchangeTokenAndFetchUserInfo(
|
|
382
|
+
const { userInfo: userResponseData, idToken } = await exchangeTokenAndFetchUserInfo(
|
|
383
|
+
config2,
|
|
384
|
+
params,
|
|
385
|
+
oidcNonce ?? ""
|
|
386
|
+
);
|
|
373
387
|
const { activateUser, jwtToken } = await handleUserAuthentication(
|
|
374
388
|
userService,
|
|
375
389
|
oauthService2,
|
|
@@ -379,6 +393,15 @@ async function oidcSignInCallback(ctx) {
|
|
|
379
393
|
config2,
|
|
380
394
|
ctx
|
|
381
395
|
);
|
|
396
|
+
if (idToken) {
|
|
397
|
+
const isProduction = strapi.config.get("environment") === "production";
|
|
398
|
+
ctx.cookies.set("oidc_id_token", idToken, {
|
|
399
|
+
httpOnly: true,
|
|
400
|
+
secure: isProduction && ctx.request.secure,
|
|
401
|
+
path: "/",
|
|
402
|
+
sameSite: "lax"
|
|
403
|
+
});
|
|
404
|
+
}
|
|
382
405
|
const nonce = node_crypto.randomUUID();
|
|
383
406
|
const html = oauthService2.renderSignUpSuccess(jwtToken, activateUser, nonce);
|
|
384
407
|
ctx.set("Content-Security-Policy", `script-src 'nonce-${nonce}'`);
|
|
@@ -390,20 +413,91 @@ async function oidcSignInCallback(ctx) {
|
|
|
390
413
|
}
|
|
391
414
|
async function logout(ctx) {
|
|
392
415
|
const config2 = strapi.config.get("plugin::strapi-plugin-oidc");
|
|
393
|
-
const logoutUrl = config2.
|
|
416
|
+
const logoutUrl = config2.OIDC_END_SESSION_ENDPOINT;
|
|
394
417
|
const isOidcSession = !!ctx.cookies.get("oidc_authenticated");
|
|
418
|
+
const idToken = ctx.cookies.get("oidc_id_token");
|
|
395
419
|
clearAuthCookies(strapi, ctx);
|
|
396
420
|
if (logoutUrl && isOidcSession) {
|
|
397
|
-
|
|
421
|
+
const url = new URL(logoutUrl);
|
|
422
|
+
if (idToken) url.searchParams.set("id_token_hint", idToken);
|
|
423
|
+
if (config2.OIDC_POST_LOGOUT_REDIRECT_URI) {
|
|
424
|
+
url.searchParams.set("post_logout_redirect_uri", config2.OIDC_POST_LOGOUT_REDIRECT_URI);
|
|
425
|
+
}
|
|
426
|
+
ctx.redirect(url.toString());
|
|
398
427
|
} else {
|
|
399
428
|
const adminPanelUrl = strapi.config.get("admin.url", "/admin");
|
|
400
429
|
ctx.redirect(`${adminPanelUrl}/auth/login`);
|
|
401
430
|
}
|
|
402
431
|
}
|
|
432
|
+
const jwksCache = /* @__PURE__ */ new Map();
|
|
433
|
+
function getJWKS(uri) {
|
|
434
|
+
const cached = jwksCache.get(uri);
|
|
435
|
+
if (cached) return cached;
|
|
436
|
+
const jwks = jose.createRemoteJWKSet(new URL(uri));
|
|
437
|
+
jwksCache.set(uri, jwks);
|
|
438
|
+
return jwks;
|
|
439
|
+
}
|
|
440
|
+
async function backchannelLogout(ctx) {
|
|
441
|
+
const config2 = strapi.config.get("plugin::strapi-plugin-oidc");
|
|
442
|
+
const logoutToken = ctx.request.body?.logout_token;
|
|
443
|
+
if (!logoutToken) {
|
|
444
|
+
ctx.status = 400;
|
|
445
|
+
ctx.body = { error: "Missing logout_token" };
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
if (!config2.OIDC_JWKS_URI || !config2.OIDC_ISSUER) {
|
|
449
|
+
ctx.status = 501;
|
|
450
|
+
ctx.body = {
|
|
451
|
+
error: "OIDC_JWKS_URI and OIDC_ISSUER must both be configured to enable backchannel logout"
|
|
452
|
+
};
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
try {
|
|
456
|
+
const JWKS = getJWKS(config2.OIDC_JWKS_URI);
|
|
457
|
+
const verifyOptions = {
|
|
458
|
+
issuer: config2.OIDC_ISSUER,
|
|
459
|
+
audience: config2.OIDC_CLIENT_ID || void 0
|
|
460
|
+
};
|
|
461
|
+
const { payload } = await jose.jwtVerify(logoutToken, JWKS, verifyOptions);
|
|
462
|
+
if ("nonce" in payload) {
|
|
463
|
+
ctx.status = 400;
|
|
464
|
+
ctx.body = { error: "logout_token must not contain nonce" };
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
const events = payload.events;
|
|
468
|
+
if (!events?.["http://schemas.openid.net/event/backchannel-logout"]) {
|
|
469
|
+
ctx.status = 400;
|
|
470
|
+
ctx.body = { error: "logout_token missing backchannel-logout event" };
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
if (!payload.sub && !("sid" in payload)) {
|
|
474
|
+
ctx.status = 400;
|
|
475
|
+
ctx.body = { error: "logout_token must contain sub or sid" };
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
if (payload.sub) {
|
|
479
|
+
const userService = strapi.service("admin::user");
|
|
480
|
+
const user = await userService.findOneByEmail(payload.sub);
|
|
481
|
+
if (user) {
|
|
482
|
+
const sessionManager = strapi.sessionManager;
|
|
483
|
+
if (sessionManager) {
|
|
484
|
+
await sessionManager("admin").invalidateRefreshToken(String(user.id));
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
ctx.status = 200;
|
|
489
|
+
ctx.body = "";
|
|
490
|
+
} catch (e) {
|
|
491
|
+
strapi.log.error("Backchannel logout failed:", e);
|
|
492
|
+
ctx.status = 400;
|
|
493
|
+
ctx.body = { error: "Invalid logout_token" };
|
|
494
|
+
}
|
|
495
|
+
}
|
|
403
496
|
const oidc = {
|
|
404
497
|
oidcSignIn,
|
|
405
498
|
oidcSignInCallback,
|
|
406
|
-
logout
|
|
499
|
+
logout,
|
|
500
|
+
backchannelLogout
|
|
407
501
|
};
|
|
408
502
|
async function find(ctx) {
|
|
409
503
|
const roleService2 = strapi.plugin("strapi-plugin-oidc").service("role");
|
|
@@ -660,6 +754,12 @@ const routes = {
|
|
|
660
754
|
handler: "oidc.logout",
|
|
661
755
|
config: { auth: false }
|
|
662
756
|
},
|
|
757
|
+
{
|
|
758
|
+
method: "POST",
|
|
759
|
+
path: "/logout",
|
|
760
|
+
handler: "oidc.backchannelLogout",
|
|
761
|
+
config: { auth: false }
|
|
762
|
+
},
|
|
663
763
|
{
|
|
664
764
|
method: "GET",
|
|
665
765
|
path: "/whitelist",
|
package/dist/server/index.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomUUID, randomBytes } from "node:crypto";
|
|
2
|
+
import { jwtVerify, createRemoteJWKSet } from "jose";
|
|
2
3
|
import pkceChallenge from "pkce-challenge";
|
|
3
4
|
import strapiUtils from "@strapi/utils";
|
|
4
5
|
import generator from "generate-password";
|
|
@@ -155,14 +156,20 @@ const config = {
|
|
|
155
156
|
OIDC_REDIRECT_URI: "http://localhost:1337/strapi-plugin-oidc/oidc/callback",
|
|
156
157
|
OIDC_CLIENT_ID: "",
|
|
157
158
|
OIDC_CLIENT_SECRET: "",
|
|
158
|
-
|
|
159
|
+
OIDC_SCOPE: "openid profile email",
|
|
159
160
|
OIDC_AUTHORIZATION_ENDPOINT: "",
|
|
160
161
|
OIDC_TOKEN_ENDPOINT: "",
|
|
161
|
-
|
|
162
|
+
OIDC_USERINFO_ENDPOINT: "",
|
|
162
163
|
OIDC_GRANT_TYPE: "authorization_code",
|
|
163
164
|
OIDC_FAMILY_NAME_FIELD: "family_name",
|
|
164
165
|
OIDC_GIVEN_NAME_FIELD: "given_name",
|
|
165
|
-
|
|
166
|
+
OIDC_END_SESSION_ENDPOINT: "",
|
|
167
|
+
OIDC_POST_LOGOUT_REDIRECT_URI: "",
|
|
168
|
+
// Where to land after the provider has logged the user out (RP-Initiated Logout)
|
|
169
|
+
OIDC_ISSUER: "",
|
|
170
|
+
// Provider issuer URL — used to validate iss claim in backchannel logout tokens
|
|
171
|
+
OIDC_JWKS_URI: "",
|
|
172
|
+
// Provider JWKS endpoint — required for backchannel logout token signature verification
|
|
166
173
|
OIDC_SSO_BUTTON_TEXT: "Login via SSO",
|
|
167
174
|
OIDC_ENFORCE: null
|
|
168
175
|
// null = use DB setting; true/false = override DB (useful for lockout recovery)
|
|
@@ -215,15 +222,17 @@ function getExpiredCookieOptions(strapi2, ctx) {
|
|
|
215
222
|
function clearAuthCookies(strapi2, ctx) {
|
|
216
223
|
const options2 = getExpiredCookieOptions(strapi2, ctx);
|
|
217
224
|
ctx.cookies.set("strapi_admin_refresh", "", options2);
|
|
218
|
-
|
|
225
|
+
const rootPathOptions = { ...options2, path: "/" };
|
|
226
|
+
ctx.cookies.set("oidc_authenticated", "", rootPathOptions);
|
|
227
|
+
ctx.cookies.set("oidc_id_token", "", rootPathOptions);
|
|
219
228
|
}
|
|
220
229
|
const REQUIRED_CONFIG_KEYS = [
|
|
221
230
|
"OIDC_CLIENT_ID",
|
|
222
231
|
"OIDC_CLIENT_SECRET",
|
|
223
232
|
"OIDC_REDIRECT_URI",
|
|
224
|
-
"
|
|
233
|
+
"OIDC_SCOPE",
|
|
225
234
|
"OIDC_TOKEN_ENDPOINT",
|
|
226
|
-
"
|
|
235
|
+
"OIDC_USERINFO_ENDPOINT",
|
|
227
236
|
"OIDC_GRANT_TYPE",
|
|
228
237
|
"OIDC_FAMILY_NAME_FIELD",
|
|
229
238
|
"OIDC_GIVEN_NAME_FIELD",
|
|
@@ -239,7 +248,7 @@ function configValidation() {
|
|
|
239
248
|
);
|
|
240
249
|
}
|
|
241
250
|
async function oidcSignIn(ctx) {
|
|
242
|
-
const { OIDC_CLIENT_ID, OIDC_REDIRECT_URI,
|
|
251
|
+
const { OIDC_CLIENT_ID, OIDC_REDIRECT_URI, OIDC_SCOPE, OIDC_AUTHORIZATION_ENDPOINT } = configValidation();
|
|
243
252
|
const { code_verifier: codeVerifier, code_challenge: codeChallenge } = await pkceChallenge();
|
|
244
253
|
const state = randomBytes(32).toString("base64url");
|
|
245
254
|
const nonce = randomBytes(32).toString("base64url");
|
|
@@ -258,7 +267,7 @@ async function oidcSignIn(ctx) {
|
|
|
258
267
|
params.append("response_type", "code");
|
|
259
268
|
params.append("client_id", OIDC_CLIENT_ID);
|
|
260
269
|
params.append("redirect_uri", OIDC_REDIRECT_URI);
|
|
261
|
-
params.append("scope",
|
|
270
|
+
params.append("scope", OIDC_SCOPE);
|
|
262
271
|
params.append("code_challenge", codeChallenge);
|
|
263
272
|
params.append("code_challenge_method", "S256");
|
|
264
273
|
params.append("state", state);
|
|
@@ -291,13 +300,14 @@ async function exchangeTokenAndFetchUserInfo(config2, params, expectedNonce) {
|
|
|
291
300
|
throw new Error("Failed to parse ID token");
|
|
292
301
|
}
|
|
293
302
|
}
|
|
294
|
-
const userResponse = await fetch(config2.
|
|
303
|
+
const userResponse = await fetch(config2.OIDC_USERINFO_ENDPOINT, {
|
|
295
304
|
headers: { Authorization: `Bearer ${tokenData.access_token}` }
|
|
296
305
|
});
|
|
297
306
|
if (!userResponse.ok) {
|
|
298
307
|
throw new Error("Failed to fetch user info");
|
|
299
308
|
}
|
|
300
|
-
|
|
309
|
+
const userInfo = await userResponse.json();
|
|
310
|
+
return { userInfo, idToken: tokenData.id_token };
|
|
301
311
|
}
|
|
302
312
|
async function registerNewUser(userService, oauthService2, roleService2, email, userResponseData, whitelistUser, config2, ctx) {
|
|
303
313
|
let roles2 = [];
|
|
@@ -363,7 +373,11 @@ async function oidcSignInCallback(ctx) {
|
|
|
363
373
|
params.append("grant_type", config2.OIDC_GRANT_TYPE);
|
|
364
374
|
params.append("code_verifier", codeVerifier ?? "");
|
|
365
375
|
try {
|
|
366
|
-
const userResponseData = await exchangeTokenAndFetchUserInfo(
|
|
376
|
+
const { userInfo: userResponseData, idToken } = await exchangeTokenAndFetchUserInfo(
|
|
377
|
+
config2,
|
|
378
|
+
params,
|
|
379
|
+
oidcNonce ?? ""
|
|
380
|
+
);
|
|
367
381
|
const { activateUser, jwtToken } = await handleUserAuthentication(
|
|
368
382
|
userService,
|
|
369
383
|
oauthService2,
|
|
@@ -373,6 +387,15 @@ async function oidcSignInCallback(ctx) {
|
|
|
373
387
|
config2,
|
|
374
388
|
ctx
|
|
375
389
|
);
|
|
390
|
+
if (idToken) {
|
|
391
|
+
const isProduction = strapi.config.get("environment") === "production";
|
|
392
|
+
ctx.cookies.set("oidc_id_token", idToken, {
|
|
393
|
+
httpOnly: true,
|
|
394
|
+
secure: isProduction && ctx.request.secure,
|
|
395
|
+
path: "/",
|
|
396
|
+
sameSite: "lax"
|
|
397
|
+
});
|
|
398
|
+
}
|
|
376
399
|
const nonce = randomUUID();
|
|
377
400
|
const html = oauthService2.renderSignUpSuccess(jwtToken, activateUser, nonce);
|
|
378
401
|
ctx.set("Content-Security-Policy", `script-src 'nonce-${nonce}'`);
|
|
@@ -384,20 +407,91 @@ async function oidcSignInCallback(ctx) {
|
|
|
384
407
|
}
|
|
385
408
|
async function logout(ctx) {
|
|
386
409
|
const config2 = strapi.config.get("plugin::strapi-plugin-oidc");
|
|
387
|
-
const logoutUrl = config2.
|
|
410
|
+
const logoutUrl = config2.OIDC_END_SESSION_ENDPOINT;
|
|
388
411
|
const isOidcSession = !!ctx.cookies.get("oidc_authenticated");
|
|
412
|
+
const idToken = ctx.cookies.get("oidc_id_token");
|
|
389
413
|
clearAuthCookies(strapi, ctx);
|
|
390
414
|
if (logoutUrl && isOidcSession) {
|
|
391
|
-
|
|
415
|
+
const url = new URL(logoutUrl);
|
|
416
|
+
if (idToken) url.searchParams.set("id_token_hint", idToken);
|
|
417
|
+
if (config2.OIDC_POST_LOGOUT_REDIRECT_URI) {
|
|
418
|
+
url.searchParams.set("post_logout_redirect_uri", config2.OIDC_POST_LOGOUT_REDIRECT_URI);
|
|
419
|
+
}
|
|
420
|
+
ctx.redirect(url.toString());
|
|
392
421
|
} else {
|
|
393
422
|
const adminPanelUrl = strapi.config.get("admin.url", "/admin");
|
|
394
423
|
ctx.redirect(`${adminPanelUrl}/auth/login`);
|
|
395
424
|
}
|
|
396
425
|
}
|
|
426
|
+
const jwksCache = /* @__PURE__ */ new Map();
|
|
427
|
+
function getJWKS(uri) {
|
|
428
|
+
const cached = jwksCache.get(uri);
|
|
429
|
+
if (cached) return cached;
|
|
430
|
+
const jwks = createRemoteJWKSet(new URL(uri));
|
|
431
|
+
jwksCache.set(uri, jwks);
|
|
432
|
+
return jwks;
|
|
433
|
+
}
|
|
434
|
+
async function backchannelLogout(ctx) {
|
|
435
|
+
const config2 = strapi.config.get("plugin::strapi-plugin-oidc");
|
|
436
|
+
const logoutToken = ctx.request.body?.logout_token;
|
|
437
|
+
if (!logoutToken) {
|
|
438
|
+
ctx.status = 400;
|
|
439
|
+
ctx.body = { error: "Missing logout_token" };
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
if (!config2.OIDC_JWKS_URI || !config2.OIDC_ISSUER) {
|
|
443
|
+
ctx.status = 501;
|
|
444
|
+
ctx.body = {
|
|
445
|
+
error: "OIDC_JWKS_URI and OIDC_ISSUER must both be configured to enable backchannel logout"
|
|
446
|
+
};
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
try {
|
|
450
|
+
const JWKS = getJWKS(config2.OIDC_JWKS_URI);
|
|
451
|
+
const verifyOptions = {
|
|
452
|
+
issuer: config2.OIDC_ISSUER,
|
|
453
|
+
audience: config2.OIDC_CLIENT_ID || void 0
|
|
454
|
+
};
|
|
455
|
+
const { payload } = await jwtVerify(logoutToken, JWKS, verifyOptions);
|
|
456
|
+
if ("nonce" in payload) {
|
|
457
|
+
ctx.status = 400;
|
|
458
|
+
ctx.body = { error: "logout_token must not contain nonce" };
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
const events = payload.events;
|
|
462
|
+
if (!events?.["http://schemas.openid.net/event/backchannel-logout"]) {
|
|
463
|
+
ctx.status = 400;
|
|
464
|
+
ctx.body = { error: "logout_token missing backchannel-logout event" };
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
if (!payload.sub && !("sid" in payload)) {
|
|
468
|
+
ctx.status = 400;
|
|
469
|
+
ctx.body = { error: "logout_token must contain sub or sid" };
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
if (payload.sub) {
|
|
473
|
+
const userService = strapi.service("admin::user");
|
|
474
|
+
const user = await userService.findOneByEmail(payload.sub);
|
|
475
|
+
if (user) {
|
|
476
|
+
const sessionManager = strapi.sessionManager;
|
|
477
|
+
if (sessionManager) {
|
|
478
|
+
await sessionManager("admin").invalidateRefreshToken(String(user.id));
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
ctx.status = 200;
|
|
483
|
+
ctx.body = "";
|
|
484
|
+
} catch (e) {
|
|
485
|
+
strapi.log.error("Backchannel logout failed:", e);
|
|
486
|
+
ctx.status = 400;
|
|
487
|
+
ctx.body = { error: "Invalid logout_token" };
|
|
488
|
+
}
|
|
489
|
+
}
|
|
397
490
|
const oidc = {
|
|
398
491
|
oidcSignIn,
|
|
399
492
|
oidcSignInCallback,
|
|
400
|
-
logout
|
|
493
|
+
logout,
|
|
494
|
+
backchannelLogout
|
|
401
495
|
};
|
|
402
496
|
async function find(ctx) {
|
|
403
497
|
const roleService2 = strapi.plugin("strapi-plugin-oidc").service("role");
|
|
@@ -654,6 +748,12 @@ const routes = {
|
|
|
654
748
|
handler: "oidc.logout",
|
|
655
749
|
config: { auth: false }
|
|
656
750
|
},
|
|
751
|
+
{
|
|
752
|
+
method: "POST",
|
|
753
|
+
path: "/logout",
|
|
754
|
+
handler: "oidc.backchannelLogout",
|
|
755
|
+
config: { auth: false }
|
|
756
|
+
},
|
|
657
757
|
{
|
|
658
758
|
method: "GET",
|
|
659
759
|
path: "/whitelist",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "strapi-plugin-oidc",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "A Strapi plugin that provides OpenID Connect (OIDC) authentication functionality for the Strapi Admin Panel.",
|
|
5
5
|
"strapi": {
|
|
6
6
|
"displayName": "OIDC Plugin",
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
"@strapi/icons": "^2.2.0",
|
|
51
51
|
"@strapi/utils": "^5.41.1",
|
|
52
52
|
"generate-password": "^1.7.1",
|
|
53
|
+
"jose": "^6.2.2",
|
|
53
54
|
"pkce-challenge": "^6.0.0",
|
|
54
55
|
"react-intl": "^6.8.9"
|
|
55
56
|
},
|
|
@@ -81,6 +82,8 @@
|
|
|
81
82
|
"@eslint/eslintrc": "^3.3.5",
|
|
82
83
|
"@eslint/js": "^10.0.1",
|
|
83
84
|
"@strapi/sdk-plugin": "^6.0.1",
|
|
85
|
+
"@strapi/types": "^5.41.1",
|
|
86
|
+
"@types/koa": "^2.16.4",
|
|
84
87
|
"@types/node": "^25.5.2",
|
|
85
88
|
"@types/supertest": "^7.2.0",
|
|
86
89
|
"@vitest/coverage-v8": "^4.1.2",
|