Skip to content

Commit

Permalink
feat: add jwt middleware
Browse files Browse the repository at this point in the history
  • Loading branch information
BoonHianLim committed Mar 14, 2024
1 parent bf0839b commit 1abb379
Show file tree
Hide file tree
Showing 18 changed files with 262 additions and 37 deletions.
6 changes: 5 additions & 1 deletion apps/challenges/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,8 @@ DO_RANKING_CALCULATION=true

// fill in the following for supabase (used in register and login)
SUPABASE_URL=
SUPABASE_ANON_KEY=
SUPABASE_ANON_KEY=

// set the secret to something (hey dont peek these are secrets! ba..baka!)
JWT_SECRET=abc
COOKIE_SECRET=abc
3 changes: 2 additions & 1 deletion apps/challenges/mock/mongo-init.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ db.createCollection("seasons");
db.createCollection("rankings");
db.createCollection("questions");
db.createCollection("submissions");
db.createCollection("users");
db.createCollection("users");
db.createCollection("tokens");
1 change: 1 addition & 0 deletions apps/challenges/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"@supabase/supabase-js": "^2.39.7",
"@types/express": "^4.17.21",
"babel-jest": "^29.7.0",
"cookie-parser": "^1.4.6",
"cron": "^3.1.6",
"cross-env": "^7.0.3",
"dotenv": "^16.3.1",
Expand Down
34 changes: 30 additions & 4 deletions apps/challenges/src/controllers/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import asyncHandler from "express-async-handler";
import { Request, Response } from "express";
import { z } from "zod";
import AuthService from "../service/authService";
import { accessTokenMaxAgeSeconds, refreshCookieMaxAgeSeconds, secondInMilliseconds } from "../model/constants";


const oauthSignIn = asyncHandler(async (req: Request, res: Response) => {
Expand All @@ -11,6 +12,7 @@ const oauthSignIn = asyncHandler(async (req: Request, res: Response) => {
const resp = await AuthService.oauthSignIn(redirectURL);
res.status(resp.status).json({ resp });
} catch (error) {
console.log("AuthService.oauthSignIn", error);
res.status(500).json({ message: 'Internal Server Error' });
}
});
Expand All @@ -32,24 +34,48 @@ const authCallback = asyncHandler(async (req: Request, res: Response) => {
nextString = z.string().optional().parse(next);

const { accessToken, refreshToken } = await AuthService.oauthCallback(codeString, nextString);
append = `#access_token=${accessToken}&refresh_token=${refreshToken}&status=auth_success`;

append = `#access_token=${accessToken}&status=auth_success`;
res.cookie('refresh_token', refreshToken, {
httpOnly: true,
maxAge: refreshCookieMaxAgeSeconds * secondInMilliseconds,
signed: true,
});
res.cookie('access_token', accessToken, {
httpOnly: true,
maxAge: accessTokenMaxAgeSeconds * secondInMilliseconds,
signed: true,
});
} catch (err) {
console.error(err);
} finally {
if (nextString) {
res.redirect(303, nextString + append);
res.redirect(302, nextString + append);
} else {
res.status(append === '#status=auth_failure' ? 400 : 200).json({ message: append });
}
}

});

const refreshToken = asyncHandler(async (req: Request, res: Response) => {
try {
const userID = req.params.userID;
const token = await AuthService.refreshToken(userID);
res.cookie('access_token', token, {
httpOnly: true,
maxAge: accessTokenMaxAgeSeconds * secondInMilliseconds,
signed: true,
});
res.status(201).json(token);
} catch (err) {
console.error(err);
res.status(500).json({ message: "Internal Server Error" })
}
})

const AuthController = {
oauthSignIn,
authCallback,
refreshToken,
}

export { AuthController as default };
28 changes: 21 additions & 7 deletions apps/challenges/src/controllers/user.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,34 @@
import UserService from "../service/userService";
import asyncHandler from "express-async-handler";
import { Request, Response } from "express";
import { StatusCodeError } from "../types/types";

const createUser = asyncHandler(async (req: Request, res: Response) => {
const { name, email } = req.body;
const getUser = asyncHandler(async (req: Request, res: Response) => {
const { userID } = req.params;

try {
const user = await UserService.createUser(name, email);
res.status(201).json(user);
} catch (error) {
res.status(500).json({ message: 'Internal Server Error' });
const user = await UserService.getUserByID(userID);
res.status(200).json(user);
} catch (err) {
if (err instanceof StatusCodeError) {
res.status(err.status).json({ message: err.message });
} else {
res.status(500).json({ message: "Internal Server Error" })
}

}
});

const checkTokens = asyncHandler(async (req: Request, res: Response) => {
const token = req.signedCookies;

console.log(token);

res.status(200);
});
const UserController = {
createUser
getUser,
checkTokens,
}

export { UserController as default };
5 changes: 4 additions & 1 deletion apps/challenges/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { connectDB } from "./config/db";
import { CronJob } from "cron";
import { rankingCalculation } from "./tasks/rankingCalculation";
import { SupabaseService } from "./utils/supabase";
import cookieParser from "cookie-parser";
dotenv.config({ path: "../.env"});

// Database
Expand All @@ -17,7 +18,7 @@ SupabaseService.initClient();

const app: Express = express();
const port = process.env.PORT || 3000;

const cookieSecret = process.env.COOKIE_SECRET || "";
// Middleware
app.use(express.json());
app.use(function(req, res, next) {
Expand All @@ -26,6 +27,8 @@ app.use(function(req, res, next) {
res.header("Access-Control-Allow-Headers", "*");
next();
});
app.use(cookieParser(cookieSecret));

// Routes
app.get("/ping", (req: Request, res: Response) => {
res.status(200).json({ message: "pong" });
Expand Down
12 changes: 8 additions & 4 deletions apps/challenges/src/middleware/jwtMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,21 @@ import jwt from "jsonwebtoken";
import { Request, Response, NextFunction } from "express";

const jwtMiddleware = (req: Request, res: Response, next: NextFunction) => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
const token = req.signedCookies.access_token;

if (token == null) {
return res.sendStatus(401);
}

jwt.verify(token, process.env.JWT_SECRET || "", (err, user) => {
jwt.verify(token, process.env.JWT_SECRET || "", (err, tokenContent: any) => {
if (err) {
return res.sendStatus(403);
return res.sendStatus(401);
}

req.params.userID = tokenContent.id;
req.params.email = tokenContent.email;


next();
});
}
Expand Down
38 changes: 38 additions & 0 deletions apps/challenges/src/middleware/jwtRefreshMiddleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// jwt middleware for express
import jwt from "jsonwebtoken";
import { Request, Response, NextFunction } from "express";
import TokenRepo from "../repo/tokenRepo";
import { refreshCookieMaxAgeSeconds, secondInMilliseconds } from "../model/constants";

const jwtRefreshMiddleware = (req: Request, res: Response, next: NextFunction) => {
const token = req.signedCookies.refreshToken;

if (token == null) {
return res.sendStatus(401);
}

jwt.verify(token, process.env.JWT_SECRET || "", (err, tokenContent: any) => {
if (err) {
return res.sendStatus(401);
}

const token = TokenRepo.extendRefreshToken(tokenContent.id)
if (token == null) {
res.status(401).json({message: "Invalid refresh token"})
return;
}

res.cookie('refresh_token', token, {
httpOnly: true,
maxAge: refreshCookieMaxAgeSeconds * secondInMilliseconds,
signed: true,
});

req.params.userID = tokenContent.id;
req.params.email = tokenContent.email;

next();
});
}

export default jwtRefreshMiddleware;
6 changes: 6 additions & 0 deletions apps/challenges/src/model/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const secondInMilliseconds = 1000;
const minuteInSeconds = 60;
const dayInSeconds = 86400;

export const accessTokenMaxAgeSeconds = 10 * minuteInSeconds;
export const refreshCookieMaxAgeSeconds = 7 * dayInSeconds;
7 changes: 4 additions & 3 deletions apps/challenges/src/model/season.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ export interface GetSeasonResp {
questions: QuestionModel[];
}

// TODO: season should store simple question model for faster retrieval
export interface SeasonModel {
_id: mongoose.Types.ObjectId;
title: string;
startDate: Date;
endDate: Date;
title: string;
startDate: Date;
endDate: Date;
createdAt: Date;
updatedAt: Date;
}
Expand Down
28 changes: 28 additions & 0 deletions apps/challenges/src/model/token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import mongoose, { Schema } from 'mongoose';

export interface TokenModel {
_id: mongoose.Types.ObjectId;
jwt: string;
userID: mongoose.Types.ObjectId;
expiry: Date;
}

const tokenSchema: Schema<TokenModel> = new Schema({
jwt: {
type: String,
unique: true,
},
expiry: {
type: Date,
required: [true, 'Please add a expiry date']
},
userID: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: [true, 'Please add a user ID']
},
})

const Token = mongoose.model<TokenModel>('Token', tokenSchema);

export { Token as default }
41 changes: 41 additions & 0 deletions apps/challenges/src/repo/tokenRepo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { refreshCookieMaxAgeSeconds, secondInMilliseconds } from "../model/constants";
import Token, { TokenModel } from "../model/token"
import mongoose from 'mongoose';

const getRefreshToken = async (userID: mongoose.Types.ObjectId): Promise<TokenModel | null> => {
const token = await Token.findOne({
userID: userID,
expiry: { $gt: new Date() }
});
return token;
}

const saveRefreshToken = async (
token: TokenModel
): Promise<TokenModel | null> => {
const dbToken = await Token.create(token);
await dbToken.save();
return dbToken;
}


const extendRefreshToken = async (userID: mongoose.Types.ObjectId): Promise<TokenModel | null> => {
const now = new Date();
const newExpiry = new Date(now.getTime() + refreshCookieMaxAgeSeconds * secondInMilliseconds);

const token = await Token.findOneAndUpdate({
userID: userID,
expiry : { $gt: new Date() }
}, {
$set: { "expiry": newExpiry }
}, { new: true })
return token
}

const TokenRepo = {
getRefreshToken,
saveRefreshToken,
extendRefreshToken,
}

export { TokenRepo as default }
3 changes: 3 additions & 0 deletions apps/challenges/src/routes/auth.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import Express from "express";
import AuthController from "../controllers/auth";
import jwtRefreshMiddleware from "../middleware/jwtRefreshMiddleware";

const router = Express.Router();

router.post("/refresh", jwtRefreshMiddleware, AuthController.refreshToken);

router.post("/oauth/signin", AuthController.oauthSignIn);
router.get("/oauth/callback", AuthController.authCallback);
// router.get("/:userID/rankings", SeasonController.getUserAllSeasonRankings);
Expand Down
4 changes: 3 additions & 1 deletion apps/challenges/src/routes/user.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import Express from "express";
import UserController from "../controllers/user";
import jwtMiddleware from "../middleware/jwtMiddleware";

const router = Express.Router();

router.get("/:userID", UserController.createUser);
router.get("/", jwtMiddleware, UserController.getUser);
router.get("/token", UserController.checkTokens);

export { router as default };
Loading

0 comments on commit 1abb379

Please sign in to comment.