feat: jwt auth
This commit is contained in:
@@ -7,6 +7,7 @@ export const authGuard: CanActivateFn = (route, state) => {
|
|||||||
const api = inject(APIService);
|
const api = inject(APIService);
|
||||||
const router = inject(Router);
|
const router = inject(Router);
|
||||||
|
|
||||||
|
//TODO check for cookie
|
||||||
return api.isAuthenticated$.pipe(
|
return api.isAuthenticated$.pipe(
|
||||||
map((isAuthenticated) => {
|
map((isAuthenticated) => {
|
||||||
if (isAuthenticated) {
|
if (isAuthenticated) {
|
||||||
|
|||||||
14
package-lock.json
generated
14
package-lock.json
generated
@@ -10465,18 +10465,6 @@
|
|||||||
"node": ">= 14.0.0"
|
"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": {
|
"node_modules/ts-node": {
|
||||||
"version": "10.9.2",
|
"version": "10.9.2",
|
||||||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
|
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
|
||||||
@@ -11300,12 +11288,12 @@
|
|||||||
"cors": "^2.8.6",
|
"cors": "^2.8.6",
|
||||||
"dotenv": "^17.3.1",
|
"dotenv": "^17.3.1",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
|
"jose": "^6.2.0",
|
||||||
"pg": "^8.20.0",
|
"pg": "^8.20.0",
|
||||||
"pg-hstore": "^2.3.4",
|
"pg-hstore": "^2.3.4",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"sequelize": "^6.37.7",
|
"sequelize": "^6.37.7",
|
||||||
"sequelize-typescript": "^2.1.6",
|
"sequelize-typescript": "^2.1.6",
|
||||||
"ts-jose": "^6.2.0",
|
|
||||||
"winston": "^3.19.0"
|
"winston": "^3.19.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
7
server/.env.example
Normal file
7
server/.env.example
Normal 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
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
"serve": "node ../dist/out-tsc/server/index.js",
|
"serve": "node ../dist/out-tsc/server/index.js",
|
||||||
"setup": "docker compose up -d",
|
"setup": "docker compose up -d",
|
||||||
"teardown": "docker compose down",
|
"teardown": "docker compose down",
|
||||||
|
"keygen": "ts-node src/scripts/keygen.ts",
|
||||||
"dev": "npm run setup && npm run node; npm run teardown"
|
"dev": "npm run setup && npm run node; npm run teardown"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -21,12 +22,12 @@
|
|||||||
"cors": "^2.8.6",
|
"cors": "^2.8.6",
|
||||||
"dotenv": "^17.3.1",
|
"dotenv": "^17.3.1",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
|
"jose": "^6.2.0",
|
||||||
"pg": "^8.20.0",
|
"pg": "^8.20.0",
|
||||||
"pg-hstore": "^2.3.4",
|
"pg-hstore": "^2.3.4",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"sequelize": "^6.37.7",
|
"sequelize": "^6.37.7",
|
||||||
"sequelize-typescript": "^2.1.6",
|
"sequelize-typescript": "^2.1.6",
|
||||||
"ts-jose": "^6.2.0",
|
|
||||||
"winston": "^3.19.0"
|
"winston": "^3.19.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -3,12 +3,14 @@ import cors from "cors";
|
|||||||
import cookieParser from "cookie-parser";
|
import cookieParser from "cookie-parser";
|
||||||
import transactionsRouter from './routes/transactions';
|
import transactionsRouter from './routes/transactions';
|
||||||
import authRouter from './routes/auth';
|
import authRouter from './routes/auth';
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
dotenv.config({quiet: true});
|
||||||
import { db, testConnection } from "./util/db";
|
import { db, testConnection } from "./util/db";
|
||||||
import { logger } from "./util/logging";
|
import { logger } from "./util/logging";
|
||||||
|
import { setKeyFromEnv } from "./util/auth";
|
||||||
|
|
||||||
const app: Express = express();
|
const app: Express = express();
|
||||||
// TODO replace with frontend URL
|
app.use(cors());
|
||||||
app.use(cors({ origin: 'http://localhost:4200', credentials: true}));
|
|
||||||
app.use(cookieParser());
|
app.use(cookieParser());
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
|
||||||
@@ -19,7 +21,7 @@ app.get("/api/health", (req: Request, res: Response) => {
|
|||||||
app.use('/api/transactions', transactionsRouter);
|
app.use('/api/transactions', transactionsRouter);
|
||||||
app.use('/api/auth', authRouter);
|
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() {
|
async function startServer() {
|
||||||
await testConnection();
|
await testConnection();
|
||||||
@@ -27,6 +29,7 @@ async function startServer() {
|
|||||||
// Sync models (use migrations in production!)
|
// Sync models (use migrations in production!)
|
||||||
// Use { force: true } to drop and recreate tables (development only!)
|
// Use { force: true } to drop and recreate tables (development only!)
|
||||||
await db.sync({ alter: true });
|
await db.sync({ alter: true });
|
||||||
|
await setKeyFromEnv();
|
||||||
|
|
||||||
app.listen(PORT, () => {
|
app.listen(PORT, () => {
|
||||||
logger.info(`🚀 Backend Server running on http://localhost:${PORT}`);
|
logger.info(`🚀 Backend Server running on http://localhost:${PORT}`);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import express, { Request } from 'express';
|
import express from 'express';
|
||||||
import { logger } from '../util/logging';
|
import { logger } from '../util/logging';
|
||||||
import User from '../model/user';
|
import User from '../model/user';
|
||||||
import { getJWT, checkJWT } from '../util/auth';
|
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' });
|
if (!isMatch) return res.status(401).json({ message: 'Invalid credentials' });
|
||||||
|
|
||||||
// successfully authenticated
|
// successfully authenticated
|
||||||
// TODO change this for production setup
|
let jwt = await getJWT(user);
|
||||||
res.cookie('jwt', getJWT(user), {
|
res.cookie('jwt', jwt, {
|
||||||
httpOnly: true, // Prevent XSS
|
httpOnly: true, // Prevent XSS
|
||||||
secure: false, // HTTPS only
|
secure: process.env.NODE_ENV === 'production',// HTTPS only
|
||||||
sameSite: 'lax', // CSRF protection
|
sameSite: 'strict', // CSRF protection
|
||||||
domain: '.localhost',
|
|
||||||
maxAge: 86400000, // 1 day
|
maxAge: 86400000, // 1 day
|
||||||
});
|
});
|
||||||
res.json({ message: 'Logged in successfully' });
|
res.json({ message: 'Logged in successfully' });
|
||||||
@@ -36,8 +35,8 @@ router.post('/logout', (req, res) => {
|
|||||||
res.json({ message: 'Logged out successfully' });
|
res.json({ message: 'Logged out successfully' });
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get('/status', (req, res) => {
|
router.get('/status', async (req, res) => {
|
||||||
if (checkJWT(req)){
|
if (await checkJWT(req)){
|
||||||
return res.status(200).json({authenticated: true});
|
return res.status(200).json({authenticated: true});
|
||||||
}
|
}
|
||||||
return res.status(401).json({authenticated: false});
|
return res.status(401).json({authenticated: false});
|
||||||
|
|||||||
11
server/src/scripts/keygen.ts
Normal file
11
server/src/scripts/keygen.ts
Normal 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);
|
||||||
|
})();
|
||||||
@@ -1,13 +1,31 @@
|
|||||||
import { Request } from "express"
|
import { Request } from "express"
|
||||||
import User from "../model/user"
|
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){
|
let key;
|
||||||
// TODO check JWT
|
|
||||||
return req.cookies.jwt
|
async function setKeyFromEnv() {
|
||||||
|
key = await importJWK(JSON.parse(process.env.FM_PRIVATE_KEY));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getJWT(user: User){
|
async function checkJWT(req: Request){
|
||||||
return 'toekn'
|
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}
|
||||||
@@ -6,11 +6,11 @@ import Transaction from '../model/transaction';
|
|||||||
// Initialize Sequelize
|
// Initialize Sequelize
|
||||||
const db = new Sequelize({
|
const db = new Sequelize({
|
||||||
dialect: 'postgres',
|
dialect: 'postgres',
|
||||||
host: process.env.DB_HOST || 'localhost',
|
host: process.env.FM_DB_HOST || 'localhost',
|
||||||
port: parseInt(process.env.DB_PORT || '5432'),
|
port: parseInt(process.env.FM_DB_PORT || '5432'),
|
||||||
database: process.env.DB_NAME || 'postgres',
|
database: process.env.FM_DB_NAME || 'postgres',
|
||||||
username: process.env.DB_USER || 'postgres',
|
username: process.env.FM_DB_USER || 'postgres',
|
||||||
password: process.env.DB_PASSWORD || 'pass',
|
password: process.env.FM_DB_PASSWORD || 'pass',
|
||||||
logging: logger.debug.bind(logger),
|
logging: logger.debug.bind(logger),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
import winston, { format } from "winston";
|
import winston, { format } from "winston";
|
||||||
import dotenv from 'dotenv';
|
|
||||||
|
|
||||||
dotenv.config();
|
|
||||||
const logger = winston.createLogger({
|
const logger = winston.createLogger({
|
||||||
level:'info',
|
level:'info',
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user