diff --git a/src/@shared/package.json b/src/@shared/package.json index a75a800..67f5258 100644 --- a/src/@shared/package.json +++ b/src/@shared/package.json @@ -9,7 +9,9 @@ "author": "", "license": "ISC", "dependencies": { + "@fastify/cookie": "^11.0.2", "@fastify/jwt": "^9.1.0", + "@sinclair/typebox": "^0.34.40", "@types/bcrypt": "^6.0.0", "bcrypt": "^6.0.0", "better-sqlite3": "^11.10.0", @@ -17,7 +19,6 @@ "fastify-plugin": "^5.0.1", "joi": "^18.0.0", "otp": "^1.1.2", - "rfc4648": "^1.5.4", "uuidv7": "^1.0.2" }, "devDependencies": { diff --git a/src/@shared/src/auth/_inner.ts b/src/@shared/src/auth/_inner.ts deleted file mode 100644 index 72534cd..0000000 --- a/src/@shared/src/auth/_inner.ts +++ /dev/null @@ -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); diff --git a/src/@shared/src/auth/index.ts b/src/@shared/src/auth/index.ts index 95137c3..b78ca38 100644 --- a/src/@shared/src/auth/index.ts +++ b/src/@shared/src/auth/index.ts @@ -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 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 jwtPlugin = fp(async (fastify, _opts) => { let env = process.env.JWT_SECRET; @@ -14,14 +30,38 @@ export const jwtPlugin = fp(async (fastify, _opts) => { secret: env, decode: { complete: false }, }); + void fastify.decorate("signJwt", (kind, who) => fastify.jwt.sign({ kind, who, createdAt: Date.now() })) }); -export type JwtClaims = { - id: user.UserId, -}; +export const JwtType = Type.Object({ + 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; -export const otpPlugin = fp(async (fastify, _opts) => { - fastify.decorate('otp', {}, ["db"]); + +export const authPlugin = fp(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(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 || []),]; + } + }) }) diff --git a/src/auth/src/routes/login.ts b/src/auth/src/routes/login.ts index f33913c..cb035d0 100644 --- a/src/auth/src/routes/login.ts +++ b/src/auth/src/routes/login.ts @@ -63,13 +63,11 @@ const route: FastifyPluginAsync = async (fastify, opts): Promise => { 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" }; diff --git a/src/auth/src/routes/otp.ts b/src/auth/src/routes/otp.ts new file mode 100644 index 0000000..2be2e6d --- /dev/null +++ b/src/auth/src/routes/otp.ts @@ -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; + +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; + +const OTP_TOKEN_TIMEOUT_SEC = 120; + +const route: FastifyPluginAsync = async (fastify, opts): Promise => { + 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(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;