Rework authentication
This commit is contained in:
parent
a85330e8cf
commit
c07d33bcc9
17 changed files with 317 additions and 128 deletions
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
14
src/index.ts
14
src/index.ts
|
|
@ -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" });
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue