@arthur.eudeline/payload-plugin-mfa 0.1.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.
Files changed (72) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +124 -0
  3. package/dist/backup-codes.d.ts +28 -0
  4. package/dist/backup-codes.d.ts.map +1 -0
  5. package/dist/backup-codes.js +66 -0
  6. package/dist/backup-codes.js.map +1 -0
  7. package/dist/coded-error.d.ts +24 -0
  8. package/dist/coded-error.d.ts.map +1 -0
  9. package/dist/coded-error.js +33 -0
  10. package/dist/coded-error.js.map +1 -0
  11. package/dist/components/login-form.d.ts +22 -0
  12. package/dist/components/login-form.d.ts.map +1 -0
  13. package/dist/components/login-form.js +91 -0
  14. package/dist/components/login-form.js.map +1 -0
  15. package/dist/components/login-view.d.ts +15 -0
  16. package/dist/components/login-view.d.ts.map +1 -0
  17. package/dist/components/login-view.js +43 -0
  18. package/dist/components/login-view.js.map +1 -0
  19. package/dist/components/mfa-panel.d.ts +16 -0
  20. package/dist/components/mfa-panel.d.ts.map +1 -0
  21. package/dist/components/mfa-panel.js +94 -0
  22. package/dist/components/mfa-panel.js.map +1 -0
  23. package/dist/components/mfa.css +92 -0
  24. package/dist/endpoints.d.ts +4 -0
  25. package/dist/endpoints.d.ts.map +1 -0
  26. package/dist/endpoints.js +190 -0
  27. package/dist/endpoints.js.map +1 -0
  28. package/dist/errors.d.ts +13 -0
  29. package/dist/errors.d.ts.map +1 -0
  30. package/dist/errors.js +12 -0
  31. package/dist/errors.js.map +1 -0
  32. package/dist/exports/client.d.ts +5 -0
  33. package/dist/exports/client.d.ts.map +1 -0
  34. package/dist/exports/client.js +5 -0
  35. package/dist/exports/client.js.map +1 -0
  36. package/dist/exports/rsc.d.ts +2 -0
  37. package/dist/exports/rsc.d.ts.map +1 -0
  38. package/dist/exports/rsc.js +2 -0
  39. package/dist/exports/rsc.js.map +1 -0
  40. package/dist/fields.d.ts +4 -0
  41. package/dist/fields.d.ts.map +1 -0
  42. package/dist/fields.js +60 -0
  43. package/dist/fields.js.map +1 -0
  44. package/dist/hooks.d.ts +21 -0
  45. package/dist/hooks.d.ts.map +1 -0
  46. package/dist/hooks.js +100 -0
  47. package/dist/hooks.js.map +1 -0
  48. package/dist/i18n.d.ts +49 -0
  49. package/dist/i18n.d.ts.map +1 -0
  50. package/dist/i18n.js +70 -0
  51. package/dist/i18n.js.map +1 -0
  52. package/dist/index.d.ts +19 -0
  53. package/dist/index.d.ts.map +1 -0
  54. package/dist/index.js +86 -0
  55. package/dist/index.js.map +1 -0
  56. package/dist/reset.d.ts +23 -0
  57. package/dist/reset.d.ts.map +1 -0
  58. package/dist/reset.js +32 -0
  59. package/dist/reset.js.map +1 -0
  60. package/dist/state.d.ts +39 -0
  61. package/dist/state.d.ts.map +1 -0
  62. package/dist/state.js +104 -0
  63. package/dist/state.js.map +1 -0
  64. package/dist/totp.d.ts +38 -0
  65. package/dist/totp.d.ts.map +1 -0
  66. package/dist/totp.js +49 -0
  67. package/dist/totp.js.map +1 -0
  68. package/dist/types.d.ts +60 -0
  69. package/dist/types.d.ts.map +1 -0
  70. package/dist/types.js +2 -0
  71. package/dist/types.js.map +1 -0
  72. package/package.json +81 -0
package/dist/totp.js ADDED
@@ -0,0 +1,49 @@
1
+ import { Secret, TOTP } from 'otpauth';
2
+ export const TOTP_DIGITS = 6;
3
+ export const TOTP_PERIOD_SECONDS = 30;
4
+ /**
5
+ * Nombre de pas de 30 s tolérés de part et d'autre du pas courant, pour absorber la dérive
6
+ * d'horloge entre le téléphone et le serveur. 1 = ±30 s, la valeur recommandée par la RFC 6238.
7
+ */
8
+ export const TOTP_WINDOW = 1;
9
+ function buildTotp(secret, identity) {
10
+ return new TOTP({
11
+ algorithm: 'SHA1',
12
+ digits: TOTP_DIGITS,
13
+ issuer: identity.issuer,
14
+ label: identity.accountLabel,
15
+ period: TOTP_PERIOD_SECONDS,
16
+ secret: Secret.fromBase32(secret),
17
+ });
18
+ }
19
+ /** Génère un secret partagé de 160 bits, encodé en base32 (le format attendu par les apps TOTP). */
20
+ export function generateTotpSecret() {
21
+ return new Secret({ size: 20 }).base32;
22
+ }
23
+ /** URI `otpauth://` à encoder dans le QR code présenté à l'utilisateur lors de l'enrôlement. */
24
+ export function buildOtpAuthUri(secret, identity) {
25
+ return buildTotp(secret, identity).toString();
26
+ }
27
+ /**
28
+ * Vérifie un code à 6 chiffres.
29
+ *
30
+ * `lastCounter` est le pas de temps du dernier code accepté pour ce compte : le rejouer est refusé,
31
+ * sans quoi un code intercepté resterait utilisable pendant toute sa fenêtre de validité (jusqu'à
32
+ * 90 s avec `TOTP_WINDOW = 1`).
33
+ */
34
+ export function verifyTotp({ lastCounter, secret, token, }) {
35
+ const normalized = token.replace(/\D/g, '');
36
+ if (normalized.length !== TOTP_DIGITS)
37
+ return { reason: 'invalid', valid: false };
38
+ // Ni l'émetteur ni le libellé n'entrent dans le calcul du code : des valeurs de façade suffisent.
39
+ const totp = buildTotp(secret, { accountLabel: 'verify', issuer: 'verify' });
40
+ const delta = totp.validate({ token: normalized, window: TOTP_WINDOW });
41
+ if (delta === null)
42
+ return { reason: 'invalid', valid: false };
43
+ const counter = Math.floor(Date.now() / 1000 / TOTP_PERIOD_SECONDS) + delta;
44
+ if (typeof lastCounter === 'number' && counter <= lastCounter) {
45
+ return { reason: 'replayed', valid: false };
46
+ }
47
+ return { counter, valid: true };
48
+ }
49
+ //# sourceMappingURL=totp.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"totp.js","sourceRoot":"","sources":["../src/totp.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAEvC,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,CAAC;AAC7B,MAAM,CAAC,MAAM,mBAAmB,GAAG,EAAE,CAAC;AAEtC;;;GAGG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,CAAC;AAS7B,SAAS,SAAS,CAAC,MAAc,EAAE,QAAsB;IACvD,OAAO,IAAI,IAAI,CAAC;QACd,SAAS,EAAE,MAAM;QACjB,MAAM,EAAE,WAAW;QACnB,MAAM,EAAE,QAAQ,CAAC,MAAM;QACvB,KAAK,EAAE,QAAQ,CAAC,YAAY;QAC5B,MAAM,EAAE,mBAAmB;QAC3B,MAAM,EAAE,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC;KAClC,CAAC,CAAC;AACL,CAAC;AAED,oGAAoG;AACpG,MAAM,UAAU,kBAAkB;IAChC,OAAO,IAAI,MAAM,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC;AACzC,CAAC;AAED,gGAAgG;AAChG,MAAM,UAAU,eAAe,CAAC,MAAc,EAAE,QAAsB;IACpE,OAAO,SAAS,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,QAAQ,EAAE,CAAC;AAChD,CAAC;AAKD;;;;;;GAMG;AACH,MAAM,UAAU,UAAU,CAAC,EACzB,WAAW,EACX,MAAM,EACN,KAAK,GAKN;IACC,MAAM,UAAU,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IAC5C,IAAI,UAAU,CAAC,MAAM,KAAK,WAAW;QAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;IAElF,kGAAkG;IAClG,MAAM,IAAI,GAAG,SAAS,CAAC,MAAM,EAAE,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAC;IAC7E,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC,CAAC;IACxE,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;IAE/D,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,GAAG,mBAAmB,CAAC,GAAG,KAAK,CAAC;IAC5E,IAAI,OAAO,WAAW,KAAK,QAAQ,IAAI,OAAO,IAAI,WAAW,EAAE,CAAC;QAC9D,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;IAC9C,CAAC;IAED,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;AAClC,CAAC"}
@@ -0,0 +1,60 @@
1
+ import type { MfaLabelOverrides } from './i18n.js';
2
+ export type MfaPluginOptions = {
3
+ /**
4
+ * Collections d'authentification à protéger. Par défaut, la seule collection admin
5
+ * (`config.admin.user`).
6
+ *
7
+ * La vue de connexion n'est remplacée que pour cette collection admin : c'est la seule qui ait un
8
+ * formulaire dans le panneau. Les autres collections gardent le hook et les endpoints, à charge
9
+ * du projet d'envoyer `mfaCode` dans sa propre requête de connexion.
10
+ */
11
+ collections?: string[];
12
+ /**
13
+ * Nom affiché comme émetteur dans l'application d'authentification, et donc dans la liste des
14
+ * comptes du téléphone. Par défaut, le `titleSuffix` de l'admin, sinon « Payload ».
15
+ */
16
+ issuer?: string;
17
+ /**
18
+ * Mettre à `false` désactive entièrement le plugin *sans retirer les champs* : la convention
19
+ * Payload, pour qu'une base déjà migrée ne perde pas ses colonnes.
20
+ */
21
+ enabled?: boolean;
22
+ /** Nom du groupe de champs ajouté à la collection. Par défaut `mfa`. */
23
+ fieldName?: string;
24
+ /** Nombre de codes de secours délivrés à l'activation. Par défaut 10. */
25
+ backupCodeCount?: number;
26
+ /** Nombre de codes invalides consécutifs avant verrouillage. Par défaut 5. */
27
+ maxAttempts?: number;
28
+ /** Durée du verrouillage, en millisecondes. Par défaut 15 minutes. */
29
+ lockDuration?: number;
30
+ /**
31
+ * Remplace la vue `/admin/login` par le formulaire en deux temps. Par défaut `true`.
32
+ *
33
+ * Passer `false` si le projet a déjà sa propre vue de connexion : il lui reste à monter
34
+ * `MfaLoginForm` (export `payload-plugin-mfa/client`) ou à gérer lui-même le code
35
+ * `MFA_REQUIRED`.
36
+ */
37
+ overrideLoginView?: boolean;
38
+ /**
39
+ * Surcharge des libellés, par code de langue de l'admin :
40
+ * `{ fr: { panelTitle: 'Double authentification' } }`. Anglais et français sont fournis.
41
+ */
42
+ labels?: MfaLabelOverrides;
43
+ /**
44
+ * Fige la langue des libellés au lieu de suivre celle de l'admin. Utile pour un projet dont
45
+ * l'interface est rédigée dans une langue mais dont l'admin Payload tourne en anglais.
46
+ */
47
+ language?: string;
48
+ };
49
+ /** Options normalisées, telles que les hooks, endpoints et composants les reçoivent. */
50
+ export type MfaContext = {
51
+ backupCodeCount: number;
52
+ collection: string;
53
+ fieldName: string;
54
+ issuer: string;
55
+ labels?: MfaLabelOverrides;
56
+ language?: string;
57
+ lockDuration: number;
58
+ maxAttempts: number;
59
+ };
60
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAEnD,MAAM,MAAM,gBAAgB,GAAG;IAC7B;;;;;;;OAOG;IACH,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IAEvB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAElB,wEAAwE;IACxE,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB,yEAAyE;IACzE,eAAe,CAAC,EAAE,MAAM,CAAC;IAEzB,8EAA8E;IAC9E,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB,sEAAsE;IACtE,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB;;;;;;OAMG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAE5B;;;OAGG;IACH,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAE3B;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,wFAAwF;AACxF,MAAM,MAAM,UAAU,GAAG;IACvB,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,81 @@
1
+ {
2
+ "name": "@arthur.eudeline/payload-plugin-mfa",
3
+ "version": "0.1.1",
4
+ "description": "Two-factor authentication (TOTP + one-time backup codes) for the Payload CMS admin panel.",
5
+ "keywords": [
6
+ "payload",
7
+ "payload-plugin",
8
+ "payloadcms",
9
+ "2fa",
10
+ "mfa",
11
+ "totp",
12
+ "authenticator"
13
+ ],
14
+ "license": "MIT",
15
+ "author": "Arthur Eudeline",
16
+ "homepage": "https://github.com/arthur-eudeline/payload-plugins/tree/main/packages/payload-plugin-mfa#readme",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/arthur-eudeline/payload-plugins.git",
20
+ "directory": "packages/payload-plugin-mfa"
21
+ },
22
+ "bugs": {
23
+ "url": "https://github.com/arthur-eudeline/payload-plugins/issues"
24
+ },
25
+ "type": "module",
26
+ "sideEffects": [
27
+ "*.css"
28
+ ],
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "exports": {
33
+ ".": {
34
+ "types": "./dist/index.d.ts",
35
+ "import": "./dist/index.js",
36
+ "default": "./dist/index.js"
37
+ },
38
+ "./client": {
39
+ "types": "./dist/exports/client.d.ts",
40
+ "import": "./dist/exports/client.js",
41
+ "default": "./dist/exports/client.js"
42
+ },
43
+ "./rsc": {
44
+ "types": "./dist/exports/rsc.d.ts",
45
+ "import": "./dist/exports/rsc.js",
46
+ "default": "./dist/exports/rsc.js"
47
+ }
48
+ },
49
+ "files": [
50
+ "dist"
51
+ ],
52
+ "dependencies": {
53
+ "otpauth": "^9.5.1",
54
+ "qrcode": "^1.5.4"
55
+ },
56
+ "peerDependencies": {
57
+ "@payloadcms/next": "^3.88.0",
58
+ "@payloadcms/ui": "^3.88.0",
59
+ "next": "^15.0.0 || ^16.0.0",
60
+ "payload": "^3.88.0",
61
+ "react": "^19.0.0"
62
+ },
63
+ "devDependencies": {
64
+ "@payloadcms/next": "3.88.0",
65
+ "@payloadcms/ui": "3.88.0",
66
+ "@types/node": "^26.2.0",
67
+ "@types/qrcode": "^1.5.6",
68
+ "@types/react": "19.2.18",
69
+ "next": "16.3.2",
70
+ "payload": "^3.88.0",
71
+ "react": "19.2.8",
72
+ "typescript": "6.0.3"
73
+ },
74
+ "engines": {
75
+ "node": ">=20.9.0"
76
+ },
77
+ "scripts": {
78
+ "build": "rm -rf dist && tsc && node scripts/copy-assets.mjs",
79
+ "typecheck": "tsc --noEmit"
80
+ }
81
+ }