feat(auth): working plugin
This commit is contained in:
parent
c545499c73
commit
ddde700494
5 changed files with 140 additions and 31 deletions
|
|
@ -9,7 +9,9 @@
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@fastify/cookie": "^11.0.2",
|
||||||
"@fastify/jwt": "^9.1.0",
|
"@fastify/jwt": "^9.1.0",
|
||||||
|
"@sinclair/typebox": "^0.34.40",
|
||||||
"@types/bcrypt": "^6.0.0",
|
"@types/bcrypt": "^6.0.0",
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
"better-sqlite3": "^11.10.0",
|
"better-sqlite3": "^11.10.0",
|
||||||
|
|
@ -17,7 +19,6 @@
|
||||||
"fastify-plugin": "^5.0.1",
|
"fastify-plugin": "^5.0.1",
|
||||||
"joi": "^18.0.0",
|
"joi": "^18.0.0",
|
||||||
"otp": "^1.1.2",
|
"otp": "^1.1.2",
|
||||||
"rfc4648": "^1.5.4",
|
|
||||||
"uuidv7": "^1.0.2"
|
"uuidv7": "^1.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
//! Anything in this file shouldn't be used...
|
|
||||||
//!
|
|
||||||
//! This file is here because it is easier to share code in here.
|
|
||||||
|
|
||||||
import type { Database } from "@shared/database";
|
|
||||||
import type { UserId } from "../database/mixin/user";
|
|
||||||
import OTP, * as OtpModules from "otp";
|
|
||||||
|
|
||||||
let secret = "JKZSGXRCBP3UHOFVDVLYQ3W43IZH3D76";
|
|
||||||
let otp = new OTP({ secret, name: "test" });
|
|
||||||
|
|
||||||
console.log(`${otp.totpURL}`);
|
|
||||||
console.log(new URL(otp.totpURL));
|
|
||||||
setInterval(() => {
|
|
||||||
console.log(`${otp.totp(Date.now())}`);
|
|
||||||
}, 999);
|
|
||||||
|
|
@ -1,10 +1,26 @@
|
||||||
import fastifyJwt from "@fastify/jwt";
|
|
||||||
import { FastifyPluginAsync } from "fastify";
|
|
||||||
import fp from 'fastify-plugin'
|
|
||||||
import { user } from "@shared/database"
|
|
||||||
import OTP from "otp";
|
import OTP from "otp";
|
||||||
|
import fastifyJwt from "@fastify/jwt";
|
||||||
|
import fp from 'fastify-plugin'
|
||||||
|
import { FastifyPluginAsync, FastifyRequest } from "fastify";
|
||||||
|
import { Static, Type } from "@sinclair/typebox"
|
||||||
|
import { useDatabase, user } from "@shared/database"
|
||||||
|
import cookie from "@fastify/cookie";
|
||||||
|
|
||||||
|
|
||||||
|
const kRouteAuthDone = Symbol('shared-route-auth-done');
|
||||||
|
|
||||||
|
declare module 'fastify' {
|
||||||
|
export interface FastifyInstance {
|
||||||
|
signJwt: (kind: "auth" | "otp", who: string) => string;
|
||||||
|
}
|
||||||
|
export interface FastifyRequest {
|
||||||
|
authUser?: user.UserId;
|
||||||
|
}
|
||||||
|
export interface FastifyContextConfig {
|
||||||
|
requireAuth?: boolean,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const Otp = OTP;
|
export const Otp = OTP;
|
||||||
export const jwtPlugin = fp<FastifyPluginAsync>(async (fastify, _opts) => {
|
export const jwtPlugin = fp<FastifyPluginAsync>(async (fastify, _opts) => {
|
||||||
let env = process.env.JWT_SECRET;
|
let env = process.env.JWT_SECRET;
|
||||||
|
|
@ -14,14 +30,38 @@ export const jwtPlugin = fp<FastifyPluginAsync>(async (fastify, _opts) => {
|
||||||
secret: env,
|
secret: env,
|
||||||
decode: { complete: false },
|
decode: { complete: false },
|
||||||
});
|
});
|
||||||
|
void fastify.decorate("signJwt", (kind, who) => fastify.jwt.sign({ kind, who, createdAt: Date.now() }))
|
||||||
});
|
});
|
||||||
|
|
||||||
export type JwtClaims = {
|
export const JwtType = Type.Object({
|
||||||
id: user.UserId,
|
kind: Type.Union([
|
||||||
};
|
Type.Const("otp", { description: "the token is only valid for otp call" }),
|
||||||
|
Type.Const("auth", { description: "the token is valid for authentication" })
|
||||||
|
]),
|
||||||
|
who: Type.String({ description: "the login of the user" }),
|
||||||
|
createdAt: Type.Integer({ description: "Unix timestamp of when the token as been created at" })
|
||||||
|
});
|
||||||
|
|
||||||
export * as _inner from "./_inner.js";
|
export type JwtType = Static<typeof JwtType>;
|
||||||
|
|
||||||
export const otpPlugin = fp<FastifyPluginAsync>(async (fastify, _opts) => {
|
|
||||||
fastify.decorate('otp', {}, ["db"]);
|
export const authPlugin = fp<FastifyPluginAsync>(async (fastify, _opts) => {
|
||||||
|
await fastify.register(useDatabase as any, {});
|
||||||
|
await fastify.register(jwtPlugin as any, {});
|
||||||
|
await fastify.register(cookie);
|
||||||
|
fastify.addHook('onRoute', (routeOpts) => {
|
||||||
|
if (routeOpts.config?.requireAuth) {
|
||||||
|
routeOpts.preValidation = [function(req, res) {
|
||||||
|
if (req.cookies.token === undefined)
|
||||||
|
return res.clearCookie("token").send({ kind: "notLoggedIn", msg_key: "" })
|
||||||
|
let tok = this.jwt.verify<JwtType>(req.cookies.token);
|
||||||
|
if (tok.kind != "auth")
|
||||||
|
return res.clearCookie("token").send({ kind: "notLoggedIn", msg_key: "" })
|
||||||
|
let user = this.db.getUserFromName(tok.who);
|
||||||
|
if (user === null)
|
||||||
|
return res.clearCookie("token").send({ kind: "notLoggedIn", msg_key: "" })
|
||||||
|
req.authUser = user.id;
|
||||||
|
}, ...(routeOpts.preValidation as any || []),];
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -63,13 +63,11 @@ const route: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
|
||||||
if (user.otp !== null) {
|
if (user.otp !== null) {
|
||||||
// yes -> we ask them to fill it,
|
// yes -> we ask them to fill it,
|
||||||
// send them somehting to verify that they indeed passed throught the user+password phase
|
// 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: this.signJwt("otp", user.name) };
|
||||||
return { kind: "otpRequired", msg_key: "login.otpRequired", token: otpToken };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// every check has been passed, they are now logged in, using this token to say who they are...
|
// 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: this.signJwt("auth", user.name) }
|
||||||
return { kind: "success", msg_key: "login.success", token: userToken }
|
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
return { kind: "failed", msg_key: "login.failed.generic" };
|
return { kind: "failed", msg_key: "login.failed.generic" };
|
||||||
|
|
|
||||||
86
src/auth/src/routes/otp.ts
Normal file
86
src/auth/src/routes/otp.ts
Normal 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;
|
||||||
Loading…
Add table
Add a link
Reference in a new issue