In this section, you’re going to implement signup and login functionality that allows your users to authenticate against your GraphQL server.
User
type to your data modelThe first thing you need is a way to represent user data in the database. You can achieve that by adding a User
type to the data model.
You also want to add a relation between the User
and the already existing Link
type to express that Link
s are posted by User
s.
You’re adding a new relation field called postedBy
to the Link
type that points to a User
instance. The User
type then has a links
field that’s a list of Link
s. This is how you express a one-to-many relationship using SDL.
After every change you’re making to the data model, you need to redeploy the Prisma service to apply your changes.
The Prisma database schema in src/generated/prisma.graphql
and along with it the API of the Prisma service have been updated. The API now also exposes CRUD operations for the User
type as well as operations to connect and disconnect User
and Link
elements according to the specified relation.
Remember the process of schema-driven development? It all starts with extending your schema definition with the new operations that you want to add to the API - in this case a signup
and login
mutation.
Next, go ahead and add the AuthPayload
along with a User
type definition to the file.
So, effectively the signup
and login
mutations behave very similarly. Both return information about the User
who’s signing up (or logging in) as well as a token
which can be used to authenticate subsequent requests against the API. This information is bundled in the AuthPayload
type.
But wait a minute 🤔 Why are you redefining the User
type this time. Isn’t this a type that could also be imported from the Prisma database schema? It sure is!
However, in this case you’re using it to hide certain information of the User
type in the application schema. Namely, the password
field (though you’re going to store a hashed version of the password as you’ll see soon - so even if it was exposed here clients wouldn’t be able to directly query it).
After extending the schema definition with the new operations, you need to implement the resolver functions for them. Before doing so, let’s actually refactor your code a bit to keep it a bit more modular!
You’ll pull out the resolvers for each type into their own files.
Next, move the implementation of the feed
resolver into Query.js
.
This is pretty straighforward. You’re just reimplementing the same functionality from before with a dedicated function in a different file. The Mutation
resolvers are next.
Let’s use the good ol’ numbered comments again to understand what’s going on here - starting with signup
.
signup
mutation, the first thing to do is encrypting the User
’s password using the bcryptjs
library which you’ll install later.Prisma
binding instance to store the new User
in the database. Notice that you’re hardcoding the id
in the selection set - nothing else. We’ll discuss this in more detail soon.APP_SECRET
. You still need to create this APP_SECRET
and also install the jwt
library that’s used here.token
and the user
.Now on the login
mutation:
User
object, you’re now using the Prisma
binding instance to retrieve the existing User
record by the email
address that was sent along in the login
mutation. If no User
with that email address was found, you’re returning a corresponding error. Notice that this time you’re asking for the id
and the password
in the selection set. The password
is needed because it needs to be compared with the one provided in the login
mutation.token
and user
again.The implementation of both resolvers is relatively straighforward - nothing too surprising. The only thing that’s not clear right now is the hardcoded selection set containing only the id
field. What happens if the incoming mutation requests more information about the User
?
AuthPayload
resolverFor example, consider this mutation that should be possible according to the GraphQL schema definition:
mutation {
login(
email: "sarah@graph.cool"
password: "graphql"
) {
token
user {
id
name
links {
url
description
}
}
}
}
This is a normal login mutation where a bit of information about the User
that’s logging in is being requested. How does the selection set for that mutation get resolved?
With the current resolver implementation, this mutation actually wouldn’t return any user data because all that could be returned about the User
is their id
(since that’s everything that is requested from Prisma). The way how to solve this is by implementing the additional AuthPayload
resolver and retrieve the field from the mutation’s selection set there.
Here is where the selection set of the incoming login
mutation is actually resolved and the requested fields are retrieved from the database.
Note: This is a bit tricky to understand at first. To learn more about this topic a bit more in-depth check out the explanation in this GitHub issue and read this blog article about the
info
object.
Now let’s go and finish up the implementation.
Next, you’ll create a few utilities that are being reused in a few places.
The APP_SECRET
is used to sign the JWTs which you’re issuing for your users. It is completely independent to the secret
that’s specified in prisma.yml
. In fact, it has nothing to do with Prisma at all, i.e. if you were to swap out the implementation of your database layer, the APP_SECRET
would continue to be used in exactly the same way.
The getUserId
function is a helper function that you’ll call in resolvers that require authentication (such as post
). It first retrieves the Authorization
header (which contains the User
’s JWT) from the incoming HTTP request. It then verifies the JWT and retrieves the user’s ID from it. Notice that if that process is not successful for any reason, the function will throw an exception. You can therefore use it to “protect” the resolvers which require authentication.
Finally, you need to import everything into Mutation.js
.
post
mutationBefore you’re going to test your authentication flow, make sure to complete your schema/resolver setup. Right now the post
resolver is still missing.
Two things have changed in the implementation compared to the previous implementation in index.js
:
getUserId
function to retrieve the ID of the User
that is stored in the JWT that’s set at the Authorization
header of the incoming HTTP request. Therefore, you know which User
is creating the Link
here. Recall that an unsuccessful retrieval of the userId
will lead to an exception and the function scope is exited before the createLink
mutation is invoked.userId
to connect the Link
to be created with the User
who is creating it. This is happening through the connect
-mutation.Awesome! The last thing you need to do now is using the new resolver implementations in index.js
.
That’s it, you’re ready to test the authentication flow! 🔓
The very first thing you’ll do is test the signup
mutation and thereby create a new User
in the database.
Note that you can “reuse” your Playground from before if you still have it open - it’s only important that you’re restarting the server so the changes you made to the implementation are actually applied.
Whenever you’re now sending a query/mutation from that tab, it will carry the authentication token.
When your server receives this mutation, it invokes the post
resolver and therefore validates the provided JWT. Additionally, the new Link
that was created is now connected to the User
for which you previously sent the signup
mutation.
To verify everything worked, you can send the following login
mutation:
mutation {
login(
email: "alice@graph.cool"
password: "graphql"
) {
token
user {
email
links {
url
description
}
}
}
}