feat(auth): working plugin

This commit is contained in:
Maieul BOYER 2025-08-27 22:25:44 +02:00 committed by Maix0
parent c545499c73
commit ddde700494
5 changed files with 140 additions and 31 deletions

View file

@ -63,13 +63,11 @@ const route: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
if (user.otp !== null) {
// yes -> we ask them to fill it,
// send them somehting to verify that they indeed passed throught the user+password phase
let otpToken = this.jwt.sign({ kind: "otp", user: user.name, createAt: Date.now() / 1000 });
return { kind: "otpRequired", msg_key: "login.otpRequired", token: otpToken };
return { kind: "otpRequired", msg_key: "login.otpRequired", token: this.signJwt("otp", user.name) };
}
// every check has been passed, they are now logged in, using this token to say who they are...
let userToken = this.jwt.sign({ kind: "auth", user: user.name, createAt: Date.now() / 1000 });
return { kind: "success", msg_key: "login.success", token: userToken }
return { kind: "success", msg_key: "login.success", token: this.signJwt("auth", user.name) }
}
catch {
return { kind: "failed", msg_key: "login.failed.generic" };

View file

@ -0,0 +1,86 @@
import { FastifyPluginAsync } from "fastify";
import { Static, Type } from "@sinclair/typebox";
import { JwtType, Otp } from "@shared/auth";
export const OtpReq = Type.Object({
token: Type.String({ description: "The token given at the login phase" }),
code: Type.String({ description: "The OTP given by the user" }),
});
export type OtpReq = Static<typeof OtpReq>;
export const OtpRes = Type.Union([
Type.Object({
kind: Type.Const("failed"),
msg_key: Type.Union([
Type.Const("otp.failed.generic"),
Type.Const("otp.failed.invalid"),
Type.Const("otp.failed.timeout"),
]),
}),
Type.Object({
kind: Type.Const("success"),
msg_key: Type.Const("otp.success"),
token: Type.String({ description: "The JWT token" }),
}),
]);
export type OtpRes = Static<typeof OtpRes>;
const OTP_TOKEN_TIMEOUT_SEC = 120;
const route: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
fastify.get<{ Body: OtpReq }>(
"/whoami",
{ schema: { body: OtpReq, response: { "2xx": OtpRes } } },
async function(req, res) {
try {
const { token, code } = req.body;
// lets try to decode+verify the jwt
let dJwt = this.jwt.verify<JwtType>(token);
// is the jwt a valid `otp` jwt ?
if (dJwt.kind != "otp")
// no ? fuck off then
return { kind: "failed", msg_key: "otp.failed.invalid" };
// is it too old ?
if (dJwt.createdAt + OTP_TOKEN_TIMEOUT_SEC * 1000 > Date.now())
// yes ? fuck off then, redo the password
return { kind: "failed", msg_key: "otp.failed.timeout" };
// get the Otp sercret from the db
let otpSecret = this.db.getUserFromName(dJwt.who)?.otp;
if (otpSecret === null)
// oops, either no user, or user without otpSecret
// fuck off
return { kind: "failed", msg_key: "otp.failed.invalid" };
// good lets now verify the token you gave us is the correct one...
let otpHandle = new Otp({ secret: otpSecret });
let now = Date.now();
const tokens = [
// we also get the last code, to mitiage the delay between client<->server roundtrip...
otpHandle.totp(now - 30 * 1000),
// this is the current token :)
otpHandle.totp(now),
];
// checking if any of the array match
if (tokens.some((c) => c === code))
// they do !
// gg you are now logged in !
return {
kind: "success",
msg_key: "otp.success",
token: this.signJwt("auth", dJwt.who),
};
} catch {
return { kind: "failed", msg_key: "otp.failed.generic" };
}
},
);
};
export default route;