@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
@@ -0,0 +1,94 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { Button, useAuth, useConfig, useDocumentInfo, useTranslation } from '@payloadcms/ui';
4
+ import { useCallback, useEffect, useState } from 'react';
5
+ import { format, resolveLabels } from '../i18n.js';
6
+ import './mfa.css';
7
+ const baseClass = 'mfa-panel';
8
+ /**
9
+ * Panneau d'enrôlement TOTP, monté comme champ `ui` sur la collection protégée.
10
+ *
11
+ * Il n'apparaît que sur son propre compte : l'API MFA n'agit jamais que sur l'utilisateur de la
12
+ * session, donc l'afficher sur la fiche d'un autre utilisateur ne ferait que proposer des actions
13
+ * qui s'appliqueraient au mauvais compte.
14
+ */
15
+ export const MfaPanel = ({ labels, language }) => {
16
+ const { config } = useConfig();
17
+ const { user } = useAuth();
18
+ const { collectionSlug, id: docID } = useDocumentInfo();
19
+ const { i18n } = useTranslation();
20
+ const t = resolveLabels(language ?? i18n.language, labels);
21
+ const apiRoute = config.routes.api;
22
+ const isSelf = Boolean(user?.id) && String(user?.id) === String(docID);
23
+ const [status, setStatus] = useState(null);
24
+ const [stage, setStage] = useState({ name: 'idle' });
25
+ const [code, setCode] = useState('');
26
+ const [backupCodes, setBackupCodes] = useState(null);
27
+ const [error, setError] = useState(null);
28
+ const [busy, setBusy] = useState(false);
29
+ const call = useCallback(async (path, body) => {
30
+ const response = await fetch(`${apiRoute}/${collectionSlug}/mfa/${path}`, {
31
+ ...(body ? { body: JSON.stringify(body) } : {}),
32
+ credentials: 'include',
33
+ headers: { 'Content-Type': 'application/json' },
34
+ method: body ? 'POST' : 'GET',
35
+ });
36
+ const json = await response.json();
37
+ if (!response.ok)
38
+ throw new Error(json?.message || t.unknownError);
39
+ return json;
40
+ }, [apiRoute, collectionSlug, t.unknownError]);
41
+ const refresh = useCallback(async () => {
42
+ setStatus(await call('status'));
43
+ }, [call]);
44
+ useEffect(() => {
45
+ if (!isSelf)
46
+ return;
47
+ refresh().catch((cause) => setError(cause.message));
48
+ }, [isSelf, refresh]);
49
+ if (!isSelf)
50
+ return null;
51
+ const run = async (action) => {
52
+ setBusy(true);
53
+ setError(null);
54
+ try {
55
+ await action();
56
+ }
57
+ catch (cause) {
58
+ setError(cause instanceof Error ? cause.message : t.unknownError);
59
+ }
60
+ finally {
61
+ setBusy(false);
62
+ }
63
+ };
64
+ const startSetup = () => run(async () => {
65
+ const { qrCode, secret } = await call('setup', {});
66
+ setBackupCodes(null);
67
+ setCode('');
68
+ setStage({ name: 'setup', qrCode, secret });
69
+ });
70
+ const activate = () => run(async () => {
71
+ const { backupCodes: codes } = await call('activate', { code });
72
+ setBackupCodes(codes);
73
+ setStage({ name: 'idle' });
74
+ setCode('');
75
+ await refresh();
76
+ });
77
+ const regenerate = () => run(async () => {
78
+ const { backupCodes: codes } = await call('backup-codes', { code });
79
+ setBackupCodes(codes);
80
+ setCode('');
81
+ await refresh();
82
+ });
83
+ const disable = () => run(async () => {
84
+ await call('disable', { code });
85
+ setBackupCodes(null);
86
+ setCode('');
87
+ await refresh();
88
+ });
89
+ return (_jsxs("div", { className: baseClass, children: [_jsx("h3", { className: `${baseClass}__title`, children: t.panelTitle }), stage.name === 'idle' &&
90
+ (status === null ? (_jsx("p", { className: `${baseClass}__hint`, children: t.loading })) : (_jsx("p", { className: `${baseClass}__hint`, children: status.enabled
91
+ ? format(t.statusEnabled, { count: status.backupCodesRemaining })
92
+ : t.statusDisabled }))), error && (_jsx("p", { className: `${baseClass}__error`, role: "alert", children: error })), backupCodes && (_jsxs("div", { className: `${baseClass}__codes`, children: [_jsxs("p", { className: `${baseClass}__hint`, children: [_jsx("strong", { children: t.backupCodesWarningLead }), " ", t.backupCodesWarning] }), _jsx("ul", { children: backupCodes.map((backupCode) => (_jsx("li", { children: backupCode }, backupCode))) })] })), status?.enabled === false && stage.name === 'idle' && (_jsx(Button, { buttonStyle: "primary", disabled: busy, onClick: startSetup, size: "small", children: t.enableButton })), stage.name === 'setup' && (_jsxs("div", { className: `${baseClass}__setup`, children: [_jsx("p", { className: `${baseClass}__hint`, children: t.setupInstructions }), _jsx("img", { alt: t.qrAlt, className: `${baseClass}__qr`, src: stage.qrCode }), _jsxs("p", { className: `${baseClass}__secret`, children: [t.manualEntry, " ", _jsx("code", { children: stage.secret })] })] })), (stage.name === 'setup' || status?.enabled) && (_jsxs("div", { className: `${baseClass}__confirm`, children: [_jsxs("div", { className: "field-type text", children: [_jsx("label", { className: "field-label", htmlFor: "mfa-panel-code", children: t.codeFieldLabel }), _jsx("input", { autoComplete: "one-time-code", id: "mfa-panel-code", inputMode: "numeric", onChange: (event) => setCode(event.target.value), type: "text", value: code })] }), _jsxs("div", { className: `${baseClass}__actions`, children: [stage.name === 'setup' && (_jsx(Button, { buttonStyle: "primary", disabled: busy, onClick: activate, size: "small", children: t.confirmButton })), status?.enabled && stage.name === 'idle' && (_jsxs(_Fragment, { children: [_jsx(Button, { buttonStyle: "secondary", disabled: busy, onClick: regenerate, size: "small", children: t.regenerateButton }), _jsx(Button, { buttonStyle: "secondary", className: `${baseClass}__danger`, disabled: busy, onClick: disable, size: "small", children: t.disableButton })] }))] })] }))] }));
93
+ };
94
+ //# sourceMappingURL=mfa-panel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mfa-panel.js","sourceRoot":"","sources":["../../src/components/mfa-panel.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,92 @@
1
+ .mfa-panel {
2
+ display: flex;
3
+ flex-direction: column;
4
+ gap: calc(var(--base) / 2);
5
+ padding: var(--base);
6
+ border: 1px solid var(--theme-elevation-150);
7
+ border-radius: var(--style-radius-m, 4px);
8
+ margin-block: var(--base);
9
+ }
10
+
11
+ .mfa-panel__title {
12
+ margin: 0;
13
+ }
14
+
15
+ .mfa-panel__hint {
16
+ margin: 0;
17
+ color: var(--theme-elevation-600);
18
+ }
19
+
20
+ .mfa-panel__error {
21
+ margin: 0;
22
+ color: var(--theme-error-500);
23
+ }
24
+
25
+ .mfa-panel__qr {
26
+ width: 240px;
27
+ height: 240px;
28
+ background: #fff;
29
+ padding: calc(var(--base) / 4);
30
+ }
31
+
32
+ .mfa-panel__secret code {
33
+ user-select: all;
34
+ word-break: break-all;
35
+ }
36
+
37
+ .mfa-panel__codes ul {
38
+ display: grid;
39
+ grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr));
40
+ gap: calc(var(--base) / 4);
41
+ padding: 0;
42
+ margin: calc(var(--base) / 2) 0 0;
43
+ list-style: none;
44
+ font-family: var(--font-mono, monospace);
45
+ }
46
+
47
+ .mfa-panel__actions {
48
+ display: flex;
49
+ flex-wrap: wrap;
50
+ gap: calc(var(--base) / 2);
51
+ align-items: center;
52
+ }
53
+
54
+ .mfa-panel__danger {
55
+ color: var(--theme-error-500);
56
+ border-color: var(--theme-error-500);
57
+ }
58
+
59
+ .mfa-panel__confirm .field-type {
60
+ max-width: 16rem;
61
+ }
62
+
63
+ /* Formulaire de connexion */
64
+ .mfa-login {
65
+ display: flex;
66
+ flex-direction: column;
67
+ gap: calc(var(--base) / 2);
68
+ }
69
+
70
+ /* Les champs occupent toute la largeur ; seuls les liens se calent à gauche. */
71
+ .mfa-login > a,
72
+ .mfa-login__back {
73
+ align-self: flex-start;
74
+ }
75
+ .mfa-login .form-submit {
76
+ width: 100%;
77
+ }
78
+
79
+ .mfa-login__error {
80
+ margin: 0;
81
+ color: var(--theme-error-500);
82
+ }
83
+
84
+ .mfa-login__back {
85
+ align-self: flex-start;
86
+ padding: 0;
87
+ border: 0;
88
+ background: none;
89
+ color: var(--theme-text);
90
+ text-decoration: underline;
91
+ cursor: pointer;
92
+ }
@@ -0,0 +1,4 @@
1
+ import type { Endpoint } from 'payload';
2
+ import type { MfaContext } from './types.js';
3
+ export declare function createMfaEndpoints(ctx: MfaContext): Endpoint[];
4
+ //# sourceMappingURL=endpoints.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"endpoints.d.ts","sourceRoot":"","sources":["../src/endpoints.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAkB,MAAM,SAAS,CAAC;AAaxD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AA+B7C,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,UAAU,GAAG,QAAQ,EAAE,CAgL9D"}
@@ -0,0 +1,190 @@
1
+ import QRCode from 'qrcode';
2
+ import { countRemainingBackupCodes, generateBackupCodes } from './backup-codes.js';
3
+ import { resolveLabels } from './i18n.js';
4
+ import { EMPTY_MFA_STATE, isMfaLocked, readMfaState, registerMfaFailure, writeMfaState, } from './state.js';
5
+ import { buildOtpAuthUri, generateTotpSecret, verifyTotp } from './totp.js';
6
+ function json(body, status = 200) {
7
+ return Response.json(body, { status });
8
+ }
9
+ async function readBody(req) {
10
+ try {
11
+ return ((await req.json?.()) ?? {});
12
+ }
13
+ catch {
14
+ return {};
15
+ }
16
+ }
17
+ function readCode(body) {
18
+ return typeof body.code === 'string' ? body.code.trim() : '';
19
+ }
20
+ /**
21
+ * Tous les endpoints agissent exclusivement sur l'utilisateur de la session : on ne prend jamais
22
+ * d'identifiant depuis la requête, pour qu'aucun compte ne puisse en enrôler ou en déverrouiller
23
+ * un autre.
24
+ */
25
+ function requireSelf(req, ctx) {
26
+ const { user } = req;
27
+ if (!user || user.collection !== ctx.collection)
28
+ return null;
29
+ return { email: user.email ?? String(user.id), id: user.id };
30
+ }
31
+ export function createMfaEndpoints(ctx) {
32
+ /**
33
+ * Fabrique un handler déjà authentifié : les cinq endpoints partagent la vérification de session,
34
+ * la résolution des libellés et la lecture de l'état.
35
+ */
36
+ const authenticated = (handler) => async (req) => {
37
+ const t = resolveLabels(ctx.language ?? req.i18n?.language, ctx.labels);
38
+ const self = requireSelf(req, ctx);
39
+ if (!self)
40
+ return json({ message: t.authRequired }, 401);
41
+ return handler({ body: await readBody(req), req, self, t });
42
+ };
43
+ return [
44
+ /** État affiché par le panneau d'enrôlement. */
45
+ {
46
+ handler: authenticated(async ({ req, self }) => {
47
+ const state = await readMfaState(req.payload, ctx, self.id);
48
+ return json({
49
+ backupCodesRemaining: countRemainingBackupCodes(state.backupCodes),
50
+ confirmedAt: state.confirmedAt,
51
+ enabled: state.enabled,
52
+ hasPendingSecret: state.pendingSecret !== null,
53
+ });
54
+ }),
55
+ method: 'get',
56
+ path: '/mfa/status',
57
+ },
58
+ /**
59
+ * Génère un secret en attente et le QR code correspondant.
60
+ *
61
+ * Le secret n'est activé qu'après confirmation par `/mfa/activate` : tant que l'utilisateur n'a
62
+ * pas prouvé que son application génère les bons codes, la connexion reste possible sans second
63
+ * facteur.
64
+ */
65
+ {
66
+ handler: authenticated(async ({ req, self, t }) => {
67
+ const state = await readMfaState(req.payload, ctx, self.id);
68
+ if (state.enabled)
69
+ return json({ message: t.alreadyEnabled }, 409);
70
+ const secret = generateTotpSecret();
71
+ await writeMfaState(req.payload, ctx, self.id, {
72
+ failedAttempts: 0,
73
+ lockedUntil: null,
74
+ pendingSecret: secret,
75
+ });
76
+ const otpauthUri = buildOtpAuthUri(secret, {
77
+ accountLabel: self.email,
78
+ issuer: ctx.issuer,
79
+ });
80
+ return json({
81
+ // Le QR code est rendu ici et renvoyé en data URL : l'URI `otpauth://` contient le secret
82
+ // et ne doit jamais transiter par un service de génération tiers.
83
+ qrCode: await QRCode.toDataURL(otpauthUri, {
84
+ errorCorrectionLevel: 'M',
85
+ margin: 1,
86
+ width: 240,
87
+ }),
88
+ secret,
89
+ });
90
+ }),
91
+ method: 'post',
92
+ path: '/mfa/setup',
93
+ },
94
+ /** Confirme le secret en attente et délivre les codes de secours. */
95
+ {
96
+ handler: authenticated(async ({ body, req, self, t }) => {
97
+ const state = await readMfaState(req.payload, ctx, self.id);
98
+ if (!state.pendingSecret)
99
+ return json({ message: t.noPendingSetup }, 409);
100
+ if (isMfaLocked(state))
101
+ return json({ message: t.tooManyAttempts }, 429);
102
+ const result = verifyTotp({ secret: state.pendingSecret, token: readCode(body) });
103
+ if (!result.valid) {
104
+ await registerMfaFailure(req.payload, ctx, self.id, state);
105
+ return json({ message: t.setupCodeInvalid }, 400);
106
+ }
107
+ const backupCodes = generateBackupCodes(ctx.backupCodeCount);
108
+ await writeMfaState(req.payload, ctx, self.id, {
109
+ backupCodes: backupCodes.stored,
110
+ confirmedAt: new Date().toISOString(),
111
+ enabled: true,
112
+ failedAttempts: 0,
113
+ lastCounter: result.counter,
114
+ lockedUntil: null,
115
+ pendingSecret: null,
116
+ secret: state.pendingSecret,
117
+ });
118
+ // Seule et unique fois où les codes de secours sont lisibles : seules leurs empreintes sont
119
+ // stockées.
120
+ return json({ backupCodes: backupCodes.plain });
121
+ }),
122
+ method: 'post',
123
+ path: '/mfa/activate',
124
+ },
125
+ /**
126
+ * Régénère les codes de secours, en invalidant les anciens. Demande un code TOTP valide, pour
127
+ * qu'une session volée ne puisse pas se fabriquer des accès durables.
128
+ */
129
+ {
130
+ handler: authenticated(async ({ body, req, self, t }) => {
131
+ const state = await readMfaState(req.payload, ctx, self.id);
132
+ if (!state.enabled || !state.secret)
133
+ return json({ message: t.notEnabled }, 409);
134
+ if (isMfaLocked(state))
135
+ return json({ message: t.tooManyAttempts }, 429);
136
+ const result = verifyTotp({
137
+ lastCounter: state.lastCounter,
138
+ secret: state.secret,
139
+ token: readCode(body),
140
+ });
141
+ if (!result.valid) {
142
+ await registerMfaFailure(req.payload, ctx, self.id, state);
143
+ return json({ message: t.codeInvalid }, 400);
144
+ }
145
+ const backupCodes = generateBackupCodes(ctx.backupCodeCount);
146
+ await writeMfaState(req.payload, ctx, self.id, {
147
+ backupCodes: backupCodes.stored,
148
+ failedAttempts: 0,
149
+ lastCounter: result.counter,
150
+ lockedUntil: null,
151
+ });
152
+ return json({ backupCodes: backupCodes.plain });
153
+ }),
154
+ method: 'post',
155
+ path: '/mfa/backup-codes',
156
+ },
157
+ /**
158
+ * Désactive le second facteur. Exige un code TOTP valide.
159
+ *
160
+ * On ne redemande pas le mot de passe : le vérifier hors du flux de connexion supposerait de
161
+ * réimplémenter le hachage interne de Payload (`authenticateLocalStrategy` n'est pas exporté),
162
+ * et `payload.login()` créerait une session parasite. Redemander le second facteur est la
163
+ * garantie qui compte ici : elle bloque une session volée, qui est justement ce contre quoi la
164
+ * MFA protège.
165
+ */
166
+ {
167
+ handler: authenticated(async ({ body, req, self, t }) => {
168
+ const state = await readMfaState(req.payload, ctx, self.id);
169
+ if (!state.enabled || !state.secret)
170
+ return json({ message: t.notEnabled }, 409);
171
+ if (isMfaLocked(state))
172
+ return json({ message: t.tooManyAttempts }, 429);
173
+ const result = verifyTotp({
174
+ lastCounter: state.lastCounter,
175
+ secret: state.secret,
176
+ token: readCode(body),
177
+ });
178
+ if (!result.valid) {
179
+ await registerMfaFailure(req.payload, ctx, self.id, state);
180
+ return json({ message: t.codeInvalid }, 400);
181
+ }
182
+ await writeMfaState(req.payload, ctx, self.id, EMPTY_MFA_STATE);
183
+ return json({ enabled: false });
184
+ }),
185
+ method: 'post',
186
+ path: '/mfa/disable',
187
+ },
188
+ ];
189
+ }
190
+ //# sourceMappingURL=endpoints.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"endpoints.js","sourceRoot":"","sources":["../src/endpoints.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Codes renvoyés dans `errors[0].data.code` par le hook `beforeLogin`.
3
+ *
4
+ * Module sans dépendance : il est importé aussi bien par le formulaire de connexion (composant
5
+ * client) que par le hook côté serveur.
6
+ */
7
+ export declare const MFA_ERROR_CODES: {
8
+ readonly invalid: "MFA_INVALID";
9
+ readonly locked: "MFA_LOCKED";
10
+ readonly required: "MFA_REQUIRED";
11
+ };
12
+ export type MfaErrorCode = (typeof MFA_ERROR_CODES)[keyof typeof MFA_ERROR_CODES];
13
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,eAAO,MAAM,eAAe;;;;CAIlB,CAAC;AAEX,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,OAAO,eAAe,CAAC,CAAC"}
package/dist/errors.js ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Codes renvoyés dans `errors[0].data.code` par le hook `beforeLogin`.
3
+ *
4
+ * Module sans dépendance : il est importé aussi bien par le formulaire de connexion (composant
5
+ * client) que par le hook côté serveur.
6
+ */
7
+ export const MFA_ERROR_CODES = {
8
+ invalid: 'MFA_INVALID',
9
+ locked: 'MFA_LOCKED',
10
+ required: 'MFA_REQUIRED',
11
+ };
12
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG;IAC7B,OAAO,EAAE,aAAa;IACtB,MAAM,EAAE,YAAY;IACpB,QAAQ,EAAE,cAAc;CAChB,CAAC"}
@@ -0,0 +1,5 @@
1
+ export { MfaLoginForm, type MfaLoginFormProps } from '../components/login-form.js';
2
+ export { MfaPanel, type MfaPanelProps } from '../components/mfa-panel.js';
3
+ export { MFA_ERROR_CODES, type MfaErrorCode } from '../errors.js';
4
+ export { builtInLabels, format, resolveLabels, type MfaLabelOverrides, type MfaLabels, } from '../i18n.js';
5
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/exports/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,KAAK,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AACnF,OAAO,EAAE,QAAQ,EAAE,KAAK,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC1E,OAAO,EAAE,eAAe,EAAE,KAAK,YAAY,EAAE,MAAM,cAAc,CAAC;AAClE,OAAO,EACL,aAAa,EACb,MAAM,EACN,aAAa,EACb,KAAK,iBAAiB,EACtB,KAAK,SAAS,GACf,MAAM,YAAY,CAAC"}
@@ -0,0 +1,5 @@
1
+ export { MfaLoginForm } from '../components/login-form.js';
2
+ export { MfaPanel } from '../components/mfa-panel.js';
3
+ export { MFA_ERROR_CODES } from '../errors.js';
4
+ export { builtInLabels, format, resolveLabels, } from '../i18n.js';
5
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/exports/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAA0B,MAAM,6BAA6B,CAAC;AACnF,OAAO,EAAE,QAAQ,EAAsB,MAAM,4BAA4B,CAAC;AAC1E,OAAO,EAAE,eAAe,EAAqB,MAAM,cAAc,CAAC;AAClE,OAAO,EACL,aAAa,EACb,MAAM,EACN,aAAa,GAGd,MAAM,YAAY,CAAC"}
@@ -0,0 +1,2 @@
1
+ export { MfaLoginView, type MfaLoginViewProps } from '../components/login-view.js';
2
+ //# sourceMappingURL=rsc.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rsc.d.ts","sourceRoot":"","sources":["../../src/exports/rsc.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,KAAK,iBAAiB,EAAE,MAAM,6BAA6B,CAAC"}
@@ -0,0 +1,2 @@
1
+ export { MfaLoginView } from '../components/login-view.js';
2
+ //# sourceMappingURL=rsc.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rsc.js","sourceRoot":"","sources":["../../src/exports/rsc.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAA0B,MAAM,6BAA6B,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type { Field } from 'payload';
2
+ import type { MfaContext, MfaPluginOptions } from './types.js';
3
+ export declare function createMfaFields(ctx: MfaContext, options: MfaPluginOptions): Field[];
4
+ //# sourceMappingURL=fields.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fields.d.ts","sourceRoot":"","sources":["../src/fields.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAErC,OAAO,KAAK,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAkB/D,wBAAgB,eAAe,CAAC,GAAG,EAAE,UAAU,EAAE,OAAO,EAAE,gBAAgB,GAAG,KAAK,EAAE,CA4CnF"}
package/dist/fields.js ADDED
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Aucun champ MFA n'est modifiable par l'API : tout passe par les endpoints `/mfa/*`, qui écrivent
3
+ * en `overrideAccess`. Sans cela, un PATCH sur `/api/<collection>/:id` depuis une session volée
4
+ * suffirait à désactiver le second facteur ou à y substituer un secret choisi par l'attaquant.
5
+ */
6
+ const readOnlyToApi = {
7
+ create: () => false,
8
+ update: () => false,
9
+ };
10
+ /** `hidden` retire le champ des réponses API et de l'admin ; `read: false` couvre le reste. */
11
+ const secretAccess = {
12
+ ...readOnlyToApi,
13
+ read: () => false,
14
+ };
15
+ export function createMfaFields(ctx, options) {
16
+ return [
17
+ {
18
+ name: ctx.fieldName,
19
+ type: 'group',
20
+ // Le groupe ne contient que de l'état ; l'interface d'enrôlement est le champ `ui` ci-dessous.
21
+ admin: { hidden: true },
22
+ fields: [
23
+ { name: 'enabled', type: 'checkbox', access: readOnlyToApi, defaultValue: false },
24
+ { name: 'confirmedAt', type: 'date', access: readOnlyToApi },
25
+ // Secret TOTP validé, chiffré avec `PAYLOAD_SECRET`.
26
+ { name: 'secret', type: 'text', access: secretAccess, hidden: true },
27
+ // Secret en attente de confirmation par un premier code valide, chiffré lui aussi.
28
+ { name: 'pendingSecret', type: 'text', access: secretAccess, hidden: true },
29
+ // Tableau de `StoredBackupCode` : sel + empreinte scrypt + date de consommation.
30
+ { name: 'backupCodes', type: 'json', access: secretAccess, hidden: true },
31
+ // Dernier pas de temps TOTP accepté, pour refuser le rejeu d'un code déjà utilisé.
32
+ { name: 'lastCounter', type: 'number', access: secretAccess, hidden: true },
33
+ {
34
+ name: 'failedAttempts',
35
+ type: 'number',
36
+ access: secretAccess,
37
+ defaultValue: 0,
38
+ hidden: true,
39
+ },
40
+ { name: 'lockedUntil', type: 'date', access: secretAccess, hidden: true },
41
+ ],
42
+ },
43
+ {
44
+ name: `${ctx.fieldName}Panel`,
45
+ type: 'ui',
46
+ admin: {
47
+ components: {
48
+ Field: {
49
+ // `clientProps` est le seul canal par lequel un composant client peut recevoir la
50
+ // configuration du plugin : il n'a accès ni aux options ni au config serveur.
51
+ clientProps: { labels: options.labels, language: options.language },
52
+ exportName: 'MfaPanel',
53
+ path: 'payload-plugin-mfa/client',
54
+ },
55
+ },
56
+ },
57
+ },
58
+ ];
59
+ }
60
+ //# sourceMappingURL=fields.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fields.js","sourceRoot":"","sources":["../src/fields.ts"],"names":[],"mappings":"AAIA;;;;GAIG;AACH,MAAM,aAAa,GAAG;IACpB,MAAM,EAAE,GAAG,EAAE,CAAC,KAAK;IACnB,MAAM,EAAE,GAAG,EAAE,CAAC,KAAK;CACpB,CAAC;AAEF,+FAA+F;AAC/F,MAAM,YAAY,GAAG;IACnB,GAAG,aAAa;IAChB,IAAI,EAAE,GAAG,EAAE,CAAC,KAAK;CAClB,CAAC;AAEF,MAAM,UAAU,eAAe,CAAC,GAAe,EAAE,OAAyB;IACxE,OAAO;QACL;YACE,IAAI,EAAE,GAAG,CAAC,SAAS;YACnB,IAAI,EAAE,OAAO;YACb,+FAA+F;YAC/F,KAAK,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE;YACvB,MAAM,EAAE;gBACN,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,aAAa,EAAE,YAAY,EAAE,KAAK,EAAE;gBACjF,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE;gBAC5D,qDAAqD;gBACrD,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,IAAI,EAAE;gBACpE,mFAAmF;gBACnF,EAAE,IAAI,EAAE,eAAe,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,IAAI,EAAE;gBAC3E,iFAAiF;gBACjF,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,IAAI,EAAE;gBACzE,mFAAmF;gBACnF,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,IAAI,EAAE;gBAC3E;oBACE,IAAI,EAAE,gBAAgB;oBACtB,IAAI,EAAE,QAAQ;oBACd,MAAM,EAAE,YAAY;oBACpB,YAAY,EAAE,CAAC;oBACf,MAAM,EAAE,IAAI;iBACb;gBACD,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,IAAI,EAAE;aAC1E;SACF;QACD;YACE,IAAI,EAAE,GAAG,GAAG,CAAC,SAAS,OAAO;YAC7B,IAAI,EAAE,IAAI;YACV,KAAK,EAAE;gBACL,UAAU,EAAE;oBACV,KAAK,EAAE;wBACL,kFAAkF;wBAClF,8EAA8E;wBAC9E,WAAW,EAAE,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE;wBACnE,UAAU,EAAE,UAAU;wBACtB,IAAI,EAAE,2BAA2B;qBAClC;iBACF;aACF;SACF;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,21 @@
1
+ import type { CollectionAfterErrorHook, CollectionBeforeLoginHook } from 'payload';
2
+ import type { MfaContext } from './types.js';
3
+ /**
4
+ * Second facteur, appliqué après la vérification du mot de passe et avant l'émission du JWT.
5
+ *
6
+ * Les écritures du chemin nominal passent par `req`, donc par la transaction ouverte par
7
+ * `loginOperation` : écrire la ligne utilisateur depuis une transaction concurrente attendrait un
8
+ * verrou que seule la fin de la connexion libère, et la requête ne se terminerait jamais.
9
+ *
10
+ * Le compteur d'échecs, lui, doit survivre à l'annulation de cette transaction : il est écrit par
11
+ * `createMfaAfterErrorHook`, qui s'exécute après le `killTransaction`.
12
+ */
13
+ export declare function createMfaBeforeLoginHook(ctx: MfaContext): CollectionBeforeLoginHook;
14
+ /**
15
+ * Incrémente le compteur d'échecs, et pose le verrou au bout de `maxAttempts`.
16
+ *
17
+ * S'exécute depuis `routeError`, donc après que `loginOperation` a annulé sa transaction : les
18
+ * verrous sont retombés, l'écriture aboutit, et elle n'est plus emportée par l'annulation.
19
+ */
20
+ export declare function createMfaAfterErrorHook(ctx: MfaContext): CollectionAfterErrorHook;
21
+ //# sourceMappingURL=hooks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../src/hooks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,wBAAwB,EACxB,yBAAyB,EAG1B,MAAM,SAAS,CAAC;AASjB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAmB7C;;;;;;;;;GASG;AACH,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,UAAU,GAAG,yBAAyB,CAwBnF;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,UAAU,GAAG,wBAAwB,CAejF"}