Skip to content

Commit 5e8816f

Browse files
committed
patch: improved session sec
1 parent f36ab11 commit 5e8816f

File tree

4 files changed

+172
-62
lines changed

4 files changed

+172
-62
lines changed

apps/api/src/controllers/auth.ts

Lines changed: 105 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import axios from "axios";
22
import bcrypt from "bcrypt";
3+
import crypto from "crypto";
34
import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
45
import jwt from "jsonwebtoken";
56
import { LRUCache } from "lru-cache";
@@ -320,22 +321,31 @@ export function authRoutes(fastify: FastifyInstance) {
320321
throw new Error("Password is not valid");
321322
}
322323

323-
var b64string = process.env.SECRET;
324-
var buf = new Buffer(b64string!, "base64"); // Ta-da
325-
326-
let token = jwt.sign(
324+
// Generate a secure session token
325+
var secret = Buffer.from(process.env.SECRET!, "base64");
326+
const token = jwt.sign(
327327
{
328-
data: { id: user!.id },
328+
data: {
329+
id: user!.id,
330+
// Add a unique identifier for this session
331+
sessionId: crypto.randomBytes(32).toString('hex')
332+
}
329333
},
330-
buf,
331-
{ expiresIn: "7d" }
334+
secret,
335+
{
336+
expiresIn: "8h",
337+
algorithm: 'HS256'
338+
}
332339
);
333340

341+
// Store session with additional security info
334342
await prisma.session.create({
335343
data: {
336344
userId: user!.id,
337345
sessionToken: token,
338-
expires: new Date(Date.now() + 60 * 60 * 1000),
346+
expires: new Date(Date.now() + 8 * 60 * 60 * 1000), // 8 hours
347+
userAgent: request.headers['user-agent'] || '',
348+
ipAddress: request.ip,
339349
},
340350
});
341351

@@ -763,18 +773,12 @@ export function authRoutes(fastify: FastifyInstance) {
763773
password: string;
764774
};
765775

766-
const bearer = request.headers.authorization!.split(" ")[1];
767-
768-
let session = await prisma.session.findUnique({
769-
where: {
770-
sessionToken: bearer,
771-
},
772-
});
776+
const session = await checkSession(request);
773777

774778
const hashedPass = await bcrypt.hash(password, 10);
775779

776780
await prisma.user.update({
777-
where: { id: session?.userId },
781+
where: { id: session?.id },
778782
data: {
779783
password: hashedPass,
780784
},
@@ -831,13 +835,7 @@ export function authRoutes(fastify: FastifyInstance) {
831835
fastify.put(
832836
"/api/v1/auth/profile",
833837
async (request: FastifyRequest, reply: FastifyReply) => {
834-
const bearer = request.headers.authorization!.split(" ")[1];
835-
836-
let session = await prisma.session.findUnique({
837-
where: {
838-
sessionToken: bearer,
839-
},
840-
});
838+
const session = await checkSession(request);
841839

842840
const { name, email, language } = request.body as {
843841
name: string;
@@ -846,7 +844,7 @@ export function authRoutes(fastify: FastifyInstance) {
846844
};
847845

848846
let user = await prisma.user.update({
849-
where: { id: session?.userId },
847+
where: { id: session?.id },
850848
data: {
851849
name: name,
852850
email: email,
@@ -864,12 +862,7 @@ export function authRoutes(fastify: FastifyInstance) {
864862
fastify.put(
865863
"/api/v1/auth/profile/notifcations/emails",
866864
async (request: FastifyRequest, reply: FastifyReply) => {
867-
const bearer = request.headers.authorization!.split(" ")[1];
868-
let session = await prisma.session.findUnique({
869-
where: {
870-
sessionToken: bearer,
871-
},
872-
});
865+
const session = await checkSession(request);
873866

874867
const {
875868
notify_ticket_created,
@@ -879,7 +872,7 @@ export function authRoutes(fastify: FastifyInstance) {
879872
} = request.body as any;
880873

881874
let user = await prisma.user.update({
882-
where: { id: session?.userId },
875+
where: { id: session?.id },
883876
data: {
884877
notify_ticket_created: notify_ticket_created,
885878
notify_ticket_assigned: notify_ticket_assigned,
@@ -912,28 +905,37 @@ export function authRoutes(fastify: FastifyInstance) {
912905
fastify.put(
913906
"/api/v1/auth/user/role",
914907
async (request: FastifyRequest, reply: FastifyReply) => {
915-
const { id, role } = request.body as { id: string; role: boolean };
916-
// check for atleast one admin on role downgrade
917-
if (role === false) {
918-
const admins = await prisma.user.findMany({
919-
where: { isAdmin: true },
920-
});
921-
if (admins.length === 1) {
922-
reply.code(400).send({
923-
message: "Atleast one admin is required",
924-
success: false,
908+
const session = await checkSession(request);
909+
910+
if (session?.isAdmin) {
911+
const { id, role } = request.body as { id: string; role: boolean };
912+
// check for atleast one admin on role downgrade
913+
if (role === false) {
914+
const admins = await prisma.user.findMany({
915+
where: { isAdmin: true },
925916
});
926-
return;
917+
if (admins.length === 1) {
918+
reply.code(400).send({
919+
message: "Atleast one admin is required",
920+
success: false,
921+
});
922+
return;
923+
}
927924
}
928-
}
929-
await prisma.user.update({
930-
where: { id },
931-
data: {
932-
isAdmin: role,
933-
},
934-
});
925+
await prisma.user.update({
926+
where: { id },
927+
data: {
928+
isAdmin: role,
929+
},
930+
});
935931

936-
reply.send({ success: true });
932+
reply.send({ success: true });
933+
} else {
934+
reply.code(401).send({
935+
message: "Unauthorized",
936+
success: false,
937+
});
938+
}
937939
}
938940
);
939941

@@ -955,4 +957,57 @@ export function authRoutes(fastify: FastifyInstance) {
955957
reply.send({ success: true });
956958
}
957959
);
960+
961+
// Add a new endpoint to list and manage active sessions
962+
fastify.get("/api/v1/auth/sessions",
963+
async (request: FastifyRequest, reply: FastifyReply) => {
964+
const currentUser = await checkSession(request);
965+
if (!currentUser) {
966+
return reply.code(401).send({ message: "Unauthorized" });
967+
}
968+
969+
const sessions = await prisma.session.findMany({
970+
where: { userId: currentUser.id },
971+
select: {
972+
id: true,
973+
userAgent: true,
974+
ipAddress: true,
975+
createdAt: true,
976+
expires: true
977+
}
978+
});
979+
980+
reply.send({ sessions });
981+
}
982+
);
983+
984+
// Add ability to revoke specific sessions
985+
fastify.delete("/api/v1/auth/sessions/:sessionId",
986+
async (request: FastifyRequest, reply: FastifyReply) => {
987+
const currentUser = await checkSession(request);
988+
if (!currentUser) {
989+
return reply.code(401).send({ message: "Unauthorized" });
990+
}
991+
992+
const { sessionId } = request.params as { sessionId: string };
993+
994+
// Only allow users to delete their own sessions
995+
const session = await prisma.session.findFirst({
996+
where: {
997+
id: sessionId,
998+
userId: currentUser.id
999+
}
1000+
});
1001+
1002+
if (!session) {
1003+
return reply.code(404).send({ message: "Session not found" });
1004+
}
1005+
1006+
await prisma.session.delete({
1007+
where: { id: sessionId }
1008+
});
1009+
1010+
reply.send({ success: true });
1011+
}
1012+
);
9581013
}

apps/api/src/lib/session.ts

Lines changed: 56 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,63 @@
1+
import { FastifyRequest } from "fastify";
2+
import jwt from "jsonwebtoken";
13
import { prisma } from "../prisma";
24

35
// Checks session token and returns user object
4-
export async function checkSession(request: any) {
5-
const token = request.headers.authorization!.split(" ")[1];
6+
export async function checkSession(request: FastifyRequest) {
7+
try {
8+
const bearer = request.headers.authorization?.split(" ")[1];
9+
if (!bearer) {
10+
return null;
11+
}
612

7-
let session = await prisma.session.findUnique({
8-
where: {
9-
sessionToken: token,
10-
},
11-
});
13+
// Verify JWT token is valid
14+
var b64string = process.env.SECRET;
15+
var secret = Buffer.from(b64string!, "base64");
1216

13-
let user = await prisma.user.findUnique({
14-
where: { id: session!.userId },
15-
});
17+
try {
18+
jwt.verify(bearer, secret);
19+
} catch (e) {
20+
// Token is invalid or expired
21+
await prisma.session.delete({
22+
where: { sessionToken: bearer },
23+
});
24+
return null;
25+
}
1626

17-
return user;
27+
// Check if session exists and is not expired
28+
const session = await prisma.session.findUnique({
29+
where: { sessionToken: bearer },
30+
include: { user: true },
31+
});
32+
33+
if (!session || session.expires < new Date()) {
34+
// Session expired or doesn't exist
35+
if (session) {
36+
await prisma.session.delete({
37+
where: { id: session.id },
38+
});
39+
}
40+
return null;
41+
}
42+
43+
// Verify the request is coming from the same client
44+
const currentUserAgent = request.headers["user-agent"];
45+
const currentIp = request.ip;
46+
47+
if (
48+
session.userAgent !== currentUserAgent &&
49+
session.ipAddress !== currentIp
50+
) {
51+
// Potential session hijacking attempt - invalidate the session
52+
await prisma.session.delete({
53+
where: { id: session.id },
54+
});
55+
56+
return null;
57+
}
58+
59+
return session.user;
60+
} catch (error) {
61+
return null;
62+
}
1863
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
-- AlterTable
2+
ALTER TABLE "Session" ADD COLUMN "apiKey" BOOLEAN NOT NULL DEFAULT false,
3+
ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
4+
ADD COLUMN "ipAddress" TEXT,
5+
ADD COLUMN "userAgent" TEXT;

apps/api/src/prisma/schema.prisma

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,12 @@ model Session {
4848
sessionToken String @unique
4949
userId String
5050
expires DateTime
51-
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
51+
createdAt DateTime @default(now())
52+
userAgent String?
53+
ipAddress String?
54+
apiKey Boolean @default(false)
55+
56+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
5257
}
5358

5459
model PasswordResetToken {

0 commit comments

Comments
 (0)