Added basic Auth-Service features
This commit is contained in:
25
auth/src/core/jwt.js
Normal file
25
auth/src/core/jwt.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
/* The secret used for signing and verifying tokens */
|
||||
const secret = process.env.AUTH_TOKEN;
|
||||
const cookieName = process.env.COOKIE_NAME;
|
||||
|
||||
//expires in 1 year
|
||||
const ttl_seconds = 60 * 60 * 24 * 365;
|
||||
|
||||
export function sign(payload) {
|
||||
return jwt.sign(payload, secret, { expiresIn: ttl_seconds });
|
||||
}
|
||||
|
||||
export async function verify(token) {
|
||||
return jwt.verify(token, secret);
|
||||
}
|
||||
|
||||
export const cookieOptions = {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV,
|
||||
sameSite: 'lax',
|
||||
path: '/',
|
||||
maxAge: ttl_seconds * 1000,
|
||||
};
|
||||
|
||||
9
auth/src/core/securitycore.js
Normal file
9
auth/src/core/securitycore.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import bcrypt from "bcrypt";
|
||||
|
||||
export async function hash(password) {
|
||||
return bcrypt.hash(password, 12);
|
||||
}
|
||||
|
||||
export async function verify(password, hash) {
|
||||
return bcrypt.compare(password, hash);
|
||||
}
|
||||
13
auth/src/data/db.js
Normal file
13
auth/src/data/db.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import pg from 'pg';
|
||||
export const pool = new pg.Pool({
|
||||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD,
|
||||
host: process.env.DB_HOST,
|
||||
port: process.env.DB_PORT,
|
||||
database: 'data', //default database for user and sessions. Not other database needed,
|
||||
max: 10,
|
||||
idleTimeoutMillis: 30_000
|
||||
});
|
||||
|
||||
|
||||
export const query = (text, params) => pool.query(text, params);
|
||||
9
auth/src/data/redis.js
Normal file
9
auth/src/data/redis.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import Redis from 'ioredis';
|
||||
|
||||
const client = new Redis({
|
||||
host: process.env.REDIS_HOST,
|
||||
port: process.env.REDIS_PORT,
|
||||
password: process.env.REDIS_PASSWORD,
|
||||
});
|
||||
|
||||
export { client as redis };
|
||||
35
auth/src/index.js
Normal file
35
auth/src/index.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import express from 'express';
|
||||
import cookieParser from 'cookie-parser';
|
||||
|
||||
import { authRouter } from './routes/auth.js';
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use(cookieParser());
|
||||
|
||||
app.get('/health', (req, res) => {
|
||||
res.send({
|
||||
status: 'ok',
|
||||
service: 'auth',
|
||||
version: {
|
||||
'major': process.env.V_MAJOR,
|
||||
'minor': process.env.V_MINOR,
|
||||
'patch': process.env.V_PATCH,
|
||||
},
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
});
|
||||
|
||||
// Public web pages
|
||||
// app.use('/login', authRouter);
|
||||
// app.use('/profile', profileRouter);
|
||||
// app.use('/profile/sessions', sessionRouter);
|
||||
|
||||
app.use('/api', authRouter);
|
||||
// app.use('/api/users', usersRouter);
|
||||
// app.use('/api/sessions', sessionRouter);
|
||||
//
|
||||
|
||||
app.listen('3000', '0.0.0.0', () => {
|
||||
console.log('Auth started');
|
||||
})
|
||||
35
auth/src/middleware/auth.js
Normal file
35
auth/src/middleware/auth.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import { verify } from "../core/jwt";
|
||||
import { query } from "../data/db";
|
||||
import { redis } from '../data/redis';
|
||||
|
||||
const cookieName = process.env.COOKIE_NAME;
|
||||
|
||||
export async function requireUserAuth(req, res, next) {
|
||||
const token = req.cookies?.[cookieName];
|
||||
if (!token) return res.status(401).json({ message: 'No token' });
|
||||
|
||||
let payload;
|
||||
try {
|
||||
payload = await verify(token);
|
||||
} catch (error) {
|
||||
return res.status(401).json({ message: 'Invalid token' });
|
||||
}
|
||||
|
||||
// Session
|
||||
const { rows } = await query(
|
||||
'select id, user_id, expires_at from sessions where id = $1',
|
||||
[payload.sessionId]
|
||||
);
|
||||
if (!rows[0]) return res.status(401).json({ message: 'Invalid session' });
|
||||
|
||||
await query('update sessions set last_activity = now() where id = $1', [payload.sessionId]).catch(() => { });
|
||||
redis.set(`onlineuser:${payload.sub}`, '1', 'EX', 60).catch(() => { });
|
||||
|
||||
req.user = {
|
||||
id: payload.sub,
|
||||
name: payload.sessionId,
|
||||
}
|
||||
next();
|
||||
|
||||
|
||||
}
|
||||
12
auth/src/pages/login.html
Normal file
12
auth/src/pages/login.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Ciao</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Ciao</h1>
|
||||
<form>
|
||||
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
70
auth/src/routes/auth.js
Normal file
70
auth/src/routes/auth.js
Normal file
@@ -0,0 +1,70 @@
|
||||
import { Router } from "express";
|
||||
import { query } from "../data/db.js";
|
||||
import { hash, verify } from "../core/securitycore.js";
|
||||
import { sign, cookieOptions } from "../core/jwt.js";
|
||||
import crypto from "crypto";
|
||||
import {redis} from "../data/redis.js";
|
||||
|
||||
const router = Router();
|
||||
const cookieName = process.env.COOKIE_NAME
|
||||
|
||||
router.post('/signup', async (req, res) => {
|
||||
const { username, password } = req.body;
|
||||
if (!username || !password) {
|
||||
return res.status(400).json({ message: 'Username and password are required' });
|
||||
}
|
||||
|
||||
const exist = await query('select 1 from users where username = $1', [username]);
|
||||
if (exist.rows.length > 0) {
|
||||
return res.status(400).json({ message: 'Username already exists' });
|
||||
}
|
||||
|
||||
const passwordHash = await hash(password);
|
||||
//TODO: Remove username sending from resposne in prod
|
||||
const { rows } = await query('insert into users (username, password_hash) values ($1, $2) returning id, username', [username, passwordHash]);
|
||||
res.status(201).json(rows[0]);
|
||||
});
|
||||
|
||||
router.post('/login', async (req, res) => {
|
||||
const { username, password } = req.body;
|
||||
if (!username || !password) {
|
||||
return res.status(400).json({ message: 'Username and password are required' });
|
||||
}
|
||||
|
||||
const { rows } = await query('select id, username, password_hash from users where username = $1', [username]);
|
||||
const user = rows[0];
|
||||
const ok = user ? await verify(password, user.password_hash) : false;
|
||||
if (!ok) {
|
||||
return res.status(400).json({ message: 'Invalid username or password' });
|
||||
}
|
||||
|
||||
const ua = req.headers['user-agent'];
|
||||
const ip = req.headers['x-forwarded-for']?.split(',')[0]?.trim() ?? req.socket.remoteAddress;
|
||||
|
||||
const sessionToken = crypto.randomUUID();
|
||||
const ttlDays = 360;
|
||||
|
||||
const { rows: srow } = await query('insert into sessions (user_id, session_token, device_name, device_os, ip_address, expires_at) values ($1, $2, $3, $4, $5, $6) returning id', [user.id, sessionToken, ua.slice(0, 100), '', ip?.slice(0, 45), ttlDays]);
|
||||
const session_id = srow[0].id;
|
||||
const jtoken = sign({ sub: user.id, session_id });
|
||||
|
||||
await redis.set(`usersession:${session_id}`, user.id, 'EX', ttlDays * 24 * 3600);
|
||||
await redis.set(`online:${user.id}`, '1', 'EX', 60);
|
||||
|
||||
res.cookie(cookieName, jtoken, cookieOptions);
|
||||
res.json({
|
||||
ok: true,
|
||||
user: user.id,
|
||||
session: session_id
|
||||
});
|
||||
|
||||
})
|
||||
|
||||
router.post('/logout', async (req, res) => {
|
||||
await query('delete from sessions where id = $1', [req.user.sessionID]);
|
||||
await redis.del(`online:${req.user.id}`);
|
||||
res.clearCookie(cookieName);
|
||||
res.json({ loggedOut: true });
|
||||
})
|
||||
|
||||
export { router as authRouter };
|
||||
Reference in New Issue
Block a user