Rework authentication

This commit is contained in:
Aslan 2026-01-01 17:06:31 +01:00
parent a85330e8cf
commit c07d33bcc9
17 changed files with 317 additions and 128 deletions

View file

@ -1,17 +1,23 @@
import { type FastifyReply, type FastifyRequest } from "fastify";
import type {
ILoginRequest,
IRegisterResponseError,
IRegisterResponseSuccess,
IRegisterRequest,
ILoginResponseError,
ILoginResponseSuccess,
IPostLoginRequest,
IPostRegisterResponseError,
IPostRegisterResponseSuccess,
IPostRegisterRequest,
IPostLoginResponseError,
IPostLoginResponseSuccess,
IGetRefreshResponseError,
IGetRefreshResponseSuccess,
} from "./types.js";
import { loginUser, registerUser } from "../../services/auth/auth.js";
import {
loginUser,
refreshSession,
registerUser,
} from "../../services/auth/auth.js";
import { API_ERROR } from "../errors.js";
const postRegister = async (request: FastifyRequest, _reply: FastifyReply) => {
const { username, password, email } = request.body as IRegisterRequest;
const postRegister = async (request: FastifyRequest, reply: FastifyReply) => {
const { username, password, email } = request.body as IPostRegisterRequest;
const newUser = await registerUser({
username: username,
@ -20,20 +26,21 @@ const postRegister = async (request: FastifyRequest, _reply: FastifyReply) => {
});
if (!newUser) {
reply.status(409);
return {
error: API_ERROR.USER_ALREADY_EXISTS,
} as IRegisterResponseError;
} as IPostRegisterResponseError;
}
return {
id: newUser.id,
username: newUser.username,
registerDate: newUser.registerDate?.getTime(),
} as IRegisterResponseSuccess;
} as IPostRegisterResponseSuccess;
};
const postLogin = async (request: FastifyRequest, _reply: FastifyReply) => {
const { username, password } = request.body as ILoginRequest;
const postLogin = async (request: FastifyRequest, reply: FastifyReply) => {
const { username, password } = request.body as IPostLoginRequest;
const session = await loginUser({
username: username,
@ -41,17 +48,44 @@ const postLogin = async (request: FastifyRequest, _reply: FastifyReply) => {
});
if (!session) {
reply.status(403);
return {
username: username,
error: API_ERROR.ACCESS_DENIED,
} as ILoginResponseError;
} as IPostLoginResponseError;
}
reply.setCookie("token", session.cookie, {
path: "/",
httpOnly: true,
sameSite: "none",
secure: true,
maxAge: 60 * 60 * 24 * 365 * 100,
});
return {
id: session.id,
ownerId: session.userId,
token: session.token,
} as ILoginResponseSuccess;
} as IPostLoginResponseSuccess;
};
export { postRegister, postLogin };
const getRefresh = async (request: FastifyRequest, reply: FastifyReply) => {
const cookie = request.cookies["token"];
const refresh = await refreshSession(cookie);
if (!refresh) {
reply.status(403);
return {
error: API_ERROR.ACCESS_DENIED,
} as IGetRefreshResponseError;
}
return {
id: refresh[0].id,
ownerId: refresh[0].userId,
token: refresh[1],
} as IGetRefreshResponseSuccess;
};
export { postRegister, postLogin, getRefresh };

View file

@ -4,6 +4,7 @@ import * as controller from "./auth.js";
const authRoutes = async (fastify: FastifyInstance) => {
fastify.post(`/register`, controller.postRegister);
fastify.post(`/login`, controller.postLogin);
fastify.get(`/refresh`, controller.getRefresh);
};
export { authRoutes };

View file

@ -1,42 +1,53 @@
import type { API_ERROR } from "../errors.js";
interface IRegisterRequest {
interface IPostRegisterRequest {
username: string;
password: string;
email?: string;
}
interface IRegisterResponseSuccess {
interface IPostRegisterResponseSuccess {
id: string;
username: string;
registerDate: number;
}
interface IRegisterResponseError {
interface IPostRegisterResponseError {
error: API_ERROR;
}
interface ILoginRequest {
interface IPostLoginRequest {
username: string;
password: string;
}
interface ILoginResponseSuccess {
interface IPostLoginResponseSuccess {
id: string;
ownerId: string;
}
interface IPostLoginResponseError {
username: string;
error: API_ERROR;
}
interface IGetRefreshResponseSuccess {
id: string;
ownerId: string;
token: string;
}
interface ILoginResponseError {
username: string;
interface IGetRefreshResponseError {
error: API_ERROR;
}
export {
type IRegisterRequest,
type IRegisterResponseSuccess,
type IRegisterResponseError,
type ILoginRequest,
type ILoginResponseSuccess,
type ILoginResponseError,
type IPostRegisterRequest,
type IPostRegisterResponseSuccess,
type IPostRegisterResponseError,
type IPostLoginRequest,
type IPostLoginResponseSuccess,
type IPostLoginResponseError,
type IGetRefreshResponseError,
type IGetRefreshResponseSuccess,
};

View file

@ -1,6 +1,4 @@
import { type FastifyReply, type FastifyRequest } from "fastify";
import { getDB } from "../../store/store.js";
import { hashPassword } from "../../services/auth/auth.js";
import { getUserFromAuth } from "../../services/auth/helpers.js";
const getPing = async (_request: FastifyRequest, _reply: FastifyReply) => {
@ -10,42 +8,6 @@ const getPing = async (_request: FastifyRequest, _reply: FastifyReply) => {
const getTest = async (_request: FastifyRequest, _reply: FastifyReply) => {
const authHeader = _request.headers["authorization"];
return [{ user: await getUserFromAuth(authHeader) }];
const owner = await getDB().user.create({
data: {
username: "TestUser",
passwordHash: await hashPassword("29144"),
email: "testuser@aslan2142.space",
admin: true,
},
});
await getDB().community.create({
data: {
name: "TestCommunity",
ownerId: owner.id,
members: {
connect: {
id: owner.id,
},
create: [
{
username: "Aslo",
passwordHash: await hashPassword("pass"),
admin: false,
},
{
username: "Aslan",
passwordHash: await hashPassword("8556"),
email: "aslan@aslan2142.space",
admin: false,
},
],
},
},
});
return [{ message: "ok" }];
};
export { getPing, getTest };

View file

@ -38,7 +38,7 @@ const getUserLogged = async (request: FastifyRequest, reply: FastifyReply) => {
const user = await getLoggedUserAuth(authHeader);
if (user === API_ERROR.ACCESS_DENIED) {
reply.status(404);
reply.status(403);
return {
error: API_ERROR.ACCESS_DENIED,
} as IGetLoggedUserResponseError;

View file

@ -1,6 +1,11 @@
import Fastify from "fastify";
import cors from "@fastify/cors";
import cookie from "@fastify/cookie";
import { config } from "./config.js";
import Fastify from "fastify";
import { getCookieSecret } from "./services/auth/helpers.js";
import { testRoutes } from "./controllers/test/routes.js";
import { authRoutes } from "./controllers/auth/routes.js";
import { userRoutes } from "./controllers/user/routes.js";
@ -14,6 +19,13 @@ const app = Fastify({
logger: true,
});
app.register(cors, {
origin: "http://localhost:3000",
credentials: true,
});
app.register(cookie, { secret: getCookieSecret() });
app.register(testRoutes);
app.register(authRoutes, { prefix: "/api/v1/auth" });
app.register(userRoutes, { prefix: "/api/v1/user" });

View file

@ -1,10 +1,12 @@
import argon2 from "argon2";
import jwt from "jsonwebtoken";
import type { User, Session } from "../../generated/prisma/client.js";
import { getDB } from "../../store/store.js";
import {
createSessionCookie,
createToken,
hashPassword,
verifyPassword,
} from "./helpers.js";
import type { IUserLogin, IUserRegistration } from "./types.js";
import { getJwtSecret } from "./helpers.js";
const registerUser = async (
registration: IUserRegistration,
@ -50,11 +52,7 @@ const loginUser = async (login: IUserLogin): Promise<Session | null> => {
return null;
}
const passwordCorrect = await argon2.verify(
user.passwordHash,
login.password,
);
if (!passwordCorrect) {
if (!(await verifyPassword(user.passwordHash, login.password))) {
return null;
}
@ -69,23 +67,30 @@ const loginUser = async (login: IUserLogin): Promise<Session | null> => {
return await getDB().session.create({
data: {
token: createToken(user.id),
cookie: createSessionCookie(),
userId: user.id,
},
});
};
const hashPassword = async (password: string): Promise<string> => {
return await argon2.hash(password, {
type: argon2.argon2id,
memoryCost: 2 ** 16,
timeCost: 4,
parallelism: 1,
const refreshSession = async (
cookie: string | undefined,
): Promise<[Session, string] | null> => {
if (!cookie) {
return null;
}
const session = await getDB().session.findFirst({
where: {
cookie: cookie,
},
});
if (!session) {
return null;
}
return [session, createToken(session.id)];
};
const createToken = (userId: string) => {
return jwt.sign({ sub: userId }, getJwtSecret());
};
export { registerUser, loginUser, hashPassword };
export { registerUser, loginUser, refreshSession };

View file

@ -1,39 +1,60 @@
import argon2 from "argon2";
import jwt from "jsonwebtoken";
import type {
Community,
Session,
User,
} from "../../generated/prisma/client.js";
import crypto from "crypto";
import type { Community, User } from "../../generated/prisma/client.js";
import { getDB } from "../../store/store.js";
import type { IOwnerCheck } from "./types.js";
import type { AccessTokenPayload, IOwnerCheck } from "./types.js";
import type { PERMISSION } from "./permission.js";
const getJwtSecret = () => {
const getJwtSecret = (): string => {
return process.env.JWT_SECRET || "";
};
const verifyToken = (token: string): string | jwt.JwtPayload | null => {
const getCookieSecret = (): string => {
return process.env.COOKIE_SECRET || "";
};
const createSessionCookie = () => {
return crypto.randomBytes(32).toString("hex");
};
const createToken = (sessionId: string) => {
return jwt.sign({ sessionId: sessionId }, getJwtSecret());
};
const verifyToken = (token: string): string | null => {
try {
return jwt.verify(token, getJwtSecret());
const payload = jwt.verify(token, getJwtSecret()) as AccessTokenPayload;
return payload.sessionId;
} catch {
return null;
}
};
const getSessionFromToken = async (token: string): Promise<Session | null> => {
return await getDB().session.findFirst({
where: {
token: token,
},
const hashPassword = async (password: string): Promise<string> => {
return await argon2.hash(password, {
type: argon2.argon2id,
memoryCost: 2 ** 16,
timeCost: 4,
parallelism: 1,
});
};
const getUserFromToken = async (token: string): Promise<User | null> => {
const session = await getSessionFromToken(token);
const verifyPassword = async (
passwordHash: string,
passwordToCheck: string,
) => {
return await argon2.verify(passwordHash, passwordToCheck);
};
const getUserBySessionId = async (sessionId: string): Promise<User | null> => {
return await getDB().user.findFirst({
where: {
id: session?.userId ?? "invalid",
Session: {
some: {
id: sessionId,
},
},
},
});
};
@ -43,12 +64,12 @@ const getUserFromAuth = async (
): Promise<User | null> => {
const token = authHeader?.replace("Bearer ", "");
const verified = verifyToken(token ?? "") !== null;
if (!verified || !token) {
const sessionId = verifyToken(token ?? "");
if (!sessionId || !token) {
return null;
}
const user = await getUserFromToken(token);
const user = await getUserBySessionId(sessionId);
if (!user) {
return null;
}
@ -187,9 +208,13 @@ const isUserInCommunity = async (
export {
getJwtSecret,
getCookieSecret,
createSessionCookie,
createToken,
verifyToken,
getSessionFromToken,
getUserFromToken,
hashPassword,
verifyPassword,
getUserBySessionId,
getUserFromAuth,
isUserOwnerOrAdmin,
getUserPermissions,

View file

@ -1,3 +1,4 @@
import type { JwtPayload } from "jsonwebtoken";
import type {
Channel,
Community,
@ -6,7 +7,10 @@ import type {
Session,
User,
} from "../../generated/prisma/client.js";
import type { PERMISSION } from "./permission.js";
interface AccessTokenPayload extends JwtPayload {
sessionId: string;
}
interface IUserRegistration {
username: string;
@ -28,4 +32,9 @@ interface IOwnerCheck {
role?: Role | null;
}
export { type IUserRegistration, type IUserLogin, type IOwnerCheck };
export {
type AccessTokenPayload,
type IUserRegistration,
type IUserLogin,
type IOwnerCheck,
};