strapi-plugin-oidc 1.5.1 → 1.5.2
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 +5 -144
- package/dist/server/index.mjs +5 -144
- 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,7 @@ 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("oidc_authenticated", "", rootPathOptions);
|
|
271
|
-
ctx.cookies.set("oidc_id_token", "", rootPathOptions);
|
|
224
|
+
ctx.cookies.set("oidc_authenticated", "", { ...options2, path: "/" });
|
|
272
225
|
}
|
|
273
226
|
const REQUIRED_CONFIG_KEYS = [
|
|
274
227
|
"OIDC_CLIENT_ID",
|
|
@@ -350,8 +303,7 @@ async function exchangeTokenAndFetchUserInfo(config2, params, expectedNonce) {
|
|
|
350
303
|
if (!userResponse.ok) {
|
|
351
304
|
throw new Error("Failed to fetch user info");
|
|
352
305
|
}
|
|
353
|
-
|
|
354
|
-
return { userInfo, idToken: tokenData.id_token };
|
|
306
|
+
return userResponse.json();
|
|
355
307
|
}
|
|
356
308
|
async function registerNewUser(userService, oauthService2, roleService2, email, userResponseData, whitelistUser, config2, ctx) {
|
|
357
309
|
let roles2 = [];
|
|
@@ -417,11 +369,7 @@ async function oidcSignInCallback(ctx) {
|
|
|
417
369
|
params.append("grant_type", config2.OIDC_GRANT_TYPE);
|
|
418
370
|
params.append("code_verifier", codeVerifier ?? "");
|
|
419
371
|
try {
|
|
420
|
-
const
|
|
421
|
-
config2,
|
|
422
|
-
params,
|
|
423
|
-
oidcNonce ?? ""
|
|
424
|
-
);
|
|
372
|
+
const userResponseData = await exchangeTokenAndFetchUserInfo(config2, params, oidcNonce ?? "");
|
|
425
373
|
const { activateUser, jwtToken } = await handleUserAuthentication(
|
|
426
374
|
userService,
|
|
427
375
|
oauthService2,
|
|
@@ -431,15 +379,6 @@ async function oidcSignInCallback(ctx) {
|
|
|
431
379
|
config2,
|
|
432
380
|
ctx
|
|
433
381
|
);
|
|
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
382
|
const nonce = node_crypto.randomUUID();
|
|
444
383
|
const html = oauthService2.renderSignUpSuccess(jwtToken, activateUser, nonce);
|
|
445
384
|
ctx.set("Content-Security-Policy", `script-src 'nonce-${nonce}'`);
|
|
@@ -453,90 +392,18 @@ async function logout(ctx) {
|
|
|
453
392
|
const config2 = strapi.config.get("plugin::strapi-plugin-oidc");
|
|
454
393
|
const logoutUrl = config2.OIDC_END_SESSION_ENDPOINT;
|
|
455
394
|
const isOidcSession = !!ctx.cookies.get("oidc_authenticated");
|
|
456
|
-
const idToken = ctx.cookies.get("oidc_id_token");
|
|
457
395
|
clearAuthCookies(strapi, ctx);
|
|
458
396
|
if (logoutUrl && isOidcSession) {
|
|
459
|
-
|
|
460
|
-
if (idToken) url.searchParams.set("id_token_hint", idToken);
|
|
461
|
-
if (config2.OIDC_POST_LOGOUT_REDIRECT_URI) {
|
|
462
|
-
url.searchParams.set("post_logout_redirect_uri", config2.OIDC_POST_LOGOUT_REDIRECT_URI);
|
|
463
|
-
}
|
|
464
|
-
ctx.redirect(url.toString());
|
|
397
|
+
ctx.redirect(logoutUrl);
|
|
465
398
|
} else {
|
|
466
399
|
const adminPanelUrl = strapi.config.get("admin.url", "/admin");
|
|
467
400
|
ctx.redirect(`${adminPanelUrl}/auth/login`);
|
|
468
401
|
}
|
|
469
402
|
}
|
|
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
|
-
}
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
ctx.status = 200;
|
|
528
|
-
ctx.body = "";
|
|
529
|
-
} catch (e) {
|
|
530
|
-
strapi.log.error("Backchannel logout failed:", e);
|
|
531
|
-
ctx.status = 400;
|
|
532
|
-
ctx.body = { error: "Invalid logout_token" };
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
403
|
const oidc = {
|
|
536
404
|
oidcSignIn,
|
|
537
405
|
oidcSignInCallback,
|
|
538
|
-
logout
|
|
539
|
-
backchannelLogout
|
|
406
|
+
logout
|
|
540
407
|
};
|
|
541
408
|
async function find(ctx) {
|
|
542
409
|
const roleService2 = strapi.plugin("strapi-plugin-oidc").service("role");
|
|
@@ -793,12 +660,6 @@ const routes = {
|
|
|
793
660
|
handler: "oidc.logout",
|
|
794
661
|
config: { auth: false }
|
|
795
662
|
},
|
|
796
|
-
{
|
|
797
|
-
method: "POST",
|
|
798
|
-
path: "/logout",
|
|
799
|
-
handler: "oidc.backchannelLogout",
|
|
800
|
-
config: { auth: false }
|
|
801
|
-
},
|
|
802
663
|
{
|
|
803
664
|
method: "GET",
|
|
804
665
|
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,7 @@ 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("oidc_authenticated", "", rootPathOptions);
|
|
265
|
-
ctx.cookies.set("oidc_id_token", "", rootPathOptions);
|
|
218
|
+
ctx.cookies.set("oidc_authenticated", "", { ...options2, path: "/" });
|
|
266
219
|
}
|
|
267
220
|
const REQUIRED_CONFIG_KEYS = [
|
|
268
221
|
"OIDC_CLIENT_ID",
|
|
@@ -344,8 +297,7 @@ async function exchangeTokenAndFetchUserInfo(config2, params, expectedNonce) {
|
|
|
344
297
|
if (!userResponse.ok) {
|
|
345
298
|
throw new Error("Failed to fetch user info");
|
|
346
299
|
}
|
|
347
|
-
|
|
348
|
-
return { userInfo, idToken: tokenData.id_token };
|
|
300
|
+
return userResponse.json();
|
|
349
301
|
}
|
|
350
302
|
async function registerNewUser(userService, oauthService2, roleService2, email, userResponseData, whitelistUser, config2, ctx) {
|
|
351
303
|
let roles2 = [];
|
|
@@ -411,11 +363,7 @@ async function oidcSignInCallback(ctx) {
|
|
|
411
363
|
params.append("grant_type", config2.OIDC_GRANT_TYPE);
|
|
412
364
|
params.append("code_verifier", codeVerifier ?? "");
|
|
413
365
|
try {
|
|
414
|
-
const
|
|
415
|
-
config2,
|
|
416
|
-
params,
|
|
417
|
-
oidcNonce ?? ""
|
|
418
|
-
);
|
|
366
|
+
const userResponseData = await exchangeTokenAndFetchUserInfo(config2, params, oidcNonce ?? "");
|
|
419
367
|
const { activateUser, jwtToken } = await handleUserAuthentication(
|
|
420
368
|
userService,
|
|
421
369
|
oauthService2,
|
|
@@ -425,15 +373,6 @@ async function oidcSignInCallback(ctx) {
|
|
|
425
373
|
config2,
|
|
426
374
|
ctx
|
|
427
375
|
);
|
|
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
376
|
const nonce = randomUUID();
|
|
438
377
|
const html = oauthService2.renderSignUpSuccess(jwtToken, activateUser, nonce);
|
|
439
378
|
ctx.set("Content-Security-Policy", `script-src 'nonce-${nonce}'`);
|
|
@@ -447,90 +386,18 @@ async function logout(ctx) {
|
|
|
447
386
|
const config2 = strapi.config.get("plugin::strapi-plugin-oidc");
|
|
448
387
|
const logoutUrl = config2.OIDC_END_SESSION_ENDPOINT;
|
|
449
388
|
const isOidcSession = !!ctx.cookies.get("oidc_authenticated");
|
|
450
|
-
const idToken = ctx.cookies.get("oidc_id_token");
|
|
451
389
|
clearAuthCookies(strapi, ctx);
|
|
452
390
|
if (logoutUrl && isOidcSession) {
|
|
453
|
-
|
|
454
|
-
if (idToken) url.searchParams.set("id_token_hint", idToken);
|
|
455
|
-
if (config2.OIDC_POST_LOGOUT_REDIRECT_URI) {
|
|
456
|
-
url.searchParams.set("post_logout_redirect_uri", config2.OIDC_POST_LOGOUT_REDIRECT_URI);
|
|
457
|
-
}
|
|
458
|
-
ctx.redirect(url.toString());
|
|
391
|
+
ctx.redirect(logoutUrl);
|
|
459
392
|
} else {
|
|
460
393
|
const adminPanelUrl = strapi.config.get("admin.url", "/admin");
|
|
461
394
|
ctx.redirect(`${adminPanelUrl}/auth/login`);
|
|
462
395
|
}
|
|
463
396
|
}
|
|
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
|
-
}
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
ctx.status = 200;
|
|
522
|
-
ctx.body = "";
|
|
523
|
-
} catch (e) {
|
|
524
|
-
strapi.log.error("Backchannel logout failed:", e);
|
|
525
|
-
ctx.status = 400;
|
|
526
|
-
ctx.body = { error: "Invalid logout_token" };
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
397
|
const oidc = {
|
|
530
398
|
oidcSignIn,
|
|
531
399
|
oidcSignInCallback,
|
|
532
|
-
logout
|
|
533
|
-
backchannelLogout
|
|
400
|
+
logout
|
|
534
401
|
};
|
|
535
402
|
async function find(ctx) {
|
|
536
403
|
const roleService2 = strapi.plugin("strapi-plugin-oidc").service("role");
|
|
@@ -787,12 +654,6 @@ const routes = {
|
|
|
787
654
|
handler: "oidc.logout",
|
|
788
655
|
config: { auth: false }
|
|
789
656
|
},
|
|
790
|
-
{
|
|
791
|
-
method: "POST",
|
|
792
|
-
path: "/logout",
|
|
793
|
-
handler: "oidc.backchannelLogout",
|
|
794
|
-
config: { auth: false }
|
|
795
|
-
},
|
|
796
657
|
{
|
|
797
658
|
method: "GET",
|
|
798
659
|
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.2",
|
|
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",
|