@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
package/dist/hooks.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { consumeBackupCode } from './backup-codes.js';
|
|
2
|
+
import { codedError } from './coded-error.js';
|
|
3
|
+
import { MFA_ERROR_CODES } from './errors.js';
|
|
4
|
+
import { resolveLabels } from './i18n.js';
|
|
5
|
+
import { isMfaLocked, readMfaState, registerMfaFailure, writeMfaState } from './state.js';
|
|
6
|
+
import { verifyTotp } from './totp.js';
|
|
7
|
+
/**
|
|
8
|
+
* Marqueur posé par `beforeLogin` pour que `afterError` sache, une fois la transaction de connexion
|
|
9
|
+
* annulée, quel compte doit voir son compteur d'échecs incrémenté.
|
|
10
|
+
*
|
|
11
|
+
* Volontairement une `WeakMap` sur la requête plutôt que `req.context` : `createLocalReq` remplace
|
|
12
|
+
* `req.context` par un nouvel objet à chaque appel de l'API locale à qui on passe `req` — tout ce
|
|
13
|
+
* qu'on y écrirait serait silencieusement perdu au prochain `payload.find`. La requête, elle, reste
|
|
14
|
+
* le même objet d'un bout à l'autre, et la clé faible ne retient rien après la réponse.
|
|
15
|
+
*/
|
|
16
|
+
const pendingFailures = new WeakMap();
|
|
17
|
+
function readSubmittedCode(data) {
|
|
18
|
+
if (typeof data !== 'object' || data === null)
|
|
19
|
+
return null;
|
|
20
|
+
const value = data.mfaCode;
|
|
21
|
+
return typeof value === 'string' && value.trim() !== '' ? value.trim() : null;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Second facteur, appliqué après la vérification du mot de passe et avant l'émission du JWT.
|
|
25
|
+
*
|
|
26
|
+
* Les écritures du chemin nominal passent par `req`, donc par la transaction ouverte par
|
|
27
|
+
* `loginOperation` : écrire la ligne utilisateur depuis une transaction concurrente attendrait un
|
|
28
|
+
* verrou que seule la fin de la connexion libère, et la requête ne se terminerait jamais.
|
|
29
|
+
*
|
|
30
|
+
* Le compteur d'échecs, lui, doit survivre à l'annulation de cette transaction : il est écrit par
|
|
31
|
+
* `createMfaAfterErrorHook`, qui s'exécute après le `killTransaction`.
|
|
32
|
+
*/
|
|
33
|
+
export function createMfaBeforeLoginHook(ctx) {
|
|
34
|
+
return async ({ req, user }) => {
|
|
35
|
+
const { payload } = req;
|
|
36
|
+
const t = resolveLabels(ctx.language ?? req.i18n?.language, ctx.labels);
|
|
37
|
+
const state = await readMfaState(payload, ctx, user.id, req);
|
|
38
|
+
if (!state.enabled || !state.secret)
|
|
39
|
+
return user;
|
|
40
|
+
if (isMfaLocked(state)) {
|
|
41
|
+
throw codedError(MFA_ERROR_CODES.locked, t.tooManyAttempts, 429);
|
|
42
|
+
}
|
|
43
|
+
const submitted = readSubmittedCode(req.data);
|
|
44
|
+
if (!submitted) {
|
|
45
|
+
throw codedError(MFA_ERROR_CODES.required, t.codeRequired, 401);
|
|
46
|
+
}
|
|
47
|
+
if (!(await acceptCode(payload, ctx, user.id, state, submitted, req))) {
|
|
48
|
+
pendingFailures.set(req, user.id);
|
|
49
|
+
throw codedError(MFA_ERROR_CODES.invalid, t.codeInvalid, 401);
|
|
50
|
+
}
|
|
51
|
+
return user;
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Incrémente le compteur d'échecs, et pose le verrou au bout de `maxAttempts`.
|
|
56
|
+
*
|
|
57
|
+
* S'exécute depuis `routeError`, donc après que `loginOperation` a annulé sa transaction : les
|
|
58
|
+
* verrous sont retombés, l'écriture aboutit, et elle n'est plus emportée par l'annulation.
|
|
59
|
+
*/
|
|
60
|
+
export function createMfaAfterErrorHook(ctx) {
|
|
61
|
+
return async ({ req }) => {
|
|
62
|
+
const userId = pendingFailures.get(req);
|
|
63
|
+
if (userId === undefined)
|
|
64
|
+
return;
|
|
65
|
+
pendingFailures.delete(req);
|
|
66
|
+
try {
|
|
67
|
+
const state = await readMfaState(req.payload, ctx, userId);
|
|
68
|
+
await registerMfaFailure(req.payload, ctx, userId, state);
|
|
69
|
+
}
|
|
70
|
+
catch (cause) {
|
|
71
|
+
// Ce hook s'exécute pendant la construction d'une réponse d'erreur : lever ici masquerait le
|
|
72
|
+
// 401 par un 500. On journalise et on laisse passer.
|
|
73
|
+
req.payload.logger.error({ err: cause }, 'MFA : échec de l’enregistrement d’une tentative.');
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Un code à 6 chiffres est traité comme un code TOTP, tout le reste comme un code de secours :
|
|
79
|
+
* les deux formats sont disjoints (`XXXX-XXXX` en base32 sans chiffres 0/1).
|
|
80
|
+
*/
|
|
81
|
+
async function acceptCode(payload, ctx, userId, state, submitted, req) {
|
|
82
|
+
const digitsOnly = submitted.replace(/\D/g, '');
|
|
83
|
+
if (digitsOnly.length === 6 && state.secret) {
|
|
84
|
+
const result = verifyTotp({
|
|
85
|
+
lastCounter: state.lastCounter,
|
|
86
|
+
secret: state.secret,
|
|
87
|
+
token: digitsOnly,
|
|
88
|
+
});
|
|
89
|
+
if (!result.valid)
|
|
90
|
+
return false;
|
|
91
|
+
await writeMfaState(payload, ctx, userId, { failedAttempts: 0, lastCounter: result.counter, lockedUntil: null }, req);
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
const { codes, matched } = consumeBackupCode(state.backupCodes, submitted);
|
|
95
|
+
if (!matched)
|
|
96
|
+
return false;
|
|
97
|
+
await writeMfaState(payload, ctx, userId, { backupCodes: codes, failedAttempts: 0, lockedUntil: null }, req);
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=hooks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hooks.js","sourceRoot":"","sources":["../src/hooks.ts"],"names":[],"mappings":"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"}
|
package/dist/i18n.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Table de libellés autonome, plutôt qu'un branchement sur `config.i18n.translations`.
|
|
3
|
+
*
|
|
4
|
+
* Le plugin doit fonctionner sans que le projet hôte ait à déclarer quoi que ce soit, y compris
|
|
5
|
+
* dans un composant client où seule la langue de l'admin est connue. La table reste minuscule, et
|
|
6
|
+
* un projet peut la surcharger langue par langue via l'option `labels`.
|
|
7
|
+
*/
|
|
8
|
+
export type MfaLabels = {
|
|
9
|
+
/** Panneau d'enrôlement */
|
|
10
|
+
panelTitle: string;
|
|
11
|
+
loading: string;
|
|
12
|
+
/** `{count}` est remplacé par le nombre de codes de secours restants. */
|
|
13
|
+
statusEnabled: string;
|
|
14
|
+
statusDisabled: string;
|
|
15
|
+
enableButton: string;
|
|
16
|
+
setupInstructions: string;
|
|
17
|
+
qrAlt: string;
|
|
18
|
+
manualEntry: string;
|
|
19
|
+
codeFieldLabel: string;
|
|
20
|
+
confirmButton: string;
|
|
21
|
+
regenerateButton: string;
|
|
22
|
+
disableButton: string;
|
|
23
|
+
backupCodesWarning: string;
|
|
24
|
+
backupCodesWarningLead: string;
|
|
25
|
+
/** Formulaire de connexion */
|
|
26
|
+
loginCodeLabel: string;
|
|
27
|
+
loginCodeDescription: string;
|
|
28
|
+
loginBack: string;
|
|
29
|
+
/** Messages d'erreur (serveur et client) */
|
|
30
|
+
alreadyEnabled: string;
|
|
31
|
+
authRequired: string;
|
|
32
|
+
codeInvalid: string;
|
|
33
|
+
codeRequired: string;
|
|
34
|
+
notEnabled: string;
|
|
35
|
+
noPendingSetup: string;
|
|
36
|
+
setupCodeInvalid: string;
|
|
37
|
+
tooManyAttempts: string;
|
|
38
|
+
unknownError: string;
|
|
39
|
+
};
|
|
40
|
+
export declare const builtInLabels: Record<string, MfaLabels>;
|
|
41
|
+
export type MfaLabelOverrides = Record<string, Partial<MfaLabels>>;
|
|
42
|
+
/**
|
|
43
|
+
* Anglais par défaut, écrasé par la langue demandée puis par les surcharges du projet. Une langue
|
|
44
|
+
* inconnue retombe donc sur l'anglais plutôt que d'afficher des clés brutes.
|
|
45
|
+
*/
|
|
46
|
+
export declare function resolveLabels(language?: string, overrides?: MfaLabelOverrides): MfaLabels;
|
|
47
|
+
/** Remplace les `{jeton}` d'un libellé par les valeurs fournies. */
|
|
48
|
+
export declare function format(label: string, values: Record<string, number | string>): string;
|
|
49
|
+
//# sourceMappingURL=i18n.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"i18n.d.ts","sourceRoot":"","sources":["../src/i18n.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,MAAM,SAAS,GAAG;IACtB,2BAA2B;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,yEAAyE;IACzE,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC;IACrB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,gBAAgB,EAAE,MAAM,CAAC;IACzB,aAAa,EAAE,MAAM,CAAC;IACtB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,sBAAsB,EAAE,MAAM,CAAC;IAE/B,8BAA8B;IAC9B,cAAc,EAAE,MAAM,CAAC;IACvB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,SAAS,EAAE,MAAM,CAAC;IAElB,4CAA4C;IAC5C,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,gBAAgB,EAAE,MAAM,CAAC;IACzB,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAsEF,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAc,CAAC;AAEnE,MAAM,MAAM,iBAAiB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC;AAEnE;;;GAGG;AACH,wBAAgB,aAAa,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,iBAAiB,GAAG,SAAS,CAGzF;AAED,oEAAoE;AACpE,wBAAgB,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,GAAG,MAAM,CAIrF"}
|
package/dist/i18n.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const en = {
|
|
2
|
+
panelTitle: 'Two-factor authentication',
|
|
3
|
+
loading: 'Loading…',
|
|
4
|
+
statusEnabled: 'Enabled. {count} backup code(s) left.',
|
|
5
|
+
statusDisabled: 'Disabled. Your account is protected by your password only.',
|
|
6
|
+
enableButton: 'Enable two-factor authentication',
|
|
7
|
+
setupInstructions: 'Scan this QR code with your authenticator app, then enter the code it shows to confirm.',
|
|
8
|
+
qrAlt: 'Setup QR code',
|
|
9
|
+
manualEntry: 'Manual entry:',
|
|
10
|
+
codeFieldLabel: '6-digit code',
|
|
11
|
+
confirmButton: 'Confirm and enable',
|
|
12
|
+
regenerateButton: 'Regenerate backup codes',
|
|
13
|
+
disableButton: 'Disable',
|
|
14
|
+
backupCodesWarningLead: 'Write these backup codes down now.',
|
|
15
|
+
backupCodesWarning: 'They will never be shown again and each one works only once. They let you sign in if you lose your phone.',
|
|
16
|
+
loginCodeLabel: 'Two-factor code',
|
|
17
|
+
loginCodeDescription: 'The 6-digit code from your authenticator app, or one of your backup codes.',
|
|
18
|
+
loginBack: 'Back to sign in',
|
|
19
|
+
alreadyEnabled: 'Two-factor authentication is already enabled. Disable it first.',
|
|
20
|
+
authRequired: 'Authentication required.',
|
|
21
|
+
codeInvalid: 'Invalid two-factor code.',
|
|
22
|
+
codeRequired: 'Two-factor code required.',
|
|
23
|
+
notEnabled: 'Two-factor authentication is not enabled.',
|
|
24
|
+
noPendingSetup: 'No setup in progress. Start enrolment again.',
|
|
25
|
+
setupCodeInvalid: 'Invalid code. Check the clock on your phone.',
|
|
26
|
+
tooManyAttempts: 'Too many invalid codes. Try again in a few minutes.',
|
|
27
|
+
unknownError: 'Something went wrong.',
|
|
28
|
+
};
|
|
29
|
+
const fr = {
|
|
30
|
+
panelTitle: 'Double authentification',
|
|
31
|
+
loading: 'Chargement…',
|
|
32
|
+
statusEnabled: 'Activée. {count} code(s) de secours restant(s).',
|
|
33
|
+
statusDisabled: 'Désactivée. Votre compte n’est protégé que par votre mot de passe.',
|
|
34
|
+
enableButton: 'Activer la double authentification',
|
|
35
|
+
setupInstructions: 'Scannez ce QR code avec votre application d’authentification, puis saisissez le code affiché pour confirmer.',
|
|
36
|
+
qrAlt: 'QR code de configuration',
|
|
37
|
+
manualEntry: 'Saisie manuelle :',
|
|
38
|
+
codeFieldLabel: 'Code à 6 chiffres',
|
|
39
|
+
confirmButton: 'Confirmer et activer',
|
|
40
|
+
regenerateButton: 'Régénérer les codes de secours',
|
|
41
|
+
disableButton: 'Désactiver',
|
|
42
|
+
backupCodesWarningLead: 'Notez ces codes de secours maintenant.',
|
|
43
|
+
backupCodesWarning: 'Ils ne seront plus jamais affichés et chacun ne fonctionne qu’une fois. Ils permettent de se connecter si vous perdez votre téléphone.',
|
|
44
|
+
loginCodeLabel: 'Code de double authentification',
|
|
45
|
+
loginCodeDescription: 'Code à 6 chiffres de votre application d’authentification, ou l’un de vos codes de secours.',
|
|
46
|
+
loginBack: 'Revenir à l’identification',
|
|
47
|
+
alreadyEnabled: 'La double authentification est déjà activée. Désactivez-la d’abord.',
|
|
48
|
+
authRequired: 'Authentification requise.',
|
|
49
|
+
codeInvalid: 'Code de double authentification invalide.',
|
|
50
|
+
codeRequired: 'Code de double authentification requis.',
|
|
51
|
+
notEnabled: 'La double authentification n’est pas activée.',
|
|
52
|
+
noPendingSetup: 'Aucune configuration en attente. Relancez l’activation.',
|
|
53
|
+
setupCodeInvalid: 'Code invalide. Vérifiez l’heure de votre téléphone.',
|
|
54
|
+
tooManyAttempts: 'Trop de codes invalides. Réessayez dans quelques minutes.',
|
|
55
|
+
unknownError: 'Une erreur est survenue.',
|
|
56
|
+
};
|
|
57
|
+
export const builtInLabels = { en, fr };
|
|
58
|
+
/**
|
|
59
|
+
* Anglais par défaut, écrasé par la langue demandée puis par les surcharges du projet. Une langue
|
|
60
|
+
* inconnue retombe donc sur l'anglais plutôt que d'afficher des clés brutes.
|
|
61
|
+
*/
|
|
62
|
+
export function resolveLabels(language, overrides) {
|
|
63
|
+
const lang = language ?? 'en';
|
|
64
|
+
return { ...en, ...builtInLabels[lang], ...overrides?.[lang] };
|
|
65
|
+
}
|
|
66
|
+
/** Remplace les `{jeton}` d'un libellé par les valeurs fournies. */
|
|
67
|
+
export function format(label, values) {
|
|
68
|
+
return label.replace(/\{(\w+)\}/g, (match, key) => key in values ? String(values[key]) : match);
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=i18n.js.map
|
package/dist/i18n.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"i18n.js","sourceRoot":"","sources":["../src/i18n.ts"],"names":[],"mappings":"AA0CA,MAAM,EAAE,GAAc;IACpB,UAAU,EAAE,2BAA2B;IACvC,OAAO,EAAE,UAAU;IACnB,aAAa,EAAE,uCAAuC;IACtD,cAAc,EAAE,4DAA4D;IAC5E,YAAY,EAAE,kCAAkC;IAChD,iBAAiB,EACf,yFAAyF;IAC3F,KAAK,EAAE,eAAe;IACtB,WAAW,EAAE,eAAe;IAC5B,cAAc,EAAE,cAAc;IAC9B,aAAa,EAAE,oBAAoB;IACnC,gBAAgB,EAAE,yBAAyB;IAC3C,aAAa,EAAE,SAAS;IACxB,sBAAsB,EAAE,oCAAoC;IAC5D,kBAAkB,EAChB,2GAA2G;IAE7G,cAAc,EAAE,iBAAiB;IACjC,oBAAoB,EAClB,4EAA4E;IAC9E,SAAS,EAAE,iBAAiB;IAE5B,cAAc,EAAE,iEAAiE;IACjF,YAAY,EAAE,0BAA0B;IACxC,WAAW,EAAE,0BAA0B;IACvC,YAAY,EAAE,2BAA2B;IACzC,UAAU,EAAE,2CAA2C;IACvD,cAAc,EAAE,8CAA8C;IAC9D,gBAAgB,EAAE,8CAA8C;IAChE,eAAe,EAAE,qDAAqD;IACtE,YAAY,EAAE,uBAAuB;CACtC,CAAC;AAEF,MAAM,EAAE,GAAc;IACpB,UAAU,EAAE,yBAAyB;IACrC,OAAO,EAAE,aAAa;IACtB,aAAa,EAAE,iDAAiD;IAChE,cAAc,EAAE,oEAAoE;IACpF,YAAY,EAAE,oCAAoC;IAClD,iBAAiB,EACf,8GAA8G;IAChH,KAAK,EAAE,0BAA0B;IACjC,WAAW,EAAE,mBAAmB;IAChC,cAAc,EAAE,mBAAmB;IACnC,aAAa,EAAE,sBAAsB;IACrC,gBAAgB,EAAE,gCAAgC;IAClD,aAAa,EAAE,YAAY;IAC3B,sBAAsB,EAAE,wCAAwC;IAChE,kBAAkB,EAChB,wIAAwI;IAE1I,cAAc,EAAE,iCAAiC;IACjD,oBAAoB,EAClB,6FAA6F;IAC/F,SAAS,EAAE,4BAA4B;IAEvC,cAAc,EAAE,qEAAqE;IACrF,YAAY,EAAE,2BAA2B;IACzC,WAAW,EAAE,2CAA2C;IACxD,YAAY,EAAE,yCAAyC;IACvD,UAAU,EAAE,+CAA+C;IAC3D,cAAc,EAAE,yDAAyD;IACzE,gBAAgB,EAAE,qDAAqD;IACvE,eAAe,EAAE,2DAA2D;IAC5E,YAAY,EAAE,0BAA0B;CACzC,CAAC;AAEF,MAAM,CAAC,MAAM,aAAa,GAA8B,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC;AAInE;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,QAAiB,EAAE,SAA6B;IAC5E,MAAM,IAAI,GAAG,QAAQ,IAAI,IAAI,CAAC;IAC9B,OAAO,EAAE,GAAG,EAAE,EAAE,GAAG,aAAa,CAAC,IAAI,CAAC,EAAE,GAAG,SAAS,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;AACjE,CAAC;AAED,oEAAoE;AACpE,MAAM,UAAU,MAAM,CAAC,KAAa,EAAE,MAAuC;IAC3E,OAAO,KAAK,CAAC,OAAO,CAAC,YAAY,EAAE,CAAC,KAAK,EAAE,GAAW,EAAE,EAAE,CACxD,GAAG,IAAI,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAC5C,CAAC;AACJ,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Plugin } from 'payload';
|
|
2
|
+
import type { MfaPluginOptions } from './types.js';
|
|
3
|
+
export { MFA_ERROR_CODES, type MfaErrorCode } from './errors.js';
|
|
4
|
+
export { builtInLabels, type MfaLabelOverrides, type MfaLabels } from './i18n.js';
|
|
5
|
+
export { resetMfa, type ResetMfaArgs } from './reset.js';
|
|
6
|
+
export type { MfaPluginOptions } from './types.js';
|
|
7
|
+
/**
|
|
8
|
+
* Double authentification TOTP pour le panneau d'administration Payload.
|
|
9
|
+
*
|
|
10
|
+
* Ajoute à chaque collection visée un groupe de champs chiffrés, cinq endpoints `/mfa/*`, un hook
|
|
11
|
+
* `beforeLogin` qui exige le second facteur et le panneau d'enrôlement dans la vue du compte. Par
|
|
12
|
+
* défaut, la vue `/admin/login` est remplacée par un formulaire en deux temps — le formulaire natif
|
|
13
|
+
* de Payload ne sait pas envoyer de champ supplémentaire.
|
|
14
|
+
*
|
|
15
|
+
* Après installation : `pnpm payload generate:types`, `pnpm payload generate:importmap`, puis une
|
|
16
|
+
* migration (le plugin ajoute des colonnes).
|
|
17
|
+
*/
|
|
18
|
+
export declare const mfaPlugin: (options?: MfaPluginOptions) => Plugin;
|
|
19
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAU,MAAM,EAAE,MAAM,SAAS,CAAC;AAK9C,OAAO,KAAK,EAAc,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAE/D,OAAO,EAAE,eAAe,EAAE,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AACjE,OAAO,EAAE,aAAa,EAAE,KAAK,iBAAiB,EAAE,KAAK,SAAS,EAAE,MAAM,WAAW,CAAC;AAClF,OAAO,EAAE,QAAQ,EAAE,KAAK,YAAY,EAAE,MAAM,YAAY,CAAC;AACzD,YAAY,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAKnD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,SAAS,GACnB,UAAS,gBAAqB,KAAG,MA+EjC,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { createMfaFields } from './fields.js';
|
|
2
|
+
import { createMfaAfterErrorHook, createMfaBeforeLoginHook } from './hooks.js';
|
|
3
|
+
import { createMfaEndpoints } from './endpoints.js';
|
|
4
|
+
export { MFA_ERROR_CODES } from './errors.js';
|
|
5
|
+
export { builtInLabels } from './i18n.js';
|
|
6
|
+
export { resetMfa } from './reset.js';
|
|
7
|
+
const DEFAULT_MAX_ATTEMPTS = 5;
|
|
8
|
+
const DEFAULT_LOCK_DURATION = 15 * 60 * 1_000;
|
|
9
|
+
/**
|
|
10
|
+
* Double authentification TOTP pour le panneau d'administration Payload.
|
|
11
|
+
*
|
|
12
|
+
* Ajoute à chaque collection visée un groupe de champs chiffrés, cinq endpoints `/mfa/*`, un hook
|
|
13
|
+
* `beforeLogin` qui exige le second facteur et le panneau d'enrôlement dans la vue du compte. Par
|
|
14
|
+
* défaut, la vue `/admin/login` est remplacée par un formulaire en deux temps — le formulaire natif
|
|
15
|
+
* de Payload ne sait pas envoyer de champ supplémentaire.
|
|
16
|
+
*
|
|
17
|
+
* Après installation : `pnpm payload generate:types`, `pnpm payload generate:importmap`, puis une
|
|
18
|
+
* migration (le plugin ajoute des colonnes).
|
|
19
|
+
*/
|
|
20
|
+
export const mfaPlugin = (options = {}) => (incoming) => {
|
|
21
|
+
const config = { ...incoming };
|
|
22
|
+
const adminUserSlug = config.admin?.user;
|
|
23
|
+
const collections = options.collections ?? (adminUserSlug ? [adminUserSlug] : []);
|
|
24
|
+
if (collections.length === 0) {
|
|
25
|
+
throw new Error('payload-plugin-mfa : aucune collection à protéger. Renseignez `admin.user` dans la ' +
|
|
26
|
+
'configuration Payload, ou passez explicitement l’option `collections`.');
|
|
27
|
+
}
|
|
28
|
+
// Convention Payload : `enabled: false` neutralise le comportement mais laisse les champs en
|
|
29
|
+
// place, pour qu'une base déjà migrée ne perde pas ses colonnes au prochain déploiement.
|
|
30
|
+
const active = options.enabled !== false;
|
|
31
|
+
const contextFor = (collection) => ({
|
|
32
|
+
backupCodeCount: options.backupCodeCount ?? 10,
|
|
33
|
+
collection,
|
|
34
|
+
fieldName: options.fieldName ?? 'mfa',
|
|
35
|
+
issuer: options.issuer ?? config.admin?.meta?.titleSuffix ?? 'Payload',
|
|
36
|
+
labels: options.labels,
|
|
37
|
+
language: options.language,
|
|
38
|
+
lockDuration: options.lockDuration ?? DEFAULT_LOCK_DURATION,
|
|
39
|
+
maxAttempts: options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS,
|
|
40
|
+
});
|
|
41
|
+
config.collections = (config.collections ?? []).map((collection) => {
|
|
42
|
+
if (!collections.includes(collection.slug))
|
|
43
|
+
return collection;
|
|
44
|
+
const ctx = contextFor(collection.slug);
|
|
45
|
+
if (!collection.auth) {
|
|
46
|
+
throw new Error(`payload-plugin-mfa : la collection "${collection.slug}" n’a pas d’option \`auth\`. ` +
|
|
47
|
+
'Le second facteur se greffe sur la connexion par mot de passe de Payload.');
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
...collection,
|
|
51
|
+
endpoints: [...(collection.endpoints || []), ...(active ? createMfaEndpoints(ctx) : [])],
|
|
52
|
+
fields: [...collection.fields, ...createMfaFields(ctx, options)],
|
|
53
|
+
hooks: {
|
|
54
|
+
...collection.hooks,
|
|
55
|
+
afterError: [
|
|
56
|
+
...(collection.hooks?.afterError || []),
|
|
57
|
+
...(active ? [createMfaAfterErrorHook(ctx)] : []),
|
|
58
|
+
],
|
|
59
|
+
beforeLogin: [
|
|
60
|
+
...(collection.hooks?.beforeLogin || []),
|
|
61
|
+
...(active ? [createMfaBeforeLoginHook(ctx)] : []),
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
});
|
|
66
|
+
if (!active || options.overrideLoginView === false)
|
|
67
|
+
return config;
|
|
68
|
+
config.admin = {
|
|
69
|
+
...config.admin,
|
|
70
|
+
components: {
|
|
71
|
+
...config.admin?.components,
|
|
72
|
+
views: {
|
|
73
|
+
...config.admin?.components?.views,
|
|
74
|
+
login: {
|
|
75
|
+
Component: {
|
|
76
|
+
clientProps: { labels: options.labels, language: options.language },
|
|
77
|
+
exportName: 'MfaLoginView',
|
|
78
|
+
path: 'payload-plugin-mfa/rsc',
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
return config;
|
|
85
|
+
};
|
|
86
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/reset.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Payload } from 'payload';
|
|
2
|
+
export type ResetMfaArgs = {
|
|
3
|
+
/** Collection d'authentification. Par défaut, la collection admin du projet. */
|
|
4
|
+
collection?: string;
|
|
5
|
+
/** Email du compte à réinitialiser. Fournir `email` ou `id`. */
|
|
6
|
+
email?: string;
|
|
7
|
+
/** Nom du groupe de champs, si l'option `fieldName` du plugin a été personnalisée. */
|
|
8
|
+
fieldName?: string;
|
|
9
|
+
id?: number | string;
|
|
10
|
+
payload: Payload;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Désactive le second facteur d'un compte, depuis du code serveur privilégié.
|
|
14
|
+
*
|
|
15
|
+
* Filet de sécurité pour le seul scénario sans issue par l'interface : téléphone perdu *et* codes
|
|
16
|
+
* de secours perdus. Les endpoints `/mfa/*` n'agissent que sur l'utilisateur connecté — personne,
|
|
17
|
+
* pas même un autre administrateur, ne peut désactiver la MFA d'un tiers via l'API. À n'appeler
|
|
18
|
+
* que depuis un script d'administration, jamais depuis une route exposée.
|
|
19
|
+
*/
|
|
20
|
+
export declare function resetMfa({ collection, email, fieldName, id, payload, }: ResetMfaArgs): Promise<{
|
|
21
|
+
id: number | string;
|
|
22
|
+
}>;
|
|
23
|
+
//# sourceMappingURL=reset.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reset.d.ts","sourceRoot":"","sources":["../src/reset.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAKvC,MAAM,MAAM,YAAY,GAAG;IACzB,gFAAgF;IAChF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gEAAgE;IAChE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sFAAsF;IACtF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACrB,OAAO,EAAE,OAAO,CAAC;CAClB,CAAC;AAEF;;;;;;;GAOG;AACH,wBAAsB,QAAQ,CAAC,EAC7B,UAAU,EACV,KAAK,EACL,SAAiB,EACjB,EAAE,EACF,OAAO,GACR,EAAE,YAAY,GAAG,OAAO,CAAC;IAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAAA;CAAE,CAAC,CAuBjD"}
|
package/dist/reset.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { EMPTY_MFA_STATE, writeMfaState } from './state.js';
|
|
2
|
+
/**
|
|
3
|
+
* Désactive le second facteur d'un compte, depuis du code serveur privilégié.
|
|
4
|
+
*
|
|
5
|
+
* Filet de sécurité pour le seul scénario sans issue par l'interface : téléphone perdu *et* codes
|
|
6
|
+
* de secours perdus. Les endpoints `/mfa/*` n'agissent que sur l'utilisateur connecté — personne,
|
|
7
|
+
* pas même un autre administrateur, ne peut désactiver la MFA d'un tiers via l'API. À n'appeler
|
|
8
|
+
* que depuis un script d'administration, jamais depuis une route exposée.
|
|
9
|
+
*/
|
|
10
|
+
export async function resetMfa({ collection, email, fieldName = 'mfa', id, payload, }) {
|
|
11
|
+
const slug = collection ?? payload.config.admin.user;
|
|
12
|
+
let userId = id;
|
|
13
|
+
if (userId === undefined) {
|
|
14
|
+
if (!email)
|
|
15
|
+
throw new Error('resetMfa: il faut fournir `email` ou `id`.');
|
|
16
|
+
const { docs } = await payload.find({
|
|
17
|
+
collection: slug,
|
|
18
|
+
limit: 1,
|
|
19
|
+
overrideAccess: true,
|
|
20
|
+
where: { email: { equals: email } },
|
|
21
|
+
});
|
|
22
|
+
if (!docs[0])
|
|
23
|
+
throw new Error(`resetMfa: aucun utilisateur avec l'email ${email}.`);
|
|
24
|
+
userId = docs[0].id;
|
|
25
|
+
}
|
|
26
|
+
// Seuls `collection` et `fieldName` sont lus par `writeMfaState` pour une remise à zéro : les
|
|
27
|
+
// bornes de verrouillage ne servent qu'au comptage des échecs.
|
|
28
|
+
const ctx = { collection: slug, fieldName };
|
|
29
|
+
await writeMfaState(payload, ctx, userId, EMPTY_MFA_STATE);
|
|
30
|
+
return { id: userId };
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=reset.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reset.js","sourceRoot":"","sources":["../src/reset.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAc5D;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,EAC7B,UAAU,EACV,KAAK,EACL,SAAS,GAAG,KAAK,EACjB,EAAE,EACF,OAAO,GACM;IACb,MAAM,IAAI,GAAG,UAAU,IAAI,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC;IAErD,IAAI,MAAM,GAAG,EAAE,CAAC;IAEhB,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,IAAI,CAAC,KAAK;YAAE,MAAM,IAAI,KAAK,CAAC,4CAA4C,CAAC,CAAC;QAC1E,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC;YAClC,UAAU,EAAE,IAAI;YAChB,KAAK,EAAE,CAAC;YACR,cAAc,EAAE,IAAI;YACpB,KAAK,EAAE,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE;SACpC,CAAC,CAAC;QACH,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,4CAA4C,KAAK,GAAG,CAAC,CAAC;QACpF,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,EAAqB,CAAC;IACzC,CAAC;IAED,8FAA8F;IAC9F,+DAA+D;IAC/D,MAAM,GAAG,GAAG,EAAE,UAAU,EAAE,IAAI,EAAE,SAAS,EAAgB,CAAC;IAC1D,MAAM,aAAa,CAAC,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,eAAe,CAAC,CAAC;IAE3D,OAAO,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC;AACxB,CAAC"}
|
package/dist/state.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Payload, PayloadRequest } from 'payload';
|
|
2
|
+
import type { StoredBackupCode } from './backup-codes.js';
|
|
3
|
+
import type { MfaContext } from './types.js';
|
|
4
|
+
/**
|
|
5
|
+
* État MFA d'un utilisateur, secrets déjà déchiffrés.
|
|
6
|
+
*
|
|
7
|
+
* Ne jamais renvoyer cet objet tel quel dans une réponse HTTP : `secret` et `pendingSecret`
|
|
8
|
+
* suffisent à générer des codes valides.
|
|
9
|
+
*/
|
|
10
|
+
export type MfaState = {
|
|
11
|
+
backupCodes: StoredBackupCode[];
|
|
12
|
+
confirmedAt: null | string;
|
|
13
|
+
enabled: boolean;
|
|
14
|
+
failedAttempts: number;
|
|
15
|
+
lastCounter: null | number;
|
|
16
|
+
lockedUntil: null | string;
|
|
17
|
+
pendingSecret: null | string;
|
|
18
|
+
secret: null | string;
|
|
19
|
+
};
|
|
20
|
+
export declare function readMfaState(payload: Payload, ctx: MfaContext, userId: number | string, req?: PayloadRequest): Promise<MfaState>;
|
|
21
|
+
/**
|
|
22
|
+
* Réécrit le groupe MFA en entier.
|
|
23
|
+
*
|
|
24
|
+
* `req` doit être fourni dès qu'on écrit depuis un hook de `loginOperation` : celle-ci ouvre une
|
|
25
|
+
* transaction et pose un verrou sur la ligne utilisateur (`resetLoginAttempts`). Écrire cette même
|
|
26
|
+
* ligne depuis une transaction distincte attendrait un verrou que seule la fin de la connexion
|
|
27
|
+
* libère — laquelle attend justement cette écriture : la requête reste bloquée indéfiniment.
|
|
28
|
+
*
|
|
29
|
+
* Le corollaire est qu'une écriture faite avec `req` est annulée si l'opération échoue. C'est pour
|
|
30
|
+
* cette raison que le compteur d'échecs est écrit depuis le hook `afterError`, une fois la
|
|
31
|
+
* transaction déjà annulée.
|
|
32
|
+
*/
|
|
33
|
+
export declare function writeMfaState(payload: Payload, ctx: MfaContext, userId: number | string, patch: Partial<MfaState>, req?: PayloadRequest): Promise<MfaState>;
|
|
34
|
+
export declare function isMfaLocked(state: MfaState): boolean;
|
|
35
|
+
/** Incrémente le compteur d'échecs et pose le verrou une fois `maxAttempts` atteint. */
|
|
36
|
+
export declare function registerMfaFailure(payload: Payload, ctx: MfaContext, userId: number | string, state: MfaState): Promise<MfaState>;
|
|
37
|
+
/** Remet l'utilisateur à zéro : plus de secret, plus de codes, plus de verrou. */
|
|
38
|
+
export declare const EMPTY_MFA_STATE: MfaState;
|
|
39
|
+
//# sourceMappingURL=state.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../src/state.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAEvD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C;;;;;GAKG;AACH,MAAM,MAAM,QAAQ,GAAG;IACrB,WAAW,EAAE,gBAAgB,EAAE,CAAC;IAChC,WAAW,EAAE,IAAI,GAAG,MAAM,CAAC;IAC3B,OAAO,EAAE,OAAO,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,IAAI,GAAG,MAAM,CAAC;IAC3B,WAAW,EAAE,IAAI,GAAG,MAAM,CAAC;IAC3B,aAAa,EAAE,IAAI,GAAG,MAAM,CAAC;IAC7B,MAAM,EAAE,IAAI,GAAG,MAAM,CAAC;CACvB,CAAC;AAqCF,wBAAsB,YAAY,CAChC,OAAO,EAAE,OAAO,EAChB,GAAG,EAAE,UAAU,EACf,MAAM,EAAE,MAAM,GAAG,MAAM,EACvB,GAAG,CAAC,EAAE,cAAc,GACnB,OAAO,CAAC,QAAQ,CAAC,CAwBnB;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CACjC,OAAO,EAAE,OAAO,EAChB,GAAG,EAAE,UAAU,EACf,MAAM,EAAE,MAAM,GAAG,MAAM,EACvB,KAAK,EAAE,OAAO,CAAC,QAAQ,CAAC,EACxB,GAAG,CAAC,EAAE,cAAc,GACnB,OAAO,CAAC,QAAQ,CAAC,CAyBnB;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,QAAQ,GAAG,OAAO,CAEpD;AAED,wFAAwF;AACxF,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,OAAO,EAChB,GAAG,EAAE,UAAU,EACf,MAAM,EAAE,MAAM,GAAG,MAAM,EACvB,KAAK,EAAE,QAAQ,GACd,OAAO,CAAC,QAAQ,CAAC,CAQnB;AAED,kFAAkF;AAClF,eAAO,MAAM,eAAe,EAAE,QAS7B,CAAC"}
|
package/dist/state.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
function decryptOrNull(payload, value) {
|
|
2
|
+
if (!value)
|
|
3
|
+
return null;
|
|
4
|
+
try {
|
|
5
|
+
return payload.decrypt(value);
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
// Un secret indéchiffrable (PAYLOAD_SECRET tourné, ligne corrompue) ne doit pas faire échouer
|
|
9
|
+
// la lecture : on le traite comme absent, ce qui désactive de fait le second facteur.
|
|
10
|
+
payload.logger.error('MFA : impossible de déchiffrer un secret, il sera ignoré.');
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function parseBackupCodes(value) {
|
|
15
|
+
if (!Array.isArray(value))
|
|
16
|
+
return [];
|
|
17
|
+
return value.filter((entry) => typeof entry === 'object' &&
|
|
18
|
+
entry !== null &&
|
|
19
|
+
typeof entry.hash === 'string' &&
|
|
20
|
+
typeof entry.salt === 'string');
|
|
21
|
+
}
|
|
22
|
+
export async function readMfaState(payload, ctx, userId, req) {
|
|
23
|
+
const user = await payload.findByID({
|
|
24
|
+
id: userId,
|
|
25
|
+
collection: ctx.collection,
|
|
26
|
+
depth: 0,
|
|
27
|
+
// Les champs MFA sont `hidden` et interdits en lecture par l'access control : seule une lecture
|
|
28
|
+
// privilégiée peut les voir.
|
|
29
|
+
overrideAccess: true,
|
|
30
|
+
req,
|
|
31
|
+
showHiddenFields: true,
|
|
32
|
+
});
|
|
33
|
+
const raw = (user[ctx.fieldName] ?? {});
|
|
34
|
+
return {
|
|
35
|
+
backupCodes: parseBackupCodes(raw.backupCodes),
|
|
36
|
+
confirmedAt: raw.confirmedAt ?? null,
|
|
37
|
+
enabled: raw.enabled === true,
|
|
38
|
+
failedAttempts: raw.failedAttempts ?? 0,
|
|
39
|
+
lastCounter: raw.lastCounter ?? null,
|
|
40
|
+
lockedUntil: raw.lockedUntil ?? null,
|
|
41
|
+
pendingSecret: decryptOrNull(payload, raw.pendingSecret),
|
|
42
|
+
secret: decryptOrNull(payload, raw.secret),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Réécrit le groupe MFA en entier.
|
|
47
|
+
*
|
|
48
|
+
* `req` doit être fourni dès qu'on écrit depuis un hook de `loginOperation` : celle-ci ouvre une
|
|
49
|
+
* transaction et pose un verrou sur la ligne utilisateur (`resetLoginAttempts`). Écrire cette même
|
|
50
|
+
* ligne depuis une transaction distincte attendrait un verrou que seule la fin de la connexion
|
|
51
|
+
* libère — laquelle attend justement cette écriture : la requête reste bloquée indéfiniment.
|
|
52
|
+
*
|
|
53
|
+
* Le corollaire est qu'une écriture faite avec `req` est annulée si l'opération échoue. C'est pour
|
|
54
|
+
* cette raison que le compteur d'échecs est écrit depuis le hook `afterError`, une fois la
|
|
55
|
+
* transaction déjà annulée.
|
|
56
|
+
*/
|
|
57
|
+
export async function writeMfaState(payload, ctx, userId, patch, req) {
|
|
58
|
+
const current = await readMfaState(payload, ctx, userId, req);
|
|
59
|
+
const next = { ...current, ...patch };
|
|
60
|
+
await payload.update({
|
|
61
|
+
id: userId,
|
|
62
|
+
collection: ctx.collection,
|
|
63
|
+
data: {
|
|
64
|
+
[ctx.fieldName]: {
|
|
65
|
+
backupCodes: next.backupCodes,
|
|
66
|
+
confirmedAt: next.confirmedAt,
|
|
67
|
+
enabled: next.enabled,
|
|
68
|
+
failedAttempts: next.failedAttempts,
|
|
69
|
+
lastCounter: next.lastCounter,
|
|
70
|
+
lockedUntil: next.lockedUntil,
|
|
71
|
+
pendingSecret: next.pendingSecret ? payload.encrypt(next.pendingSecret) : null,
|
|
72
|
+
secret: next.secret ? payload.encrypt(next.secret) : null,
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
depth: 0,
|
|
76
|
+
overrideAccess: true,
|
|
77
|
+
req,
|
|
78
|
+
});
|
|
79
|
+
return next;
|
|
80
|
+
}
|
|
81
|
+
export function isMfaLocked(state) {
|
|
82
|
+
return state.lockedUntil !== null && new Date(state.lockedUntil).getTime() > Date.now();
|
|
83
|
+
}
|
|
84
|
+
/** Incrémente le compteur d'échecs et pose le verrou une fois `maxAttempts` atteint. */
|
|
85
|
+
export async function registerMfaFailure(payload, ctx, userId, state) {
|
|
86
|
+
const failedAttempts = state.failedAttempts + 1;
|
|
87
|
+
const locked = failedAttempts >= ctx.maxAttempts;
|
|
88
|
+
return writeMfaState(payload, ctx, userId, {
|
|
89
|
+
failedAttempts: locked ? 0 : failedAttempts,
|
|
90
|
+
lockedUntil: locked ? new Date(Date.now() + ctx.lockDuration).toISOString() : state.lockedUntil,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
/** Remet l'utilisateur à zéro : plus de secret, plus de codes, plus de verrou. */
|
|
94
|
+
export const EMPTY_MFA_STATE = {
|
|
95
|
+
backupCodes: [],
|
|
96
|
+
confirmedAt: null,
|
|
97
|
+
enabled: false,
|
|
98
|
+
failedAttempts: 0,
|
|
99
|
+
lastCounter: null,
|
|
100
|
+
lockedUntil: null,
|
|
101
|
+
pendingSecret: null,
|
|
102
|
+
secret: null,
|
|
103
|
+
};
|
|
104
|
+
//# sourceMappingURL=state.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"state.js","sourceRoot":"","sources":["../src/state.ts"],"names":[],"mappings":"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"}
|
package/dist/totp.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export declare const TOTP_DIGITS = 6;
|
|
2
|
+
export declare const TOTP_PERIOD_SECONDS = 30;
|
|
3
|
+
/**
|
|
4
|
+
* Nombre de pas de 30 s tolérés de part et d'autre du pas courant, pour absorber la dérive
|
|
5
|
+
* d'horloge entre le téléphone et le serveur. 1 = ±30 s, la valeur recommandée par la RFC 6238.
|
|
6
|
+
*/
|
|
7
|
+
export declare const TOTP_WINDOW = 1;
|
|
8
|
+
type TotpIdentity = {
|
|
9
|
+
/** Ce qui s'affiche sous le nom de l'émetteur dans l'app d'authentification. */
|
|
10
|
+
accountLabel: string;
|
|
11
|
+
/** Nom du site, affiché comme entête de l'entrée dans l'app d'authentification. */
|
|
12
|
+
issuer: string;
|
|
13
|
+
};
|
|
14
|
+
/** Génère un secret partagé de 160 bits, encodé en base32 (le format attendu par les apps TOTP). */
|
|
15
|
+
export declare function generateTotpSecret(): string;
|
|
16
|
+
/** URI `otpauth://` à encoder dans le QR code présenté à l'utilisateur lors de l'enrôlement. */
|
|
17
|
+
export declare function buildOtpAuthUri(secret: string, identity: TotpIdentity): string;
|
|
18
|
+
export type TotpVerification = {
|
|
19
|
+
counter: number;
|
|
20
|
+
valid: true;
|
|
21
|
+
} | {
|
|
22
|
+
reason: 'invalid' | 'replayed';
|
|
23
|
+
valid: false;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Vérifie un code à 6 chiffres.
|
|
27
|
+
*
|
|
28
|
+
* `lastCounter` est le pas de temps du dernier code accepté pour ce compte : le rejouer est refusé,
|
|
29
|
+
* sans quoi un code intercepté resterait utilisable pendant toute sa fenêtre de validité (jusqu'à
|
|
30
|
+
* 90 s avec `TOTP_WINDOW = 1`).
|
|
31
|
+
*/
|
|
32
|
+
export declare function verifyTotp({ lastCounter, secret, token, }: {
|
|
33
|
+
lastCounter?: null | number;
|
|
34
|
+
secret: string;
|
|
35
|
+
token: string;
|
|
36
|
+
}): TotpVerification;
|
|
37
|
+
export {};
|
|
38
|
+
//# sourceMappingURL=totp.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"totp.d.ts","sourceRoot":"","sources":["../src/totp.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,WAAW,IAAI,CAAC;AAC7B,eAAO,MAAM,mBAAmB,KAAK,CAAC;AAEtC;;;GAGG;AACH,eAAO,MAAM,WAAW,IAAI,CAAC;AAE7B,KAAK,YAAY,GAAG;IAClB,gFAAgF;IAChF,YAAY,EAAE,MAAM,CAAC;IACrB,mFAAmF;IACnF,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAaF,oGAAoG;AACpG,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAED,gGAAgG;AAChG,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,YAAY,GAAG,MAAM,CAE9E;AAED,MAAM,MAAM,gBAAgB,GAC1B;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,MAAM,EAAE,SAAS,GAAG,UAAU,CAAC;IAAC,KAAK,EAAE,KAAK,CAAA;CAAE,CAAC;AAEtF;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,EACzB,WAAW,EACX,MAAM,EACN,KAAK,GACN,EAAE;IACD,WAAW,CAAC,EAAE,IAAI,GAAG,MAAM,CAAC;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACf,GAAG,gBAAgB,CAenB"}
|