Unlocking Secure User Authentication with JSON Web Tokens and Bcrypt
Setting Up the Foundation
To get started, we’ll create a new Apollo Server project using Prisma as the ORM. First, let’s set up the project directory with a package.json
file.
{
"name": "apollo-server-prisma",
"version": "1.0.0",
"scripts": {
"start": "node index.js"
}
}
Next, we’ll create an index.js
file to bootstrap the application.
const { ApolloServer } = require('apollo-server');
const { PrismaClient } = require('@prisma/prisma-client');
const prisma = new PrismaClient();
const server = new ApolloServer({
//...
});
server.listen().then(({ url }) => {
console.log(`Server ready at ${url}`);
});
Configuring Prisma and PostgreSQL
To use Prisma as the ORM, we’ll need to have Docker installed. We’ll configure PostgreSQL as the database of choice on the Docker host. After running the necessary commands, we’ll have the required files generated from the datamodel.prisma
file.
docker run -d --name prisma-postgres -e POSTGRES_PASSWORD=mysecretpassword -p 5432:5432 postgres
Restructuring the Project
Now that we have Prisma set up, let’s restructure our project by creating schema.js
and resolvers.js
files in the project root. We’ll also update our index.js
file to import the Prisma instance.
// schema.js
const { gql } = require('apollo-server');
const typeDefs = gql`
type Query {
me: User
}
type Mutation {
signUp(name: String!, email: String!, password: String!): AuthPayload!
login(email: String!, password: String!): AuthPayload!
}
type AuthPayload {
token: String!
user: User!
}
type User {
id: ID!
name: String!
email: String!
}
`;
module.exports = typeDefs;
Installing Required Libraries
To proceed, we’ll need to install a few libraries, including bcrypt
and jsonwebtoken
. We’ll also add a script to our package.json
file to enable us to start our server with ease.
npm install bcrypt jsonwebtoken
Updating the Datamodel and Schema
Next, we’ll update our datamodel.prisma
file to include user authentication fields.
model User {
id String @id @default(cuid())
name String
email String @unique
password String
}
We’ll then update our schema.js
file to include mutations for signing up and logging in users.
// resolvers.js
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const resolvers = {
Mutation: {
async signUp(parent, { name, email, password }) {
const hashedPassword = await bcrypt.hash(password, 10);
const user = await prisma.createUser({ name, email, password: hashedPassword });
const token = jwt.sign({ userId: user.id }, 'ysecretkey', { expiresIn: '1h' });
return { token, user };
},
async login(parent, { email, password }) {
const user = await prisma.getUser({ email });
if (!user) {
throw new Error('Invalid email or password');
}
const isValid = await bcrypt.compare(password, user.password);
if (!isValid) {
throw new Error('Invalid email or password');
}
const token = jwt.sign({ userId: user.id }, 'ysecretkey', { expiresIn: '1h' });
return { token, user };
},
},
};
module.exports = resolvers;
Validating User Identity
To validate the user’s identity, we’ll modify the context function to pass the token from the client to the server. We’ll create an authenticate.js
file to handle user authentication and update our resolvers to reflect these changes.
// authenticate.js
const jwt = require('jsonwebtoken');
const authenticate = async (context) => {
const token = context.request.headers.authorization;
if (!token) {
throw new Error('Not authorized');
}
const { userId } = jwt.verify(token, 'ysecretkey');
return { userId };
};
module.exports = authenticate;
Decoding Tokens and Authorization
To make our decoded token more versatile, we’ll update it to handle authorization. We’ll also supply our login token via the HTTP HEADERS section in the GraphQL playground.
mutation {
login(email: "[email protected]", password: "mypassword") {
token
}
}
In the HTTP HEADERS section:
Authorization: Bearer YOUR_TOKEN_HERE