add refresh token endpoints

This commit is contained in:
Kai Ritthaler 2025-06-19 22:22:28 +02:00 committed by Luisa Bellitto
parent 9be66a0a2f
commit a9bda19891
8 changed files with 235 additions and 35 deletions

View file

@ -25,8 +25,9 @@ model User {
posts Post[]
comments Comment[]
likes Like[]
media Media[] @relation("UploadedMedia")
refreshToken RefreshToken[]
media Media[] @relation("UploadedMedia")
following Follow[] @relation("Following")
followers Follow[] @relation("Followers")
}
@ -125,3 +126,9 @@ model Follow {
@@id([followingUserId, followedUserId])
}
model RefreshToken {
id String @id @default(uuid())
expiresAt DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId String
}

View file

@ -9,6 +9,7 @@
"requiredKeys": [
{ "name": "DATABASE_URL", "generated": true },
{ "name": "TOKEN_SECRET", "generated": true },
{ "name": "REFRESH_TOKEN_SECRET", "generated": true },
{
"name": "DB_USER",
"generated": false,

View file

@ -54,9 +54,9 @@ if (fs.existsSync(".env")) {
);
} while (!input || input.length < setting.minLength);
process.env[setting.name] = input;
} else if (setting.name === "TOKEN_SECRET") {
} else if (setting.name === "TOKEN_SECRET" || setting.name === "REFRESH_TOKEN_SECRET") {
// generating a random JWT secret
const jwtSecret: string = crypto.randomBytes(32).toString("hex"); // 64 Zeichen
const jwtSecret: string = crypto.randomBytes(32).toString("hex"); // 64 character
process.env[setting.name] = jwtSecret;
}
}

View file

@ -1,24 +1,63 @@
import express, { Request, Response } from "express";
import { PrismaClient, User } from "../../prisma/app/generated/prisma/client";
import {
PrismaClient,
User,
RefreshToken,
} from "../../prisma/app/generated/prisma/client";
import { UserLoginDto, UserRegistrationDto } from "../schemas/userSchemas";
import jwt from "jsonwebtoken";
import jwt, { TokenExpiredError } from "jsonwebtoken";
import dotenv from "dotenv";
import bcrypt from "bcryptjs";
import { StatusCodes } from "http-status-codes";
import { RefreshTokenPayload } from "../types/tokens";
const app = express();
app.use(express.json());
const prisma = new PrismaClient();
// load environment variables from .env file
dotenv.config();
const JWT_SECRET: string = process.env.TOKEN_SECRET!; // this secret is used to sign the JWT token
const REFRESH_TOKEN_SECRET: string = process.env.REFRESH_TOKEN_SECRET!;
// Generate a JWT token with the username as payload and a secret from the environment variables which expires in 1800 seconds (30 minutes)
function generateAccessToken(username: string, userId: string, role: string) {
return jwt.sign({ username: username, role: role, sub: userId }, JWT_SECRET, {
expiresIn: "1800s",
issuer: "VogelApi",
});
function generateAccessToken(
username: string,
userId: string,
role: string,
refreshTokenId: string
) {
return jwt.sign(
{ username: username, role: role, sub: userId, jti: refreshTokenId },
JWT_SECRET,
{
expiresIn: "1800s",
issuer: "VogelApi",
}
);
}
async function generateRefreshToken(
userId: string
): Promise<{ token: string; id: string }> {
const expiresAt = new Date(Date.now() + 100 * 60 * 60 * 1000); // 100 h
let refreshToken: RefreshToken;
{
refreshToken = await prisma.refreshToken.create({
data: {
expiresAt: expiresAt,
user: {
connect: {
id: userId,
},
},
},
});
}
return {
token: jwt.sign({ jti: refreshToken.id }, REFRESH_TOKEN_SECRET, {
expiresIn: "100h",
}),
id: refreshToken.id,
};
}
// Endpoint to register a new user
@ -78,7 +117,14 @@ export const registerUser = async (req: Request, res: Response) => {
});
return;
}
const token: string = generateAccessToken(user.username, user.id, user.role); // generate a JWT token with the username and userId as payload
const refreshToken = await generateRefreshToken(user.id);
res.set("Refresh-Token", refreshToken.token);
const token: string = generateAccessToken(
user.username,
user.id,
user.role,
refreshToken.id
); // generate a JWT token with the username and userId as payload
res.set("Authorization", `Bearer ${token}`); // set the token in the response header
res.status(StatusCodes.CREATED).json({
message: "user created",
@ -113,7 +159,14 @@ export const loginUser = async (req: Request, res: Response) => {
});
return;
}
const token: string = generateAccessToken(user.username, user.id, user.role); // generate a JWT token with the username and userId as payload
const refreshToken = await generateRefreshToken(user.id);
res.set("Refresh-Token", refreshToken.token);
const token: string = generateAccessToken(
user.username,
user.id,
user.role,
refreshToken.id
); // generate a JWT token with the username and userId as payload
res.set("Authorization", `Bearer ${token}`); // set the token in the response header
res.status(StatusCodes.OK).json({ message: "User logged in successfully" });
};
@ -150,3 +203,90 @@ export const getUser = async (req: Request, res: Response) => {
},
});
};
export const refreshToken = async (req: Request, res: Response) => {
const refreshToken: string | undefined = req.headers[
"refresh-token"
] as string;
if (!refreshToken) {
res.status(StatusCodes.UNAUTHORIZED).json({
error: "Unauthorized",
details: [{ message: "No token provided" }],
});
return;
}
await jwt.verify(
refreshToken,
REFRESH_TOKEN_SECRET,
async (err: any, decoded: any) => {
if (err) {
if (err instanceof jwt.TokenExpiredError) {
res.status(StatusCodes.UNAUTHORIZED).json({
error: "Refreshtoken expired",
details: [{ message: "Refreshtoken has expired" }],
});
return;
}
res.status(StatusCodes.FORBIDDEN).json({
error: "Invalid refreshtoken",
details: [{ message: "Refreshtoken is invalid" }],
});
return;
}
const payload = decoded as RefreshTokenPayload;
try {
const now = new Date();
const storedToken = await prisma.refreshToken.findUnique({
where: {
id: payload.jti,
expiresAt: {
gt: now, // expiresAt > now
},
},
include: { user: true },
});
if (!storedToken) {
res.status(StatusCodes.UNAUTHORIZED).json({
error: "InvalidRefreshToken",
details: [
{ message: "Refresh token is invalid or no longer exists" },
],
});
return;
}
await prisma.refreshToken.delete({
where: { id: payload.jti },
});
const refreshToken = await generateRefreshToken(storedToken.user.id);
res.set("Refresh-Token", refreshToken.token);
const token: string = generateAccessToken(
storedToken.user.username,
storedToken.user.id,
storedToken.user.role,
refreshToken.id
); // generate a JWT token with the username and userId as payload
res.set("Authorization", `Bearer ${token}`); // set the token in the response header
res.status(StatusCodes.OK).send();
} catch {
res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
error: "Server error",
details: [{ message: "Server Error" }],
});
return;
}
}
);
};
export const logout = async (req: Request, res: Response) => {
const jti: string = req.query.jti as string;
try {
await prisma.refreshToken.delete({ where: { id: jti } });
res.removeHeader("Authorization");
res.removeHeader("Refresh-Token");
res.status(StatusCodes.NO_CONTENT).send();
} catch {
res.status(StatusCodes.INTERNAL_SERVER_ERROR);
}
};

View file

@ -2,20 +2,14 @@ import express, { NextFunction, Request, Response } from "express";
import jwt, { TokenExpiredError } from "jsonwebtoken";
import dotenv from "dotenv";
import { StatusCodes } from "http-status-codes";
import { JwtPayload } from "../types/tokens";
dotenv.config();
// Import the JWT secret from environment variables
const JWT_SECRET: string = process.env.TOKEN_SECRET!;
if (!JWT_SECRET) console.error("No JWT secret provided");
// Define the structure of the JWT payload
interface JwtPayload {
id: string;
username: string;
role: string;
iat: number;
exp: number;
}
// Extend the Express Request interface to include the user property
declare global {

View file

@ -8,7 +8,6 @@ import { upload } from "../middleware/fileUpload";
import { validateData } from "../middleware/validationMiddleware";
import { uploadPostSchema } from "../schemas/postSchemas";
import { get } from "http";
const router = express.Router();
/**
@ -80,27 +79,20 @@ router.post("/upload", upload, validateData(uploadPostSchema), uploadPost);
* 401:
* description: not authenticated
*/
router.get("/getPost/:userId", getPost);
router.get("/getPost/:postId", getPost);
/**
* @swagger
* /api/posts/getUserPosts/{userId}:
* /api/posts/getUserPosts/:
* get:
* summary: Get Post
* tags: [posts]
* security:
* - bearerAuth: []
* parameters:
* - in: query
* name: postId
* required: true
* schema:
* type: string
* description: The user id
* responses:
* 200:
* description: Ok
* 401:
* description: not authenticated
*/
router.get("/getuserposts/:userId", getUserPosts);
router.get("/getuserposts/", getUserPosts);
export default router;

View file

@ -6,7 +6,7 @@ import {
userLoginSchema,
} from "../schemas/userSchemas";
import { authenticateToken } from "../middleware/authenticateToken";
import { logout, refreshToken } from "../controllers/userController";
const userRouter = express.Router();
import {
@ -50,7 +50,7 @@ import {
* /api/user/register:
* post:
* summary: Register a new user
* tags: [User]
* tags: [Auth]
* requestBody:
* required: true
* content:
@ -73,7 +73,7 @@ userRouter.post(
* /api/user/login:
* post:
* summary: Log in a user
* tags: [User]
* tags: [Auth]
* requestBody:
* required: true
* content:
@ -109,5 +109,57 @@ userRouter.post("/login", validateData(userLoginSchema), loginUser);
* description: Ungültige Anmeldedaten
*/
userRouter.get("/getUser/:username", authenticateToken(), getUser);
/**
* @swagger
* /api/user/refreshToken:
* get:
* summary: Refresh JWT tokens
* description: |
* Verifiziert einen bereitgestellten Refresh-Token (im Header) und gibt neue Tokens im Header zurück.
* tags:
* - Auth
* parameters:
* - in: header
* name: Refresh-Token
* required: true
* schema:
* type: string
* description: Der gültige JWT-Refresh-Token
* responses:
* 200:
* description: Tokens erfolgreich erneuert
* headers:
* Authorization:
* description: Neuer Access-Token im Bearer-Format
* schema:
* type: string
* Refresh-Token:
* description: Neuer Refresh-Token
* schema:
* type: string
* 401:
* description: Ungültiger oder abgelaufener Refresh-Token
* 403:
* description: Fehlerhafte Signatur oder ungültiger Token
* 500:
* description: Serverfehler
*/
userRouter.get("/refreshToken", refreshToken);
/**
* @swagger
* /api/user/logout/:
* delete:
* summary: logout
* tags: [Auth]
* security:
* - bearerAuth: []
* responses:
* 204:
* description: logged out
* 401:
* description: not authenticated
*/
userRouter.delete("/logout", authenticateToken(), logout);
export default userRouter;

View file

@ -0,0 +1,14 @@
export interface JwtPayload {
id: string;
username: string;
role: string;
iat: number;
exp: number;
jti: string;
}
export interface RefreshTokenPayload {
jti: string;
iat: number;
exp: number;
}