GraphQL and Node js — Part 2

Tókos Bence
8 min readJan 15, 2024

--

GraphQL and Node Js

Introduction

Welcome back to our GraphQL journey! In part 2, we’re taking the next step by introducing authentication with GraphQL and showcasing the process of crafting order mutations. Let’s dive in!

As you saw in the first part we have a query for orders but to try that, first, we need to save our orders into the database. Let’s make the createOrder mutation:

//Orders mutation
createOrder: {
type: GraphQLString,
args: {
userId: { type: GraphQLString },
firstName: { type: GraphQLString },
lastName: { type: GraphQLString },
address: { type: GraphQLString },
city: { type: GraphQLString },
country: { type: GraphQLString },
zipCode: { type: GraphQLString },
totalAmount: { type: GraphQLFloat },
items: { type: GraphQLString },
},
async resolve(parent, args, req) {
console.log(args);
const newOrder = new Order({
userId: args.userId,
firstName: args.firstName,
lastName: args.lastName,
address: args.address,
city: args.city,
country: args.country,
zipCode: args.zipCode,
totalAmount: args.totalAmount,
items: args.items,
createdDate: new Date().toLocaleDateString(),
});

await newOrder.save();
const data = {
message: "success",
};

return JSON.stringify(data);
},
},

Note! Here in the items field we expect a stringify array.

Here as you see we get back the message that we defined in our mutation but you can retrieve any order attributes.

To generate a password hash, we must first install the bcrypt library. Hashing the password is a crucial method for securely storing passwords in the database, effectively preventing unauthorized individuals from accessing the actual password and adding an extra layer of security.

npm install bcrypt

Now let’s create the user mutations. Add the following code under the other mutations:

//don't forget to import the bcrypt package
const bcrypt = require("bcrypt")


//Users mutations
createUser: {
type: GraphQLString,
args: {
username: { type: GraphQLString },
email: { type: GraphQLString },
password: { type: GraphQLString },
isAdmin: { type: graphql.GraphQLBoolean },
},
async resolve(parent, args) {
console.log(args);
const newUser = new User({
username: args.username,
email: args.email,
password: args.password,
isAdmin: args.isAdmin,
});

const user = await User.findOne({ email: newUser.email });
if (user) {
throw new Error("Already in db");
}

const salt = await bcrypt.genSalt(10);
newUser.password = await bcrypt.hash(newUser.password, salt);

await newUser.save();
const token = newUser.generateAuthToken();

const data = {
token: token,
id: newUser.id,
isAdmin: newUser.isAdmin,
};
return JSON.stringify(data);
},
},

loginUser: {
type: GraphQLString,
args: {
email: { type: GraphQLString },
password: { type: GraphQLString },
},
async resolve(parent, args) {
const user = await User.findOne({ email: args.email });
if (!user) {
throw new Error("Not user with that email");
}
const validPassword = await bcrypt.compare(
args.password,
user.password
);

if (!validPassword) {
throw new Error("Invalid password");
}
const token = user.generateAuthToken();
const data = {
token: token,
userId: user.id,
isAdmin: user.isAdmin,
};
return JSON.stringify(data);
},
},

1. createUser Mutation:

  • This mutation is used for creating a new user in your application.
  • It accepts several input arguments, including username, email, password, and isAdmin.
  • In the resolve function, a new user object is created based on the provided arguments.
  • It checks if a user with the same email already exists in the database. If one is found, it throws an error.
  • It generates a random “salt” and uses it to hash the user’s password for security.
  • The new user is then saved to the database, and an authentication token is generated for the user.
  • The result of this mutation is a JSON string containing the authentication token, user ID, and isAdmin status.

2. loginUser Mutation:

  • This mutation is used for authenticating and logging in a user.
  • It takes email and password as input arguments.
  • In the resolve function, it searches for a user with the provided email in the database. If no user is found, it throws an error.
  • It then checks if the provided password matches the stored hashed password using bcrypt.compare. If the password is invalid, it throws an error.
  • If the user is successfully authenticated, an authentication token is generated for the user.
  • The result of this mutation is a JSON string containing the authentication token, user ID, and isAdmin status.

These mutations are fundamental for user registration and login, ensuring the secure storage and validation of user credentials. The generated authentication tokens can be used to authorize and authenticate users in your application.

But before using these methods we need to little bit modify our user models. First, install the jsonwebtoken library.

npm i jsonwebtoken

The jsonwebtoken npm package is a library for creating and verifying JSON Web Tokens (JWT). JWTs are a compact, self-contained way to represent information between parties securely. They are often used for user authentication and authorization in web applications. With jsonwebtoken, you can create JWTs that encode user data and claims, and verify them to ensure their authenticity and integrity. This package is commonly used for implementing user authentication and authorization in web applications.

Let’t add this function for the user model:

userSchema.methods.generateAuthToken = function () {
const token = jwt.sign(
{ _id: this._id, isAdmin: this.isAdmin },
"jwtPrivateKey"
);
return token;
};

1. userSchema.methods.generateAuthToken = function () {

  • This function is added to the userSchema as a method, which means it can be called on a user instance.

2. const token = jwt.sign( { _id: this._id, isAdmin: this.isAdmin }, "jwtPrivateKey");

  • It uses the jwt.sign method from the jsonwebtoken library to create a JWT.
  • The first argument to jwt.sign is an object containing the data you want to include in the token. In this case, it typically includes the user's ID (_id) and their admin status (isAdmin).
  • The second argument is a secret key, usually referred to as a “JWT secret.” It should be stored securely and kept secret, typically in an environment variable. It’s important to use a strong, complex key for security.

3. return token;

  • Finally, the function returns the generated token, which can be used for authenticating and authorizing the user. The token contains the user’s data and a digital signature to verify its authenticity.

We defined the functions so now we can try the createUser mutation which is the sign-up function.

mutation{
createUser(username:"testUsername",
email:"test@gmail.com",
password: "testPassword",
isAdmin: false)
}

Voilà! We’ve obtained the token, user ID, and the isAdmin field.

Next, let’s test the loginUser mutation:

mutation{
loginUser( email:"test@gmail.com", password: "testPassword")
}

And we receive the same data as in the signIn method. Our final step is to create the authentication middleware, which can be used to secure our mutations and queries.

To do this, let’s create a folder named “middleware” and add a file called “auth.js”

const jwt = require("jsonwebtoken");
const config = require("config");

function isAuth(req, res, next) {
const authHeader = req.get("Authorization");
if (!authHeader) {
req.isAuth = false;
return next();
}

const token = authHeader.split(" ")[1];
if (!token || token === "") {
{
req.isAuth = false;
return next();
}
}

try {
const decoded = jwt.verify(token, config.get("jwtPrivateKey"));
req.user = decoded;
req.isAuth = true;
next();
} catch (ex) {
req.isAuth = false;
next();
}
}

module.exports = isAuth;
  • When a request arrives, the isAuth function first attempts to retrieve the "Authorization" header from the request. This header often contains a JSON Web Token (JWT) that verifies a user's identity.
  • If there’s no “Authorization” header, it means the user is not authenticated. In this case, the req.isAuth property is set to false, and the request is allowed to continue through the subsequent middleware or route handlers without authentication.
  • If an “Authorization” header exists, the function extracts the JWT from the header, typically in the format “Bearer <token>”. It checks if the token exists and is not empty.
  • If no valid token is found, or if the token is empty, the user is considered unauthenticated. The req.isAuth property is set to false, and the request continues without authentication.
  • If a valid token is present, the function proceeds to verify the token’s authenticity using jwt.verify. This method checks the token's validity and ensures it hasn't been tampered with.
  • If the token is successfully verified, it is decoded to access user-related data, such as the user’s ID and potentially other information. This data is attached to the req object, making it available for use in subsequent parts of the application.
  • The req.isAuth property is set to true, indicating that the user is authenticated.
  • Regardless of whether the token verification succeeds or not, the middleware always calls next() to allow the request to continue processing through subsequent middleware or reach the designated route handler.
  • If there’s an error during token verification, such as an invalid or expired token, the middleware catches the exception. In this case, the user is considered unauthenticated (req.isAuth = false), and the request continues to be processed.

In essence, this middleware ensures that incoming requests are checked for valid JWTs, and authenticated users can access protected resources while unauthenticated users can proceed without authentication.

Add the middleware function to our main index.js file:

const isAuth = require("./middleware/auth");
...
//before our endpoint
app.use(isAuth);

See it in action. If you want to secure your “getAllProduct” query, you can add the following line (Note: It’s important to add the “req” attribute to the resolve function):

getAllProduct: {
type: new GraphQLList(ProductType),
args: { id: { type: GraphQLString } },
async resolve(parent, args ,req) {

if (!req.isAuth) {
throw new Error("Unauthenticated");
}

const productList = await Product.find();
return productList;
},
},

Here’s what we’ve learned:

  1. GraphQL: We’ve delved into GraphQL, a powerful query language and runtime for APIs, understanding how it enables efficient data retrieval and modification by allowing clients to request precisely what they need.
  2. User Authentication: We’ve implemented user authentication, a fundamental aspect of web applications, using JSON Web Tokens (JWTs). We’ve seen how to create, verify, and use JWTs to authenticate and authorize users securely.
  3. Database Interaction: We’ve interacted with databases, including MongoDB, for storing and retrieving data. We’ve learned how to create, update, and delete data using GraphQL mutations.
  4. Middleware: We’ve explored the concept of middleware in Node.js, understanding how it allows us to execute code during the request-response cycle, enabling tasks like authentication and data processing.
  5. Security Best Practices: We’ve emphasized the importance of security best practices, such as hashing user passwords and keeping sensitive information, like JWT secrets, confidential and securely stored.

Thank you for reading this brief tutorial on connecting Node.js and GraphQL. I hope you found it enjoyable and insightful. If you have any questions or find anything confusing, you can refer to the GitHub repository here. Additionally, if you’re interested in learning more about MERN stack applications, you can explore my other tutorials.

See you soon!

--

--

Tókos Bence

Hi everyone! I'm an enthusiastic full-stack developer. Please feel free to reach out to me via email (tokosbex@gmail.com) or Twitter (@tokosbex).