strapi-plugin-oidc 1.0.5 → 1.0.6

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
@@ -2,8 +2,11 @@
2
2
  <img src="https://raw.githubusercontent.com/edmogeor/strapi-plugin-oidc/main/assets/icon.png" width="140" alt="OIDC Login for Strapi Logo"/>
3
3
  <h1>OIDC Login for Strapi</h1>
4
4
  <p>
5
+ <a href="https://www.npmjs.com/package/strapi-plugin-oidc">
6
+ <img src="https://img.shields.io/npm/v/strapi-plugin-oidc.svg" alt="npm version">
7
+ </a>
5
8
  <a href="https://github.com/edmogeor/strapi-plugin-oidc/actions/workflows/test.yml">
6
- <img src="https://github.com/edmogeor/strapi-plugin-oidc/actions/workflows/test.yml/badge.svg" alt="Tests">
9
+ <img src="https://github.com/edmogeor/strapi-plugin-oidc/actions/workflows/test.yml/badge.svg?branch=main" alt="Tests">
7
10
  </a>
8
11
  </p>
9
12
  </div>
@@ -67,13 +70,22 @@ module.exports = ({ env }) => ({
67
70
 
68
71
  Make sure to replace the placeholder values (e.g., `[Client ID from OpenID Provider]`) with the actual connection details from your chosen OIDC identity provider.
69
72
 
73
+ ## How to Login
74
+
75
+ Once configured, you can initiate the OIDC login flow by navigating to:
76
+ `http://<your-strapi-domain>/strapi-plugin-oidc/oidc`
77
+
78
+ (e.g., `http://localhost:1337/strapi-plugin-oidc/oidc` for local development).
79
+
80
+ When the **Enforce OIDC Login** option is enabled in the Admin Settings, the standard Strapi admin login page will be automatically redirected to this URL.
81
+
70
82
  ## Admin Settings
71
83
 
72
84
  Once the plugin is installed and configured, you can manage the OIDC settings from the Strapi Admin Panel under **Settings** > **OIDC Plugin**.
73
85
 
74
86
  - **Whitelist Management**: Restrict login to specific users by adding their email addresses to the whitelist. You can also whitelist entire email domains (e.g., `*@company.com`). If the whitelist is empty, any user who successfully authenticates via your OIDC provider will be able to log in and an account will be automatically created for them.
75
87
  - **Default Role Assignment**: Select the default Strapi admin role that will be assigned to newly created users when they log in for the first time via OIDC.
76
- - **Enforce OIDC Login**: When enabled, the default Strapi email and password login form will be disabled, forcing all administrators to log in using your OIDC provider. _(Note: This option is automatically disabled and grayed out if your whitelist is empty to prevent accidentally locking everyone out of the admin panel)._
88
+ - **Enforce OIDC Login**: When enabled, the default Strapi email and password login form will be disabled and the standard login will be redirected to the OIDC login URL, forcing all administrators to log in using your OIDC provider. _(Note: This option is automatically disabled and grayed out if your whitelist is empty to prevent accidentally locking everyone out of the admin panel)._
77
89
 
78
90
  ## Credits & Changes
79
91
 
@@ -85,7 +97,9 @@ This plugin is a hard fork of the original [`strapi-plugin-sso`](https://github.
85
97
  - Redesigned the Whitelist and Role management UI (switched to native Strapi cards, added pagination, etc.).
86
98
  - Added an OIDC logout redirect URL.
87
99
  - Added an option to "Enforce OIDC login" with an admin toggle (automatically disabled if the whitelist is empty).
100
+ - Added configurable "Remember Me" duration for sessions (`REMEMBER_ME_DAYS`).
88
101
  - Migrated the testing framework to Vitest and added comprehensive test coverage for controllers and services.
89
102
  - Cleaned up dead code and unused dependencies to improve maintainability.
90
103
  - Upgraded to use newer versions of Node.js.
104
+ - Added styled success and error pages.
91
105
  - Added misc. quality of life improvements and bug fixes.
@@ -55,7 +55,7 @@ const index = {
55
55
  defaultMessage: "Configuration"
56
56
  },
57
57
  Component: async () => {
58
- return await Promise.resolve().then(() => require("./index-DlQ8NUBY.js"));
58
+ return await Promise.resolve().then(() => require("./index-CLDWnBI9.js"));
59
59
  },
60
60
  permissions: [{ action: "plugin::strapi-plugin-oidc.read", subject: null }]
61
61
  }
@@ -7,7 +7,7 @@ const react = require("react");
7
7
  const designSystem = require("@strapi/design-system");
8
8
  const icons = require("@strapi/icons");
9
9
  const reactIntl = require("react-intl");
10
- const index = require("./index-B525UaV3.js");
10
+ const index = require("./index-BITZIRCD.js");
11
11
  const en = require("./en-jFPbEFeK.js");
12
12
  const styled = require("styled-components");
13
13
  const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
@@ -395,10 +395,18 @@ function useOidcSettings() {
395
395
  setUsers([...users, newUser]);
396
396
  };
397
397
  const onDeleteWhitelist = async (email) => {
398
- setUsers(users.filter((u) => u.email !== email));
398
+ const updatedUsers = users.filter((u) => u.email !== email);
399
+ setUsers(updatedUsers);
400
+ if (useWhitelist && updatedUsers.length === 0) {
401
+ setEnforceOIDC(false);
402
+ }
399
403
  };
400
404
  const onToggleWhitelist = (e) => {
401
- setUseWhitelist(e.target.checked);
405
+ const checked = e.target.checked;
406
+ setUseWhitelist(checked);
407
+ if (checked && users.length === 0) {
408
+ setEnforceOIDC(false);
409
+ }
402
410
  };
403
411
  const onToggleEnforce = (e) => {
404
412
  setEnforceOIDC(e.target.checked);
@@ -413,13 +421,13 @@ function useOidcSettings() {
413
421
  role: role.role
414
422
  }))
415
423
  });
424
+ const syncResponse = await put("/strapi-plugin-oidc/whitelist/sync", {
425
+ users: users.map((u) => ({ email: u.email, roles: u.roles }))
426
+ });
416
427
  await put("/strapi-plugin-oidc/whitelist/settings", {
417
428
  useWhitelist,
418
429
  enforceOIDC
419
430
  });
420
- const syncResponse = await put("/strapi-plugin-oidc/whitelist/sync", {
421
- users: users.map((u) => ({ email: u.email, roles: u.roles }))
422
- });
423
431
  setInitialOIDCRoles(JSON.parse(JSON.stringify(oidcRoles)));
424
432
  setInitialUseWhitelist(useWhitelist);
425
433
  setInitialEnforceOIDC(enforceOIDC);
@@ -54,7 +54,7 @@ const index = {
54
54
  defaultMessage: "Configuration"
55
55
  },
56
56
  Component: async () => {
57
- return await import("./index-BbD-7Z4N.mjs");
57
+ return await import("./index-p9ncVp1G.mjs");
58
58
  },
59
59
  permissions: [{ action: "plugin::strapi-plugin-oidc.read", subject: null }]
60
60
  }
@@ -5,7 +5,7 @@ import { useState, useCallback, useEffect, memo } from "react";
5
5
  import { Typography, Flex, Box, MultiSelect, MultiSelectOption, Field, Button, Divider, Thead, Tr, Th, Tbody, Td, Dialog, IconButton, Pagination, PreviousLink, PageLink, NextLink, Table, Alert } from "@strapi/design-system";
6
6
  import { Plus, Trash, WarningCircle } from "@strapi/icons";
7
7
  import { useIntl } from "react-intl";
8
- import { p as pluginId } from "./index-D3AvxXlB.mjs";
8
+ import { p as pluginId } from "./index-YOG9buUz.mjs";
9
9
  import en from "./en-f0TxVfx7.mjs";
10
10
  import styled from "styled-components";
11
11
  function getTrad(id) {
@@ -391,10 +391,18 @@ function useOidcSettings() {
391
391
  setUsers([...users, newUser]);
392
392
  };
393
393
  const onDeleteWhitelist = async (email) => {
394
- setUsers(users.filter((u) => u.email !== email));
394
+ const updatedUsers = users.filter((u) => u.email !== email);
395
+ setUsers(updatedUsers);
396
+ if (useWhitelist && updatedUsers.length === 0) {
397
+ setEnforceOIDC(false);
398
+ }
395
399
  };
396
400
  const onToggleWhitelist = (e) => {
397
- setUseWhitelist(e.target.checked);
401
+ const checked = e.target.checked;
402
+ setUseWhitelist(checked);
403
+ if (checked && users.length === 0) {
404
+ setEnforceOIDC(false);
405
+ }
398
406
  };
399
407
  const onToggleEnforce = (e) => {
400
408
  setEnforceOIDC(e.target.checked);
@@ -409,13 +417,13 @@ function useOidcSettings() {
409
417
  role: role.role
410
418
  }))
411
419
  });
420
+ const syncResponse = await put("/strapi-plugin-oidc/whitelist/sync", {
421
+ users: users.map((u) => ({ email: u.email, roles: u.roles }))
422
+ });
412
423
  await put("/strapi-plugin-oidc/whitelist/settings", {
413
424
  useWhitelist,
414
425
  enforceOIDC
415
426
  });
416
- const syncResponse = await put("/strapi-plugin-oidc/whitelist/sync", {
417
- users: users.map((u) => ({ email: u.email, roles: u.roles }))
418
- });
419
427
  setInitialOIDCRoles(JSON.parse(JSON.stringify(oidcRoles)));
420
428
  setInitialUseWhitelist(useWhitelist);
421
429
  setInitialEnforceOIDC(enforceOIDC);
@@ -1,4 +1,4 @@
1
1
  "use strict";
2
2
  Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toStringTag]: { value: "Module" } });
3
- const index = require("./index-B525UaV3.js");
3
+ const index = require("./index-BITZIRCD.js");
4
4
  exports.default = index.index;
@@ -1,4 +1,4 @@
1
- import { i } from "./index-D3AvxXlB.mjs";
1
+ import { i } from "./index-YOG9buUz.mjs";
2
2
  export {
3
3
  i as default
4
4
  };
@@ -201,7 +201,7 @@ async function registerNewUser(userService, oauthService2, roleService2, email,
201
201
  return activateUser;
202
202
  }
203
203
  async function handleUserAuthentication(userService, oauthService2, roleService2, whitelistService2, userResponseData, config2, ctx) {
204
- const email = userResponseData.email;
204
+ const email = String(userResponseData.email).toLowerCase();
205
205
  const whitelistUser = await whitelistService2.checkWhitelistForEmail(email);
206
206
  const dbUser = await userService.findOneByEmail(email);
207
207
  let activateUser;
@@ -316,8 +316,14 @@ async function info(ctx) {
316
316
  };
317
317
  }
318
318
  async function updateSettings(ctx) {
319
- const { useWhitelist, enforceOIDC } = ctx.request.body;
319
+ let { useWhitelist, enforceOIDC } = ctx.request.body;
320
320
  const whitelistService2 = strapi.plugin("strapi-plugin-oidc").service("whitelist");
321
+ if (useWhitelist && enforceOIDC) {
322
+ const users = await whitelistService2.getUsers();
323
+ if (users.length === 0) {
324
+ enforceOIDC = false;
325
+ }
326
+ }
321
327
  await whitelistService2.setSettings({ useWhitelist, enforceOIDC });
322
328
  ctx.body = { useWhitelist, enforceOIDC };
323
329
  }
@@ -334,7 +340,8 @@ async function register(ctx) {
334
340
  ctx.body = { message: "Please enter a valid email address" };
335
341
  return;
336
342
  }
337
- const emailList = Array.isArray(email) ? email : email.split(",").map((e) => e.trim()).filter(Boolean);
343
+ const rawEmails = Array.isArray(email) ? email : email.split(",");
344
+ const emailList = rawEmails.map((e) => String(e).trim().toLowerCase()).filter(Boolean);
338
345
  const existingUsers = await strapi.query("admin::user").findMany({
339
346
  where: { email: { $in: emailList } },
340
347
  populate: ["roles"]
@@ -364,7 +371,8 @@ async function removeEmail(ctx) {
364
371
  ctx.body = {};
365
372
  }
366
373
  async function syncUsers(ctx) {
367
- const { users } = ctx.request.body;
374
+ let { users } = ctx.request.body;
375
+ users = users.map((u) => ({ ...u, email: String(u.email).toLowerCase() }));
368
376
  const whitelistService2 = strapi.plugin("strapi-plugin-oidc").service("whitelist");
369
377
  const currentUsers = await whitelistService2.getUsers();
370
378
  let matchedExistingUsersCount = 0;
@@ -410,6 +418,23 @@ const controllers = {
410
418
  role,
411
419
  whitelist
412
420
  };
421
+ const rateLimitMap = /* @__PURE__ */ new Map();
422
+ const RATE_LIMIT_WINDOW = 6e4;
423
+ const MAX_REQUESTS = 20;
424
+ const rateLimitMiddleware = async (ctx, next) => {
425
+ const ip = ctx.request.ip;
426
+ const now = Date.now();
427
+ const windowStart = now - RATE_LIMIT_WINDOW;
428
+ const requestStamps = (rateLimitMap.get(ip) || []).filter((timestamp) => timestamp > windowStart);
429
+ if (requestStamps.length >= MAX_REQUESTS) {
430
+ ctx.status = 429;
431
+ ctx.body = "Too Many Requests";
432
+ return;
433
+ }
434
+ requestStamps.push(now);
435
+ rateLimitMap.set(ip, requestStamps);
436
+ await next();
437
+ };
413
438
  const routes = [
414
439
  {
415
440
  method: "GET",
@@ -429,7 +454,10 @@ const routes = [
429
454
  config: {
430
455
  policies: [
431
456
  "admin::isAuthenticatedAdmin",
432
- { name: "admin::hasPermissions", config: { actions: ["plugin::strapi-plugin-oidc.update"] } }
457
+ {
458
+ name: "admin::hasPermissions",
459
+ config: { actions: ["plugin::strapi-plugin-oidc.update"] }
460
+ }
433
461
  ]
434
462
  }
435
463
  },
@@ -438,7 +466,8 @@ const routes = [
438
466
  path: "/oidc",
439
467
  handler: "oidc.oidcSignIn",
440
468
  config: {
441
- auth: false
469
+ auth: false,
470
+ middlewares: [rateLimitMiddleware]
442
471
  }
443
472
  },
444
473
  {
@@ -446,7 +475,8 @@ const routes = [
446
475
  path: "/oidc/callback",
447
476
  handler: "oidc.oidcSignInCallback",
448
477
  config: {
449
- auth: false
478
+ auth: false,
479
+ middlewares: [rateLimitMiddleware]
450
480
  }
451
481
  },
452
482
  {
@@ -475,7 +505,10 @@ const routes = [
475
505
  config: {
476
506
  policies: [
477
507
  "admin::isAuthenticatedAdmin",
478
- { name: "admin::hasPermissions", config: { actions: ["plugin::strapi-plugin-oidc.update"] } }
508
+ {
509
+ name: "admin::hasPermissions",
510
+ config: { actions: ["plugin::strapi-plugin-oidc.update"] }
511
+ }
479
512
  ]
480
513
  }
481
514
  },
@@ -494,7 +527,10 @@ const routes = [
494
527
  config: {
495
528
  policies: [
496
529
  "admin::isAuthenticatedAdmin",
497
- { name: "admin::hasPermissions", config: { actions: ["plugin::strapi-plugin-oidc.update"] } }
530
+ {
531
+ name: "admin::hasPermissions",
532
+ config: { actions: ["plugin::strapi-plugin-oidc.update"] }
533
+ }
498
534
  ]
499
535
  }
500
536
  },
@@ -505,7 +541,10 @@ const routes = [
505
541
  config: {
506
542
  policies: [
507
543
  "admin::isAuthenticatedAdmin",
508
- { name: "admin::hasPermissions", config: { actions: ["plugin::strapi-plugin-oidc.update"] } }
544
+ {
545
+ name: "admin::hasPermissions",
546
+ config: { actions: ["plugin::strapi-plugin-oidc.update"] }
547
+ }
509
548
  ]
510
549
  }
511
550
  },
@@ -516,12 +555,132 @@ const routes = [
516
555
  config: {
517
556
  policies: [
518
557
  "admin::isAuthenticatedAdmin",
519
- { name: "admin::hasPermissions", config: { actions: ["plugin::strapi-plugin-oidc.update"] } }
558
+ {
559
+ name: "admin::hasPermissions",
560
+ config: { actions: ["plugin::strapi-plugin-oidc.update"] }
561
+ }
520
562
  ]
521
563
  }
522
564
  }
523
565
  ];
524
566
  const policies = {};
567
+ function renderHtmlTemplate(title, content) {
568
+ return `
569
+ <!doctype html>
570
+ <html lang="en">
571
+ <head>
572
+ <meta charset="utf-8">
573
+ <meta name="viewport" content="width=device-width, initial-scale=1">
574
+ <title>${title}</title>
575
+ <style>
576
+ :root {
577
+ --bg-color: #f6f6f9;
578
+ --card-bg: #ffffff;
579
+ --text-color: #32324d;
580
+ --text-muted: #666687;
581
+ --btn-bg: #4945ff;
582
+ --btn-hover: #271fe0;
583
+ --btn-text: #ffffff;
584
+ --icon-bg: #fcecea;
585
+ --icon-color: #d02b20;
586
+ --success-bg: #eafbe7;
587
+ --success-color: #328048;
588
+ --shadow: 0 1px 4px rgba(33, 33, 52, 0.1);
589
+ }
590
+ @media (prefers-color-scheme: dark) {
591
+ :root {
592
+ --bg-color: #181826;
593
+ --card-bg: #212134;
594
+ --text-color: #ffffff;
595
+ --text-muted: #a5a5ba;
596
+ --btn-bg: #4945ff;
597
+ --btn-hover: #7b79ff;
598
+ --btn-text: #ffffff;
599
+ --icon-bg: #4a2123;
600
+ --icon-color: #f23628;
601
+ --success-bg: #1c3523;
602
+ --success-color: #55ca76;
603
+ --shadow: 0 1px 4px rgba(0, 0, 0, 0.5);
604
+ }
605
+ }
606
+ body {
607
+ margin: 0;
608
+ padding: 0;
609
+ display: flex;
610
+ justify-content: center;
611
+ align-items: center;
612
+ height: 100vh;
613
+ background-color: var(--bg-color);
614
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
615
+ color: var(--text-color);
616
+ }
617
+ .card {
618
+ background: var(--card-bg);
619
+ padding: 32px 40px;
620
+ border-radius: 8px;
621
+ box-shadow: var(--shadow);
622
+ max-width: 400px;
623
+ width: 100%;
624
+ text-align: center;
625
+ box-sizing: border-box;
626
+ }
627
+ .icon {
628
+ width: 48px;
629
+ height: 48px;
630
+ background-color: var(--icon-bg);
631
+ color: var(--icon-color);
632
+ border-radius: 50%;
633
+ display: inline-flex;
634
+ justify-content: center;
635
+ align-items: center;
636
+ margin-bottom: 24px;
637
+ }
638
+ .icon.success {
639
+ background-color: var(--success-bg);
640
+ color: var(--success-color);
641
+ }
642
+ .icon svg {
643
+ width: 24px;
644
+ height: 24px;
645
+ stroke: currentColor;
646
+ stroke-width: 2;
647
+ stroke-linecap: round;
648
+ stroke-linejoin: round;
649
+ fill: none;
650
+ }
651
+ h1 {
652
+ margin: 0 0 12px 0;
653
+ font-size: 20px;
654
+ font-weight: 600;
655
+ color: var(--text-color);
656
+ }
657
+ p {
658
+ margin: 0 0 32px 0;
659
+ font-size: 14px;
660
+ line-height: 1.5;
661
+ color: var(--text-muted);
662
+ }
663
+ .btn {
664
+ display: inline-block;
665
+ background-color: var(--btn-bg);
666
+ color: var(--btn-text);
667
+ padding: 10px 16px;
668
+ border-radius: 4px;
669
+ text-decoration: none;
670
+ font-size: 14px;
671
+ font-weight: 500;
672
+ transition: background-color 0.2s;
673
+ }
674
+ .btn:hover {
675
+ background-color: var(--btn-hover);
676
+ }
677
+ </style>
678
+ </head>
679
+ <body>
680
+ ${content}
681
+ </body>
682
+ </html>`;
683
+ }
525
684
  function oauthService({ strapi: strapi2 }) {
526
685
  return {
527
686
  async createUser(email, lastname, firstname, locale, roles2 = []) {
@@ -602,40 +761,48 @@ function oauthService({ strapi: strapi2 }) {
602
761
  const config2 = strapi2.config.get("plugin::strapi-plugin-oidc");
603
762
  const REMEMBER_ME = config2["REMEMBER_ME"];
604
763
  const isRememberMe = !!REMEMBER_ME;
605
- return `
606
- <!doctype html>
607
- <html>
608
- <head>
609
- <noscript>
610
- <h3>JavaScript must be enabled for authentication</h3>
611
- </noscript>
612
- <script nonce="${nonce}">
613
- window.addEventListener('load', function() {
614
- if(${isRememberMe}){
615
- localStorage.setItem('jwtToken', '"${jwtToken}"');
616
- }else{
617
- document.cookie = 'jwtToken=${encodeURIComponent(jwtToken)}; Path=/';
618
- }
619
- localStorage.setItem('isLoggedIn', 'true');
620
- location.href = '${strapi2.config.admin.url}'
621
- })
622
- <\/script>
623
- </head>
624
- <body>
625
- </body>
626
- </html>`;
764
+ const content = `
765
+ <noscript>
766
+ <div class="card">
767
+ <div class="icon success">
768
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-check">
769
+ <path d="M20 6 9 17l-5-5"/>
770
+ </svg>
771
+ </div>
772
+ <h1>JavaScript Required</h1>
773
+ <p>JavaScript must be enabled for authentication to complete.</p>
774
+ </div>
775
+ </noscript>
776
+ <script nonce="${nonce}">
777
+ window.addEventListener('load', function() {
778
+ if(${isRememberMe}){
779
+ localStorage.setItem('jwtToken', '"${jwtToken}"');
780
+ }else{
781
+ document.cookie = 'jwtToken=${encodeURIComponent(jwtToken)}; Path=/';
782
+ }
783
+ localStorage.setItem('isLoggedIn', 'true');
784
+ location.href = '${strapi2.config.admin.url}'
785
+ })
786
+ <\/script>`;
787
+ return renderHtmlTemplate("Authenticating...", content);
627
788
  },
628
789
  // Sign In Error
629
790
  renderSignUpError(message) {
630
- return `
631
- <!doctype html>
632
- <html>
633
- <head></head>
634
- <body>
635
- <h3>Authentication failed</h3>
636
- <p>${message}</p>
637
- </body>
638
- </html>`;
791
+ const safeMessage = String(message).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
792
+ const content = `
793
+ <div class="card">
794
+ <div class="icon">
795
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-triangle-alert">
796
+ <path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"/>
797
+ <path d="M12 9v4"/>
798
+ <path d="M12 17h.01"/>
799
+ </svg>
800
+ </div>
801
+ <h1>Authentication Failed</h1>
802
+ <p>${safeMessage}</p>
803
+ <a href="${strapi2.config.admin.url}" class="btn">Return to Login</a>
804
+ </div>`;
805
+ return renderHtmlTemplate("Authentication Failed", content);
639
806
  },
640
807
  async generateToken(user, ctx) {
641
808
  const sessionManager = strapi2.sessionManager;
@@ -729,39 +896,35 @@ function roleService({ strapi: strapi2 }) {
729
896
  };
730
897
  }
731
898
  function whitelistService({ strapi: strapi2 }) {
899
+ const getPluginStore = () => strapi2.store({
900
+ environment: "",
901
+ type: "plugin",
902
+ name: "strapi-plugin-oidc"
903
+ });
904
+ const getWhitelistQuery = () => strapi2.query("plugin::strapi-plugin-oidc.whitelists");
732
905
  return {
733
906
  async getSettings() {
734
- const pluginStore = strapi2.store({ type: "plugin", name: "strapi-plugin-oidc" });
735
- let settings = await pluginStore.get({ key: "settings" });
907
+ let settings = await getPluginStore().get({ key: "settings" });
736
908
  if (!settings) {
737
909
  settings = { useWhitelist: true, enforceOIDC: false };
738
- await pluginStore.set({ key: "settings", value: settings });
910
+ await getPluginStore().set({ key: "settings", value: settings });
739
911
  }
740
912
  return settings;
741
913
  },
742
914
  async setSettings(settings) {
743
- const pluginStore = strapi2.store({ type: "plugin", name: "strapi-plugin-oidc" });
744
- await pluginStore.set({ key: "settings", value: settings });
915
+ await getPluginStore().set({ key: "settings", value: settings });
745
916
  },
746
917
  async getUsers() {
747
- const query = strapi2.query("plugin::strapi-plugin-oidc.whitelists");
748
- return query.findMany();
918
+ return getWhitelistQuery().findMany();
749
919
  },
750
920
  async registerUser(email, roles2) {
751
- const query = strapi2.query("plugin::strapi-plugin-oidc.whitelists");
752
- await query.create({
753
- data: {
754
- email,
755
- roles: roles2
756
- }
921
+ await getWhitelistQuery().create({
922
+ data: { email, roles: roles2 }
757
923
  });
758
924
  },
759
925
  async removeUser(id) {
760
- const query = strapi2.query("plugin::strapi-plugin-oidc.whitelists");
761
- await query.delete({
762
- where: {
763
- id
764
- }
926
+ await getWhitelistQuery().delete({
927
+ where: { id }
765
928
  });
766
929
  },
767
930
  async checkWhitelistForEmail(email) {
@@ -770,11 +933,8 @@ function whitelistService({ strapi: strapi2 }) {
770
933
  if (!settings.useWhitelist) {
771
934
  return null;
772
935
  }
773
- const query = strapi2.query("plugin::strapi-plugin-oidc.whitelists");
774
- const result = await query.findOne({
775
- where: {
776
- email
777
- }
936
+ const result = await getWhitelistQuery().findOne({
937
+ where: { email }
778
938
  });
779
939
  console.log("checkWhitelistForEmail result:", result);
780
940
  if (!result) {
@@ -194,7 +194,7 @@ async function registerNewUser(userService, oauthService2, roleService2, email,
194
194
  return activateUser;
195
195
  }
196
196
  async function handleUserAuthentication(userService, oauthService2, roleService2, whitelistService2, userResponseData, config2, ctx) {
197
- const email = userResponseData.email;
197
+ const email = String(userResponseData.email).toLowerCase();
198
198
  const whitelistUser = await whitelistService2.checkWhitelistForEmail(email);
199
199
  const dbUser = await userService.findOneByEmail(email);
200
200
  let activateUser;
@@ -309,8 +309,14 @@ async function info(ctx) {
309
309
  };
310
310
  }
311
311
  async function updateSettings(ctx) {
312
- const { useWhitelist, enforceOIDC } = ctx.request.body;
312
+ let { useWhitelist, enforceOIDC } = ctx.request.body;
313
313
  const whitelistService2 = strapi.plugin("strapi-plugin-oidc").service("whitelist");
314
+ if (useWhitelist && enforceOIDC) {
315
+ const users = await whitelistService2.getUsers();
316
+ if (users.length === 0) {
317
+ enforceOIDC = false;
318
+ }
319
+ }
314
320
  await whitelistService2.setSettings({ useWhitelist, enforceOIDC });
315
321
  ctx.body = { useWhitelist, enforceOIDC };
316
322
  }
@@ -327,7 +333,8 @@ async function register(ctx) {
327
333
  ctx.body = { message: "Please enter a valid email address" };
328
334
  return;
329
335
  }
330
- const emailList = Array.isArray(email) ? email : email.split(",").map((e) => e.trim()).filter(Boolean);
336
+ const rawEmails = Array.isArray(email) ? email : email.split(",");
337
+ const emailList = rawEmails.map((e) => String(e).trim().toLowerCase()).filter(Boolean);
331
338
  const existingUsers = await strapi.query("admin::user").findMany({
332
339
  where: { email: { $in: emailList } },
333
340
  populate: ["roles"]
@@ -357,7 +364,8 @@ async function removeEmail(ctx) {
357
364
  ctx.body = {};
358
365
  }
359
366
  async function syncUsers(ctx) {
360
- const { users } = ctx.request.body;
367
+ let { users } = ctx.request.body;
368
+ users = users.map((u) => ({ ...u, email: String(u.email).toLowerCase() }));
361
369
  const whitelistService2 = strapi.plugin("strapi-plugin-oidc").service("whitelist");
362
370
  const currentUsers = await whitelistService2.getUsers();
363
371
  let matchedExistingUsersCount = 0;
@@ -403,6 +411,23 @@ const controllers = {
403
411
  role,
404
412
  whitelist
405
413
  };
414
+ const rateLimitMap = /* @__PURE__ */ new Map();
415
+ const RATE_LIMIT_WINDOW = 6e4;
416
+ const MAX_REQUESTS = 20;
417
+ const rateLimitMiddleware = async (ctx, next) => {
418
+ const ip = ctx.request.ip;
419
+ const now = Date.now();
420
+ const windowStart = now - RATE_LIMIT_WINDOW;
421
+ const requestStamps = (rateLimitMap.get(ip) || []).filter((timestamp) => timestamp > windowStart);
422
+ if (requestStamps.length >= MAX_REQUESTS) {
423
+ ctx.status = 429;
424
+ ctx.body = "Too Many Requests";
425
+ return;
426
+ }
427
+ requestStamps.push(now);
428
+ rateLimitMap.set(ip, requestStamps);
429
+ await next();
430
+ };
406
431
  const routes = [
407
432
  {
408
433
  method: "GET",
@@ -422,7 +447,10 @@ const routes = [
422
447
  config: {
423
448
  policies: [
424
449
  "admin::isAuthenticatedAdmin",
425
- { name: "admin::hasPermissions", config: { actions: ["plugin::strapi-plugin-oidc.update"] } }
450
+ {
451
+ name: "admin::hasPermissions",
452
+ config: { actions: ["plugin::strapi-plugin-oidc.update"] }
453
+ }
426
454
  ]
427
455
  }
428
456
  },
@@ -431,7 +459,8 @@ const routes = [
431
459
  path: "/oidc",
432
460
  handler: "oidc.oidcSignIn",
433
461
  config: {
434
- auth: false
462
+ auth: false,
463
+ middlewares: [rateLimitMiddleware]
435
464
  }
436
465
  },
437
466
  {
@@ -439,7 +468,8 @@ const routes = [
439
468
  path: "/oidc/callback",
440
469
  handler: "oidc.oidcSignInCallback",
441
470
  config: {
442
- auth: false
471
+ auth: false,
472
+ middlewares: [rateLimitMiddleware]
443
473
  }
444
474
  },
445
475
  {
@@ -468,7 +498,10 @@ const routes = [
468
498
  config: {
469
499
  policies: [
470
500
  "admin::isAuthenticatedAdmin",
471
- { name: "admin::hasPermissions", config: { actions: ["plugin::strapi-plugin-oidc.update"] } }
501
+ {
502
+ name: "admin::hasPermissions",
503
+ config: { actions: ["plugin::strapi-plugin-oidc.update"] }
504
+ }
472
505
  ]
473
506
  }
474
507
  },
@@ -487,7 +520,10 @@ const routes = [
487
520
  config: {
488
521
  policies: [
489
522
  "admin::isAuthenticatedAdmin",
490
- { name: "admin::hasPermissions", config: { actions: ["plugin::strapi-plugin-oidc.update"] } }
523
+ {
524
+ name: "admin::hasPermissions",
525
+ config: { actions: ["plugin::strapi-plugin-oidc.update"] }
526
+ }
491
527
  ]
492
528
  }
493
529
  },
@@ -498,7 +534,10 @@ const routes = [
498
534
  config: {
499
535
  policies: [
500
536
  "admin::isAuthenticatedAdmin",
501
- { name: "admin::hasPermissions", config: { actions: ["plugin::strapi-plugin-oidc.update"] } }
537
+ {
538
+ name: "admin::hasPermissions",
539
+ config: { actions: ["plugin::strapi-plugin-oidc.update"] }
540
+ }
502
541
  ]
503
542
  }
504
543
  },
@@ -509,12 +548,132 @@ const routes = [
509
548
  config: {
510
549
  policies: [
511
550
  "admin::isAuthenticatedAdmin",
512
- { name: "admin::hasPermissions", config: { actions: ["plugin::strapi-plugin-oidc.update"] } }
551
+ {
552
+ name: "admin::hasPermissions",
553
+ config: { actions: ["plugin::strapi-plugin-oidc.update"] }
554
+ }
513
555
  ]
514
556
  }
515
557
  }
516
558
  ];
517
559
  const policies = {};
560
+ function renderHtmlTemplate(title, content) {
561
+ return `
562
+ <!doctype html>
563
+ <html lang="en">
564
+ <head>
565
+ <meta charset="utf-8">
566
+ <meta name="viewport" content="width=device-width, initial-scale=1">
567
+ <title>${title}</title>
568
+ <style>
569
+ :root {
570
+ --bg-color: #f6f6f9;
571
+ --card-bg: #ffffff;
572
+ --text-color: #32324d;
573
+ --text-muted: #666687;
574
+ --btn-bg: #4945ff;
575
+ --btn-hover: #271fe0;
576
+ --btn-text: #ffffff;
577
+ --icon-bg: #fcecea;
578
+ --icon-color: #d02b20;
579
+ --success-bg: #eafbe7;
580
+ --success-color: #328048;
581
+ --shadow: 0 1px 4px rgba(33, 33, 52, 0.1);
582
+ }
583
+ @media (prefers-color-scheme: dark) {
584
+ :root {
585
+ --bg-color: #181826;
586
+ --card-bg: #212134;
587
+ --text-color: #ffffff;
588
+ --text-muted: #a5a5ba;
589
+ --btn-bg: #4945ff;
590
+ --btn-hover: #7b79ff;
591
+ --btn-text: #ffffff;
592
+ --icon-bg: #4a2123;
593
+ --icon-color: #f23628;
594
+ --success-bg: #1c3523;
595
+ --success-color: #55ca76;
596
+ --shadow: 0 1px 4px rgba(0, 0, 0, 0.5);
597
+ }
598
+ }
599
+ body {
600
+ margin: 0;
601
+ padding: 0;
602
+ display: flex;
603
+ justify-content: center;
604
+ align-items: center;
605
+ height: 100vh;
606
+ background-color: var(--bg-color);
607
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
608
+ color: var(--text-color);
609
+ }
610
+ .card {
611
+ background: var(--card-bg);
612
+ padding: 32px 40px;
613
+ border-radius: 8px;
614
+ box-shadow: var(--shadow);
615
+ max-width: 400px;
616
+ width: 100%;
617
+ text-align: center;
618
+ box-sizing: border-box;
619
+ }
620
+ .icon {
621
+ width: 48px;
622
+ height: 48px;
623
+ background-color: var(--icon-bg);
624
+ color: var(--icon-color);
625
+ border-radius: 50%;
626
+ display: inline-flex;
627
+ justify-content: center;
628
+ align-items: center;
629
+ margin-bottom: 24px;
630
+ }
631
+ .icon.success {
632
+ background-color: var(--success-bg);
633
+ color: var(--success-color);
634
+ }
635
+ .icon svg {
636
+ width: 24px;
637
+ height: 24px;
638
+ stroke: currentColor;
639
+ stroke-width: 2;
640
+ stroke-linecap: round;
641
+ stroke-linejoin: round;
642
+ fill: none;
643
+ }
644
+ h1 {
645
+ margin: 0 0 12px 0;
646
+ font-size: 20px;
647
+ font-weight: 600;
648
+ color: var(--text-color);
649
+ }
650
+ p {
651
+ margin: 0 0 32px 0;
652
+ font-size: 14px;
653
+ line-height: 1.5;
654
+ color: var(--text-muted);
655
+ }
656
+ .btn {
657
+ display: inline-block;
658
+ background-color: var(--btn-bg);
659
+ color: var(--btn-text);
660
+ padding: 10px 16px;
661
+ border-radius: 4px;
662
+ text-decoration: none;
663
+ font-size: 14px;
664
+ font-weight: 500;
665
+ transition: background-color 0.2s;
666
+ }
667
+ .btn:hover {
668
+ background-color: var(--btn-hover);
669
+ }
670
+ </style>
671
+ </head>
672
+ <body>
673
+ ${content}
674
+ </body>
675
+ </html>`;
676
+ }
518
677
  function oauthService({ strapi: strapi2 }) {
519
678
  return {
520
679
  async createUser(email, lastname, firstname, locale, roles2 = []) {
@@ -595,40 +754,48 @@ function oauthService({ strapi: strapi2 }) {
595
754
  const config2 = strapi2.config.get("plugin::strapi-plugin-oidc");
596
755
  const REMEMBER_ME = config2["REMEMBER_ME"];
597
756
  const isRememberMe = !!REMEMBER_ME;
598
- return `
599
- <!doctype html>
600
- <html>
601
- <head>
602
- <noscript>
603
- <h3>JavaScript must be enabled for authentication</h3>
604
- </noscript>
605
- <script nonce="${nonce}">
606
- window.addEventListener('load', function() {
607
- if(${isRememberMe}){
608
- localStorage.setItem('jwtToken', '"${jwtToken}"');
609
- }else{
610
- document.cookie = 'jwtToken=${encodeURIComponent(jwtToken)}; Path=/';
611
- }
612
- localStorage.setItem('isLoggedIn', 'true');
613
- location.href = '${strapi2.config.admin.url}'
614
- })
615
- <\/script>
616
- </head>
617
- <body>
618
- </body>
619
- </html>`;
757
+ const content = `
758
+ <noscript>
759
+ <div class="card">
760
+ <div class="icon success">
761
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-check">
762
+ <path d="M20 6 9 17l-5-5"/>
763
+ </svg>
764
+ </div>
765
+ <h1>JavaScript Required</h1>
766
+ <p>JavaScript must be enabled for authentication to complete.</p>
767
+ </div>
768
+ </noscript>
769
+ <script nonce="${nonce}">
770
+ window.addEventListener('load', function() {
771
+ if(${isRememberMe}){
772
+ localStorage.setItem('jwtToken', '"${jwtToken}"');
773
+ }else{
774
+ document.cookie = 'jwtToken=${encodeURIComponent(jwtToken)}; Path=/';
775
+ }
776
+ localStorage.setItem('isLoggedIn', 'true');
777
+ location.href = '${strapi2.config.admin.url}'
778
+ })
779
+ <\/script>`;
780
+ return renderHtmlTemplate("Authenticating...", content);
620
781
  },
621
782
  // Sign In Error
622
783
  renderSignUpError(message) {
623
- return `
624
- <!doctype html>
625
- <html>
626
- <head></head>
627
- <body>
628
- <h3>Authentication failed</h3>
629
- <p>${message}</p>
630
- </body>
631
- </html>`;
784
+ const safeMessage = String(message).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
785
+ const content = `
786
+ <div class="card">
787
+ <div class="icon">
788
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-triangle-alert">
789
+ <path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"/>
790
+ <path d="M12 9v4"/>
791
+ <path d="M12 17h.01"/>
792
+ </svg>
793
+ </div>
794
+ <h1>Authentication Failed</h1>
795
+ <p>${safeMessage}</p>
796
+ <a href="${strapi2.config.admin.url}" class="btn">Return to Login</a>
797
+ </div>`;
798
+ return renderHtmlTemplate("Authentication Failed", content);
632
799
  },
633
800
  async generateToken(user, ctx) {
634
801
  const sessionManager = strapi2.sessionManager;
@@ -722,39 +889,35 @@ function roleService({ strapi: strapi2 }) {
722
889
  };
723
890
  }
724
891
  function whitelistService({ strapi: strapi2 }) {
892
+ const getPluginStore = () => strapi2.store({
893
+ environment: "",
894
+ type: "plugin",
895
+ name: "strapi-plugin-oidc"
896
+ });
897
+ const getWhitelistQuery = () => strapi2.query("plugin::strapi-plugin-oidc.whitelists");
725
898
  return {
726
899
  async getSettings() {
727
- const pluginStore = strapi2.store({ type: "plugin", name: "strapi-plugin-oidc" });
728
- let settings = await pluginStore.get({ key: "settings" });
900
+ let settings = await getPluginStore().get({ key: "settings" });
729
901
  if (!settings) {
730
902
  settings = { useWhitelist: true, enforceOIDC: false };
731
- await pluginStore.set({ key: "settings", value: settings });
903
+ await getPluginStore().set({ key: "settings", value: settings });
732
904
  }
733
905
  return settings;
734
906
  },
735
907
  async setSettings(settings) {
736
- const pluginStore = strapi2.store({ type: "plugin", name: "strapi-plugin-oidc" });
737
- await pluginStore.set({ key: "settings", value: settings });
908
+ await getPluginStore().set({ key: "settings", value: settings });
738
909
  },
739
910
  async getUsers() {
740
- const query = strapi2.query("plugin::strapi-plugin-oidc.whitelists");
741
- return query.findMany();
911
+ return getWhitelistQuery().findMany();
742
912
  },
743
913
  async registerUser(email, roles2) {
744
- const query = strapi2.query("plugin::strapi-plugin-oidc.whitelists");
745
- await query.create({
746
- data: {
747
- email,
748
- roles: roles2
749
- }
914
+ await getWhitelistQuery().create({
915
+ data: { email, roles: roles2 }
750
916
  });
751
917
  },
752
918
  async removeUser(id) {
753
- const query = strapi2.query("plugin::strapi-plugin-oidc.whitelists");
754
- await query.delete({
755
- where: {
756
- id
757
- }
919
+ await getWhitelistQuery().delete({
920
+ where: { id }
758
921
  });
759
922
  },
760
923
  async checkWhitelistForEmail(email) {
@@ -763,11 +926,8 @@ function whitelistService({ strapi: strapi2 }) {
763
926
  if (!settings.useWhitelist) {
764
927
  return null;
765
928
  }
766
- const query = strapi2.query("plugin::strapi-plugin-oidc.whitelists");
767
- const result = await query.findOne({
768
- where: {
769
- email
770
- }
929
+ const result = await getWhitelistQuery().findOne({
930
+ where: { email }
771
931
  });
772
932
  console.log("checkWhitelistForEmail result:", result);
773
933
  if (!result) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "strapi-plugin-oidc",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
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",