Skip to content

Commit

Permalink
patch: improved session sec
Browse files Browse the repository at this point in the history
  • Loading branch information
potts99 committed Nov 14, 2024
1 parent f36ab11 commit 5e8816f
Show file tree
Hide file tree
Showing 4 changed files with 172 additions and 62 deletions.
155 changes: 105 additions & 50 deletions apps/api/src/controllers/auth.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import axios from "axios";
import bcrypt from "bcrypt";
import crypto from "crypto";
import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
import jwt from "jsonwebtoken";
import { LRUCache } from "lru-cache";
Expand Down Expand Up @@ -320,22 +321,31 @@ export function authRoutes(fastify: FastifyInstance) {
throw new Error("Password is not valid");
}

var b64string = process.env.SECRET;
var buf = new Buffer(b64string!, "base64"); // Ta-da

let token = jwt.sign(
// Generate a secure session token
var secret = Buffer.from(process.env.SECRET!, "base64");
const token = jwt.sign(
{
data: { id: user!.id },
data: {
id: user!.id,
// Add a unique identifier for this session
sessionId: crypto.randomBytes(32).toString('hex')
}
},
buf,
{ expiresIn: "7d" }
secret,
{
expiresIn: "8h",
algorithm: 'HS256'
}
);

// Store session with additional security info
await prisma.session.create({
data: {
userId: user!.id,
sessionToken: token,
expires: new Date(Date.now() + 60 * 60 * 1000),
expires: new Date(Date.now() + 8 * 60 * 60 * 1000), // 8 hours
userAgent: request.headers['user-agent'] || '',
ipAddress: request.ip,
},
});

Expand Down Expand Up @@ -763,18 +773,12 @@ export function authRoutes(fastify: FastifyInstance) {
password: string;
};

const bearer = request.headers.authorization!.split(" ")[1];

let session = await prisma.session.findUnique({
where: {
sessionToken: bearer,
},
});
const session = await checkSession(request);

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

await prisma.user.update({
where: { id: session?.userId },
where: { id: session?.id },
data: {
password: hashedPass,
},
Expand Down Expand Up @@ -831,13 +835,7 @@ export function authRoutes(fastify: FastifyInstance) {
fastify.put(
"/api/v1/auth/profile",
async (request: FastifyRequest, reply: FastifyReply) => {
const bearer = request.headers.authorization!.split(" ")[1];

let session = await prisma.session.findUnique({
where: {
sessionToken: bearer,
},
});
const session = await checkSession(request);

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

let user = await prisma.user.update({
where: { id: session?.userId },
where: { id: session?.id },
data: {
name: name,
email: email,
Expand All @@ -864,12 +862,7 @@ export function authRoutes(fastify: FastifyInstance) {
fastify.put(
"/api/v1/auth/profile/notifcations/emails",
async (request: FastifyRequest, reply: FastifyReply) => {
const bearer = request.headers.authorization!.split(" ")[1];
let session = await prisma.session.findUnique({
where: {
sessionToken: bearer,
},
});
const session = await checkSession(request);

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

let user = await prisma.user.update({
where: { id: session?.userId },
where: { id: session?.id },
data: {
notify_ticket_created: notify_ticket_created,
notify_ticket_assigned: notify_ticket_assigned,
Expand Down Expand Up @@ -912,28 +905,37 @@ export function authRoutes(fastify: FastifyInstance) {
fastify.put(
"/api/v1/auth/user/role",
async (request: FastifyRequest, reply: FastifyReply) => {
const { id, role } = request.body as { id: string; role: boolean };
// check for atleast one admin on role downgrade
if (role === false) {
const admins = await prisma.user.findMany({
where: { isAdmin: true },
});
if (admins.length === 1) {
reply.code(400).send({
message: "Atleast one admin is required",
success: false,
const session = await checkSession(request);

if (session?.isAdmin) {
const { id, role } = request.body as { id: string; role: boolean };
// check for atleast one admin on role downgrade
if (role === false) {
const admins = await prisma.user.findMany({
where: { isAdmin: true },
});
return;
if (admins.length === 1) {
reply.code(400).send({
message: "Atleast one admin is required",
success: false,
});
return;
}
}
}
await prisma.user.update({
where: { id },
data: {
isAdmin: role,
},
});
await prisma.user.update({
where: { id },
data: {
isAdmin: role,
},
});

reply.send({ success: true });
reply.send({ success: true });
} else {
reply.code(401).send({
message: "Unauthorized",
success: false,
});
}
}
);

Expand All @@ -955,4 +957,57 @@ export function authRoutes(fastify: FastifyInstance) {
reply.send({ success: true });
}
);

// Add a new endpoint to list and manage active sessions
fastify.get("/api/v1/auth/sessions",
async (request: FastifyRequest, reply: FastifyReply) => {
const currentUser = await checkSession(request);
if (!currentUser) {
return reply.code(401).send({ message: "Unauthorized" });
}

const sessions = await prisma.session.findMany({
where: { userId: currentUser.id },
select: {
id: true,
userAgent: true,
ipAddress: true,
createdAt: true,
expires: true
}
});

reply.send({ sessions });
}
);

// Add ability to revoke specific sessions
fastify.delete("/api/v1/auth/sessions/:sessionId",
async (request: FastifyRequest, reply: FastifyReply) => {
const currentUser = await checkSession(request);
if (!currentUser) {
return reply.code(401).send({ message: "Unauthorized" });
}

const { sessionId } = request.params as { sessionId: string };

// Only allow users to delete their own sessions
const session = await prisma.session.findFirst({
where: {
id: sessionId,
userId: currentUser.id
}
});

if (!session) {
return reply.code(404).send({ message: "Session not found" });
}

await prisma.session.delete({
where: { id: sessionId }
});

reply.send({ success: true });
}
);
}
67 changes: 56 additions & 11 deletions apps/api/src/lib/session.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,63 @@
import { FastifyRequest } from "fastify";
import jwt from "jsonwebtoken";
import { prisma } from "../prisma";

// Checks session token and returns user object
export async function checkSession(request: any) {
const token = request.headers.authorization!.split(" ")[1];
export async function checkSession(request: FastifyRequest) {
try {
const bearer = request.headers.authorization?.split(" ")[1];
if (!bearer) {
return null;
}

let session = await prisma.session.findUnique({
where: {
sessionToken: token,
},
});
// Verify JWT token is valid
var b64string = process.env.SECRET;
var secret = Buffer.from(b64string!, "base64");

let user = await prisma.user.findUnique({
where: { id: session!.userId },
});
try {
jwt.verify(bearer, secret);
} catch (e) {
// Token is invalid or expired
await prisma.session.delete({
where: { sessionToken: bearer },
});
return null;
}

return user;
// Check if session exists and is not expired
const session = await prisma.session.findUnique({
where: { sessionToken: bearer },
include: { user: true },
});

if (!session || session.expires < new Date()) {
// Session expired or doesn't exist
if (session) {
await prisma.session.delete({
where: { id: session.id },
});
}
return null;
}

// Verify the request is coming from the same client
const currentUserAgent = request.headers["user-agent"];
const currentIp = request.ip;

if (
session.userAgent !== currentUserAgent &&
session.ipAddress !== currentIp
) {
// Potential session hijacking attempt - invalidate the session
await prisma.session.delete({
where: { id: session.id },
});

return null;
}

return session.user;
} catch (error) {
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE "Session" ADD COLUMN "apiKey" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "ipAddress" TEXT,
ADD COLUMN "userAgent" TEXT;
7 changes: 6 additions & 1 deletion apps/api/src/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,12 @@ model Session {
sessionToken String @unique
userId String
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
userAgent String?
ipAddress String?
apiKey Boolean @default(false)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}

model PasswordResetToken {
Expand Down

0 comments on commit 5e8816f

Please sign in to comment.