implement auth
This commit is contained in:
parent
96761cb911
commit
1434756b10
12 changed files with 130 additions and 13 deletions
40
src/auth.ts
Normal file
40
src/auth.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { constants, createPrivateKey, privateDecrypt } from 'crypto';
|
||||
import * as fs from 'fs';
|
||||
import { validate as validateUUID } from 'uuid';
|
||||
|
||||
/**
|
||||
* Simple auth implementation using RSA.
|
||||
* Client sends a base64 UUID encrypted with public key, server decrypts with private key and checks validity.
|
||||
*/
|
||||
export class Auth {
|
||||
constructor() {}
|
||||
|
||||
decryptToken(token: string): string {
|
||||
const keyPem = fs.readFileSync('auth_key.pem', 'utf-8');
|
||||
const privateKey = createPrivateKey({ key: keyPem, format: 'pem' });
|
||||
const encryptedBuffer = Buffer.from(token, 'base64');
|
||||
const decrypted = privateDecrypt(
|
||||
{
|
||||
key: privateKey,
|
||||
padding: constants.RSA_PKCS1_PADDING,
|
||||
},
|
||||
encryptedBuffer
|
||||
);
|
||||
return decrypted.toString('utf-8');
|
||||
}
|
||||
|
||||
isTokenValid(decryptedToken: string): boolean {
|
||||
return validateUUID(decryptedToken);
|
||||
}
|
||||
|
||||
isTokenExpired(issuedAt: number): boolean {
|
||||
const tokenLifetimeSeconds = 300;
|
||||
const allowedDriftSeconds = 10;
|
||||
const now = Math.floor(Date.now() / 1000); // current unix timestamp in seconds
|
||||
const diff = now - issuedAt;
|
||||
return (
|
||||
diff < -allowedDriftSeconds ||
|
||||
diff > tokenLifetimeSeconds + allowedDriftSeconds
|
||||
);
|
||||
}
|
||||
}
|
||||
40
src/authMiddleware.ts
Normal file
40
src/authMiddleware.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import type { NextFunction, Request, Response } from 'express';
|
||||
import { Auth } from './auth.js';
|
||||
import { AuthPayload } from './types.js';
|
||||
|
||||
const auth = new Auth();
|
||||
|
||||
export function bearerAuthMiddleware(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const authHeader = req.headers['authorization'];
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
return res
|
||||
.status(401)
|
||||
.json({ error: 'Missing or invalid Authorization header' });
|
||||
}
|
||||
|
||||
try {
|
||||
const token = authHeader.substring('Bearer '.length);
|
||||
const decrypted = auth.decryptToken(token);
|
||||
if (!decrypted) {
|
||||
return res.status(401).json({ error: 'Invalid token' });
|
||||
}
|
||||
|
||||
const payload: AuthPayload = JSON.parse(decrypted);
|
||||
if (!payload.token || typeof payload.issuedAt !== 'number') {
|
||||
return res.status(401).json({ error: 'Token payload missing fields' });
|
||||
}
|
||||
if (!auth.isTokenValid(payload.token)) {
|
||||
return res.status(401).json({ error: 'Token is not a valid UUID' });
|
||||
}
|
||||
if (auth.isTokenExpired(payload.issuedAt)) {
|
||||
return res.status(401).json({ error: 'Token expired' });
|
||||
}
|
||||
next();
|
||||
} catch (e) {
|
||||
return res.status(500).json({ error: (e as Error).message });
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import 'dotenv/config';
|
||||
import express from 'express';
|
||||
import { bearerAuthMiddleware } from './authMiddleware.js';
|
||||
import {
|
||||
onGetImages,
|
||||
onGetQuoteById,
|
||||
|
|
@ -9,6 +10,8 @@ import {
|
|||
|
||||
const app = express();
|
||||
|
||||
app.use(bearerAuthMiddleware);
|
||||
|
||||
app.get('/quotes/random', onGetRandomQuote);
|
||||
app.get('/quotes/:id', onGetQuoteById);
|
||||
app.get('/translations', onGetTranslations);
|
||||
|
|
|
|||
|
|
@ -22,8 +22,8 @@ function onGetQuoteById(req: Request, res: Response): void {
|
|||
const id = parseInt(idParam);
|
||||
const quote = getQuote(id);
|
||||
res.status(200).send(quote);
|
||||
} catch (error: any) {
|
||||
res.status(500).send({ error: error.message });
|
||||
} catch (e) {
|
||||
res.status(500).send({ error: (e as Error).message });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -31,8 +31,8 @@ function onGetTranslations(_req: Request, res: Response): void {
|
|||
try {
|
||||
const translations = listTranslations();
|
||||
res.status(200).send(translations);
|
||||
} catch (error: any) {
|
||||
res.status(500).send({ error: error.message });
|
||||
} catch (e) {
|
||||
res.status(500).send({ error: (e as Error).message });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -40,8 +40,8 @@ async function onGetImages(_req: Request, res: Response): Promise<void> {
|
|||
try {
|
||||
const images = await listRandomImages();
|
||||
res.status(200).send(images);
|
||||
} catch (error: any) {
|
||||
res.status(500).send({ error: error.message });
|
||||
} catch (e) {
|
||||
res.status(500).send({ error: (e as Error).message });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
9
src/types.d.ts
vendored
9
src/types.d.ts
vendored
|
|
@ -1,3 +1,12 @@
|
|||
export interface AuthPayload {
|
||||
// An UUID string, unique per request.
|
||||
token: string;
|
||||
|
||||
// Unix time in seconds. It is the time when the token was issued.
|
||||
// Will expire after a time window.
|
||||
issuedAt: number;
|
||||
}
|
||||
|
||||
export interface Quote {
|
||||
id: number;
|
||||
text: string;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue