This article is a guest post by Osmar Pérez. He wrote this blog post through the Write for the Community program. If you are passionate about everything Jamstack, open-source, or javascript and want to share, join the writer's guild!
In this tutorial, we will learn how to implement a local authentication system with Strapi and integrate it into the Frontend using the Next.js Framework and NextAuth.js.
This tutorial was made with Ubuntu 20.04 and YARN
What you will learn:
Strapi is an attractive open-source Headless CMS that gives us the possibility to create an API in seconds. Its most important features are:
And many other features that will grab you at the first instant...
This architecture will consist of two main components:
To get started, we will create the main folder containing our project.
1mkdir next-strapi-auth
2cd next-strapi-auth
Now, we will proceed to create a database with PostgreSQL:
1sudo -i -u postgres
Once inside our Postgres user we run the command:
1psql
In order to enter the PostgreSQL bash and create our database as follows:
1CREATE DATABASE strapi;
2CREATE ROLE strapiu WITH LOGIN PASSWORD 'strongpassword';
3GRANT ALL PRIVILEGES ON DATABASE strapi TO strapiu;
Now, we close the postgresql bash and the user's session by typing twice exit.
1postgres=# exit
2postgres@user:~$ exit
Now we are ready to do our Strapi installation. To install Strapi we can use your preferred package manager:
1yarn create strapi-app api //using yarn where api is the name of the project
2npx create-strapi-app api //using npx where api is the name of the project
In this tutorial we will use postgresql, so we will use the custom installation, to use the database created before.
1? Choose your installation type (Use arrow keys)
2 Quickstart (recommended)
3❯ Custom (manual settings)
Select postgres as our database:
1? Choose your installation type Custom (manual settings)
2? Choose your default database client
3 sqlite
4❯ postgres
5 mysql
6 mongo
Then we enter the name of our database, in this case, Strapi.
1? Choose your installation type Custom (manual settings)
2? Choose your default database client postgres
3? Database name: strapi
Then we select the host of our database, as we are working locally the host is the one that is indicated by default, so we simply press enter.
1? Choose your installation type Custom (manual settings)
2? Choose your default database client postgres
3? Database name: strapi
4? Host: (127.0.0.1)
Strapi also needs to know the port of PostgreSQL, the standard port that is used by postgres is 5432 and will most certainly be that in your case as well. Otherwise, specify your own custom port number.
1? Choose your installation type Custom (manual settings)
2? Choose your default database client postgres
3? Database name: strapi
4? Host: 127.0.0.1
5? Port: (5432)
Now we enter the name of the user with rights in the database, in this case "strapiu".
1? Choose your installation type Custom (manual settings)
2? Choose your default database client postgres
3? Database name: strapi
4? Host: 127.0.0.1
5? Port: 5432
6? Username: strapiu
Now enter the chosen password for the database user:
1? Choose your installation type Custom (manual settings)
2? Choose your default database client postgres
3? Database name: strapi
4? Host: 127.0.0.1
5? Port: 5432
6? Username: strapiu
7? Password: ***********
And finally, we will use the default configuration of SSL, so we just hit enter.
1? Choose your installation type Custom (manual settings)
2? Choose your default database client postgres
3? Database name: strapi
4? Host: 127.0.0.1
5? Port: 5432
6? Username: strapiu
7? Password: ***********
8? Enable SSL connection: No
The next step is to create our frontend with next.js for this we will use the following command:
1npx create-next-app app
2# or
3yarn create next-app app
Up to this point we will have a file structure as follows:
1.
2├── api
3│ ├── api
4│ ├── build
5│ ├── config
6│ ├── extensions
7│ ├── favicon.ico
8│ ├── node_modules
9│ ├── package.json
10│ ├── public
11│ ├── README.md
12│ └── yarn.lock
13└── app
14 ├── node_modules
15 ├── package.json
16 ├── pages
17 ├── public
18 ├── README.md
19 ├── styles
20 └── yarn.lock
To start our development servers we must enter the following commands in the terminal:
1 /app yarn dev
2 /api yarn strapi dev
What is a JWT?
JSON Web Token (JWT) is a JSON-based RFC 7519 open standard proposed by the IETF for the creation of access tokens that enable identity and privilege propagation between 2 peers.
Due to its size, a JWT can be sent through a URL, through a POST parameter, or within an HTTP header.
This token contains all the necessary information about an entity to avoid unnecessary queries to the database.
Advantages of JWT
A JWT token is composed of 3 parts:
1.- JavaScript Object Signing and Encryption (JOSE) Header: Contains metadata about the type of token and the cryptographic algorithm used to secure its content.
2.- Payload: Contains all the information about the entity (usually the user) and its different attributes.
3.- Signature: It is used to verify the validity of the user sending the token and ensures that the message has not been changed along the way.
In the end, these 3 parts are concatenated in base64 in the following way: [header][payload][signature].
About token storage
In Strapi our JWT is generated by making a POST request, containing our user ID and password as follows:
1{
2 "identifier": "reader@strapi.io",
3 "password": "strapi"
4}
to the route http://localhost:1337/auth/local, this route will return us the JWT:
1{
2 "jwt": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwiaWF0IjoxNTc2OTM4MTUwLCJleHAiOjE1Nzk1MzAxNTB9.UgsjjXkAZ-anD257BF7y1hbjuY3ogNceKfTAQtzDEsU",
3 "user": {
4 "id": 1,
5 "username": "reader",
6 ...
7 }
8}
Yeah, the request is made once every time the user wants to make a request to our API. So if the requirement is to store this token in the browser's memory, it's a pretty safe and easy solution. But what if I want my user to be able to refresh his screen or sync different tabs without having to log in again?
Let's discuss possible solutions:
LOCAL STORAGE
Many developers tend to store their tokens in the browsers localstorage for persistence between tabs and ease of use, remembering that these are not sent on every request like cookies, so they are not susceptible to CSRF attacks, but if an attacker can execute javascript code "XSS" in one of your NODE_MODULES packages or in some third party CDN like google analytics, immediately this attacker can get access to your token by executing code like the following.
1<script>alert(localStorage.getItem(‘jwt’))</script>
It is important to understand that all token saving methods are vulnerable to XSS and, in some cases, will only make them more difficult to access.
I recommend that you watch this video to know what an XSS attack is and its implications.
SESSION STORAGE As with local storage, session storage presents the same problem, the only change is that when the user closes the window his token will be deleted, so this is not a viable option.
IN BROWSER MEMORY This is a secure solution, but if the user reloads the page, they will have to log in again. So it is only recommended if the use case is that the user has to log in again when reloading the page or the token is one-time use.
COOKIES My opinion, and that of many others, is that the best way to store these tokens is with cookies. But why?
Cookies are sent automatically on every browser request and have different security methods that allow you to give more reliability to your system, among them:
Secure: This flag ensures that the server will only send requests if the request is encrypted over the HTTPS protocol, this flag will help us to avoid man-in-the-middle attacks by not allowing the use of our cookies with the HTTP protocol.
SameSite: It is a cookie attribute defined in RFC6265, it allows us to add some security against CSRF type attacks, it has 3 different options.
HttpOnly: Let's suppose that our application has an XSS vulnerability. Then the attacker can take advantage of this vulnerability to steal our authentication cookie. To prevent this, we use the HttpOnly flag, which prevents our cookie from being accessed through JavaScript.
With the above, we mitigate the possibility of using XSS to obtain our token, but still not, however, there is a type of attack called XST that allows bypassing the HttpOnly flag in order to obtain our cookie.
This type of attack is independent of the security settings of our cookie and uses the HTTP Trace method combined with XSS to read the authentication cookie, even when the HTTP Only flag is set.
To prevent this type of attack, disable in the configuration of our HTTP server (Apache, Nginx, etc...) the possibility of sending HTTP trace requests, thus making it impossible for this type of attack to be carried out.
Now we know how to protect ourselves from XSS attacks, but what about CSRF attacks?
As mentioned before, cookies send on each request all the information stored in them, so let's assume the following scenario:
Our user is authenticated in our application and his cookie has been successfully generated.
A malicious actor sends an email with an offer to his email address using social engineering.
The user clicks on the button to claim the offer, this button sends a password change request or makes use of some other API entry of your application to obtain the user's protected information. At that point, the cookies in the browser will be sent automatically and it will be a valid request as it uses the cookies of the currently authenticated user, thus obtaining all his information. At that point, the malicious actor will have successfully performed a CSRF attack.
If you want to know more about this type of attacks I recommend the following video:
How can we protect ourselves from CSRF attacks?
The most common way to avoid this type of attack is by using a CSRF token. This token is randomly generated by the backend of the application, and in each request that is made, it will be sent and validated by the application itself. In this way the attacker will never know the value of this token, and will not be able to make a successful request outside our application.
But even after all these security measures if we are susceptible to an XSS attack, couldn't the attacker send a request directly from our application? The answer is, yes.
Normally react/next saves us the work of cleaning up user input, greatly reducing XSS attacks, however, there are ways in which these types of attacks can occur using these technologies. So I recommend you review this cheatsheet with some methods to avoid them.
Now you may ask, do I have to implement each of these measures? The answer is no.
In this guide, we will use NextAuth, which implements all of these measures from the ground up, with some of them, such as JWT encryption (JWE), to secure your JWT token.
Continuing with our tutorial we will go to our app/ directory:
1cd app/
Then install next-auth and axios to be able to make a request to the Strapi path /auth/local:
1yarn add next-auth axios
Following the next-auth documentation, we must create an environment file in the main path of our project, in this case, app/ and another configuration file called ...nextauth.js in the "/app/pages/api/auth/" path of our next project. First, we will create the environment file with the name ".env.development" in our root directory of next, in this case "/app":
1touch .env.development
In this file we will declare 3 constants, the base URL of our frontend ("NEXTAUTH_URL") which uses NexAuth as canonical URL in its code, then the following is the URL of the database we have created before which will be used for both Strapi and NextAuth.
The Postgresql URL of our database is given by: postgres://dbuser:password@localhost:5432/database
Finally, the Strapi URL in this case localhost:1337 :
1NEXTAUTH_URL=http://localhost:3000
2NEXT_PUBLIC_DATABASE_URL=postgres://strapiu:strongpassword@localhost:5432/strapi?synchronize=true
3NEXT_PUBLIC_API_URL=http://localhost:1337
Then we will create the "auth" directory:
1mkdir pages/api/auth/
2cd pages/api/auth/
3touch [...nextauth].js
With the content of the file:
1import NextAuth from "next-auth";
2import Providers from "next-auth/providers";
3import axios from 'axios'
4
5const options = {
6 providers: [
7 Providers.Credentials({
8 name: 'Credentials',
9 credentials: {
10 username: { label: "Email", type: "email", placeholder: "jsmith" },
11 password: { label: "Password", type: "password" }
12 },
13 authorize: async (credentials) => {
14 try {
15 const user = await axios.post(`${process.env.NEXT_PUBLIC_API_URL}/auth/local`, {
16 identifier: credentials.username,
17 password: credentials.password,
18 });
19 if (user.data) {
20 return user.data
21 } else {
22 return null
23 }
24 } catch (error) {
25 const errorMessage = error.response.data.message[0].messages[0].message
26 throw new Error(errorMessage)
27 }
28 }
29 }),
30 ],
31 database: process.env.NEXT_PUBLIC_DATABASE_URL,
32 session: {
33 jwt: true,
34 },
35 callbacks: {
36 jwt: async (token, user) => {
37 if (user){
38 token.jwt = user.jwt;
39 token.user = user.user;
40 }
41 return Promise.resolve(token);
42 },
43 session: async (session, token) => {
44 session.jwt = token.jwt;
45 session.user = token.user;
46 return Promise.resolve(session);
47 },
48 },
49 pages:{
50 signIn: '/login',
51 error: '/login'
52 }
53};
54const Auth = (req, res) =>
55 NextAuth(req, res, options);
56export default Auth;
Now I will explain the most important parts of this file:
NextAuth has different types of providers, you can see all providers, but our tutorial focuses on a username and password model, so our provider will be Credentials.
1import Providers from "next-auth/providers";
The credentials object in line 3 serves to identify the input of our form, and basically tells NextAuth which values belong to the user and which to the password.
1Providers.Credentials({
2 name: 'Credentials',
3 credentials: {
4 username: { label: "Email", type: "email", placeholder: "jsmith" },
5 password: { label: "Password", type: "password" }
6 }
Now NextAuth already knows how to get the credentials of your user but, how is it going to get the JWT token from Strapi to be able to make a request to the API? With the function authorize; This function is in charge of sending a request to the Strapi API (auth/local) through axios, and receive the JWT token and the object of our user.
1 authorize: async (credentials) => {
2 try {
3 const user = await axios.post(`${process.env.NEXT_PUBLIC_API_URL}/auth/local`, {
4 identifier: credentials.username,
5 password: credentials.password,
6 });
7 if (user.data) {
8 return user.data
9 } else {
10 return null
11 }
12 } catch (error) {
13 const errorMessage = error.response.data.message
14 throw new Error(errorMessage + '&email=' + credentials.email)
15 }
16 }
NextAuth can work without a database, but this is only recommended if you do not need persistent accounts. (e.g. for billing, to contact customers, etc).
In this tutorial, we will use the database created before to be able to keep persistent the accounts of our users, therefore in line 31 we define the connection to the database with our environment variable, NEXT_PUBLIC_DATABASE_URL.
1 database: process.env.NEXT_PUBLIC_DATABASE_URL,
NextAuth lets us choose between saving our data as a session or as a jwt token, for this tutorial we will use jwt for the advantages it offers.
1 session: {
2 jwt: true,
3 },
After that, we have the callbacks. The jwt function receives the information from the user and send it to our callback function to be able to use that data in the cookie.
1 jwt: async (token, user) => {
2 if (user){
3 token.jwt = user.jwt;
4 token.user = user.user;
5 }
6 return Promise.resolve(token);
7 },
8 session: async (session, token) => {
9 session.jwt = token.jwt;
10 session.user = token.user;
11 return Promise.resolve(session);
12 },
And finally, we have pages. NextAuth has default pages for login and signup, but in this tutorial, we will make our own so we give the default path in this case: "/login".
1 pages:{
2 signIn: '/login',
3 error: '/login'
4 }
Now we will create a simple interface in Next.js that will be composed of 3 main pages:
4.1 Login page
As we decided to implement a personalized login, we will have to create a directory in the /pages directory of our "app" called login and inside it, an index.js file, the structure of the pages folder would be:
1.
2├── api
3│ └── auth
4├── _app.js
5├── index.js
6├── login
7 └── index.js
In this file we will do two main things:
To show errors we need a react hook: "useState" this hook will collect the authentication errors and the credentials of the user in our app.
To send our credentials, we will use an asynchronous request with the help of a NextAuth method called signIn() this method ensures that the user ends up on the page where he started after completing a login flow. This method will contain in its body the provider (in this case credentials), the user's identifier (email), and the password. In response we will get a promise with the following content: { error: string | undefined //The error message status: number //HTTP Status code ok: boolean // True if sign was successful url: string | null //the URL the user should have been redirected to. }
Note: It is important that the name of our inputs match the ones configured in the ...nextauth.js file.
2nd Note: All styles are available on the GitHub repo
1import { signIn } from 'next-auth/client'
2import { useRouter } from 'next/router'
3import { useState } from 'react'
4//import styles from '../../styles/Login.module.css'
5
6export default function Login() {
7 const [credentials, setCredentials] = useState({ username: '', password: '' })
8 const [loginError, setError] = useState('')
9 const router = useRouter()
10 function handleUpdate(update) {
11 setCredentials({ ...credentials, ...update })
12 }
13 return (
14 <div className={styles.login}>
15 <div className={styles.wrapper}>
16 <div className={styles.var}>
17 <label htmlFor="username">Email</label>
18 <input name='username' type='email' onChange={(e) => handleUpdate({ username: e.target.value })} />
19 </div>
20 <div className={styles.var}>
21 <label htmlFor="password">Password</label>
22 <input name='password' type='password' onChange={(e) => handleUpdate({ password: e.target.value })} />
23 </div>
24 <span className={styles.error}>{loginError}</span>
25 <button
26 onClick={async () => {
27 const response = await signIn('credentials', {
28 redirect: false,
29 ...credentials
30 })
31 if (response.error) {
32 setError(response.error)
33 } else if (response.ok) {
34 router.push("/")
35 }
36 }}
37 >
38 Sign in
39 </button>
40 </div>
41 </div>
42 )
43}
The resulting view would be:
When an error occurs, it will be displayed as follows:
4.2 Index page
For the main page, we will create a basic interface that contains:
To implement this view we need to know if our user is currently authenticated or not, for this we use the NextAuth hook useSession(), which will return the value of the session and if this hook is loading and another NextAuth function called signOut that will serve us so that the user can close the session.
This is a client-side hook and works best if we configure the NextAuth Session Provider called Provider for this we need to modify our _app.js file in the root of our Next.js project "/app" as follows:
1import { Provider } from 'next-auth/client'
2
3export default function MyApp ({ Component, pageProps }) {
4 return (
5 <Provider session={pageProps.session}>
6 <Component {...pageProps} />
7 </Provider>
8 )
9}
Now we can access these values easily:
1import Head from 'next/head'
2import { useSession, signOut } from 'next-auth/client'
3//import styles from '../styles/Home.module.css'
4
5export default function Home() {
6 const [session, loading] = useSession()
7 return (
8 <div className={styles.container}>
9 <Head>
10 <title>Create Next App</title>
11 <link rel="icon" href="/favicon.ico" />
12 </Head>
13 <nav className={styles.navbar}>
14 <div className={styles.wrapper}>
15 <div className={styles.left}>
16 <p>Your logo</p>
17 </div>
18 <div className={styles.right}>
19 {session && session.user ?
20 <div className={styles.navlist}>
21 <a href="/protected">Protected page</a>
22 <a
23 href={`/api/auth/signout`}
24 className={styles.button}
25 onClick={(e) => {
26 e.preventDefault()
27 signOut()
28 }}
29 >Logout
30 </a>
31 </div>
32 :
33 <a className={styles.button} href="/login">Login</a>
34 }
35 </div>
36 </div>
37 </nav>
38 <main className={styles.main}>
39 <h1 className={styles.title}>
40 {session ? <span>Welcome back <p>{session.user.username}</p> to </span> : 'Welcome to'}
41 <p>Next.js! + Strapi + NextAuth app</p>
42 </h1>
43 <p className={styles.description}>
44 {session ? <span>Your JWT is {session.jwt}</span> : <span>Get started by click in <a href="/login">login</a>{' '}</span>}
45 </p>
46 </main>
47 <footer className={styles.footer}>
48 <a
49 href="https://ixtlan.dev"
50 target="_blank"
51 rel="noopener noreferrer"
52 >
53 Powered by Ixtlan.dev
54 </a>
55 </footer>
56 </div>
57 )
58}
As we can see, once this token is working, we will be able to access it through the "session" object and the data returned from the user specifically through "session.user" or the token with "session.jwt".
Now you may be wondering, to know if my user is authenticated Do i need to call that function every time? Yes, but we will simplify this task by adding a new function called Auth to our _app.js file.
1//import '../styles/globals.css'
2import { useEffect } from 'react'
3import { useRouter } from 'next/router'
4import { useSession } from 'next-auth/client'
5import { Provider } from 'next-auth/client'
6
7function MyApp({ Component, pageProps }) {
8 return (
9 <Provider session={pageProps.session}>
10 {Component.auth
11 ? <Auth><Component {...pageProps} /></Auth>
12 : <Component {...pageProps} />
13 }
14 </Provider>
15 )
16}
17
18function Auth({ children }) {
19 const [session, loading] = useSession()
20 const isUser = !!session?.user
21 const router = useRouter()
22 useEffect(() => {
23 if (loading) return // Do nothing while loading
24 if (!isUser) router.push('/login')
25 // If not authenticated, force log in
26 }, [isUser, loading])
27 if (isUser) {
28 return children
29 }
30 // Session is being fetched, or no user.
31 // If no user, useEffect() will redirect.
32 return <div>Loading...</div>
33}
34export default MyApp
This function will validate if the user is currently logged in and can access the protected view, to test it, we will create a new page called protected.
4.3 Protected As we did with login we will create a folder called protected inside pages and we will add an index.js file in it, the structure of pages would look like this:
1.
2├── api
3│ └── auth
4│ └── [...nextauth].js
5├── _app.js
6├── index.js
7├── login
8│ └── index.js
9└── protected
10 └── index.js
This page is simple, we will only return a message that is protected for example:
1export default function PortectedPage(){
2 return <h1>Im protected page</h1>
3}
4PortectedPage.auth = true
The most important part of the previous code would be to add the "auth" attribute at the end of our component, this part will help the function created in _app.js to validate if the user can access this page or not.
In order to obtain our token we must first create a user in Strapi, for this we navigate to the path "localhost:1337" and fill out the following form to create an administrator account.
Then go to the user's section and click on "+Add New User".
Now we put some test data, in this case. I created a Test User with email "test@gmail.com" and a password, then we click on the "Save" button.
Now we can access your jwt token by sending your username and password to the Strapi "/auth/local" path.
To test our simply log in with the previously registered user, but first, we must start the two test servers (Next and Strapi) with:
1 /app yarn dev
2 /api yarn strapi dev
and go to localhost:3000 and start to test the app.
In this tutorial, we learned how to authenticate an application in Next.js with Strapi and NextAuth using the Strapi user credentials. We also learned how to use NextAuth hooks to retrieve the user's jwt.
Note: The database used in this tutorial is optional.
The code for this tutorial is available on GitHub.