Nuxt GraphQL Authentication With Strapi
May 20th 2020
GraphQL has changed the way we think about API endpoints and is quickly becoming an important layer in the development stack for contemporary web apps. If you are using GraphQL in your Nuxt.js project there will likely come a time when you will need to authenticate a user to protect content in your GraphQL queries. Most GraphQL implementations use an Authorization Bearer
header approach which sends a JSON Web Token in the GraphQL request to validate a user can make the request. Since we are using Strapi.io in our tutorial, the User-Permission Policies for content are control are executed before our our GraphQL Queries/Mutations are executed.
We enjoy using the Nuxt/GraphQL/Strapi technology stack and want to share how we’ve been successful with implementing GraphQL authentication in our Nuxt project as Strapi as our Authentication Provider.
Creating a Nuxt Project
We can start by installing a new Nuxt.js project with:
yarn create nuxt-app nuxt-graphql-strapi
In the configuration options during install we will leave everything unchecked as we will install our own dependencies. Once built we can make sure our project installed correctly by running yarn dev
in our nuxt-graphql-strapi
folder. We can navigate to http://localhost:3000/ and we’ll see our default Nuxt project welcome screen:
With our Nuxt project installed we can move on to installing our Strapi installation. We will use the Strapi quickstart installation to quickly get our Strapi project up and running which will use a SQLite database configuration:
yarn create strapi-app strapi-graphql --quickstart
Once installed we can navigate to http://localhost:1337/admin and create an admin user for our Strapi project. Once logged in we can install the Strapi GraphQL plugin by navigating to the Marketplace http://localhost:1337/admin/marketplace and installing the GraphQL plugin. Alternatively, you can install the plugin from the command line by running:
yarn strapi install graphql
Once installed you should be able to navigate to the GraphQL playground to make sure the GraphiQL is working: http://localhost:1337/graphql.
Next, we will want to create another user (besides our admin user) to authenticate against since our admin user cannot use the normal User/Permissions endpoint and for security reasons. You can navigate to “Users” in the left hand panel or by going to http://localhost:1337/admin/plugins/content-manager/collectionType/plugins::users-permissions.user and clicking the “Add New User” button in the upper right hand corner. Enter an email, username and password and make sure “confirmed” field is checked to true. Also, you can add the “Authenticated” role to the user and click “Save”. Now that we have a user we’ll want to create a content type that the user can manage.
Navigate to the “Content-Types Builder” or by going to http://localhost:1337/admin/plugins/content-type-builder/content-types/ and click on “Create new collection type”. In the modal screen we can add a content type called article
making sure that it is singular. We will then be prompt to add fields to our content type. We can add a field of type “text” called title
and click on Advanced Settings to make sure the field is required
. We can then add a second field of type “UID” which is a unique identifier field with the Name being slug
and we can “Attach the field” to our newly created title
field. Once again, under advanced settings we can make sure the field is required and click “Add Another Field”. We will select the field type of “Rich text” and name the field content
and click on Finish and click on Save to save our new content type. The application should restart and you should see an “Articles” link in the left hand sidebar navigation.
In order to use our Articles content type we will need to add some permissions. First, we can navigate to “Roles and Permissions” (or by http://localhost:1337/admin/plugins/users-permissions/roles) and click on the “Public” role. Under our “Article” content type we can select: count, find and findone and then navigate up to “Save”. Returning to the “Roles and Permissions” screen we can click on “Authenticated” role and scrolling down to our Article content type we can select: create, delete and update and then click “Save”. To make sure this is working let’s create a new content type in our admin panel.
Navigate to “Articles” in the left hand sidebar. In the title field we can add “Article One” and the slug should be auto-generated for us. In the content field we can input some placeholder text for now. We can do the same for a second article naming it “Article Two”. With those two pieces created we can navigate to our GraphiQL playground to make sure everything is working as expected.
GraphiQL Playground
Since we set our “Article” content type for find and findone to our Public role, any user should be able to access the content without permission. We can validate by running the GraphQL query command:
query{
articles{
id
title
slug
content
}
}
And we should get a valid response:
{
"data": {
"articles": [
{
"id": "5ec42a11fd4dce5913783647",
"title": "Article One",
"slug": "article-one",
"content": "### Article One\n\nLorem ipsum ..."
},
{
"id": "5ec42a30fd4dce5913783648",
"title": "Article Two",
"slug": "article-two",
"content": "Article Two\n\nLorem ipsum..."
}
]
}
}
Now, let’s try executing a create
mutation to create a new article and see what happens:
You can see that we got a 403 Forbidden response from the server which is the intended response since we were not sending the mutation request as an authenticated user. Let’s open a new GraphiQL tab and get a jwt token to authenticate our user:
mutation{
login(input: {
identifier: "jdoe",
password: "secretpass"
}){
user{
id
username
email
role{
name
type
description
}
}
jwt
}
}
Make sure you replace the identifier
field with the username you created for your Strapi user and the respective password. We should see a valid response:
{
"data": {
"login": {
"user": {
"id": "5ebaa45e1de01f050014fcf3",
"username": "jdoe",
"email": "[email protected]",
"role": {
"name": "Authenticated",
"type": "authenticated",
"description": "Default role given to authenticated user."
}
},
"jwt": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVlYmFhNDVlMWRlMDFmMDUwMDE0ZmNmMyIsImlhdRghfnetuiUyOSwiZXhwIjoxNTkyNTA3NTI5fQ.quOrIxr3lxb5rX5eWkKzGuNo6g92eRjOYGdwV_S07KOw"
}
}
}
Now that we have our jwt
token we can use that to send an authenticated createArticle
mutation to create a new article. We can navigate back to our “createArticle” tab and down by our “Variables” tab we can click on the “Headers” and copy our jwt
to and Authorization: Bearer
header such as:
{
"Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVlYmFhNDVlMWRlMDFmMDUwMDE0ZmNmMyIsImlhdRghfnetuiUyOSwiZXhwIjoxNTkyNTA3NTI5fQ.quOrIxr3lxb5rX5eWkKzGuNo6g92eRjOYGdwV_S07KOw"
}
Now executing the command we should see that “Article Three” is in fact created for us since we’re sending a mutation request from an Authenticated user that has the roles/permissions to create an article!
This is great news! We now know that we’re able to use a jwt
authorization header to authenticate a user request from our application. In the previous example we used the GraphiQL playground but we will now walk through how to use GraphQL Authentication in our Nuxt.js app.
Using Nuxt Apollo for GraphQL
One of the defacto GraphQL clients to interface with your GraphQL server is Apollo GraphQL Client.
Apollo Client is a complete state management library for JavaScript apps. Simply write a GraphQL query, and Apollo Client will take care of requesting and caching your data, as well as updating your UI.
https://www.apollographql.com/docs/react/
Fortunately, in the Vue.js community there is great support for using the Apollo Client in your Vue.js app. Furthermore, the project has been converted into a Nuxt module for us to use inside our Nuxt.js project. The @nuxtjs/apollo
dependency allows us to utilize the Apollo Client to interface with our Strapi GraphQL layer. We can install the module and by running:
yarn add @nuxtjs/apollo graphql-tag
Note we also installed graphql-tag
to use files that end with .gql
so our webpack compiler can transform GraphQL query strings into GraphQL AST. Now we can setup some simple config for our @nuxtjs/apollo
module. In your Nuxt.js project folder open up the nuxt.config.js
file and add:
require('dotenv').config()
export default {
...
env: {
graphEndpoint: process.env.graphEndpoint
},
modules: [
'@nuxtjs/apollo'
],
apollo: {
clientConfigs: {
default: {
httpEndpoint: `${process.env.graphEndpoint}`
}
}
},
}
If you run this you will likely get an error if you did not install the dotenv
dependency. You can add it by running yarn add dotenv
. Finally, we’ll need to add the .env
file to our Nuxt.js project directory specifying our graphEndpoint
variable:
graphEndpoint="http://localhost:1337/graphql"
Also, you may run into an core-js
error since one of our @nuxtjs/apollo
dependencies requires core-js
. You can fix this by installing the core-js
dependency:
yarn add -D core-js@2 @babel/runtime-corejs2
With that in place we can start our nuxt development server with yarn dev
and navigate back to http://localhost:3000 to see the Nuxt.js welcome screen. Not much looks different but if we install the Chrome Apollo Developer Tools we can open up the developer console: Option + ⌘ + J
and navigate to the Apollo tab and we can try using our GraphQL Mutation to login with our user as we did in the GraphQL playground:
mutation{
login(input: {
identifier: "jdoe",
password: "secretpass"
}){
user{
id
username
email
role{
name
type
description
}
}
jwt
}
}
Executing this we can see that we have successfully linked our Apollo Client to our GraphQL endpoint in our project using the @nuxtjs/apollo
module!
Nuxt Login GraphQL Authentication
We can now create a login page that will handle the functionality of logging in our user and setting the jwt
token for further requests (such as creating an article). First, we’ll create a nuxt page located at ./pages/login.vue
. Inside we can scaffold out our <template>
for our form:
<template>
<section class="py-24">
<div class="container mx-auto">
<div class="flex justify-center">
<div class="w-full md:w-1/2 flex flex-col items-center mx-auto bg-gray-200 p-8">
<h3 class="text-3xl mb-8">Login Page</h3>
<div class="w-full max-w-xs">
<form @submit.prevent="login()" class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2" for="username">Username</label>
<input
v-model="form.identifier"
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
id="username"
type="text"
placeholder="Username"
/>
</div>
<div class="mb-6">
<label class="block text-gray-700 text-sm font-bold mb-2" for="password">Password</label>
<input
v-model="form.password"
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline"
id="password"
type="password"
placeholder="******************"
/>
</div>
<div class="flex items-center justify-between">
<button
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
type="submit"
>Sign In</button>
<a
class="inline-block align-baseline font-bold text-sm text-blue-500 hover:text-blue-800"
href="#"
>Forgot Password?</a>
</div>
</form>
<p class="text-center text-gray-500 text-xs">&copy;2020 Acme Corp. All rights reserved.</p>
</div>
</div>
</div>
</div>
</section>
</template>
In our html we have two form fields mapped to form.username
and form.password
respectively. We also are preventing the default submit event and using a method we’ll define called login()
. We are using our favorite utility library Tailwind CSS for some formatting 😊
Now we can focus on our <script>
portion of our Vue single file component to define our login()
method.
<script>
import gql from "graphql-tag";
export default {
data() {
return {
form: {
identifier: "",
password: ""
}
};
},
methods: {
async login(){
const credentials = this.form
}
},
async mounted(){
//clear apollo-token from cookies to make sure user is fully logged out
await this.$apolloHelpers.onLogout()
}
}
</script>
We have our data bindings form.username
and form.password
and when the form is submitted our login()
function is invoked and we are setting the const credentials
to this.form
. We are using async
declaration so we can await
promises to resolve. Let’s focus on the login()
function and then we can step through what’s happening:
methods: {
async login() {
const credentials = this.form;
try {
const { data: { login: { user, jwt } } } = await this.$apollo.mutate({
mutation: gql`
mutation($identifier: String!, $password: String!) {
login(input: { identifier: $identifier, password: $password }) {
user {
id
username
email
role {
name
type
description
}
}
jwt
}
}
`,
variables: credentials
})
//set the jwt to the this.$apolloHelpers.onLogin
await this.$apolloHelpers.onLogin(jwt)
} catch (e) {
console.error(e)
}
}
}
Since we have the @nuxtjs/apollo
module installed we have access to this.$apollo.mutate
which is a helper to send mutation requests to our GraphQL endpoint. We are also importing the gql
tag so we can pass a graph-tag literal to be converted into AST format. We are passing in two variable to our mutation $identifier
and $password
which are both required to authenticate our user. This is the mutation that we’ve ran in both our GraphiQL playground and Apollo Developer tools. We are destructuring the user
and jwt
variable from the response data and now that we have the jwt
we can use the this.$apolloHelpers.onLogin(jwt)
to set the Authorization Header for future requests to Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
so we will be making an authenticated request!
Nuxt Middleware Protected Route with GraphQL Authorization Token
Now that we have a jwt
token saved to our Apollo Token we can use this to protect routes in Nuxt with Nuxt’s Middleware ability. First, we’ll make a page in our application called protected.vue
which will be a page that we will use the middleware function on to make sure the user is logged in. Create the page at ./pages/protected.vue
and we can add:
<template>
<div class="container mx-auto">
<div class="flex">
<div class="w-full p-8">
<pre class="p-4 bg-gray-100">{{ JSON.stringify(me, null, 2) }}</pre>
</div>
</div>
</div>
</template>
<script>
import gql from 'graphql-tag'
export default {
middleware: ['authenticated'],
data(){
return {
me: {}
}
},
apollo: {
me: {
query: gql`
query {
me{
id
email
username
role{
name
}
}
}
`
}
}
}
</script>
All we’re doing is making a call to the GraphQL query to the provided me
endpoint by Strapi User and Permissions Plugin. Since we’re already setting our apollo-token
authorization token the token will be passed in the request which is necessary to know who “me” is. We are also defining middleware called 'authenticated'
which will look for a file in the project’s middleware
directory called authenticated.js
with a default exported function. Let’s write the middleware to check if the user is authenticated when accessing the protected route. Create a file in ./middleware/authenticated.js
of your project:
import gql from 'graphql-tag'
export default async function ({app, redirect}) {
const hasToken = !!app.$apolloHelpers.getToken()
if (!hasToken) {
return redirect('/login')
}
//make sure the token is still valid
try{
const { data: { me } } = await app.apolloProvider.defaultClient.query({
query: gql`
query {
me{
id
email
username
role{
name
}
}
}
`
})
if(!Object.keys(me).length){
return redirect('/login')
}
//we are good to go and validated
}catch(e){
//token is not valid
//logout user to clear storage
try{
const result = await app.$apolloHelpers.onLogout()
//redirect them to login page
return redirect('/login')
}catch(e){
console.error(e)
}
}
}
Our first check that we want to do is make sure that there is an apollo-token
set by using the app.$apolloHelpers.getToken()
helper function which returns the token. We are casting this to a boolean value by using !!app.$apolloHelpers.getToken()
so it’s either true
if there is a token or false
if there is not. If we do not have a token we’ll use the redirect
function to redirect the user to our login page. The second check that we want to do is make sure that the jwt
is still valid. We can do this by making an GraphQL query to the me
endpoint (as we’re doing in our protected.vue
page). Since the me
endpoint requires the jwt
auth token, if the token is expired or no longer valid (ie. it has been manipulated) we can redirect the user to the login page as well. Otherwise, if everything is valid with our jwt
token we can proceed to the protected.vue
page.
We have successfully used our Strapi GraphQL endpoint to integrate into our Nuxt.js app for user authentication. We can take this a step further by using the roles
value in our middleware function(s) to validate if a user is able to navigate to a specific route. Hopefully you can use this to build your next web app with these trending technologies!