Skip to content

Commit

Permalink
using http only cookie and blacklist tokens
Browse files Browse the repository at this point in the history
  • Loading branch information
farooqpk committed Jun 10, 2024
1 parent ad88d9c commit 882d1f9
Show file tree
Hide file tree
Showing 18 changed files with 238 additions and 44 deletions.
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"bcrypt": "^5.1.1",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"dayjs": "^1.11.11",
"dotenv": "^16.1.4",
"express": "^4.18.2",
"express-rate-limit": "^7.2.0",
Expand Down
5 changes: 4 additions & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const {
R2_ACCESS_KEY,
R2_SECRET_KEY,
R2_BUCKET_NAME,
NODE_ENV,
} = process.env;

if (
Expand All @@ -21,7 +22,8 @@ if (
!R2_ACCOUNT_ID ||
!R2_ACCESS_KEY ||
!R2_SECRET_KEY ||
!R2_BUCKET_NAME
!R2_BUCKET_NAME ||
!NODE_ENV
) {
throw new Error("Missing environment variables");
}
Expand All @@ -37,4 +39,5 @@ export {
R2_ACCESS_KEY,
R2_SECRET_KEY,
R2_BUCKET_NAME,
NODE_ENV,
};
20 changes: 17 additions & 3 deletions src/controllers/auth/loginToken.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { Request, Response } from "express";
import { CookieOptions, Request, Response } from "express";
import { clearFromRedis, getDataFromRedis } from "../../redis";
import { prisma } from "../../utils/prisma";
import { createJwtToken } from "../../utils/createJwtToken";
import { NODE_ENV } from "../../config";
import dayjs from "dayjs";

export const loginToken = async (req: Request, res: Response) => {
try {
Expand Down Expand Up @@ -61,11 +63,23 @@ export const loginToken = async (req: Request, res: Response) => {
"refresh"
);

const cookieOptions: CookieOptions = {
httpOnly: true,
secure: NODE_ENV === "development" ? false : true,
};

res.cookie("accesstoken", accesstoken, {
...cookieOptions,
expires: dayjs().add(1, "hours").toDate(),
});
res.cookie("refreshtoken", refreshtoken, {
...cookieOptions,
expires: dayjs().add(14, "days").toDate(),
});

return res.status(200).json({
success: true,
message: "login token verified successfully",
accesstoken,
refreshtoken,
});
} catch (error) {
res.status(500).json(error);
Expand Down
28 changes: 28 additions & 0 deletions src/controllers/auth/logout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Request, Response } from "express";
import { setDataAsSetInRedis } from "../../redis/set-data-as-set";

export const logout = async (req: Request, res: Response) => {
try {
const accesstoken = req.cookies.accesstoken;
const refreshtoken = req.cookies.refreshtoken;
if (!accesstoken || !refreshtoken) {
return res
.status(401)
.json({ success: false, message: "token is missing" });
}

// blacklist the tokens
await setDataAsSetInRedis({
key: "blacklistedTokens",
data: [accesstoken, refreshtoken],
isString: true,
});

res.clearCookie("accesstoken");
res.clearCookie("refreshtoken");

res.status(200).json({ success: true, message: "logged out successfully" });
} catch (error) {
res.status(500).json(error);
}
};
43 changes: 35 additions & 8 deletions src/controllers/auth/refreshToken.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { Request, Response } from "express";
import { CookieOptions, Request, Response } from "express";
import jwt from "jsonwebtoken";
import { createJwtToken } from "../../utils/createJwtToken";
import { REFRESH_TOKEN_SECRET } from "../../config";
import { NODE_ENV, REFRESH_TOKEN_SECRET } from "../../config";
import dayjs from "dayjs";
import { checkItemInSetRedis } from "../../redis/check-Item-In-set";

export const createAccessTokenFromRefreshToken = async (
req: Request,
res: Response
) => {
try {
const refreshToken = req.headers.authorization?.split(" ")[1];
const refreshToken = req.cookies.refreshtoken;

if (!refreshToken) {
return res.status(401).json({
Expand All @@ -17,22 +19,47 @@ export const createAccessTokenFromRefreshToken = async (
});
}

const decodedData = jwt.verify(
refreshToken,
REFRESH_TOKEN_SECRET!
) as any;
// check refresh token blacklisted or not
const isBlacklisted = await checkItemInSetRedis(
"blacklistedTokens",
refreshToken
);

if (isBlacklisted) {
return res.status(401).json({
success: false,
message: "Refresh token is blacklisted.",
});
}

const decodedData = jwt.verify(refreshToken, REFRESH_TOKEN_SECRET!) as any;

if (!decodedData) {
return res.status(401).json({
success: false,
message: "Refresh token is invalid.",
});
}

// create new access token
const newAccessToken = createJwtToken(
decodedData.userId,
decodedData.username,
decodedData.publicKey,
"access"
);

const cookieOptions: CookieOptions = {
httpOnly: true,
secure: NODE_ENV === "development" ? false : true,
expires: dayjs().add(1, "hours").toDate(),
};

res.cookie("accesstoken", newAccessToken, cookieOptions);

return res.status(200).json({
success: true,
message: "Tokens refreshed successfully.",
accesstoken: newAccessToken,
});
} catch (error) {
return res.status(500).json({
Expand Down
22 changes: 18 additions & 4 deletions src/controllers/auth/signup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { createJwtToken } from "../../utils/createJwtToken";
import { prisma } from "../../utils/prisma";
import * as bcrypt from "bcrypt";
import { clearFromRedis } from "../../redis";
import { NODE_ENV } from "../../config";
import dayjs from "dayjs";

export const signup = async (req: Request, res: Response) => {
try {
Expand Down Expand Up @@ -38,13 +40,13 @@ export const signup = async (req: Request, res: Response) => {
},
});

const acessToken = createJwtToken(
const accesstoken = createJwtToken(
user.userId,
user.username,
user.publicKey,
"access"
);
const refreshToken = createJwtToken(
const refreshtoken = createJwtToken(
user.userId,
user.username,
user.publicKey,
Expand All @@ -53,11 +55,23 @@ export const signup = async (req: Request, res: Response) => {

await clearFromRedis({ pattern: `userid_not:*` });

const cookieOptions: CookieOptions = {
httpOnly: true,
secure: NODE_ENV === "development" ? false : true,
};

res.cookie("accesstoken", accesstoken, {
...cookieOptions,
expires: dayjs().add(1, "hours").toDate(),
});
res.cookie("refreshtoken", refreshtoken, {
...cookieOptions,
expires: dayjs().add(14, "days").toDate(),
});

return res.status(201).send({
success: true,
message: "User created successfully",
accesstoken: acessToken,
refreshtoken: refreshToken,
user,
});
} catch (error: any) {
Expand Down
19 changes: 16 additions & 3 deletions src/controllers/auth/verifyRoute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,32 @@ import { Request, Response } from "express";
import jwt from "jsonwebtoken";
import { DecodedPayload } from "../../types/DecodedPayload";
import { ACCESS_TOKEN_SECRET } from "../../config";
import { checkItemInSetRedis } from "../../redis/check-Item-In-set";

export const verifyRoute = async (req: Request, res: Response) => {
const token = req.headers.authorization?.split(" ")?.[1];
const accesstoken = req.cookies.accesstoken;

if (!token) {
if (!accesstoken) {
return res
.status(401)
.json({ success: false, message: "token is missing" });
}

try {
const isBlacklisted = await checkItemInSetRedis(
"blacklistedTokens",
accesstoken
);

if (isBlacklisted) {
return res.status(401).json({
success: false,
message: "token is blacklisted",
});
}

const tokenDecoded = jwt.verify(
token,
accesstoken,
ACCESS_TOKEN_SECRET!
) as DecodedPayload;
return res.status(200).json({
Expand Down
34 changes: 23 additions & 11 deletions src/middlewares/verifyToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,49 @@ import { NextFunction, Response, Request } from "express";
import jwt from "jsonwebtoken";
import { DecodedPayload } from "../types/DecodedPayload";
import { ACCESS_TOKEN_SECRET } from "../config";
import { checkItemInSetRedis } from "../redis/check-Item-In-set";

export const verifyToken = async (
req: Request,
res: Response,
next: NextFunction
) => {
const token = req.headers.authorization?.split(" ")[1];
const accesstoken = req.cookies.accesstoken;

if (!token)
if (!accesstoken)
return res
.status(401)
.json({ success: false, message: "token is missing" });

try {
const isBlacklisted = await checkItemInSetRedis(
"blacklistedTokens",
accesstoken
);

if (isBlacklisted) {
return res.status(401).json({
success: false,
message: "token is blacklisted",
});
}

const tokenDecoded = jwt.verify(
token,
accesstoken,
ACCESS_TOKEN_SECRET!
) as DecodedPayload;
if (tokenDecoded) {
req.userId = tokenDecoded.userId;
req.username = tokenDecoded.username;
next();
} else {

if (!tokenDecoded) {
return res
.status(401)
.json({ success: false, message: "token is invalid" });
}

req.userId = tokenDecoded.userId;
req.username = tokenDecoded.username;
next();
} catch (err) {
console.log(err);
return res
.status(500)
.json({ success: false, message: "Internal server error" });
return res.status(500).json(err);
}
};
14 changes: 14 additions & 0 deletions src/redis/check-Item-In-set.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { redisClient } from "../utils/redis";

export const checkItemInSetRedis = async (
key: string,
item: string
): Promise<boolean> => {
try {
const isMember = await redisClient.sismember(key, item);
return isMember === 1;
} catch (error) {
console.error(`Error checking item in set ${key}:`, error);
throw error;
}
};
30 changes: 30 additions & 0 deletions src/redis/set-data-as-set.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { redisClient } from "../utils/redis";

export const setDataAsSetInRedis = async ({
key,
data,
expirationTimeInSeconds,
isString,
}: {
key: string;
data: Array<any>;
expirationTimeInSeconds?: number;
isString?: boolean;
}) => {
try {
const stringDataArray = data.map((d) => (isString ? d : JSON.stringify(d)));

if (expirationTimeInSeconds) {
return await redisClient
.multi()
.sadd(key, ...stringDataArray)
.expire(key, expirationTimeInSeconds)
.exec();
} else {
return await redisClient.sadd(key, ...stringDataArray);
}
} catch (error) {
console.error("Error setting data as set in Redis:", error);
throw error;
}
};
3 changes: 3 additions & 0 deletions src/routes/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
updateUsernameSchema,
} from "../schemas/authSchema";
import { loginToken } from "../controllers/auth/loginToken";
import { logout } from "../controllers/auth/logout";

export const authRouter: Router = Express.Router();

Expand All @@ -31,3 +32,5 @@ authRouter.post(
[verifyToken, validateData(updateUsernameSchema)],
updateUsername
);

authRouter.delete("/logout",logout)
Loading

0 comments on commit 882d1f9

Please sign in to comment.