feat: auth surface exposée + ChatService partagé (v0.19.0)
Auth login fort : mot de passe scrypt + TOTP 2FA (otplib) + JWT HS256 (jose), login tout-ou-rien sans indice. ChatService factorise le tour d'agent pour toutes les surfaces (Telegram refactoré). 60 tests, 0 vuln. Palier de risque : reversible (logique d'auth ; surface API câblée en v0.20). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { generateSecret, generateSync } from "otplib";
|
||||
import {
|
||||
hashPassword,
|
||||
verifyPassword,
|
||||
verifyTotp,
|
||||
issueJwt,
|
||||
verifyJwt,
|
||||
login,
|
||||
type AuthConfig,
|
||||
} from "../src/api/auth.js";
|
||||
|
||||
describe("password (scrypt)", () => {
|
||||
it("vérifie un mot de passe correct et rejette un faux", () => {
|
||||
const h = hashPassword("s3cr3t!");
|
||||
expect(verifyPassword("s3cr3t!", h)).toBe(true);
|
||||
expect(verifyPassword("wrong", h)).toBe(false);
|
||||
});
|
||||
|
||||
it("rejette un hash malformé", () => {
|
||||
expect(verifyPassword("x", "pasunhash")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("TOTP", () => {
|
||||
it("valide un code généré pour le secret", () => {
|
||||
const secret = generateSecret();
|
||||
const code = generateSync({ secret: secret });
|
||||
expect(verifyTotp(code, secret)).toBe(true);
|
||||
expect(verifyTotp("000000", secret)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
const cfg = (): AuthConfig => ({
|
||||
adminUser: "kantin",
|
||||
adminPasswordHash: hashPassword("pw"),
|
||||
totpSecret: generateSecret(),
|
||||
jwtSecret: "a-very-long-jwt-secret-value-32+chars",
|
||||
});
|
||||
|
||||
describe("JWT", () => {
|
||||
it("émet et vérifie un JWT", async () => {
|
||||
const c = cfg();
|
||||
const token = await issueJwt(c);
|
||||
expect(await verifyJwt(token, c.jwtSecret)).toBe("kantin");
|
||||
});
|
||||
|
||||
it("rejette un JWT avec mauvais secret", async () => {
|
||||
const c = cfg();
|
||||
const token = await issueJwt(c);
|
||||
expect(await verifyJwt(token, "autre-secret-completement-different")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("login (mdp + TOTP, tout-ou-rien)", () => {
|
||||
it("réussit avec les bons facteurs", async () => {
|
||||
const c = cfg();
|
||||
const token = await login(c, {
|
||||
user: "kantin",
|
||||
password: "pw",
|
||||
totp: generateSync({ secret: c.totpSecret }),
|
||||
});
|
||||
expect(token).not.toBeNull();
|
||||
});
|
||||
|
||||
it("échoue si un seul facteur est faux", async () => {
|
||||
const c = cfg();
|
||||
const good = generateSync({ secret: c.totpSecret });
|
||||
expect(await login(c, { user: "kantin", password: "bad", totp: good })).toBeNull();
|
||||
expect(await login(c, { user: "kantin", password: "pw", totp: "000000" })).toBeNull();
|
||||
expect(await login(c, { user: "intrus", password: "pw", totp: good })).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user