@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.
- package/LICENSE +21 -0
- package/README.md +124 -0
- package/dist/backup-codes.d.ts +28 -0
- package/dist/backup-codes.d.ts.map +1 -0
- package/dist/backup-codes.js +66 -0
- package/dist/backup-codes.js.map +1 -0
- package/dist/coded-error.d.ts +24 -0
- package/dist/coded-error.d.ts.map +1 -0
- package/dist/coded-error.js +33 -0
- package/dist/coded-error.js.map +1 -0
- package/dist/components/login-form.d.ts +22 -0
- package/dist/components/login-form.d.ts.map +1 -0
- package/dist/components/login-form.js +91 -0
- package/dist/components/login-form.js.map +1 -0
- package/dist/components/login-view.d.ts +15 -0
- package/dist/components/login-view.d.ts.map +1 -0
- package/dist/components/login-view.js +43 -0
- package/dist/components/login-view.js.map +1 -0
- package/dist/components/mfa-panel.d.ts +16 -0
- package/dist/components/mfa-panel.d.ts.map +1 -0
- package/dist/components/mfa-panel.js +94 -0
- package/dist/components/mfa-panel.js.map +1 -0
- package/dist/components/mfa.css +92 -0
- package/dist/endpoints.d.ts +4 -0
- package/dist/endpoints.d.ts.map +1 -0
- package/dist/endpoints.js +190 -0
- package/dist/endpoints.js.map +1 -0
- package/dist/errors.d.ts +13 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +12 -0
- package/dist/errors.js.map +1 -0
- package/dist/exports/client.d.ts +5 -0
- package/dist/exports/client.d.ts.map +1 -0
- package/dist/exports/client.js +5 -0
- package/dist/exports/client.js.map +1 -0
- package/dist/exports/rsc.d.ts +2 -0
- package/dist/exports/rsc.d.ts.map +1 -0
- package/dist/exports/rsc.js +2 -0
- package/dist/exports/rsc.js.map +1 -0
- package/dist/fields.d.ts +4 -0
- package/dist/fields.d.ts.map +1 -0
- package/dist/fields.js +60 -0
- package/dist/fields.js.map +1 -0
- package/dist/hooks.d.ts +21 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +100 -0
- package/dist/hooks.js.map +1 -0
- package/dist/i18n.d.ts +49 -0
- package/dist/i18n.d.ts.map +1 -0
- package/dist/i18n.js +70 -0
- package/dist/i18n.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +86 -0
- package/dist/index.js.map +1 -0
- package/dist/reset.d.ts +23 -0
- package/dist/reset.d.ts.map +1 -0
- package/dist/reset.js +32 -0
- package/dist/reset.js.map +1 -0
- package/dist/state.d.ts +39 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +104 -0
- package/dist/state.js.map +1 -0
- package/dist/totp.d.ts +38 -0
- package/dist/totp.d.ts.map +1 -0
- package/dist/totp.js +49 -0
- package/dist/totp.js.map +1 -0
- package/dist/types.d.ts +60 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- 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 @@
|
|
|
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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"}
|
package/dist/errors.d.ts
ADDED
|
@@ -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 @@
|
|
|
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 @@
|
|
|
1
|
+
{"version":3,"file":"rsc.js","sourceRoot":"","sources":["../../src/exports/rsc.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAA0B,MAAM,6BAA6B,CAAC"}
|
package/dist/fields.d.ts
ADDED
|
@@ -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"}
|
package/dist/hooks.d.ts
ADDED
|
@@ -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"}
|