strapi-plugin-oidc 1.7.5 → 1.8.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 CHANGED
@@ -44,19 +44,38 @@ module.exports = ({ env }) => ({
44
44
  OIDC_GRANT_TYPE: 'authorization_code',
45
45
  OIDC_FAMILY_NAME_FIELD: 'family_name',
46
46
  OIDC_GIVEN_NAME_FIELD: 'given_name',
47
- OIDC_END_SESSION_ENDPOINT: '', // Provider end-session URL for RP-initiated logout
47
+ OIDC_END_SESSION_ENDPOINT: '', // Provider end-session URL (from discovery `end_session_endpoint`)
48
48
  OIDC_SSO_BUTTON_TEXT: 'Login via SSO',
49
49
  OIDC_ENFORCE: null, // null = use Admin UI toggle; true/false = override in config
50
50
  REMEMBER_ME: false, // Persist session across browser restarts
51
51
  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
52
  OIDC_GROUP_FIELD: 'groups', // OIDC claim field containing group membership
53
53
  OIDC_GROUP_ROLE_MAP: '{}', // JSON map of group names to Strapi role names
54
+ OIDC_REQUIRE_EMAIL_VERIFIED: true, // Reject logins when provider does not report email_verified=true (set false to disable)
55
+ OIDC_TRUSTED_IP_HEADER: '', // Optional: 'cf-connecting-ip' for Cloudflare; read only when Strapi trusts the proxy
56
+ OIDC_JWKS_URI: '', // Provider's JWKS URI (from discovery `jwks_uri`) — enables ID token signature verification
57
+ OIDC_ISSUER: '', // Provider's issuer (from discovery `issuer`) — verified against ID token's `iss`
58
+ OIDC_FORCE_SECURE_COOKIES: false, // Set true when behind a trusted HTTPS proxy that Strapi can't auto-detect
54
59
  },
55
60
  },
56
61
  });
57
62
  ```
58
63
 
59
- All required values come from your provider's OIDC discovery document, typically available at `https://your-provider/.well-known/openid-configuration`.
64
+ All OIDC values come from your provider's discovery document, typically available at `https://your-provider/.well-known/openid-configuration`.
65
+
66
+ ### Security features
67
+
68
+ - **ID token verification** — `OIDC_JWKS_URI` and `OIDC_ISSUER` enable signature, issuer, audience, and expiry validation via [`jose`](https://github.com/panva/jose)
69
+ - **Email verification** — `OIDC_REQUIRE_EMAIL_VERIFIED: true` (default) rejects unverified emails
70
+ - **CSRF protection** — OIDC state/nonce and POST-only logout endpoint
71
+ - **Rate limiting** — 1 000 req/min per IP+UA (in-process; use a reverse-proxy-level limiter for multi-node)
72
+ - **Secure cookies** — `OIDC_FORCE_SECURE_COOKIES` ensures cookies are marked Secure
73
+
74
+ ### Client IP attribution and reverse proxies
75
+
76
+ 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.
77
+
78
+ Set `OIDC_TRUSTED_IP_HEADER: 'cf-connecting-ip'` when behind Cloudflare. The header is only honoured when `server.proxy: true` is set.
60
79
 
61
80
  ## Login
62
81
 
@@ -64,27 +83,27 @@ Navigate to `/strapi-plugin-oidc/oidc` to start the OIDC flow, or click the **Lo
64
83
 
65
84
  ## Logout
66
85
 
67
- When `OIDC_END_SESSION_ENDPOINT` is set, clicking logout in Strapi redirects the browser 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.
86
+ When `OIDC_END_SESSION_ENDPOINT` is set, 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.
87
+
88
+ The logout endpoint is `POST /strapi-plugin-oidc/logout`. Using POST instead of GET prevents CSRF-forced-logout attacks.
68
89
 
69
90
  ## Admin Settings
70
91
 
71
92
  Manage the plugin under **Settings → OIDC Plugin**.
72
93
 
73
- **Default Roles** — Select which Strapi admin role(s) are assigned to new users on first login.
94
+ **Default Roles** — Strapi admin role(s) assigned to new users on first login.
74
95
 
75
- **Whitelist** — Restrict access to specific email addresses. When enabled, only listed emails can log in. When empty, any successfully authenticated OIDC user gets an account. The whitelist supports:
96
+ **Whitelist** — Restrict access to specific email addresses. When empty, any authenticated OIDC user gets an account. Supports:
76
97
 
77
- - Adding individual emails with optional role overrides
78
- - JSON import / export (see [format](#import-format) below)
98
+ - Individual emails with optional role overrides
99
+ - JSON import / export
79
100
  - Bulk delete with confirmation
80
- - Unsaved changes are held in the UI until **Save Changes** is clicked
81
101
 
82
- **Audit Logs** — Every authentication event is recorded in the plugin's audit log table and visible in the **Audit Logs** section at the bottom of the settings page. Entries can be filtered by action, email, IP address, and date, and a **Download** button exports the current (filtered) view as NDJSON (newline-delimited JSON), compatible with SIEM tools and log processors. Setting `AUDIT_LOG_RETENTION_DAYS` to `0` disables audit logging entirely. Otherwise records older than the configured value (default: 90 days) are automatically purged by a daily cron job. The audit log is also accessible [via API](#audit-log-api).
102
+ **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
103
 
84
- **Enforce OIDC Login** — Removes the standard email/password fields from the login page and blocks direct login API calls server-side. Automatically disabled when the whitelist is empty to prevent lockout.
104
+ **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
105
 
86
- - The toggle is grayed out and locked when `OIDC_ENFORCE` is set in config.
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.
106
+ 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
107
 
89
108
  ## Group-to-Role Mapping
90
109
 
@@ -118,18 +137,26 @@ Role names are the **display names** shown in **Settings → Roles** (e.g. `"Edi
118
137
 
119
138
  ### Role assignment precedence
120
139
 
121
- 1. **User's OIDC groups match `OIDC_GROUP_ROLE_MAP`** → use the mapped Strapi roles
122
- 2. **No group match or no mapping configured** → use the default OIDC roles (new users only — see below)
140
+ 1. **OIDC groups match `OIDC_GROUP_ROLE_MAP`** → mapped Strapi roles
141
+ 2. **No match or no mapping** → default OIDC roles (new users only)
123
142
 
124
143
  ### Role updates on subsequent logins
125
144
 
126
- - **New users** — Roles are always assigned on first login: group-mapped roles if a match is found, otherwise the configured default OIDC roles.
127
- - **Existing users with a group mapping match** — Roles are updated to reflect the current mapping. If a user's groups change between logins, their Strapi roles are updated accordingly.
128
- - **Existing users with no group mapping match** — Roles are left unchanged, regardless of what the default OIDC roles are set to. Manually-assigned roles are never overwritten by a default fallback.
145
+ - **New users** — Roles assigned on first login (group-mapped or default).
146
+ - **Existing users with group match** — Roles updated to reflect current mapping.
147
+ - **Existing users without group match** — Roles left unchanged. Manually-assigned roles are never overwritten.
129
148
 
130
149
  ## Whitelist API
131
150
 
132
- The whitelist can be managed programmatically using a Strapi **API token** (Settings → API Tokens → Full Access). All endpoints are under `/api/strapi-plugin-oidc` and require `Authorization: Bearer <token>`.
151
+ The whitelist can be managed programmatically using a Strapi **API token**. All endpoints are under `/api/strapi-plugin-oidc` and require `Authorization: Bearer <token>`.
152
+
153
+ **Full-access tokens** can call all routes. **Custom tokens** must be granted one of the following scopes (Settings → API Tokens → Custom → plugin permissions):
154
+
155
+ | Scope | Routes |
156
+ | --------------------------------------------- | ----------------------------------------------- |
157
+ | `plugin::strapi-plugin-oidc.whitelist.read` | `GET /whitelist`, `GET /whitelist/export` |
158
+ | `plugin::strapi-plugin-oidc.whitelist.write` | `POST /whitelist`, `POST /whitelist/import` |
159
+ | `plugin::strapi-plugin-oidc.whitelist.delete` | `DELETE /whitelist`, `DELETE /whitelist/:email` |
133
160
 
134
161
  | Method | Path | Description |
135
162
  | -------- | ------------------------------------------ | ---------------------- |
@@ -185,7 +212,14 @@ curl -X DELETE -H "Authorization: Bearer <token>" \
185
212
 
186
213
  ## Audit Log API
187
214
 
188
- Audit log entries can be fetched programmatically using a Strapi **API token** (Settings → API Tokens → Full Access). Endpoints are under `/api/strapi-plugin-oidc` and require `Authorization: Bearer <token>`.
215
+ Audit log entries can be fetched programmatically using a Strapi **API token**. Endpoints are under `/api/strapi-plugin-oidc` and require `Authorization: Bearer <token>`.
216
+
217
+ **Full-access tokens** can call all routes. **Custom tokens** must be granted one of the following scopes:
218
+
219
+ | Scope | Routes |
220
+ | ----------------------------------------- | ------------------------------------------- |
221
+ | `plugin::strapi-plugin-oidc.audit.read` | `GET /audit-logs`, `GET /audit-logs/export` |
222
+ | `plugin::strapi-plugin-oidc.audit.delete` | `DELETE /audit-logs` |
189
223
 
190
224
  | Method | Path | Description |
191
225
  | -------- | ------------------------------------------- | ----------------------------------- |
@@ -246,18 +280,20 @@ curl -H "Authorization: Bearer <token>" -G \
246
280
 
247
281
  ### Recorded actions
248
282
 
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
- | `logout` | User logged out via `/logout` |
260
- | `session_expired` | Logout attempted but provider session already stale |
283
+ | Action | Trigger |
284
+ | ----------------------- | ----------------------------------------------------------------- |
285
+ | `login_success` | Successful OIDC authentication |
286
+ | `user_created` | New Strapi admin user created during login |
287
+ | `login_failure` | Unexpected error during the OIDC login flow |
288
+ | `missing_code` | Callback received without an authorisation code |
289
+ | `state_mismatch` | CSRF state cookie does not match callback parameter |
290
+ | `nonce_mismatch` | ID token nonce does not match the session nonce |
291
+ | `token_exchange_failed` | Provider returned an error during token exchange |
292
+ | `whitelist_rejected` | Email not present in the active whitelist |
293
+ | `email_not_verified` | Provider did not report `email_verified=true` |
294
+ | `id_token_invalid` | ID token failed signature, issuer, audience, or expiry validation |
295
+ | `logout` | User logged out via `/logout` |
296
+ | `session_expired` | Logout attempted but provider session already stale |
261
297
 
262
298
  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
299
 
@@ -280,16 +316,16 @@ This plugin is a hard fork of [`strapi-plugin-sso`](https://github.com/yasudaclo
280
316
 
281
317
  ### Changes from the original:
282
318
 
283
- - Removed alternative SSO methods to focus solely on OIDC.
284
- - Redesigned the Whitelist and Role management UI using native Strapi components.
285
- - Added OIDC enforcement with an admin toggle and config override (`OIDC_ENFORCE`).
286
- - Added RP-initiated logout with smart session detection — skips the provider redirect if the session is already expired.
287
- - Migrated to Vitest with comprehensive e2e test coverage.
288
- - Config variable names aligned with OIDC discovery document field names (`OIDC_SCOPE`, `OIDC_USERINFO_ENDPOINT`, `OIDC_END_SESSION_ENDPOINT`).
289
- - Always injects a **Login via SSO** button on the Strapi login page. Button text is configurable via `OIDC_SSO_BUTTON_TEXT`.
290
- - Whitelist: programmatic REST API with JSON import/export, bulk delete, delete by email, and unsaved changes guard.
291
- - Hardened OIDC flow: server-generated state and nonce, PKCE, Bearer token auth for userinfo, and generic error messages on failure.
292
- - Audit log: records all auth events to a queryable table with Admin UI, JSON export, and REST API. API responses use a single `datetime` field and omit framework metadata (id, documentId, locale, publishedAt, etc.). A separate `user_created` event is emitted when a Strapi admin is provisioned during login.
319
+ - OIDC-only (removed other SSO methods)
320
+ - Redesigned whitelist and role management UI using native Strapi components
321
+ - OIDC enforcement via admin toggle or `OIDC_ENFORCE` config
322
+ - RP-initiated logout with smart session detection
323
+ - Migrated to Vitest with e2e coverage
324
+ - Config variable names aligned with OIDC discovery document field names
325
+ - **Login via SSO** button always injected; text configurable via `OIDC_SSO_BUTTON_TEXT`
326
+ - Whitelist REST API with JSON import/export, bulk delete, delete by email
327
+ - Hardened OIDC flow: server-generated state and nonce, PKCE, Bearer token auth for userinfo, generic error messages on failure
328
+ - Audit log: records all auth events to a queryable table with UI, JSON/NDJSON export, and REST API
293
329
 
294
330
  ## License
295
331
 
@@ -2,10 +2,10 @@ import { jsxs, Fragment, jsx } from "react/jsx-runtime";
2
2
  import { useBlocker, Routes, Route } from "react-router-dom";
3
3
  import { useNotification, useFetchClient, Page, Layouts } from "@strapi/strapi/admin";
4
4
  import { useState, useRef, useId, useEffect, useCallback, useReducer, useMemo, memo } from "react";
5
- import { Typography, Flex, Box, MultiSelect, MultiSelectOption, Dialog, Button, Table, Pagination, PreviousLink, NextLink, PageLink, Field, Divider, Thead, Tr, Th, Tbody, Td, IconButton, Tooltip, Alert } from "@strapi/design-system";
5
+ import { Typography, Flex, Box, MultiSelect, MultiSelectOption, Button, Dialog, Table, Pagination, PreviousLink, NextLink, PageLink, Field, Divider, Thead, Tr, Th, Tbody, Td, IconButton, Tooltip, Alert } from "@strapi/design-system";
6
6
  import { Cross, WarningCircle, Plus, Download, Upload, Trash, Calendar, Mail, Information } from "@strapi/icons";
7
7
  import { useIntl } from "react-intl";
8
- import { g as getTrad } from "./index-BfX_taLq.mjs";
8
+ import { g as getTrad } from "./index-B-K4X_N9.mjs";
9
9
  import styled from "styled-components";
10
10
  import { Filter, ClipboardList, Server } from "lucide-react";
11
11
  function Role({ oidcRoles, roles, onChangeRole }) {
@@ -66,15 +66,21 @@ const TagInputWrapper = styled(Box)`
66
66
  flex-wrap: wrap;
67
67
  gap: 4px;
68
68
  align-items: center;
69
- padding: 8px 16px;
69
+ padding-inline: ${({ theme }) => theme.spaces[4]};
70
+ padding-block: ${({ theme }) => theme.spaces[3]};
70
71
  border-radius: 4px;
71
72
  border: 1px solid ${({ theme }) => theme.colors.neutral200};
72
73
  background-color: ${({ theme }) => theme.colors.neutral0};
73
74
  cursor: text;
74
75
  min-width: 220px;
75
- min-height: 4rem;
76
+ min-height: 4.8rem;
76
77
  flex: 0 0 auto;
77
78
 
79
+ ${({ theme }) => theme.breakpoints.medium} {
80
+ padding-block: ${({ theme }) => theme.spaces[2]};
81
+ min-height: 4rem;
82
+ }
83
+
78
84
  &:focus-within {
79
85
  border-color: ${({ theme }) => theme.colors.primary600};
80
86
  box-shadow: 0 0 0 2px ${({ theme }) => theme.colors.primary100};
@@ -139,7 +145,6 @@ function TagInputShell({
139
145
  inputProps,
140
146
  children
141
147
  }) {
142
- const inputId = useId();
143
148
  return /* @__PURE__ */ jsxs(TagInputWrapper, { ref: wrapperRef, onClick: () => inputRef.current?.focus(), children: [
144
149
  /* @__PURE__ */ jsxs(Flex, { gap: 2, wrap: "wrap", alignItems: "center", style: { flex: 1, minWidth: 0 }, children: [
145
150
  startIcon && /* @__PURE__ */ jsx(StartIconSlot, { children: startIcon }),
@@ -151,14 +156,6 @@ function TagInputShell({
151
156
  type: "text",
152
157
  placeholder: value.length === 0 ? placeholder : "",
153
158
  "aria-label": placeholder,
154
- autoComplete: "off",
155
- autoCorrect: "off",
156
- autoCapitalize: "off",
157
- spellCheck: false,
158
- name: `tag-input-${inputId}`,
159
- "data-form-type": "other",
160
- "data-lpignore": "true",
161
- "data-1p-ignore": "true",
162
159
  ...inputProps
163
160
  }
164
161
  )
@@ -743,6 +740,16 @@ function TagDateInput({ value = [], onChange, placeholder, startIcon }) {
743
740
  }
744
741
  );
745
742
  }
743
+ const SizedButton = styled(Button)`
744
+ && {
745
+ height: 4.8rem;
746
+ }
747
+ ${({ theme }) => theme.breakpoints.medium} {
748
+ && {
749
+ height: 4rem;
750
+ }
751
+ }
752
+ `;
746
753
  const Icon = styled.span`
747
754
  display: inline-flex;
748
755
  align-items: center;
@@ -907,91 +914,98 @@ function Whitelist({
907
914
  return /* @__PURE__ */ jsxs(Box, { children: [
908
915
  /* @__PURE__ */ jsx(Typography, { tag: "p", variant: "omega", textColor: "neutral600", marginBottom: 4, children: formatMessage(getTrad("whitelist.description")) }),
909
916
  useWhitelist && /* @__PURE__ */ jsxs(Fragment, { children: [
910
- /* @__PURE__ */ jsxs(Flex, { gap: 8, marginTop: 5, marginBottom: 5, alignItems: "stretch", wrap: "wrap", children: [
911
- /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", style: { minWidth: "280px", flex: "1 1 280px" }, children: [
912
- /* @__PURE__ */ jsx(Box, { style: { flex: 1, minWidth: "200px" }, children: /* @__PURE__ */ jsx(Field.Root, { children: /* @__PURE__ */ jsx(
913
- Field.Input,
914
- {
915
- type: "text",
916
- disabled: loading,
917
- value: email,
918
- hasError: Boolean(email && !EMAIL_REGEX.test(email)),
919
- onChange: (e) => setEmail(e.currentTarget.value),
920
- placeholder: formatMessage(getTrad("whitelist.email.placeholder")),
921
- style: { fontSize: "1.4rem", lineHeight: "2.2rem" }
922
- }
923
- ) }) }),
924
- /* @__PURE__ */ jsx(
925
- Button,
926
- {
927
- size: "S",
928
- startIcon: /* @__PURE__ */ jsx(Plus, {}),
929
- style: { paddingTop: "1.1rem", paddingBottom: "1.1rem", height: "auto" },
930
- disabled: loading || email.trim() === "" || !EMAIL_REGEX.test(email),
931
- loading,
932
- onClick: onSaveEmail,
933
- children: formatMessage(getTrad("page.add"))
934
- }
935
- )
936
- ] }),
937
- /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", children: [
938
- /* @__PURE__ */ jsx(
939
- Button,
940
- {
941
- size: "S",
942
- variant: "tertiary",
943
- startIcon: /* @__PURE__ */ jsx(Download, {}),
944
- onClick: onExport,
945
- disabled: users.length === 0,
946
- style: { paddingTop: "1.1rem", paddingBottom: "1.1rem", height: "auto" },
947
- children: formatMessage(getTrad("whitelist.export"))
948
- }
949
- ),
950
- /* @__PURE__ */ jsx(
951
- Button,
952
- {
953
- size: "S",
954
- variant: "tertiary",
955
- startIcon: /* @__PURE__ */ jsx(Upload, {}),
956
- onClick: () => fileInputRef.current?.click(),
957
- style: { paddingTop: "1.1rem", paddingBottom: "1.1rem", height: "auto" },
958
- children: formatMessage(getTrad("whitelist.import"))
959
- }
960
- ),
961
- /* @__PURE__ */ jsx(
962
- "input",
963
- {
964
- ref: fileInputRef,
965
- type: "file",
966
- accept: ".json,application/json",
967
- style: { display: "none" },
968
- onChange: handleImport
969
- }
970
- ),
971
- /* @__PURE__ */ jsx(
972
- ConfirmDialog,
973
- {
974
- trigger: /* @__PURE__ */ jsx(
975
- Button,
917
+ /* @__PURE__ */ jsxs(
918
+ Flex,
919
+ {
920
+ gap: 8,
921
+ marginTop: 5,
922
+ marginBottom: 5,
923
+ alignItems: "stretch",
924
+ wrap: "wrap",
925
+ style: { rowGap: "0.8rem" },
926
+ children: [
927
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", style: { minWidth: "280px", flex: "1 1 280px" }, children: [
928
+ /* @__PURE__ */ jsx(Box, { style: { flex: 1, minWidth: "200px" }, children: /* @__PURE__ */ jsx(Field.Root, { children: /* @__PURE__ */ jsx(
929
+ Field.Input,
930
+ {
931
+ type: "text",
932
+ disabled: loading,
933
+ value: email,
934
+ hasError: Boolean(email && !EMAIL_REGEX.test(email)),
935
+ onChange: (e) => setEmail(e.currentTarget.value),
936
+ placeholder: formatMessage(getTrad("whitelist.email.placeholder")),
937
+ style: { fontSize: "1.4rem", lineHeight: "2.2rem" }
938
+ }
939
+ ) }) }),
940
+ /* @__PURE__ */ jsx(
941
+ SizedButton,
976
942
  {
977
943
  size: "S",
978
- variant: "danger-light",
979
- startIcon: /* @__PURE__ */ jsx(Trash, {}),
944
+ startIcon: /* @__PURE__ */ jsx(Plus, {}),
945
+ disabled: loading || email.trim() === "" || !EMAIL_REGEX.test(email),
946
+ loading,
947
+ onClick: onSaveEmail,
948
+ children: formatMessage(getTrad("page.add"))
949
+ }
950
+ )
951
+ ] }),
952
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", children: [
953
+ /* @__PURE__ */ jsx(
954
+ SizedButton,
955
+ {
956
+ size: "S",
957
+ variant: "tertiary",
958
+ startIcon: /* @__PURE__ */ jsx(Download, {}),
959
+ onClick: onExport,
980
960
  disabled: users.length === 0,
981
- style: { paddingTop: "1.1rem", paddingBottom: "1.1rem", height: "auto" },
982
- children: formatMessage(getTrad("whitelist.delete.all.label"))
961
+ children: formatMessage(getTrad("whitelist.export"))
983
962
  }
984
963
  ),
985
- title: formatMessage(getTrad("whitelist.delete.all.title")),
986
- body: /* @__PURE__ */ jsx(Flex, { justifyContent: "center", children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral800", textAlign: "center", children: formatMessage(getTrad("whitelist.delete.all.description"), {
987
- count: users.length
988
- }) }) }),
989
- confirmLabel: formatMessage(getTrad("whitelist.delete.all.label")),
990
- onConfirm: onDeleteAll
991
- }
992
- )
993
- ] })
994
- ] }),
964
+ /* @__PURE__ */ jsx(
965
+ SizedButton,
966
+ {
967
+ size: "S",
968
+ variant: "tertiary",
969
+ startIcon: /* @__PURE__ */ jsx(Upload, {}),
970
+ onClick: () => fileInputRef.current?.click(),
971
+ children: formatMessage(getTrad("whitelist.import"))
972
+ }
973
+ ),
974
+ /* @__PURE__ */ jsx(
975
+ "input",
976
+ {
977
+ ref: fileInputRef,
978
+ type: "file",
979
+ accept: ".json,application/json",
980
+ style: { display: "none" },
981
+ onChange: handleImport
982
+ }
983
+ ),
984
+ /* @__PURE__ */ jsx(
985
+ ConfirmDialog,
986
+ {
987
+ trigger: /* @__PURE__ */ jsx(
988
+ SizedButton,
989
+ {
990
+ size: "S",
991
+ variant: "danger-light",
992
+ startIcon: /* @__PURE__ */ jsx(Trash, {}),
993
+ disabled: users.length === 0,
994
+ children: formatMessage(getTrad("whitelist.delete.all.label"))
995
+ }
996
+ ),
997
+ title: formatMessage(getTrad("whitelist.delete.all.title")),
998
+ body: /* @__PURE__ */ jsx(Flex, { justifyContent: "center", children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral800", textAlign: "center", children: formatMessage(getTrad("whitelist.delete.all.description"), {
999
+ count: users.length
1000
+ }) }) }),
1001
+ confirmLabel: formatMessage(getTrad("whitelist.delete.all.label")),
1002
+ onConfirm: onDeleteAll
1003
+ }
1004
+ )
1005
+ ] })
1006
+ ]
1007
+ }
1008
+ ),
995
1009
  /* @__PURE__ */ jsx(Divider, {}),
996
1010
  /* @__PURE__ */ jsxs(CustomTable, { colCount: 4, rowCount: users.length, children: [
997
1011
  /* @__PURE__ */ jsx(Thead, { children: /* @__PURE__ */ jsxs(Tr, { children: [
@@ -3597,6 +3611,8 @@ const AUDIT_ACTIONS = [
3597
3611
  "nonce_mismatch",
3598
3612
  "token_exchange_failed",
3599
3613
  "whitelist_rejected",
3614
+ "email_not_verified",
3615
+ "id_token_invalid",
3600
3616
  "logout",
3601
3617
  "session_expired",
3602
3618
  "user_created"
@@ -3736,14 +3752,13 @@ function AuditLog({ title } = {}) {
3736
3752
  title ?? /* @__PURE__ */ jsx("span", {}),
3737
3753
  /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
3738
3754
  /* @__PURE__ */ jsx(
3739
- Button,
3755
+ SizedButton,
3740
3756
  {
3741
3757
  size: "S",
3742
3758
  variant: "tertiary",
3743
3759
  startIcon: /* @__PURE__ */ jsx(Download, {}),
3744
3760
  onClick: handleExport,
3745
3761
  disabled: pagination.total === 0,
3746
- style: { paddingTop: "1.1rem", paddingBottom: "1.1rem", height: "auto" },
3747
3762
  children: formatMessage(getTrad("auditlog.export"))
3748
3763
  }
3749
3764
  ),
@@ -3751,13 +3766,12 @@ function AuditLog({ title } = {}) {
3751
3766
  ConfirmDialog,
3752
3767
  {
3753
3768
  trigger: /* @__PURE__ */ jsx(
3754
- Button,
3769
+ SizedButton,
3755
3770
  {
3756
3771
  size: "S",
3757
3772
  variant: "danger-light",
3758
3773
  startIcon: /* @__PURE__ */ jsx(Trash, {}),
3759
3774
  disabled: pagination.total === 0,
3760
- style: { paddingTop: "1.1rem", paddingBottom: "1.1rem", height: "auto" },
3761
3775
  children: formatMessage(getTrad("auditlog.clear"))
3762
3776
  }
3763
3777
  ),
@@ -3838,13 +3852,12 @@ function AuditLog({ title } = {}) {
3838
3852
  }
3839
3853
  ),
3840
3854
  hasActiveFilters && /* @__PURE__ */ jsx(
3841
- Button,
3855
+ SizedButton,
3842
3856
  {
3843
3857
  size: "S",
3844
3858
  variant: "danger-light",
3845
3859
  startIcon: /* @__PURE__ */ jsx(Trash, {}),
3846
3860
  onClick: clearFilters,
3847
- style: { height: "4rem" },
3848
3861
  children: formatMessage(getTrad("auditlog.filters.clear"))
3849
3862
  }
3850
3863
  )
@@ -4365,7 +4378,7 @@ function HomePage() {
4365
4378
  ] }) })
4366
4379
  ] })
4367
4380
  ] }),
4368
- /* @__PURE__ */ jsx(Flex, { justifyContent: "flex-end", children: /* @__PURE__ */ jsx(
4381
+ /* @__PURE__ */ jsx(Flex, { justifyContent: "flex-end", marginBottom: 8, children: /* @__PURE__ */ jsx(
4369
4382
  Button,
4370
4383
  {
4371
4384
  size: "L",
@@ -125,6 +125,8 @@ const en = {
125
125
  "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
126
  "auditlog.action.token_exchange_failed": "The authorisation code could not be exchanged for tokens. The OIDC provider rejected the request.",
127
127
  "auditlog.action.whitelist_rejected": "The user's email address is not on the whitelist. Access was denied.",
128
+ "auditlog.action.email_not_verified": "The OIDC provider did not confirm the user's email address as verified. Access was denied.",
129
+ "auditlog.action.id_token_invalid": "The ID token failed signature, issuer, audience, or expiry validation. Access was denied.",
128
130
  "auth.page.authenticating.title": "Authenticating...",
129
131
  "auth.page.authenticating.noscript.heading": "JavaScript Required",
130
132
  "auth.page.authenticating.noscript.body": "JavaScript must be enabled for authentication to complete.",
@@ -165,7 +167,7 @@ const index = {
165
167
  defaultMessage: "Configuration"
166
168
  },
167
169
  Component: async () => {
168
- return await import("./index-BjmTHbr9.mjs");
170
+ return await import("./index-8YTLPV3h.mjs");
169
171
  },
170
172
  permissions: [{ action: "plugin::strapi-plugin-oidc.read", subject: null }]
171
173
  }
@@ -268,7 +270,11 @@ const index = {
268
270
  window.sessionStorage.removeItem("isLoggedIn");
269
271
  document.cookie = "jwtToken=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/";
270
272
  document.cookie = "jwtToken=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/admin";
271
- window.location.href = "/strapi-plugin-oidc/logout";
273
+ const form = document.createElement("form");
274
+ form.method = "POST";
275
+ form.action = "/strapi-plugin-oidc/logout";
276
+ document.body.appendChild(form);
277
+ form.submit();
272
278
  return new Promise(() => {
273
279
  });
274
280
  }
@@ -126,6 +126,8 @@ const en = {
126
126
  "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.",
127
127
  "auditlog.action.token_exchange_failed": "The authorisation code could not be exchanged for tokens. The OIDC provider rejected the request.",
128
128
  "auditlog.action.whitelist_rejected": "The user's email address is not on the whitelist. Access was denied.",
129
+ "auditlog.action.email_not_verified": "The OIDC provider did not confirm the user's email address as verified. Access was denied.",
130
+ "auditlog.action.id_token_invalid": "The ID token failed signature, issuer, audience, or expiry validation. Access was denied.",
129
131
  "auth.page.authenticating.title": "Authenticating...",
130
132
  "auth.page.authenticating.noscript.heading": "JavaScript Required",
131
133
  "auth.page.authenticating.noscript.body": "JavaScript must be enabled for authentication to complete.",
@@ -166,7 +168,7 @@ const index = {
166
168
  defaultMessage: "Configuration"
167
169
  },
168
170
  Component: async () => {
169
- return await Promise.resolve().then(() => require("./index-CacQfQ3a.js"));
171
+ return await Promise.resolve().then(() => require("./index-CgG_mHzZ.js"));
170
172
  },
171
173
  permissions: [{ action: "plugin::strapi-plugin-oidc.read", subject: null }]
172
174
  }
@@ -269,7 +271,11 @@ const index = {
269
271
  window.sessionStorage.removeItem("isLoggedIn");
270
272
  document.cookie = "jwtToken=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/";
271
273
  document.cookie = "jwtToken=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/admin";
272
- window.location.href = "/strapi-plugin-oidc/logout";
274
+ const form = document.createElement("form");
275
+ form.method = "POST";
276
+ form.action = "/strapi-plugin-oidc/logout";
277
+ document.body.appendChild(form);
278
+ form.submit();
273
279
  return new Promise(() => {
274
280
  });
275
281
  }