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": "",
|
||||
"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": {
|
||||
|
|
|
|||
|
|
@ -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 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<FastifyPluginAsync>(async (fastify, _opts) => {
|
||||
let env = process.env.JWT_SECRET;
|
||||
|
|
@ -14,14 +30,38 @@ export const jwtPlugin = fp<FastifyPluginAsync>(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<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) {
|
||||
// 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" };
|
||||
|
|
|
|||
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