feat: jwt auth

This commit is contained in:
eneller
2026-03-16 23:41:13 +01:00
parent fa2203927a
commit 8be35e9403
10 changed files with 66 additions and 41 deletions

View File

@@ -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) {

14
package-lock.json generated
View File

@@ -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": {

7
server/.env.example Normal file
View File

@@ -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

View File

@@ -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": {

View File

@@ -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}`);

View File

@@ -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});

View File

@@ -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);
})();

View File

@@ -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'
}
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}

View File

@@ -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),
});

View File

@@ -1,7 +1,4 @@
import winston, { format } from "winston";
import dotenv from 'dotenv';
dotenv.config();
const logger = winston.createLogger({
level:'info',
})