strapi-plugin-oidc 1.5.1 → 1.5.3
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 +5 -20
- package/dist/server/index.js +29 -142
- package/dist/server/index.mjs +29 -142
- package/package.json +1 -4
package/README.md
CHANGED
|
@@ -36,27 +36,15 @@ module.exports = ({ env }) => ({
|
|
|
36
36
|
OIDC_TOKEN_ENDPOINT: env('OIDC_TOKEN_ENDPOINT'),
|
|
37
37
|
OIDC_USERINFO_ENDPOINT: env('OIDC_USERINFO_ENDPOINT'),
|
|
38
38
|
|
|
39
|
-
// Optional
|
|
39
|
+
// Optional — defaults shown
|
|
40
40
|
OIDC_SCOPE: 'openid profile email',
|
|
41
41
|
OIDC_GRANT_TYPE: 'authorization_code',
|
|
42
42
|
OIDC_FAMILY_NAME_FIELD: 'family_name',
|
|
43
43
|
OIDC_GIVEN_NAME_FIELD: 'given_name',
|
|
44
|
+
OIDC_END_SESSION_ENDPOINT: '', // Provider end-session URL; omit to redirect to Strapi login
|
|
44
45
|
OIDC_SSO_BUTTON_TEXT: 'Login via SSO',
|
|
45
|
-
OIDC_ENFORCE: null, // null = use Admin UI toggle; true/false = override
|
|
46
|
-
REMEMBER_ME: false,
|
|
47
|
-
|
|
48
|
-
// Optional — RP-Initiated Logout
|
|
49
|
-
// Redirects the browser to the provider's end-session page on logout.
|
|
50
|
-
// Both found in your provider's /.well-known/openid-configuration.
|
|
51
|
-
OIDC_END_SESSION_ENDPOINT: env('OIDC_END_SESSION_ENDPOINT', ''),
|
|
52
|
-
OIDC_POST_LOGOUT_REDIRECT_URI: env('OIDC_POST_LOGOUT_REDIRECT_URI', ''),
|
|
53
|
-
|
|
54
|
-
// Optional — Backchannel Logout
|
|
55
|
-
// Allows the provider to revoke Strapi sessions server-to-server.
|
|
56
|
-
// Set your provider's logout URI to: https://your-strapi.com/strapi-plugin-oidc/logout
|
|
57
|
-
// Both found in your provider's /.well-known/openid-configuration.
|
|
58
|
-
OIDC_ISSUER: env('OIDC_ISSUER', ''),
|
|
59
|
-
OIDC_JWKS_URI: env('OIDC_JWKS_URI', ''),
|
|
46
|
+
OIDC_ENFORCE: null, // null = use Admin UI toggle; true/false = override in config
|
|
47
|
+
REMEMBER_ME: false, // Persist session across browser restarts
|
|
60
48
|
},
|
|
61
49
|
},
|
|
62
50
|
});
|
|
@@ -146,6 +134,7 @@ This plugin is a hard fork of [`strapi-plugin-sso`](https://github.com/yasudaclo
|
|
|
146
134
|
|
|
147
135
|
- Removed alternative SSO methods to simplify the plugin.
|
|
148
136
|
- Redesigned the Whitelist and Role management UI (switched to native Strapi cards, added pagination, etc.).
|
|
137
|
+
- Added an OIDC logout redirect URL.
|
|
149
138
|
- Added an option to "Enforce OIDC login" with an admin toggle (automatically disabled if the whitelist is empty).
|
|
150
139
|
- Migrated the testing framework to Vitest and added comprehensive test coverage for controllers and services.
|
|
151
140
|
- Cleaned up dead code and unused dependencies to improve maintainability.
|
|
@@ -157,8 +146,4 @@ This plugin is a hard fork of [`strapi-plugin-sso`](https://github.com/yasudaclo
|
|
|
157
146
|
- Bulk delete all entries with a confirmation dialog.
|
|
158
147
|
- Unsaved changes confirmation when navigating away from the settings page.
|
|
159
148
|
- Programmatic API for managing the whitelist via Strapi API tokens (list, register, import, delete, delete all).
|
|
160
|
-
- **RP-Initiated Logout** (OpenID Connect RP-Initiated Logout 1.0): on logout, Strapi redirects the browser to the provider's end-session endpoint with `id_token_hint` and `post_logout_redirect_uri`, cleanly terminating the SSO session. Configured via `OIDC_END_SESSION_ENDPOINT` and `OIDC_POST_LOGOUT_REDIRECT_URI`.
|
|
161
|
-
- **Backchannel Logout** (OIDC Back-Channel Logout 1.0): `POST /strapi-plugin-oidc/logout` accepts a signed logout token from the provider, validates it, and revokes the user's Strapi admin session — keeping Strapi in sync when a user logs out elsewhere. Configured via `OIDC_ISSUER` and `OIDC_JWKS_URI`.
|
|
162
|
-
- Renamed config keys to match OIDC discovery document field names: `OIDC_LOGOUT_URL` → `OIDC_END_SESSION_ENDPOINT`, `OIDC_USER_INFO_ENDPOINT` → `OIDC_USERINFO_ENDPOINT`, `OIDC_SCOPES` → `OIDC_SCOPE`.
|
|
163
|
-
- Security hardening: PKCE (`S256`), server-side `state` generation (CSRF protection), nonce validation (ID token replay prevention), `Authorization: Bearer` header for userinfo requests, generic error messages on callback failure.
|
|
164
149
|
- Added misc. quality of life improvements and bug fixes.
|
package/dist/server/index.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
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");
|
|
5
4
|
const pkceChallenge = require("pkce-challenge");
|
|
6
5
|
const strapiUtils = require("@strapi/utils");
|
|
7
6
|
const generator = require("generate-password");
|
|
@@ -25,17 +24,6 @@ function resolveEnforceOIDC(strapi2, dbValue) {
|
|
|
25
24
|
if (configValue !== null) return configValue;
|
|
26
25
|
return dbValue ?? false;
|
|
27
26
|
}
|
|
28
|
-
const revoked = /* @__PURE__ */ new Map();
|
|
29
|
-
const CLEANUP_DELAY_MS = 24 * 60 * 60 * 1e3;
|
|
30
|
-
function revokeUser(userId) {
|
|
31
|
-
revoked.set(userId, Date.now());
|
|
32
|
-
setTimeout(() => revoked.delete(userId), CLEANUP_DELAY_MS);
|
|
33
|
-
}
|
|
34
|
-
function isUserRevoked(userId, iat) {
|
|
35
|
-
const logoutTime = revoked.get(userId);
|
|
36
|
-
if (!logoutTime) return false;
|
|
37
|
-
return iat * 1e3 <= logoutTime;
|
|
38
|
-
}
|
|
39
27
|
async function bootstrap({ strapi: strapi2 }) {
|
|
40
28
|
const enforceOidcMiddleware = async (ctx, next) => {
|
|
41
29
|
const adminUrl = strapi2.config.get("admin.url", "/admin");
|
|
@@ -88,36 +76,9 @@ async function bootstrap({ strapi: strapi2 }) {
|
|
|
88
76
|
}
|
|
89
77
|
await next();
|
|
90
78
|
};
|
|
91
|
-
const denylistMiddleware = async (ctx, next) => {
|
|
92
|
-
const auth = ctx.request.headers.authorization;
|
|
93
|
-
if (auth?.startsWith("Bearer ")) {
|
|
94
|
-
try {
|
|
95
|
-
const payload = JSON.parse(
|
|
96
|
-
Buffer.from(auth.slice(7).split(".")[1], "base64url").toString()
|
|
97
|
-
);
|
|
98
|
-
if (payload.id && isUserRevoked(String(payload.id), payload.iat ?? 0)) {
|
|
99
|
-
ctx.status = 401;
|
|
100
|
-
ctx.body = {
|
|
101
|
-
data: null,
|
|
102
|
-
error: {
|
|
103
|
-
status: 401,
|
|
104
|
-
name: "UnauthorizedError",
|
|
105
|
-
message: "Session revoked",
|
|
106
|
-
details: {}
|
|
107
|
-
}
|
|
108
|
-
};
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
} catch {
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
await next();
|
|
115
|
-
};
|
|
116
79
|
if (strapi2.server.app && Array.isArray(strapi2.server.app.middleware)) {
|
|
117
80
|
strapi2.server.app.middleware.unshift(enforceOidcMiddleware);
|
|
118
|
-
strapi2.server.app.middleware.unshift(denylistMiddleware);
|
|
119
81
|
} else {
|
|
120
|
-
strapi2.server.use(denylistMiddleware);
|
|
121
82
|
strapi2.server.use(enforceOidcMiddleware);
|
|
122
83
|
}
|
|
123
84
|
const actions = [
|
|
@@ -208,12 +169,6 @@ const config = {
|
|
|
208
169
|
OIDC_FAMILY_NAME_FIELD: "family_name",
|
|
209
170
|
OIDC_GIVEN_NAME_FIELD: "given_name",
|
|
210
171
|
OIDC_END_SESSION_ENDPOINT: "",
|
|
211
|
-
OIDC_POST_LOGOUT_REDIRECT_URI: "",
|
|
212
|
-
// Where to land after the provider has logged the user out (RP-Initiated Logout)
|
|
213
|
-
OIDC_ISSUER: "",
|
|
214
|
-
// Provider issuer URL — used to validate iss claim in backchannel logout tokens
|
|
215
|
-
OIDC_JWKS_URI: "",
|
|
216
|
-
// Provider JWKS endpoint — required for backchannel logout token signature verification
|
|
217
172
|
OIDC_SSO_BUTTON_TEXT: "Login via SSO",
|
|
218
173
|
OIDC_ENFORCE: null
|
|
219
174
|
// null = use DB setting; true/false = override DB (useful for lockout recovery)
|
|
@@ -266,9 +221,8 @@ function getExpiredCookieOptions(strapi2, ctx) {
|
|
|
266
221
|
function clearAuthCookies(strapi2, ctx) {
|
|
267
222
|
const options2 = getExpiredCookieOptions(strapi2, ctx);
|
|
268
223
|
ctx.cookies.set("strapi_admin_refresh", "", options2);
|
|
269
|
-
|
|
270
|
-
ctx.cookies.set("
|
|
271
|
-
ctx.cookies.set("oidc_id_token", "", rootPathOptions);
|
|
224
|
+
ctx.cookies.set("oidc_authenticated", "", { ...options2, path: "/" });
|
|
225
|
+
ctx.cookies.set("oidc_access_token", "", { ...options2, path: "/" });
|
|
272
226
|
}
|
|
273
227
|
const REQUIRED_CONFIG_KEYS = [
|
|
274
228
|
"OIDC_CLIENT_ID",
|
|
@@ -351,7 +305,7 @@ async function exchangeTokenAndFetchUserInfo(config2, params, expectedNonce) {
|
|
|
351
305
|
throw new Error("Failed to fetch user info");
|
|
352
306
|
}
|
|
353
307
|
const userInfo = await userResponse.json();
|
|
354
|
-
return { userInfo,
|
|
308
|
+
return { userInfo, accessToken: tokenData.access_token };
|
|
355
309
|
}
|
|
356
310
|
async function registerNewUser(userService, oauthService2, roleService2, email, userResponseData, whitelistUser, config2, ctx) {
|
|
357
311
|
let roles2 = [];
|
|
@@ -417,29 +371,28 @@ async function oidcSignInCallback(ctx) {
|
|
|
417
371
|
params.append("grant_type", config2.OIDC_GRANT_TYPE);
|
|
418
372
|
params.append("code_verifier", codeVerifier ?? "");
|
|
419
373
|
try {
|
|
420
|
-
const { userInfo
|
|
374
|
+
const { userInfo, accessToken } = await exchangeTokenAndFetchUserInfo(
|
|
421
375
|
config2,
|
|
422
376
|
params,
|
|
423
377
|
oidcNonce ?? ""
|
|
424
378
|
);
|
|
379
|
+
const isProduction = strapi.config.get("environment") === "production";
|
|
380
|
+
ctx.cookies.set("oidc_access_token", accessToken, {
|
|
381
|
+
httpOnly: true,
|
|
382
|
+
maxAge: 3e5,
|
|
383
|
+
// 5 minutes — matches typical provider access token lifetime
|
|
384
|
+
secure: isProduction && ctx.request.secure,
|
|
385
|
+
sameSite: "lax"
|
|
386
|
+
});
|
|
425
387
|
const { activateUser, jwtToken } = await handleUserAuthentication(
|
|
426
388
|
userService,
|
|
427
389
|
oauthService2,
|
|
428
390
|
roleService2,
|
|
429
391
|
whitelistService2,
|
|
430
|
-
|
|
392
|
+
userInfo,
|
|
431
393
|
config2,
|
|
432
394
|
ctx
|
|
433
395
|
);
|
|
434
|
-
if (idToken) {
|
|
435
|
-
const isProduction = strapi.config.get("environment") === "production";
|
|
436
|
-
ctx.cookies.set("oidc_id_token", idToken, {
|
|
437
|
-
httpOnly: true,
|
|
438
|
-
secure: isProduction && ctx.request.secure,
|
|
439
|
-
path: "/",
|
|
440
|
-
sameSite: "lax"
|
|
441
|
-
});
|
|
442
|
-
}
|
|
443
396
|
const nonce = node_crypto.randomUUID();
|
|
444
397
|
const html = oauthService2.renderSignUpSuccess(jwtToken, activateUser, nonce);
|
|
445
398
|
ctx.set("Content-Security-Policy", `script-src 'nonce-${nonce}'`);
|
|
@@ -452,91 +405,31 @@ async function oidcSignInCallback(ctx) {
|
|
|
452
405
|
async function logout(ctx) {
|
|
453
406
|
const config2 = strapi.config.get("plugin::strapi-plugin-oidc");
|
|
454
407
|
const logoutUrl = config2.OIDC_END_SESSION_ENDPOINT;
|
|
408
|
+
const adminPanelUrl = strapi.config.get("admin.url", "/admin");
|
|
455
409
|
const isOidcSession = !!ctx.cookies.get("oidc_authenticated");
|
|
456
|
-
const
|
|
410
|
+
const accessToken = ctx.cookies.get("oidc_access_token");
|
|
457
411
|
clearAuthCookies(strapi, ctx);
|
|
458
|
-
if (logoutUrl && isOidcSession) {
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
} else {
|
|
466
|
-
const adminPanelUrl = strapi.config.get("admin.url", "/admin");
|
|
467
|
-
ctx.redirect(`${adminPanelUrl}/auth/login`);
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
const jwksCache = /* @__PURE__ */ new Map();
|
|
471
|
-
function getJWKS(uri) {
|
|
472
|
-
const cached = jwksCache.get(uri);
|
|
473
|
-
if (cached) return cached;
|
|
474
|
-
const jwks = jose.createRemoteJWKSet(new URL(uri));
|
|
475
|
-
jwksCache.set(uri, jwks);
|
|
476
|
-
return jwks;
|
|
477
|
-
}
|
|
478
|
-
async function backchannelLogout(ctx) {
|
|
479
|
-
const config2 = strapi.config.get("plugin::strapi-plugin-oidc");
|
|
480
|
-
const logoutToken = ctx.request.body?.logout_token;
|
|
481
|
-
if (!logoutToken) {
|
|
482
|
-
ctx.status = 400;
|
|
483
|
-
ctx.body = { error: "Missing logout_token" };
|
|
484
|
-
return;
|
|
485
|
-
}
|
|
486
|
-
if (!config2.OIDC_JWKS_URI || !config2.OIDC_ISSUER) {
|
|
487
|
-
ctx.status = 501;
|
|
488
|
-
ctx.body = {
|
|
489
|
-
error: "OIDC_JWKS_URI and OIDC_ISSUER must both be configured to enable backchannel logout"
|
|
490
|
-
};
|
|
491
|
-
return;
|
|
492
|
-
}
|
|
493
|
-
try {
|
|
494
|
-
const JWKS = getJWKS(config2.OIDC_JWKS_URI);
|
|
495
|
-
const verifyOptions = {
|
|
496
|
-
issuer: config2.OIDC_ISSUER,
|
|
497
|
-
audience: config2.OIDC_CLIENT_ID || void 0
|
|
498
|
-
};
|
|
499
|
-
const { payload } = await jose.jwtVerify(logoutToken, JWKS, verifyOptions);
|
|
500
|
-
if ("nonce" in payload) {
|
|
501
|
-
ctx.status = 400;
|
|
502
|
-
ctx.body = { error: "logout_token must not contain nonce" };
|
|
503
|
-
return;
|
|
504
|
-
}
|
|
505
|
-
const events = payload.events;
|
|
506
|
-
if (!events?.["http://schemas.openid.net/event/backchannel-logout"]) {
|
|
507
|
-
ctx.status = 400;
|
|
508
|
-
ctx.body = { error: "logout_token missing backchannel-logout event" };
|
|
509
|
-
return;
|
|
510
|
-
}
|
|
511
|
-
if (!payload.sub && !("sid" in payload)) {
|
|
512
|
-
ctx.status = 400;
|
|
513
|
-
ctx.body = { error: "logout_token must contain sub or sid" };
|
|
514
|
-
return;
|
|
515
|
-
}
|
|
516
|
-
if (payload.sub) {
|
|
517
|
-
const userService = strapi.service("admin::user");
|
|
518
|
-
const user = await userService.findOneByEmail(payload.sub);
|
|
519
|
-
if (user) {
|
|
520
|
-
revokeUser(String(user.id));
|
|
521
|
-
const sessionManager = strapi.sessionManager;
|
|
522
|
-
if (sessionManager) {
|
|
523
|
-
await sessionManager("admin").invalidateRefreshToken(String(user.id));
|
|
524
|
-
}
|
|
412
|
+
if (logoutUrl && isOidcSession && accessToken) {
|
|
413
|
+
try {
|
|
414
|
+
const response = await fetch(config2.OIDC_USERINFO_ENDPOINT, {
|
|
415
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
416
|
+
});
|
|
417
|
+
if (response.ok) {
|
|
418
|
+
return ctx.redirect(logoutUrl);
|
|
525
419
|
}
|
|
420
|
+
} catch {
|
|
526
421
|
}
|
|
527
|
-
ctx.
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
ctx.status = 400;
|
|
532
|
-
ctx.body = { error: "Invalid logout_token" };
|
|
422
|
+
return ctx.redirect(`${adminPanelUrl}/auth/login`);
|
|
423
|
+
}
|
|
424
|
+
if (logoutUrl && isOidcSession) {
|
|
425
|
+
return ctx.redirect(logoutUrl);
|
|
533
426
|
}
|
|
427
|
+
ctx.redirect(`${adminPanelUrl}/auth/login`);
|
|
534
428
|
}
|
|
535
429
|
const oidc = {
|
|
536
430
|
oidcSignIn,
|
|
537
431
|
oidcSignInCallback,
|
|
538
|
-
logout
|
|
539
|
-
backchannelLogout
|
|
432
|
+
logout
|
|
540
433
|
};
|
|
541
434
|
async function find(ctx) {
|
|
542
435
|
const roleService2 = strapi.plugin("strapi-plugin-oidc").service("role");
|
|
@@ -793,12 +686,6 @@ const routes = {
|
|
|
793
686
|
handler: "oidc.logout",
|
|
794
687
|
config: { auth: false }
|
|
795
688
|
},
|
|
796
|
-
{
|
|
797
|
-
method: "POST",
|
|
798
|
-
path: "/logout",
|
|
799
|
-
handler: "oidc.backchannelLogout",
|
|
800
|
-
config: { auth: false }
|
|
801
|
-
},
|
|
802
689
|
{
|
|
803
690
|
method: "GET",
|
|
804
691
|
path: "/whitelist",
|
package/dist/server/index.mjs
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { randomUUID, randomBytes } from "node:crypto";
|
|
2
|
-
import { jwtVerify, createRemoteJWKSet } from "jose";
|
|
3
2
|
import pkceChallenge from "pkce-challenge";
|
|
4
3
|
import strapiUtils from "@strapi/utils";
|
|
5
4
|
import generator from "generate-password";
|
|
@@ -19,17 +18,6 @@ function resolveEnforceOIDC(strapi2, dbValue) {
|
|
|
19
18
|
if (configValue !== null) return configValue;
|
|
20
19
|
return dbValue ?? false;
|
|
21
20
|
}
|
|
22
|
-
const revoked = /* @__PURE__ */ new Map();
|
|
23
|
-
const CLEANUP_DELAY_MS = 24 * 60 * 60 * 1e3;
|
|
24
|
-
function revokeUser(userId) {
|
|
25
|
-
revoked.set(userId, Date.now());
|
|
26
|
-
setTimeout(() => revoked.delete(userId), CLEANUP_DELAY_MS);
|
|
27
|
-
}
|
|
28
|
-
function isUserRevoked(userId, iat) {
|
|
29
|
-
const logoutTime = revoked.get(userId);
|
|
30
|
-
if (!logoutTime) return false;
|
|
31
|
-
return iat * 1e3 <= logoutTime;
|
|
32
|
-
}
|
|
33
21
|
async function bootstrap({ strapi: strapi2 }) {
|
|
34
22
|
const enforceOidcMiddleware = async (ctx, next) => {
|
|
35
23
|
const adminUrl = strapi2.config.get("admin.url", "/admin");
|
|
@@ -82,36 +70,9 @@ async function bootstrap({ strapi: strapi2 }) {
|
|
|
82
70
|
}
|
|
83
71
|
await next();
|
|
84
72
|
};
|
|
85
|
-
const denylistMiddleware = async (ctx, next) => {
|
|
86
|
-
const auth = ctx.request.headers.authorization;
|
|
87
|
-
if (auth?.startsWith("Bearer ")) {
|
|
88
|
-
try {
|
|
89
|
-
const payload = JSON.parse(
|
|
90
|
-
Buffer.from(auth.slice(7).split(".")[1], "base64url").toString()
|
|
91
|
-
);
|
|
92
|
-
if (payload.id && isUserRevoked(String(payload.id), payload.iat ?? 0)) {
|
|
93
|
-
ctx.status = 401;
|
|
94
|
-
ctx.body = {
|
|
95
|
-
data: null,
|
|
96
|
-
error: {
|
|
97
|
-
status: 401,
|
|
98
|
-
name: "UnauthorizedError",
|
|
99
|
-
message: "Session revoked",
|
|
100
|
-
details: {}
|
|
101
|
-
}
|
|
102
|
-
};
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
} catch {
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
await next();
|
|
109
|
-
};
|
|
110
73
|
if (strapi2.server.app && Array.isArray(strapi2.server.app.middleware)) {
|
|
111
74
|
strapi2.server.app.middleware.unshift(enforceOidcMiddleware);
|
|
112
|
-
strapi2.server.app.middleware.unshift(denylistMiddleware);
|
|
113
75
|
} else {
|
|
114
|
-
strapi2.server.use(denylistMiddleware);
|
|
115
76
|
strapi2.server.use(enforceOidcMiddleware);
|
|
116
77
|
}
|
|
117
78
|
const actions = [
|
|
@@ -202,12 +163,6 @@ const config = {
|
|
|
202
163
|
OIDC_FAMILY_NAME_FIELD: "family_name",
|
|
203
164
|
OIDC_GIVEN_NAME_FIELD: "given_name",
|
|
204
165
|
OIDC_END_SESSION_ENDPOINT: "",
|
|
205
|
-
OIDC_POST_LOGOUT_REDIRECT_URI: "",
|
|
206
|
-
// Where to land after the provider has logged the user out (RP-Initiated Logout)
|
|
207
|
-
OIDC_ISSUER: "",
|
|
208
|
-
// Provider issuer URL — used to validate iss claim in backchannel logout tokens
|
|
209
|
-
OIDC_JWKS_URI: "",
|
|
210
|
-
// Provider JWKS endpoint — required for backchannel logout token signature verification
|
|
211
166
|
OIDC_SSO_BUTTON_TEXT: "Login via SSO",
|
|
212
167
|
OIDC_ENFORCE: null
|
|
213
168
|
// null = use DB setting; true/false = override DB (useful for lockout recovery)
|
|
@@ -260,9 +215,8 @@ function getExpiredCookieOptions(strapi2, ctx) {
|
|
|
260
215
|
function clearAuthCookies(strapi2, ctx) {
|
|
261
216
|
const options2 = getExpiredCookieOptions(strapi2, ctx);
|
|
262
217
|
ctx.cookies.set("strapi_admin_refresh", "", options2);
|
|
263
|
-
|
|
264
|
-
ctx.cookies.set("
|
|
265
|
-
ctx.cookies.set("oidc_id_token", "", rootPathOptions);
|
|
218
|
+
ctx.cookies.set("oidc_authenticated", "", { ...options2, path: "/" });
|
|
219
|
+
ctx.cookies.set("oidc_access_token", "", { ...options2, path: "/" });
|
|
266
220
|
}
|
|
267
221
|
const REQUIRED_CONFIG_KEYS = [
|
|
268
222
|
"OIDC_CLIENT_ID",
|
|
@@ -345,7 +299,7 @@ async function exchangeTokenAndFetchUserInfo(config2, params, expectedNonce) {
|
|
|
345
299
|
throw new Error("Failed to fetch user info");
|
|
346
300
|
}
|
|
347
301
|
const userInfo = await userResponse.json();
|
|
348
|
-
return { userInfo,
|
|
302
|
+
return { userInfo, accessToken: tokenData.access_token };
|
|
349
303
|
}
|
|
350
304
|
async function registerNewUser(userService, oauthService2, roleService2, email, userResponseData, whitelistUser, config2, ctx) {
|
|
351
305
|
let roles2 = [];
|
|
@@ -411,29 +365,28 @@ async function oidcSignInCallback(ctx) {
|
|
|
411
365
|
params.append("grant_type", config2.OIDC_GRANT_TYPE);
|
|
412
366
|
params.append("code_verifier", codeVerifier ?? "");
|
|
413
367
|
try {
|
|
414
|
-
const { userInfo
|
|
368
|
+
const { userInfo, accessToken } = await exchangeTokenAndFetchUserInfo(
|
|
415
369
|
config2,
|
|
416
370
|
params,
|
|
417
371
|
oidcNonce ?? ""
|
|
418
372
|
);
|
|
373
|
+
const isProduction = strapi.config.get("environment") === "production";
|
|
374
|
+
ctx.cookies.set("oidc_access_token", accessToken, {
|
|
375
|
+
httpOnly: true,
|
|
376
|
+
maxAge: 3e5,
|
|
377
|
+
// 5 minutes — matches typical provider access token lifetime
|
|
378
|
+
secure: isProduction && ctx.request.secure,
|
|
379
|
+
sameSite: "lax"
|
|
380
|
+
});
|
|
419
381
|
const { activateUser, jwtToken } = await handleUserAuthentication(
|
|
420
382
|
userService,
|
|
421
383
|
oauthService2,
|
|
422
384
|
roleService2,
|
|
423
385
|
whitelistService2,
|
|
424
|
-
|
|
386
|
+
userInfo,
|
|
425
387
|
config2,
|
|
426
388
|
ctx
|
|
427
389
|
);
|
|
428
|
-
if (idToken) {
|
|
429
|
-
const isProduction = strapi.config.get("environment") === "production";
|
|
430
|
-
ctx.cookies.set("oidc_id_token", idToken, {
|
|
431
|
-
httpOnly: true,
|
|
432
|
-
secure: isProduction && ctx.request.secure,
|
|
433
|
-
path: "/",
|
|
434
|
-
sameSite: "lax"
|
|
435
|
-
});
|
|
436
|
-
}
|
|
437
390
|
const nonce = randomUUID();
|
|
438
391
|
const html = oauthService2.renderSignUpSuccess(jwtToken, activateUser, nonce);
|
|
439
392
|
ctx.set("Content-Security-Policy", `script-src 'nonce-${nonce}'`);
|
|
@@ -446,91 +399,31 @@ async function oidcSignInCallback(ctx) {
|
|
|
446
399
|
async function logout(ctx) {
|
|
447
400
|
const config2 = strapi.config.get("plugin::strapi-plugin-oidc");
|
|
448
401
|
const logoutUrl = config2.OIDC_END_SESSION_ENDPOINT;
|
|
402
|
+
const adminPanelUrl = strapi.config.get("admin.url", "/admin");
|
|
449
403
|
const isOidcSession = !!ctx.cookies.get("oidc_authenticated");
|
|
450
|
-
const
|
|
404
|
+
const accessToken = ctx.cookies.get("oidc_access_token");
|
|
451
405
|
clearAuthCookies(strapi, ctx);
|
|
452
|
-
if (logoutUrl && isOidcSession) {
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
} else {
|
|
460
|
-
const adminPanelUrl = strapi.config.get("admin.url", "/admin");
|
|
461
|
-
ctx.redirect(`${adminPanelUrl}/auth/login`);
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
const jwksCache = /* @__PURE__ */ new Map();
|
|
465
|
-
function getJWKS(uri) {
|
|
466
|
-
const cached = jwksCache.get(uri);
|
|
467
|
-
if (cached) return cached;
|
|
468
|
-
const jwks = createRemoteJWKSet(new URL(uri));
|
|
469
|
-
jwksCache.set(uri, jwks);
|
|
470
|
-
return jwks;
|
|
471
|
-
}
|
|
472
|
-
async function backchannelLogout(ctx) {
|
|
473
|
-
const config2 = strapi.config.get("plugin::strapi-plugin-oidc");
|
|
474
|
-
const logoutToken = ctx.request.body?.logout_token;
|
|
475
|
-
if (!logoutToken) {
|
|
476
|
-
ctx.status = 400;
|
|
477
|
-
ctx.body = { error: "Missing logout_token" };
|
|
478
|
-
return;
|
|
479
|
-
}
|
|
480
|
-
if (!config2.OIDC_JWKS_URI || !config2.OIDC_ISSUER) {
|
|
481
|
-
ctx.status = 501;
|
|
482
|
-
ctx.body = {
|
|
483
|
-
error: "OIDC_JWKS_URI and OIDC_ISSUER must both be configured to enable backchannel logout"
|
|
484
|
-
};
|
|
485
|
-
return;
|
|
486
|
-
}
|
|
487
|
-
try {
|
|
488
|
-
const JWKS = getJWKS(config2.OIDC_JWKS_URI);
|
|
489
|
-
const verifyOptions = {
|
|
490
|
-
issuer: config2.OIDC_ISSUER,
|
|
491
|
-
audience: config2.OIDC_CLIENT_ID || void 0
|
|
492
|
-
};
|
|
493
|
-
const { payload } = await jwtVerify(logoutToken, JWKS, verifyOptions);
|
|
494
|
-
if ("nonce" in payload) {
|
|
495
|
-
ctx.status = 400;
|
|
496
|
-
ctx.body = { error: "logout_token must not contain nonce" };
|
|
497
|
-
return;
|
|
498
|
-
}
|
|
499
|
-
const events = payload.events;
|
|
500
|
-
if (!events?.["http://schemas.openid.net/event/backchannel-logout"]) {
|
|
501
|
-
ctx.status = 400;
|
|
502
|
-
ctx.body = { error: "logout_token missing backchannel-logout event" };
|
|
503
|
-
return;
|
|
504
|
-
}
|
|
505
|
-
if (!payload.sub && !("sid" in payload)) {
|
|
506
|
-
ctx.status = 400;
|
|
507
|
-
ctx.body = { error: "logout_token must contain sub or sid" };
|
|
508
|
-
return;
|
|
509
|
-
}
|
|
510
|
-
if (payload.sub) {
|
|
511
|
-
const userService = strapi.service("admin::user");
|
|
512
|
-
const user = await userService.findOneByEmail(payload.sub);
|
|
513
|
-
if (user) {
|
|
514
|
-
revokeUser(String(user.id));
|
|
515
|
-
const sessionManager = strapi.sessionManager;
|
|
516
|
-
if (sessionManager) {
|
|
517
|
-
await sessionManager("admin").invalidateRefreshToken(String(user.id));
|
|
518
|
-
}
|
|
406
|
+
if (logoutUrl && isOidcSession && accessToken) {
|
|
407
|
+
try {
|
|
408
|
+
const response = await fetch(config2.OIDC_USERINFO_ENDPOINT, {
|
|
409
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
410
|
+
});
|
|
411
|
+
if (response.ok) {
|
|
412
|
+
return ctx.redirect(logoutUrl);
|
|
519
413
|
}
|
|
414
|
+
} catch {
|
|
520
415
|
}
|
|
521
|
-
ctx.
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
ctx.status = 400;
|
|
526
|
-
ctx.body = { error: "Invalid logout_token" };
|
|
416
|
+
return ctx.redirect(`${adminPanelUrl}/auth/login`);
|
|
417
|
+
}
|
|
418
|
+
if (logoutUrl && isOidcSession) {
|
|
419
|
+
return ctx.redirect(logoutUrl);
|
|
527
420
|
}
|
|
421
|
+
ctx.redirect(`${adminPanelUrl}/auth/login`);
|
|
528
422
|
}
|
|
529
423
|
const oidc = {
|
|
530
424
|
oidcSignIn,
|
|
531
425
|
oidcSignInCallback,
|
|
532
|
-
logout
|
|
533
|
-
backchannelLogout
|
|
426
|
+
logout
|
|
534
427
|
};
|
|
535
428
|
async function find(ctx) {
|
|
536
429
|
const roleService2 = strapi.plugin("strapi-plugin-oidc").service("role");
|
|
@@ -787,12 +680,6 @@ const routes = {
|
|
|
787
680
|
handler: "oidc.logout",
|
|
788
681
|
config: { auth: false }
|
|
789
682
|
},
|
|
790
|
-
{
|
|
791
|
-
method: "POST",
|
|
792
|
-
path: "/logout",
|
|
793
|
-
handler: "oidc.backchannelLogout",
|
|
794
|
-
config: { auth: false }
|
|
795
|
-
},
|
|
796
683
|
{
|
|
797
684
|
method: "GET",
|
|
798
685
|
path: "/whitelist",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "strapi-plugin-oidc",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.3",
|
|
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,7 +50,6 @@
|
|
|
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",
|
|
54
53
|
"pkce-challenge": "^6.0.0",
|
|
55
54
|
"react-intl": "^6.8.9"
|
|
56
55
|
},
|
|
@@ -82,8 +81,6 @@
|
|
|
82
81
|
"@eslint/eslintrc": "^3.3.5",
|
|
83
82
|
"@eslint/js": "^10.0.1",
|
|
84
83
|
"@strapi/sdk-plugin": "^6.0.1",
|
|
85
|
-
"@strapi/types": "^5.41.1",
|
|
86
|
-
"@types/koa": "^2.16.4",
|
|
87
84
|
"@types/node": "^25.5.2",
|
|
88
85
|
"@types/supertest": "^7.2.0",
|
|
89
86
|
"@vitest/coverage-v8": "^4.1.2",
|