Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion backend/src/middleware/authGuard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,13 @@

import { Request, Response, NextFunction } from "express";
import jwt, { JwtPayload } from "jsonwebtoken";
import { StrKey } from "@stellar/stellar-sdk";
import { isTokenBlacklisted } from "../routes/auth";
import type { UserRole } from "./rbac";

/** Augments Express Request with the decoded JWT payload. */
export interface AuthRequest extends Request {
auth?: JwtPayload & { address: string; jti: string };
auth?: JwtPayload & { address: string; jti: string; role?: UserRole };
}

const ACCESS_TOKEN_COOKIE = "lance_access_token";
Expand Down Expand Up @@ -74,6 +76,14 @@ export async function authGuard(
return;
}

// Validate the address claim is a real Stellar key. A tampered or
// manually-crafted token that carries a non-address payload fails here
// before it can reach any business logic (session-hijacking guard).
if (!StrKey.isValidEd25519PublicKey(decoded.address)) {
res.status(401).json({ error: "Token contains invalid address claim" });
return;
}

// Redis blacklist check — single GET, O(1), target < 1 ms.
const revoked = await isTokenBlacklisted(decoded.jti).catch(() => false);
if (revoked) {
Expand Down
36 changes: 36 additions & 0 deletions backend/src/middleware/rbac.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* rbac.ts — Role-Based Access Control middleware (Freelancer vs Client)
*
* Usage:
* router.get("/my-jobs", authGuard, requireRole("freelancer"), handler);
* router.post("/post-job", authGuard, requireRole("client", "admin"), handler);
*/

import { Request, Response, NextFunction } from "express";

export type UserRole = "freelancer" | "client" | "admin";

/**
* Returns middleware that rejects the request with 403 if the authenticated
* user's role (carried in `req.auth.role`) is not in the allowed set.
* Must be applied after `authGuard`, which populates `req.auth`.
*/
export function requireRole(...allowed: UserRole[]) {
return (req: Request, res: Response, next: NextFunction): void => {
const role = (req as Request & { auth?: { role?: string } }).auth?.role as
| UserRole
| undefined;

if (!role) {
res.status(403).json({ error: "Role information missing from token" });
return;
}

if (!allowed.includes(role)) {
res.status(403).json({ error: "Insufficient permissions" });
return;
}

next();
};
}
Loading
Loading