strapi-plugin-oidc 1.7.6 → 1.8.1
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 +77 -47
- package/dist/admin/{index-DRJ6Ty2J.mjs → index-Bb9-aYb4.mjs} +54 -6
- package/dist/admin/{index-D2rlNx1-.js → index-Bmg4eTYb.js} +115 -88
- package/dist/admin/{index-pieFAsgM.mjs → index-BqWd-Iiq.mjs} +74 -47
- package/dist/admin/{index-CrnGXADu.js → index-Dk6TYtio.js} +58 -8
- package/dist/admin/index.js +3 -1
- package/dist/admin/index.mjs +3 -1
- package/dist/server/index.js +266 -92
- package/dist/server/index.mjs +266 -92
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -31,32 +31,45 @@ module.exports = ({ env }) => ({
|
|
|
31
31
|
'strapi-plugin-oidc': {
|
|
32
32
|
enabled: true,
|
|
33
33
|
config: {
|
|
34
|
-
// Required
|
|
34
|
+
// Required
|
|
35
|
+
OIDC_DISCOVERY_URL: env('OIDC_DISCOVERY_URL'), // https://your-provider/.well-known/openid-configuration
|
|
35
36
|
OIDC_CLIENT_ID: env('OIDC_CLIENT_ID'),
|
|
36
37
|
OIDC_CLIENT_SECRET: env('OIDC_CLIENT_SECRET'),
|
|
37
38
|
OIDC_REDIRECT_URI: env('OIDC_REDIRECT_URI'), // https://your-strapi.com/strapi-plugin-oidc/oidc/callback
|
|
38
|
-
OIDC_AUTHORIZATION_ENDPOINT: env('OIDC_AUTHORIZATION_ENDPOINT'),
|
|
39
|
-
OIDC_TOKEN_ENDPOINT: env('OIDC_TOKEN_ENDPOINT'),
|
|
40
|
-
OIDC_USERINFO_ENDPOINT: env('OIDC_USERINFO_ENDPOINT'),
|
|
41
39
|
|
|
42
40
|
// Optional — defaults shown
|
|
43
|
-
OIDC_SCOPE: 'openid profile email',
|
|
44
|
-
OIDC_GRANT_TYPE: 'authorization_code',
|
|
41
|
+
OIDC_SCOPE: 'openid profile email', // space-separated scopes
|
|
45
42
|
OIDC_FAMILY_NAME_FIELD: 'family_name',
|
|
46
43
|
OIDC_GIVEN_NAME_FIELD: 'given_name',
|
|
47
|
-
OIDC_END_SESSION_ENDPOINT: '', // Provider end-session URL for RP-initiated logout
|
|
48
44
|
OIDC_SSO_BUTTON_TEXT: 'Login via SSO',
|
|
49
45
|
OIDC_ENFORCE: null, // null = use Admin UI toggle; true/false = override in config
|
|
50
46
|
REMEMBER_ME: false, // Persist session across browser restarts
|
|
51
47
|
AUDIT_LOG_RETENTION_DAYS: 90, // Set to 0 to disable audit logging; otherwise entries older than this many days are purged daily at midnight
|
|
52
48
|
OIDC_GROUP_FIELD: 'groups', // OIDC claim field containing group membership
|
|
53
49
|
OIDC_GROUP_ROLE_MAP: '{}', // JSON map of group names to Strapi role names
|
|
50
|
+
OIDC_REQUIRE_EMAIL_VERIFIED: true, // Reject logins when provider does not report email_verified=true (set false to disable)
|
|
51
|
+
OIDC_TRUSTED_IP_HEADER: '', // Optional: 'cf-connecting-ip' for Cloudflare; read only when Strapi trusts the proxy
|
|
52
|
+
OIDC_FORCE_SECURE_COOKIES: false, // Set true when behind a trusted HTTPS proxy that Strapi can't auto-detect
|
|
54
53
|
},
|
|
55
54
|
},
|
|
56
55
|
});
|
|
57
56
|
```
|
|
58
57
|
|
|
59
|
-
|
|
58
|
+
`OIDC_DISCOVERY_URL` is the URL of your provider's OpenID Connect discovery document (`/.well-known/openid-configuration`). The plugin fetches it at startup and automatically configures all endpoints, JWKS URI, and issuer.
|
|
59
|
+
|
|
60
|
+
### Security features
|
|
61
|
+
|
|
62
|
+
- **ID token verification** — Enabled automatically when the discovery document includes a `jwks_uri`. Validates signature, issuer, audience, and expiry via [`jose`](https://github.com/panva/jose)
|
|
63
|
+
- **Email verification** — `OIDC_REQUIRE_EMAIL_VERIFIED: true` (default) rejects unverified emails
|
|
64
|
+
- **CSRF protection** — OIDC state/nonce and POST-only logout endpoint
|
|
65
|
+
- **Rate limiting** — 1 000 req/min per IP+UA (in-process; use a reverse-proxy-level limiter for multi-node)
|
|
66
|
+
- **Secure cookies** — `OIDC_FORCE_SECURE_COOKIES` ensures cookies are marked Secure
|
|
67
|
+
|
|
68
|
+
### Client IP attribution and reverse proxies
|
|
69
|
+
|
|
70
|
+
The plugin logs client IPs for rate-limit buckets and audit logs. When Strapi runs behind a reverse proxy, **set `server.proxy: true`** so Koa trusts `X-Forwarded-For`; otherwise all IPs will be the proxy's.
|
|
71
|
+
|
|
72
|
+
Set `OIDC_TRUSTED_IP_HEADER: 'cf-connecting-ip'` when behind Cloudflare. The header is only honoured when `server.proxy: true` is set.
|
|
60
73
|
|
|
61
74
|
## Login
|
|
62
75
|
|
|
@@ -64,27 +77,27 @@ Navigate to `/strapi-plugin-oidc/oidc` to start the OIDC flow, or click the **Lo
|
|
|
64
77
|
|
|
65
78
|
## Logout
|
|
66
79
|
|
|
67
|
-
When
|
|
80
|
+
When the discovery document includes an `end_session_endpoint`, clicking logout redirects to the provider's end-session URL (RP-initiated logout). If the provider session has already expired, Strapi skips the redirect and goes straight to the login page.
|
|
81
|
+
|
|
82
|
+
The logout endpoint is `POST /strapi-plugin-oidc/logout`. Using POST instead of GET prevents CSRF-forced-logout attacks.
|
|
68
83
|
|
|
69
84
|
## Admin Settings
|
|
70
85
|
|
|
71
86
|
Manage the plugin under **Settings → OIDC Plugin**.
|
|
72
87
|
|
|
73
|
-
**Default Roles** —
|
|
88
|
+
**Default Roles** — Strapi admin role(s) assigned to new users on first login.
|
|
74
89
|
|
|
75
|
-
**Whitelist** — Restrict access to specific email addresses. When
|
|
90
|
+
**Whitelist** — Restrict access to specific email addresses. When empty, any authenticated OIDC user gets an account. Supports:
|
|
76
91
|
|
|
77
|
-
-
|
|
78
|
-
- JSON import / export
|
|
92
|
+
- Individual emails with optional role overrides
|
|
93
|
+
- JSON import / export
|
|
79
94
|
- Bulk delete with confirmation
|
|
80
|
-
- Unsaved changes are held in the UI until **Save Changes** is clicked
|
|
81
95
|
|
|
82
|
-
**Audit Logs** —
|
|
96
|
+
**Audit Logs** — Authentication events recorded and visible in the settings page. Filter by action, email, IP, and date. **Download** exports the current view as NDJSON. Set `AUDIT_LOG_RETENTION_DAYS` to `0` to disable. Records older than the configured value (default: 90 days) are purged daily.
|
|
83
97
|
|
|
84
|
-
**Enforce OIDC Login** — Removes
|
|
98
|
+
**Enforce OIDC Login** — Removes email/password fields from the login page and blocks direct login API calls. Automatically disabled when the whitelist is empty to prevent lockout.
|
|
85
99
|
|
|
86
|
-
|
|
87
|
-
- **Lockout recovery**: set `OIDC_ENFORCE: false` in your plugin config and restart Strapi. This writes through to the database so removing the variable afterwards keeps the setting.
|
|
100
|
+
The toggle is grayed out when `OIDC_ENFORCE` is set in config. **Lockout recovery**: set `OIDC_ENFORCE: false` in your plugin config and restart Strapi.
|
|
88
101
|
|
|
89
102
|
## Group-to-Role Mapping
|
|
90
103
|
|
|
@@ -118,18 +131,26 @@ Role names are the **display names** shown in **Settings → Roles** (e.g. `"Edi
|
|
|
118
131
|
|
|
119
132
|
### Role assignment precedence
|
|
120
133
|
|
|
121
|
-
1. **
|
|
122
|
-
2. **No
|
|
134
|
+
1. **OIDC groups match `OIDC_GROUP_ROLE_MAP`** → mapped Strapi roles
|
|
135
|
+
2. **No match or no mapping** → default OIDC roles (new users only)
|
|
123
136
|
|
|
124
137
|
### Role updates on subsequent logins
|
|
125
138
|
|
|
126
|
-
- **New users** — Roles
|
|
127
|
-
- **Existing users with
|
|
128
|
-
- **Existing users
|
|
139
|
+
- **New users** — Roles assigned on first login (group-mapped or default).
|
|
140
|
+
- **Existing users with group match** — Roles updated to reflect current mapping.
|
|
141
|
+
- **Existing users without group match** — Roles left unchanged. Manually-assigned roles are never overwritten.
|
|
129
142
|
|
|
130
143
|
## Whitelist API
|
|
131
144
|
|
|
132
|
-
The whitelist can be managed programmatically using a Strapi **API token
|
|
145
|
+
The whitelist can be managed programmatically using a Strapi **API token**. All endpoints are under `/api/strapi-plugin-oidc` and require `Authorization: Bearer <token>`.
|
|
146
|
+
|
|
147
|
+
**Full-access tokens** can call all routes. **Custom tokens** must be granted one of the following scopes (Settings → API Tokens → Custom → plugin permissions):
|
|
148
|
+
|
|
149
|
+
| Scope | Routes |
|
|
150
|
+
| --------------------------------------------- | ----------------------------------------------- |
|
|
151
|
+
| `plugin::strapi-plugin-oidc.whitelist.read` | `GET /whitelist`, `GET /whitelist/export` |
|
|
152
|
+
| `plugin::strapi-plugin-oidc.whitelist.write` | `POST /whitelist`, `POST /whitelist/import` |
|
|
153
|
+
| `plugin::strapi-plugin-oidc.whitelist.delete` | `DELETE /whitelist`, `DELETE /whitelist/:email` |
|
|
133
154
|
|
|
134
155
|
| Method | Path | Description |
|
|
135
156
|
| -------- | ------------------------------------------ | ---------------------- |
|
|
@@ -185,7 +206,14 @@ curl -X DELETE -H "Authorization: Bearer <token>" \
|
|
|
185
206
|
|
|
186
207
|
## Audit Log API
|
|
187
208
|
|
|
188
|
-
Audit log entries can be fetched programmatically using a Strapi **API token
|
|
209
|
+
Audit log entries can be fetched programmatically using a Strapi **API token**. Endpoints are under `/api/strapi-plugin-oidc` and require `Authorization: Bearer <token>`.
|
|
210
|
+
|
|
211
|
+
**Full-access tokens** can call all routes. **Custom tokens** must be granted one of the following scopes:
|
|
212
|
+
|
|
213
|
+
| Scope | Routes |
|
|
214
|
+
| ----------------------------------------- | ------------------------------------------- |
|
|
215
|
+
| `plugin::strapi-plugin-oidc.audit.read` | `GET /audit-logs`, `GET /audit-logs/export` |
|
|
216
|
+
| `plugin::strapi-plugin-oidc.audit.delete` | `DELETE /audit-logs` |
|
|
189
217
|
|
|
190
218
|
| Method | Path | Description |
|
|
191
219
|
| -------- | ------------------------------------------- | ----------------------------------- |
|
|
@@ -246,18 +274,20 @@ curl -H "Authorization: Bearer <token>" -G \
|
|
|
246
274
|
|
|
247
275
|
### Recorded actions
|
|
248
276
|
|
|
249
|
-
| Action | Trigger
|
|
250
|
-
| ----------------------- |
|
|
251
|
-
| `login_success` | Successful OIDC authentication
|
|
252
|
-
| `user_created` | New Strapi admin user created during login
|
|
253
|
-
| `login_failure` | Unexpected error during the OIDC login flow
|
|
254
|
-
| `missing_code` | Callback received without an authorisation code
|
|
255
|
-
| `state_mismatch` | CSRF state cookie does not match callback parameter
|
|
256
|
-
| `nonce_mismatch` | ID token nonce does not match the session nonce
|
|
257
|
-
| `token_exchange_failed` | Provider returned an error during token exchange
|
|
258
|
-
| `whitelist_rejected` | Email not present in the active whitelist
|
|
259
|
-
| `
|
|
260
|
-
| `
|
|
277
|
+
| Action | Trigger |
|
|
278
|
+
| ----------------------- | ----------------------------------------------------------------- |
|
|
279
|
+
| `login_success` | Successful OIDC authentication |
|
|
280
|
+
| `user_created` | New Strapi admin user created during login |
|
|
281
|
+
| `login_failure` | Unexpected error during the OIDC login flow |
|
|
282
|
+
| `missing_code` | Callback received without an authorisation code |
|
|
283
|
+
| `state_mismatch` | CSRF state cookie does not match callback parameter |
|
|
284
|
+
| `nonce_mismatch` | ID token nonce does not match the session nonce |
|
|
285
|
+
| `token_exchange_failed` | Provider returned an error during token exchange |
|
|
286
|
+
| `whitelist_rejected` | Email not present in the active whitelist |
|
|
287
|
+
| `email_not_verified` | Provider did not report `email_verified=true` |
|
|
288
|
+
| `id_token_invalid` | ID token failed signature, issuer, audience, or expiry validation |
|
|
289
|
+
| `logout` | User logged out via `/logout` |
|
|
290
|
+
| `session_expired` | Logout attempted but provider session already stale |
|
|
261
291
|
|
|
262
292
|
Each event is also emitted on Strapi's internal eventHub as `strapi-plugin-oidc::auth.<action>`, which Enterprise audit log listeners pick up automatically.
|
|
263
293
|
|
|
@@ -280,16 +310,16 @@ This plugin is a hard fork of [`strapi-plugin-sso`](https://github.com/yasudaclo
|
|
|
280
310
|
|
|
281
311
|
### Changes from the original:
|
|
282
312
|
|
|
283
|
-
-
|
|
284
|
-
- Redesigned
|
|
285
|
-
-
|
|
286
|
-
-
|
|
287
|
-
- Migrated to Vitest with
|
|
288
|
-
- Config variable names aligned with OIDC discovery document field names
|
|
289
|
-
-
|
|
290
|
-
- Whitelist
|
|
291
|
-
- Hardened OIDC flow: server-generated state and nonce, PKCE, Bearer token auth for userinfo,
|
|
292
|
-
- Audit log: records all auth events to a queryable table with
|
|
313
|
+
- OIDC-only (removed other SSO methods)
|
|
314
|
+
- Redesigned whitelist and role management UI using native Strapi components
|
|
315
|
+
- OIDC enforcement via admin toggle or `OIDC_ENFORCE` config
|
|
316
|
+
- RP-initiated logout with smart session detection
|
|
317
|
+
- Migrated to Vitest with e2e coverage
|
|
318
|
+
- Config variable names aligned with OIDC discovery document field names
|
|
319
|
+
- **Login via SSO** button always injected; text configurable via `OIDC_SSO_BUTTON_TEXT`
|
|
320
|
+
- Whitelist REST API with JSON import/export, bulk delete, delete by email
|
|
321
|
+
- Hardened OIDC flow: server-generated state and nonce, PKCE, Bearer token auth for userinfo, generic error messages on failure
|
|
322
|
+
- Audit log: records all auth events to a queryable table with UI, JSON/NDJSON export, and REST API
|
|
293
323
|
|
|
294
324
|
## License
|
|
295
325
|
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import { useRef, useEffect } from "react";
|
|
1
|
+
import React, { useRef, useEffect, useState } from "react";
|
|
2
|
+
import { createRoot } from "react-dom/client";
|
|
3
|
+
import { jsx } from "react/jsx-runtime";
|
|
4
|
+
import { DesignSystemProvider, darkTheme, lightTheme, Loader } from "@strapi/design-system";
|
|
2
5
|
const __variableDynamicImportRuntimeHelper = (glob, path, segs) => {
|
|
3
6
|
const v = glob[path];
|
|
4
7
|
if (v) {
|
|
@@ -32,6 +35,42 @@ function Initializer({ setPlugin }) {
|
|
|
32
35
|
}, []);
|
|
33
36
|
return null;
|
|
34
37
|
}
|
|
38
|
+
const LOGOUT_EVENT = "strapi-oidc:logout";
|
|
39
|
+
function Overlay({ bg }) {
|
|
40
|
+
const [active, setActive] = useState(false);
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
const handler = () => setActive(true);
|
|
43
|
+
window.addEventListener(LOGOUT_EVENT, handler);
|
|
44
|
+
return () => window.removeEventListener(LOGOUT_EVENT, handler);
|
|
45
|
+
}, []);
|
|
46
|
+
if (!active) return null;
|
|
47
|
+
return /* @__PURE__ */ jsx(
|
|
48
|
+
"div",
|
|
49
|
+
{
|
|
50
|
+
style: {
|
|
51
|
+
position: "fixed",
|
|
52
|
+
inset: 0,
|
|
53
|
+
zIndex: 1e4,
|
|
54
|
+
display: "flex",
|
|
55
|
+
alignItems: "center",
|
|
56
|
+
justifyContent: "center",
|
|
57
|
+
background: bg,
|
|
58
|
+
backdropFilter: "blur(2px)"
|
|
59
|
+
},
|
|
60
|
+
children: /* @__PURE__ */ jsx(Loader, {})
|
|
61
|
+
}
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
function resolveTheme() {
|
|
65
|
+
const stored = window.localStorage.getItem("STRAPI_THEME") ?? "system";
|
|
66
|
+
const isDark = stored === "dark" || stored === "system" && (window.matchMedia?.("(prefers-color-scheme: dark)").matches ?? false);
|
|
67
|
+
return isDark ? darkTheme : lightTheme;
|
|
68
|
+
}
|
|
69
|
+
function LogoutOverlay() {
|
|
70
|
+
const theme = resolveTheme();
|
|
71
|
+
const bg = theme === darkTheme ? "rgba(24, 24, 38, 0.85)" : "rgba(255, 255, 255, 0.85)";
|
|
72
|
+
return /* @__PURE__ */ jsx(DesignSystemProvider, { theme, children: /* @__PURE__ */ jsx(Overlay, { bg }) });
|
|
73
|
+
}
|
|
35
74
|
const en = {
|
|
36
75
|
"global.plugins.strapi-plugin-oidc": "OIDC Plugin",
|
|
37
76
|
"page.title": "Configure OIDC default role(s) and access controls.",
|
|
@@ -94,7 +133,6 @@ const en = {
|
|
|
94
133
|
"auditlog.table.ip": "IP",
|
|
95
134
|
"auditlog.table.details": "Details",
|
|
96
135
|
"auditlog.table.empty": "No audit log entries",
|
|
97
|
-
"auditlog.loading": "Loading…",
|
|
98
136
|
"auditlog.clear": "Clear Logs",
|
|
99
137
|
"auditlog.clear.title": "Clear All Logs",
|
|
100
138
|
"auditlog.clear.description": "This will permanently delete all {count, plural, one {# audit log entry} other {# audit log entries}}. This action cannot be undone.",
|
|
@@ -125,6 +163,8 @@ const en = {
|
|
|
125
163
|
"auditlog.action.nonce_mismatch": "The nonce in the ID token did not match the one generated at login. This may indicate a token replay attack.",
|
|
126
164
|
"auditlog.action.token_exchange_failed": "The authorisation code could not be exchanged for tokens. The OIDC provider rejected the request.",
|
|
127
165
|
"auditlog.action.whitelist_rejected": "The user's email address is not on the whitelist. Access was denied.",
|
|
166
|
+
"auditlog.action.email_not_verified": "The OIDC provider did not confirm the user's email address as verified. Access was denied.",
|
|
167
|
+
"auditlog.action.id_token_invalid": "The ID token failed signature, issuer, audience, or expiry validation. Access was denied.",
|
|
128
168
|
"auth.page.authenticating.title": "Authenticating...",
|
|
129
169
|
"auth.page.authenticating.noscript.heading": "JavaScript Required",
|
|
130
170
|
"auth.page.authenticating.noscript.body": "JavaScript must be enabled for authentication to complete.",
|
|
@@ -165,7 +205,7 @@ const index = {
|
|
|
165
205
|
defaultMessage: "Configuration"
|
|
166
206
|
},
|
|
167
207
|
Component: async () => {
|
|
168
|
-
return await import("./index-
|
|
208
|
+
return await import("./index-BqWd-Iiq.mjs");
|
|
169
209
|
},
|
|
170
210
|
permissions: [{ action: "plugin::strapi-plugin-oidc.read", subject: null }]
|
|
171
211
|
}
|
|
@@ -177,6 +217,9 @@ const index = {
|
|
|
177
217
|
});
|
|
178
218
|
},
|
|
179
219
|
bootstrap() {
|
|
220
|
+
const overlayContainer = document.createElement("div");
|
|
221
|
+
document.body.appendChild(overlayContainer);
|
|
222
|
+
createRoot(overlayContainer).render(React.createElement(LogoutOverlay));
|
|
180
223
|
const defaultButtonText = t("login.sso");
|
|
181
224
|
const isAuthRoute = (path) => /\/auth\/(login|register|forgot-password|reset-password)/.test(path);
|
|
182
225
|
let ssoButtonInjected = false;
|
|
@@ -236,6 +279,7 @@ const index = {
|
|
|
236
279
|
if (!isAuthRoute(window.location.pathname)) return;
|
|
237
280
|
injectSSOButton(buttonText);
|
|
238
281
|
if (enforced) removeEnforcedElements();
|
|
282
|
+
if (ssoButtonInjected && !enforced) loginObserver?.disconnect();
|
|
239
283
|
};
|
|
240
284
|
tick();
|
|
241
285
|
loginObserver = new MutationObserver(tick);
|
|
@@ -256,23 +300,27 @@ const index = {
|
|
|
256
300
|
}
|
|
257
301
|
};
|
|
258
302
|
applySettings();
|
|
303
|
+
if (window.__strapiOidcFetchPatched) return;
|
|
304
|
+
window.__strapiOidcFetchPatched = true;
|
|
259
305
|
const originalFetch = window.fetch;
|
|
260
306
|
window.fetch = async (...args) => {
|
|
261
307
|
const url = typeof args[0] === "string" ? args[0] : args[0].url;
|
|
262
308
|
const isLogout = url?.endsWith("/admin/logout") && args[1]?.method?.toUpperCase() === "POST";
|
|
263
|
-
|
|
264
|
-
|
|
309
|
+
if (isLogout) {
|
|
310
|
+
window.dispatchEvent(new CustomEvent(LOGOUT_EVENT));
|
|
265
311
|
window.localStorage.removeItem("jwtToken");
|
|
266
312
|
window.localStorage.removeItem("isLoggedIn");
|
|
267
313
|
window.sessionStorage.removeItem("jwtToken");
|
|
268
314
|
window.sessionStorage.removeItem("isLoggedIn");
|
|
269
315
|
document.cookie = "jwtToken=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/";
|
|
270
316
|
document.cookie = "jwtToken=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/admin";
|
|
317
|
+
originalFetch(...args).catch(() => {
|
|
318
|
+
});
|
|
271
319
|
window.location.href = "/strapi-plugin-oidc/logout";
|
|
272
320
|
return new Promise(() => {
|
|
273
321
|
});
|
|
274
322
|
}
|
|
275
|
-
return
|
|
323
|
+
return originalFetch(...args);
|
|
276
324
|
};
|
|
277
325
|
},
|
|
278
326
|
async registerTrads({ locales }) {
|