From 8be35e94033d20c35309d8d8b3036dc967e359ab Mon Sep 17 00:00:00 2001 From: eneller Date: Mon, 16 Mar 2026 23:41:13 +0100 Subject: [PATCH] feat: jwt auth --- client/src/app/services/auth-guard.ts | 1 + package-lock.json | 14 +---------- server/.env.example | 7 ++++++ server/package.json | 3 ++- server/src/index.ts | 9 ++++--- server/src/routes/auth.ts | 15 ++++++------ server/src/scripts/keygen.ts | 11 +++++++++ server/src/util/auth.ts | 34 ++++++++++++++++++++------- server/src/util/db.ts | 10 ++++---- server/src/util/logging.ts | 3 --- 10 files changed, 66 insertions(+), 41 deletions(-) create mode 100644 server/.env.example create mode 100644 server/src/scripts/keygen.ts diff --git a/client/src/app/services/auth-guard.ts b/client/src/app/services/auth-guard.ts index a0dad63..e7f6856 100644 --- a/client/src/app/services/auth-guard.ts +++ b/client/src/app/services/auth-guard.ts @@ -7,6 +7,7 @@ export const authGuard: CanActivateFn = (route, state) => { const api = inject(APIService); const router = inject(Router); + //TODO check for cookie return api.isAuthenticated$.pipe( map((isAuthenticated) => { if (isAuthenticated) { diff --git a/package-lock.json b/package-lock.json index 6ba4116..4fbe704 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10465,18 +10465,6 @@ "node": ">= 14.0.0" } }, - "node_modules/ts-jose": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/ts-jose/-/ts-jose-6.2.0.tgz", - "integrity": "sha512-KbuVu70utxPDrfjbPyjEs63GLA6ogBZlr7joyFmO/IvNWjwINa4LoZa5LhG/lp9Y8zb8HEkSTaAOCdnqIeMGqA==", - "license": "MIT", - "dependencies": { - "jose": "6.2.0" - }, - "engines": { - "node": ">=20" - } - }, "node_modules/ts-node": { "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", @@ -11300,12 +11288,12 @@ "cors": "^2.8.6", "dotenv": "^17.3.1", "express": "^5.2.1", + "jose": "^6.2.0", "pg": "^8.20.0", "pg-hstore": "^2.3.4", "reflect-metadata": "^0.2.2", "sequelize": "^6.37.7", "sequelize-typescript": "^2.1.6", - "ts-jose": "^6.2.0", "winston": "^3.19.0" }, "devDependencies": { diff --git a/server/.env.example b/server/.env.example new file mode 100644 index 0000000..b815421 --- /dev/null +++ b/server/.env.example @@ -0,0 +1,7 @@ +FM_PRIVATE_KEY='{"kty":"oct","k":"WoX65SSouJ4c8l_OxwWspo9H5dhPBq9sW0lsOJB6Ygc"}' +NODE_ENV='dev' +FM_DB_HOST +FM_DB_PORT +FM_DB_NAME +FM_DB_USER +FM_DB_PASSWORD \ No newline at end of file diff --git a/server/package.json b/server/package.json index 98cdc05..76431c3 100644 --- a/server/package.json +++ b/server/package.json @@ -14,6 +14,7 @@ "serve": "node ../dist/out-tsc/server/index.js", "setup": "docker compose up -d", "teardown": "docker compose down", + "keygen": "ts-node src/scripts/keygen.ts", "dev": "npm run setup && npm run node; npm run teardown" }, "dependencies": { @@ -21,12 +22,12 @@ "cors": "^2.8.6", "dotenv": "^17.3.1", "express": "^5.2.1", + "jose": "^6.2.0", "pg": "^8.20.0", "pg-hstore": "^2.3.4", "reflect-metadata": "^0.2.2", "sequelize": "^6.37.7", "sequelize-typescript": "^2.1.6", - "ts-jose": "^6.2.0", "winston": "^3.19.0" }, "devDependencies": { diff --git a/server/src/index.ts b/server/src/index.ts index eacf787..a607f78 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -3,12 +3,14 @@ import cors from "cors"; import cookieParser from "cookie-parser"; import transactionsRouter from './routes/transactions'; import authRouter from './routes/auth'; +import dotenv from 'dotenv'; +dotenv.config({quiet: true}); import { db, testConnection } from "./util/db"; import { logger } from "./util/logging"; +import { setKeyFromEnv } from "./util/auth"; const app: Express = express(); -// TODO replace with frontend URL -app.use(cors({ origin: 'http://localhost:4200', credentials: true})); +app.use(cors()); app.use(cookieParser()); app.use(express.json()); @@ -19,7 +21,7 @@ app.get("/api/health", (req: Request, res: Response) => { app.use('/api/transactions', transactionsRouter); app.use('/api/auth', authRouter); -const PORT: number = parseInt(process.env.PORT as string) || 3000; +const PORT: number = parseInt(process.env.FM_PORT as string) || 3000; async function startServer() { await testConnection(); @@ -27,6 +29,7 @@ async function startServer() { // Sync models (use migrations in production!) // Use { force: true } to drop and recreate tables (development only!) await db.sync({ alter: true }); + await setKeyFromEnv(); app.listen(PORT, () => { logger.info(`🚀 Backend Server running on http://localhost:${PORT}`); diff --git a/server/src/routes/auth.ts b/server/src/routes/auth.ts index de76044..335b02b 100644 --- a/server/src/routes/auth.ts +++ b/server/src/routes/auth.ts @@ -1,4 +1,4 @@ -import express, { Request } from 'express'; +import express from 'express'; import { logger } from '../util/logging'; import User from '../model/user'; import { getJWT, checkJWT } from '../util/auth'; @@ -16,12 +16,11 @@ router.post('/login', async (req, res) => { if (!isMatch) return res.status(401).json({ message: 'Invalid credentials' }); // successfully authenticated - // TODO change this for production setup - res.cookie('jwt', getJWT(user), { + let jwt = await getJWT(user); + res.cookie('jwt', jwt, { httpOnly: true, // Prevent XSS - secure: false, // HTTPS only - sameSite: 'lax', // CSRF protection - domain: '.localhost', + secure: process.env.NODE_ENV === 'production',// HTTPS only + sameSite: 'strict', // CSRF protection maxAge: 86400000, // 1 day }); res.json({ message: 'Logged in successfully' }); @@ -36,8 +35,8 @@ router.post('/logout', (req, res) => { res.json({ message: 'Logged out successfully' }); }); -router.get('/status', (req, res) => { - if (checkJWT(req)){ +router.get('/status', async (req, res) => { + if (await checkJWT(req)){ return res.status(200).json({authenticated: true}); } return res.status(401).json({authenticated: false}); diff --git a/server/src/scripts/keygen.ts b/server/src/scripts/keygen.ts new file mode 100644 index 0000000..7a2f49a --- /dev/null +++ b/server/src/scripts/keygen.ts @@ -0,0 +1,11 @@ +import { exportJWK, generateSecret } from "jose" + +async function printSecret(secret){ + const jwk = await exportJWK(secret); + console.log('🔑Generated Secret:'); + console.log(JSON.stringify(jwk)); +} +(async () => { + const secret = await generateSecret('HS256', {extractable: true}); + await printSecret(secret); +})(); diff --git a/server/src/util/auth.ts b/server/src/util/auth.ts index 0a0038a..b2b7c55 100644 --- a/server/src/util/auth.ts +++ b/server/src/util/auth.ts @@ -1,13 +1,31 @@ import { Request } from "express" import User from "../model/user" -import { JWT, JWK } from 'ts-jose'; +import { importJWK, SignJWT, jwtVerify } from "jose"; -const privateKey = process.env.FM_PRIVATE_KEY; -export function checkJWT(req: Request){ - // TODO check JWT - return req.cookies.jwt + +let key; + +async function setKeyFromEnv() { + key = await importJWK(JSON.parse(process.env.FM_PRIVATE_KEY)); } -export function getJWT(user: User){ - return 'toekn' -} \ No newline at end of file +async function checkJWT(req: Request){ + try { + let jwt= await jwtVerify(req.cookies.jwt, key); + const user = await User.findOne({where: { userID: jwt.payload.sub}}); + return user + } catch (error) { + return null + } +} + +async function getJWT(user: User){ + let jwt = await new SignJWT() + .setSubject(user.userID) + .setProtectedHeader({ alg: 'HS256' }) + .setIssuedAt() + .sign(key); + + return jwt +} +export {getJWT, checkJWT, setKeyFromEnv} \ No newline at end of file diff --git a/server/src/util/db.ts b/server/src/util/db.ts index d92d16c..0df2c06 100644 --- a/server/src/util/db.ts +++ b/server/src/util/db.ts @@ -6,11 +6,11 @@ import Transaction from '../model/transaction'; // Initialize Sequelize const db = new Sequelize({ dialect: 'postgres', - host: process.env.DB_HOST || 'localhost', - port: parseInt(process.env.DB_PORT || '5432'), - database: process.env.DB_NAME || 'postgres', - username: process.env.DB_USER || 'postgres', - password: process.env.DB_PASSWORD || 'pass', + host: process.env.FM_DB_HOST || 'localhost', + port: parseInt(process.env.FM_DB_PORT || '5432'), + database: process.env.FM_DB_NAME || 'postgres', + username: process.env.FM_DB_USER || 'postgres', + password: process.env.FM_DB_PASSWORD || 'pass', logging: logger.debug.bind(logger), }); diff --git a/server/src/util/logging.ts b/server/src/util/logging.ts index 4d2835a..75ecdab 100644 --- a/server/src/util/logging.ts +++ b/server/src/util/logging.ts @@ -1,7 +1,4 @@ import winston, { format } from "winston"; -import dotenv from 'dotenv'; - -dotenv.config(); const logger = winston.createLogger({ level:'info', })