-
Notifications
You must be signed in to change notification settings - Fork 26
Implementing Authentication using JWT, Bcrypt and GraphQL Nexus
You've finished coding the skeleton for your application, but it's missing one thing - authentication. This can be added using JSON Web Tokens and Bcrypt. The basis of this tutorial should be similar for most schema construction frameworks, but we will be using GraphQL Nexus. We're also using Prisma as our ORM, but any other ORM or database would work.
This tutorial assumes you have knowledge of GraphQL mutations, queries, resolvers and context- If you don't know GraphQL, How to GraphQL is a great place to start.
The final application will allow users to create an account and log in by storing and using a JSON Web Token. JWTs are strings that contain information to transfer between parties, and are a great way to authenticate users because they can securely store user information, and provide a digital signatures.
Our application will allow users to log in and register using these JWTs. On the backend, we will create a payload, add a JWT secret, and set up login and signup mutations to properly generate authorization headers. On the frontend, we'll pass an authorization token into our headers, and set up our queries to get the current logged in user.
First thing's first, we'll need to install Bcrypt and JSON Web Tokens!
yarn add bcrypt jsonwebtoken
Now you're ready to get startedโจ
We can set up our JWT secret- in our config.ts
file, the following was added:
export default {
...
jwt: {
JWT_SECRET: 'super-secret',
},
}
For us to properly return the token and user information to the requester, we need to set up a payload.
export const UserLoginPayload = objectType({
name: 'UserLoginPayload',
definition: t => {
t.field('user', {
type: 'User',
})
t.string('token')
},
})
What we're doing here is creating an object type named userLoginPayload
. We define the type as being able to return our User
field, along with the token generated when the user registers or logs in.
To set up user registration and login, we create two new mutation fields, userLogin
and userRegister
. We can set the return type to UserLoginPayload
to return the User
and a token
, and our arguments are the username and password collected from a form in the frontend. Here is what the mutations would look like in GraphQL Nexus:
export const userLogin = mutationField('userLogin', {
type: UserLoginPayload,
args: {
username: stringArg({ required: true }),
password: stringArg({ required: true }),
},
})
export const userRegister = mutationField('userRegister', {
type: UserLoginPayload,
args: {
username: stringArg({ required: true }),
password: stringArg({ required: true }),
},
})
After this, a resolver is added to the mutations.
export const userLogin = mutationField('userLogin', {
type: UserLoginPayload,
args: {
username: stringArg({ required: true }),
password: stringArg({ required: true }),
},
resolve: async (root, args, context, info) => {
try {
const { password, ...user } = await context.prisma.user({
where: {
userName: args.username,
},
})
var validpass = await bcrypt.compareSync(args.password, password)
if (validpass) {
const token = jwt.sign(user, config.jwt.JWT_SECRET)
return {
user: user,
token,
}
}
return null
} catch (e) {
console.log(e)
}
},
})
We've added our resolver. This might be a bit overwhelming, so let's break it up into pieces.
const { password, ...user } = await context.prisma.user({
where: {
userName: args.username,
},
})
Here, we're trying to get User
data. await context.prisma.users({where: {userName: args.username}
gets our User
information from the database, storing the information in password, ...user
. We've separated the password so it won't be included in our user
variable or the JSON Web Token data, as shown in the next step.
var validpass = await bcrypt.compareSync(args.password, password)
if (validpass) {
const token = jwt.sign(user, config.jwt.JWT_SECRET)
return {
user: user,
token,
}
}
return null
We use Bcrypt to compare to see if our password values are equal. If the passwords match, a JWT is generated using our JWT secret from the config file and user
. (If we didn't separate the password data beforehand, it would have been returned with the user data and stored in the JWT ๐ฑ!) Though at last, we're now returning our payload (the user
data along with the JWT)!
The process for registration is relatively similar.
export const userRegister = mutationField('userRegister', {
type: UserLoginPayload,
args: {
username: stringArg({ required: true }),
password: stringArg({ required: true }),
},
resolve: async (root, args, context) => {
try {
const existingUser = await context.prisma.user({
where: {
userName: args.username,
},
})
if (existingUser) {
throw new Error('ERROR: Username already used.')
}
var hash = bcrypt.hashSync(args.password, 10)
const { password, ...register } = await context.prisma.createUser({
userName: args.username,
password: hash,
})
const token = jwt.sign(register, config.jwt.JWT_SECRET)
return {
user: register,
token: token,
}
} catch (e) {
console.log(e)
return null
}
},
})
Let's break this up again.
const existingUser = await context.prisma.user({
where: {
userName: args.username,
},
})
if (existingUser) {
throw new Error('ERROR: Username already used.')
}
Previously, we queried to see if an username existed. This is relatively the same, only now we are throwing an error if something is returned, because each username should be unique.
var hash = bcrypt.hashSync(args.password, 10)
const { password, ...register } = await context.prisma.createUser({
userName: args.username,
password: hash,
})
We hash the password passed into the form using bcrypt, passing in the password and the salt length we want to generate. After that, the createUser
mutation makes a new user with our username and newly hashed password.
const token = jwt.sign(register, config.jwt.JWT_SECRET)
return {
user: register,
token: token,
}
The payload is generated and returned in the same way as the user login.
Our user can now log in and register! Now we can create a query and viewer field to return that information to the frontend.
Let's start out by adding the current user to the context.
export interface Context {
prisma: Prisma
currentUser: User
}
export default async ({ req }) => {
const currentUser = await getUser(
req.get('Authorization'),
config.jwt,
prisma,
)
return {
prisma,
currentUser
}
}
Here, we're adding the variable currentUser
of type User
to be exported from our Context
. We can use a getUser
function (we'll go over how to make this function in the next step- in summary, it returns our User
type) to return our user's information by passing in our token with req.get('Authorization')
(which fetches our token from our header), our JWT secret, and the Prisma client.
Because we want to query for user information in our application, we need to get our user's token from the headers.
export default async (authorization, secrets, prisma: Prisma) => {
const bearerLength = 'Bearer '.length
if (authorization && authorization.length > bearerLength) {
const token = authorization.slice(bearerLength)
const { ok, result } = await new Promise(resolve =>
jwt.verify(token, secrets.JWT_SECRET, (err, result) => {
if (err) {
resolve({
ok: false,
result: err,
})
} else {
resolve({
ok: true,
result,
})
}
}),
)
if (ok) {
const user = await prisma.user({
id: result.id,
})
return user
} else {
console.error(result)
return null
}
}
return null
}
Let's go through this step by step.
const bearerLength = 'Bearer '.length
if (authorization && authorization.length > bearerLength) {
const token = authorization.slice(bearerLength)
...
}
return null
}
Here we have some basic error checking to see if the token is longer than our Bearer
string- If it is, we can extract the token by slicing off the Bearer
string.
const { ok, result } = await new Promise(resolve =>
jwt.verify(token, secrets.JWT_SECRET, (err, result) => {
if (err) {
resolve({
ok: false,
result: err,
})
} else {
resolve({
ok: true,
result,
})
}
})
)
Now we are verifying the token with our secret, and resolving our promise with whether the passed in token was valid or not, along with the result
from our JWT (which is our user
type).
if (ok) {
const user = await prisma.user({
id: result.id,
})
return user
} else {
console.error(result)
return null
}
}
Lastly, if the token was valid, we query for the user with the ID we got from our token and return it!
We can create a viewer field and user query so we are able to query for the currently logged in user's information in our application.
t.string('getCurrentUser', {
resolve: async (root, args, context, info) => {
return context.prisma.user
},
})
We can create a new query, getCurrentUser
- this returns the value of what we got in our Context
function, making it so we can now easily query for whatever user is currently logged in!
Lastly, we should add a viewer
field to our query.
t.field('viewer', {
type: 'User',
nullable: true,
resolve: (root, args, context) => {
return context.currentUser
},
})
This simply returns the currentUser
that we added to our context.
Now that our backend is complete, we can implement a simple frontend solution using the resolvers we created in the backend.
const SIGNUP_MUTATION = gql`
mutation UserRegister($username: String!, $password: String!) {
userRegister(username: $username, password: $password) {
user {
id
userName
}
token
}
}
`;
Here is a simple signup mutation that creates a new user when the form is submitted. We are using the userRegister
function that we created on the backend, and simply passing in a username and password while returning any desired information.
<Mutation
mutation={SIGNUP_MUTATION}
onCompleted={data => _confirm(data)}
>
...
</Mutation>
Next, we can add the signup mutation to our Mutation
component provided by react-apollo
. When the mutation is completed, we call the function _confirm
.
_confirm = async data => {
const { token } = data.userLogin;
this._saveUserData(token);
};
_saveUserData = async token => {
try {
await AsyncStorage.setItem(AUTH_TOKEN, token);
} catch (e) {
console.log("ERROR: ", e);
}
};
What our _confirm
function does is take the data
we were returned from our mutation and extracts the token from it, passing it to _saveUserData
. This function stores the token
in AsyncStorage
(if you're not developing using Native, the token would be stored in LocalStorage
).
The process for logging in is extremely similar, we would just swap out our SIGNUP_MUTATION
with our LOGIN_MUTATION
.
As a side note, using localStorage
to store our JWT isnโt the best idea in production- you can read more about that here.
const authLink = setContext(async (_, { headers }) => {
const token = await AsyncStorage.getItem(AUTH_TOKEN);
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : ""
}
};
});
We are using apollo-link-context
's setContext
function to set the headers of our application. We are getting our authorization token from AsyncStorage
and then storing it in our header.
Because of all of our hard work, we can query for the user's information anywhere we want in the app- Yep, it's that simple!
const GET_USER = gql`
query getUser {
viewer {
id
}
}
`;
And with that, your authentication is now set up! We have now created resolvers to return the desired payload and can query for the current logged in user anywhere in the application. This tutorial was inspired by Spencer Carli's great tutorial, GraphQL Authentication with React Native & Apollo - give it a look if you'd like a more in depth look on the things we went over in this tutorial. If you have any questions or suggestions, feel free to create an issue or pull request. Thank you!