Skip to content

Commit

Permalink
implement auth with jwt
Browse files Browse the repository at this point in the history
Signed-off-by: UtkarshAhuja2003 <[email protected]>
  • Loading branch information
UtkarshAhuja2003 committed Oct 16, 2024
1 parent 9b81bf4 commit 4bd00f9
Show file tree
Hide file tree
Showing 8 changed files with 310 additions and 37 deletions.
19 changes: 12 additions & 7 deletions users/src/apollo.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
const { ApolloServer } = require("@apollo/server");
const typeDefs = require("./schemas/userSchema");

const resolvers = {
Query: {
hello: () => "Hello, world!"
}
};
const { registerUser, loginUser, getCurrentUser, logoutUser, refreshAccessToken } = require("./resolvers/user");

const createApolloServer = async () => {
const server = new ApolloServer({
typeDefs,
resolvers,
resolvers: {
Query: {
getCurrentUser,
},
Mutation: {
registerUser,
loginUser,
logoutUser,
refreshAccessToken
},
},
});

await server.start();
Expand Down
4 changes: 1 addition & 3 deletions users/src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,7 @@ const startServer = async () => {
const server = await createApolloServer();
app.use("/user",
expressMiddleware(server, {
context: ({ req }) => {
return req;
}
context: ({ req, res }) => ({ req, res })
})
);
}
Expand Down
41 changes: 41 additions & 0 deletions users/src/middlewares/auth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
const jwt = require("jsonwebtoken");
const { GraphQLResponse } = require("../utils/GraphQLResponse");
const User = require("../models/user");

/**
* Middleware to verify JWT token in GraphQL context.
* @param {Object} context - GraphQL context object
* @returns {Promise<GraphQLResponse>} - GraphQLResponse object
*/
const verifyJWT = async (context) => {
let token;
if (!context.req.headers && !context.token) {
return new GraphQLResponse(null, false, "Unauthorized request", ["Token missing"], new Error().stack);
}
else if(context.req.headers) {
token = context.req.headers.authorization?.replace("Bearer ", "");
}
else {
token = context.token;
}

if (!token) {
return new GraphQLResponse(null, false, "Unauthorized request", ["Token missing"], new Error().stack);
}

try {
const decodedToken = jwt.verify(token, process.env.ACCESS_TOKEN_SECRET);
const user = await User.findById(decodedToken._id);

if (!user) {
return new GraphQLResponse(null, false, "Invalid access token", ["User not found"], new Error().stack);
}

context.user = user;
return new GraphQLResponse(user, true, "User authenticated successfully");
} catch (error) {
return new GraphQLResponse(null, false, "Invalid access token", ["User not found"], new Error().stack);
}
};

module.exports = verifyJWT;
179 changes: 179 additions & 0 deletions users/src/resolvers/user.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
const jwt = require("jsonwebtoken");
const User = require("../models/user");
const verifyJWT = require("../middlewares/auth");
const { GraphQLResponse } = require("../utils/GraphQLResponse");
const { validateUserInput } = require("../validators/userValidator");

const registerUser = async (_, args, context) => {
const { name, email, password } = args.input;

try {
validateUserInput(args.input, "register");
const existingUser = await User.findOne({ email });
if (existingUser) {
return new GraphQLResponse(null, false, "User already exists", ["User with this email already exists"], new Error().stack);
}

const user = await User.create({ email, password, name });
await user.save();

const { accessToken, refreshToken } = await generateAccessAndRefreshToken(user._id);
const createdUser = await User.findById(user._id).select("-password -refreshToken");
if (!createdUser) {
return new GraphQLResponse(null, false, "User registration unsuccessful", ["Failed to register user"], new Error().stack);
}

const cookieOptions = {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "Strict",
};
context.res.cookie("accessToken", accessToken, { ...cookieOptions, maxAge: 1000 * 60 * 15 });
context.res.cookie("refreshToken", refreshToken, { ...cookieOptions, maxAge: 1000 * 60 * 60 * 24 * 7 });

return new GraphQLResponse(createdUser, true, "User registered successfully");
} catch (error) {
return new GraphQLResponse(null, false, error.message, [error.message], error.stack);
}
};

const loginUser = async (_, args, context) => {
const { email, password } = args.input;

try {
validateUserInput(args.input, "login");

const user = await User.findOne({ email });
if (!user) {
return new GraphQLResponse(null, false, "User not found", ["User with this email not found"], new Error().stack);
}

const isPasswordValid = await user.comparePassword(password);
if (!isPasswordValid) {
return new GraphQLResponse(null, false, "Invalid credentials", ["Incorrect password"], new Error().stack);
}

const { accessToken, refreshToken } = await generateAccessAndRefreshToken(user._id);
const loggedInUser = await User.findById(user._id).select("-password -refreshToken");

if (!loggedInUser) {
return new GraphQLResponse(null, false, "User login unsuccessful", ["Failed to log in user"], new Error().stack);
}

const cookieOptions = {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "Strict",
};
context.res.cookie("accessToken", accessToken, { ...cookieOptions, maxAge: 1000 * 60 * 15 });
context.res.cookie("refreshToken", refreshToken, { ...cookieOptions, maxAge: 1000 * 60 * 60 * 24 * 7 });

return new GraphQLResponse(loggedInUser, true, "User logged in successfully");

} catch (error) {
return new GraphQLResponse(null, false, error.message, [error.message], error.stack);
}
};

const getCurrentUser = async (_, __, context) => {
try {
const jwtResponse = await verifyJWT(context);
if(!jwtResponse.success) return jwtResponse;

const user = jwtResponse.data;
if (!user) {
return new GraphQLResponse(null, false, "User not found", ["User not authenticated"], new Error().stack);
}

const { _id, name, email } = user;
return new GraphQLResponse({ _id, name, email }, true, "User retrieved successfully");
} catch (error) {
return new GraphQLResponse(null, false, error.message, [error.message], error.stack);
}
};

const logoutUser = async (_, args, context) => {
try {
const jwtResponse = await verifyJWT(context);
if(!jwtResponse.success) return jwtResponse;

const user = jwtResponse.data;
if (!user) {
return new GraphQLResponse(null, false, "User not found", ["User not authenticated"], new Error().stack);
}

await User.findByIdAndUpdate(user._id, { refreshToken: "" });
context.res.cookie("accessToken", "", { maxAge: 0 });
context.res.cookie("refreshToken", "", { maxAge: 0 });

return new GraphQLResponse(null, true, "User logged out successfully");
} catch (error) {
return new GraphQLResponse(null, false, error.message, [error.message], error.stack);
}
};

const generateAccessAndRefreshToken = async (userId) => {
try {
const user = await User.findById(userId);

if (!user) {
throw new Error("User not found");
}

const accessToken = user.generateAccessToken();
const refreshToken = user.generateRefreshToken();

user.refreshToken = refreshToken;
await user.save();

return { accessToken, refreshToken };
} catch (error) {
throw new Error(error.message || "Failed to generate tokens");
}
};

const refreshAccessToken = async (_, args, context) => {
const { incomingRefreshToken } = args.input;
if (!incomingRefreshToken) {
return new GraphQLResponse(null, false, "Invalid refresh token", ["Refresh token is missing"], new Error().stack);
}

try {
const decodedToken = jwt.verify(incomingRefreshToken, process.env.REFRESH_TOKEN_SECRET);
const user = await User.findById(decodedToken._id);

if (!user) {
return new GraphQLResponse(null, false, "User not found", ["User with this token not found"], new Error().stack);
}

if (incomingRefreshToken !== user.refreshToken) {
return new GraphQLResponse(null, false, "Refresh token is expired or used", ["Invalid or expired refresh token"], new Error().stack);
}

try {
const { accessToken, refreshToken } = await generateAccessAndRefreshToken(user._id);
const cookieOptions = {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "Strict",
};
context.res.cookie("accessToken", accessToken, { ...cookieOptions, maxAge: 1000 * 60 * 15 });
context.res.cookie("refreshToken", refreshToken, { ...cookieOptions, maxAge: 1000 * 60 * 60 * 24 * 7 });

return new GraphQLResponse(null, true, "Tokens refreshed successfully");
} catch (tokenError) {
return new GraphQLResponse(null, false, "Token generation failed", [tokenError.message], tokenError.stack);
}

} catch (error) {
return new GraphQLResponse(null, false, error.message, [error.message], error.stack);
}
};

module.exports = {
refreshAccessToken,
registerUser,
loginUser,
logoutUser,
getCurrentUser
};
42 changes: 37 additions & 5 deletions users/src/schemas/userSchema.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,50 @@
const commonResponseFields = `
success: Boolean!
message: String!
success: Boolean
message: String
errors: [String]
stack: String
`;

const typeDefs = `
type Response {
type User {
_id: ID
email: String
name: String
}
type UserResponse {
${commonResponseFields}
data: String
data: User
}
type MessageResponse {
${commonResponseFields}
}
input RegisterInput {
name: String!
email: String!
password: String!
}
input LoginInput {
email: String!
password: String!
}
input RefreshTokenInput {
incomingRefreshToken: String!
}
type Query {
hello: Response
getCurrentUser: UserResponse!
}
type Mutation {
registerUser(input: RegisterInput!): UserResponse!
loginUser(input: LoginInput!): UserResponse!
logoutUser: MessageResponse!
refreshAccessToken(input: RefreshTokenInput!): MessageResponse!
}
`;

Expand Down
17 changes: 0 additions & 17 deletions users/src/utils/GraphQLErrorResponse.js

This file was deleted.

13 changes: 8 additions & 5 deletions users/src/utils/GraphQLResponse.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,18 @@ class GraphQLResponse {
/**
* @param {any} data - The data to be returned in the response.
* @param {string} message - A success message.
* @param {string[]} errors - An array of errors.
* @param {string} stack - The error stack
* @param {boolean} success - A boolean indicating if the operation was successful.
*/
constructor(data, message = "Success") {
constructor(data, success = false, message = "Success", errors = null, stack = null) {
this.data = data;
this.message = message;
this.success = true;
this.errors = null;
this.stack = null;
this.success = success;
this.errors = errors;
this.stack = stack;
}
}

export { GraphQLResponse };
module.exports = { GraphQLResponse };

32 changes: 32 additions & 0 deletions users/src/validators/userValidator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
const validator = require("validator");
const { GraphQLResponse } = require("../utils/GraphQLResponse");

const validateUserInput = (input, type) => {
const { name, email, password } = input;

if (!email || !password) {
throw new GraphQLResponse(null, false, "Email and password are required", ["Email and password must be provided"], new Error().stack);
}
if (!validator.isEmail(email)) {
throw new GraphQLResponse(null, false, "Invalid email address", ["The provided email is not valid"], new Error().stack);
}
if (password.length < 8) {
throw new GraphQLResponse(null, false, "Password must be at least 8 characters long", ["Password must be at least 8 characters"], new Error().stack);
}
if (!/[a-z]/.test(password) || !/[A-Z]/.test(password) || !/\d/.test(password) || !/[!@#$%^&*()]/.test(password)) {
throw new GraphQLResponse(null, false, "Password must contain one lowercase, uppercase, number, and special character", ["Password must meet complexity requirements"], new Error().stack);
}

if (type === "register") {
if (!name) {
throw new GraphQLResponse(null, false, "All fields are required", ["Name is required for registration"], new Error().stack);
}
if (name.length < 2 || name.length > 200) {
throw new GraphQLResponse(null, false, "Name should be between 2 and 200 characters", ["Name length is invalid"], new Error().stack);
}
}
};

module.exports = {
validateUserInput
};

0 comments on commit 4bd00f9

Please sign in to comment.