Authentication with Adonisjs v6 and access token (OAT)

Maxime
6 min readMay 18, 2024

--

Introduction

In this article, I will show you how to set up an AdonisJS version 6 application and create an OAT (Opaque Access Token) authentication system.

Before starting the tutorial, I invite you to read this part of the documentation to find out if this type of authentication is really the best for your application.

Here is the github repository with the complete code.

Feel free to give a ⭐ to my repository, it really help me a lot !

🇫🇷 Tu parles français ? voici le lien de l’article en français

Installation

To begin, verify that you use node v22 or at least node v20.6

node -v # v22.x.x

There are three different official starter kits available so you don’t have to start developing your application from scratch.

There’s the “slim” kit, which contains just the core of the framework and the default file and folder structure of AdonisJS.

Next comes the “web” kit, which includes a variety of AdonisJS packages like Lucid, which is the framework’s ORM, and a template engine called Edge. The web kit is a good base for creating an application that renders views in HTML or Alpine.js, for example.

Finally, the third kit, and the one we will use, is the “API” kit. It allows you to easily create APIs that render JSON.

Below is the command to install it. Here, for the -K flag, we will choose “api” and we will specify that we want OAT tokens with this flag: --auth-guard=access_tokens

npm init adonisjs@latest -- -K=api --auth-guard=access_tokens

After entering the various details requested by the CLI and going into the newly created folder, we can proceed to the next step.

Migrations

During the installation of the kit, we could see that Lucid, the ORM of AdonisJS, was automatically configured to use SQLite as the database. For practical reasons, we will keep this database for the rest of the tutorial. The authentication package was also configured to use OATs, so there’s nothing more for us to do on that front.

If we run node ace migration:status, we can see that the kit has automatically created two migrations: one for the tokens and one for the user. All that's left is to migrate the database with the following command.

node ace migration:run

Controller

Once the tables have been migrated it is time to create our controller for authentication and give it the name “auth”.

node ace make:controller auth

The new controller is situated at app/controller

Creation of the register route

To begin we will create the register route which will allow us to register user to our application.

import type { HttpContext } from '@adonisjs/core/http'
import { registerValidator } from '#validators/auth'
import User from '#models/user'

export default class AuthController {
async register({ request, response }: HttpContext) {
const payload = await request.validateUsing(registerValidator)

const user = await User.create(payload)

return response.created(user)
}
}

To validate data that will be transmitted to our backend we will create a validator.

node ace make:validator auth

Go to app/validator/auth.ts

We will define an object that will have 3 properties:

fullName, which must have a minimum length of 3 characters and a maximum of 64, and must be of type string.

Email, which must be of type string and be an email :) and, most importantly, must be unique in the database.

Password, of type string with a minimum length of 12 characters up to 512.

import vine from '@vinejs/vine'

export const registerValidator = vine.compile(
vine.object({
fullName: vine.string().minLength(3).maxLength(64),
email: vine
.string()
.email()
.unique(async (query, field) => {
const user = await query.from('users').where('email', field).first()
return !user
}),
password: vine.string().minLength(12).maxLength(512),
})
)

We have one last step before testing our register route. Go to start/routes.ts to create the route using our controller.

We will replace the existing code by the code below:

import router from '@adonisjs/core/services/router'

const AuthController = () => import('#controllers/auth_controller')

router.group(() => {
router.post('register', [AuthController, 'register'])
}).prefix('user')

We will import our controller and create a POST route linked to the register method of our controller.

You can see that I have created a router group with a user prefix. All the routes we put in this group will have the user prefix, which means our register route will not have the URL /register but /user/register.

Start the development server with this command: node ace serve and then test the route http://localhost:3333/user/register using Postman or another tool.

Example body:

{
"fullName": "Maxime",
"email": "max@ime.test",
"name": "Maxime",
"password": "12345678"
}

If everything goes well, the server should return a 201 Created response with the user's information.

To prevent the server from returning the hash of the password when you request the user, go to app/models/users.ts and add { serializeAs: null } in the argument of the @column decorator for the password.

@column({ serializeAs: null })
declare password: string

Now, when the server returns the user, it will replace the value of the password field with null. Feel free to visit here to learn more.

Creation of the login route

We will add a validator on the validator file.

import vine from '@vinejs/vine'

// new validator
export const loginValidator = vine.compile(
vine.object({
email: vine.string().email(),
password: vine.string().minLength(8).maxLength(32),
})
)

export const registerValidator = vine.compile(
vine.object({
fullName: vine.string().minLength(3).maxLength(64),
email: vine
.string()
.email()
.unique(async (db, value) => {
const user = await db.from('users').where('email', value).first()
return !user
}),
password: vine.string().minLength(12).maxLength(512),
})
)

As well as a login method in our controller

import type { HttpContext } from '@adonisjs/core/http'
import User from '#models/user'
import { registerValidator, loginValidator } from '#validators/auth'

export default class AuthController {
// new login method
async login({ request, response }: HttpContext) {
const { email, password } = await request.validateUsing(loginValidator)

const user = await User.verifyCredentials(email, password)
const token = await User.accessTokens.create(user)

return response.ok({
token: token,
...user.serialize(),
})
}
async register({ request, response }: HttpContext) {
const payload = await request.validateUsing(registerValidator)

const user = await User.create(payload)

return response.created(user)
}
}

Then add the login route to the router

import router from '@adonisjs/core/services/router'

const AuthController = () => import('#controllers/auth_controller')

router.group(() => {
router.post('register', [AuthController, 'register'])
router.post('login', [AuthController, 'login'])
}).prefix('user')

When we test the route it will return the user as well as the token.

Creation of a token protected route

Now that we have our login and register routes, we will create a route that is accessible only by a registered user.

import router from '@adonisjs/core/services/router'
import { middleware } from './kernel.js'

const AuthController = () => import('#controllers/auth_controller')

router.group(() => {
router.post('register', [AuthController, 'register'])
router.post('login', [AuthController, 'login'])
}).prefix('user')

// add this route
router.get('me', async ({ auth, response }) => {
try {
const user = auth.getUserOrFail()
return response.ok(user)
} catch (error) {
return response.unauthorized({ error: 'User not found' })
}
})
.use(middleware.auth())

The route will return the user’s information. We can see that we defined a middleware with the api guard and used the auth object to access the user. To test the route, include the Authorization header with the value of the token (Bearer token).

Creation of the logout route

Now that we have our login and register routes, the only thing missing is the route for logging out. To log out a user, we simply need to delete their token from the database.

We will add a new logout method in our controller:

import type { HttpContext } from '@adonisjs/core/http'
import User from '#models/user'
import { registerValidator, loginValidator } from '#validators/auth'

export default class AuthController {
async login({ request, response }: HttpContext) {
const { email, password } = await request.validateUsing(loginValidator)
const user = await User.verifyCredentials(email, password)
const token = await User.accessTokens.create(user)
return response.ok({
token: token,
...user.serialize(),
})
}
async register({ request, response }: HttpContext) {
const payload = await request.validateUsing(registerValidator)
const user = await User.create(payload)
return response.created(user)
}
// Notre nouvelle route logout
async logout({ auth, response }: HttpContext) {
const user = auth.getUserOrFail()
const token = auth.user?.currentAccessToken.identifier
if (!token) {
return response.badRequest({ message: 'Token not found' })
}
await User.accessTokens.delete(user, token)
return response.ok({ message: 'Logged out' })
}
}

The auth object allows us to retrieve the authenticated user and their token. We will then check that this token is not undefined (which shouldn't happen since the user is authenticated), delete the token, and inform the client that the operation was successful.

Then, add the new route:

router.group(() => {
router.post('register', [AuthController, 'register'])
router.post('login', [AuthController, 'login'])
// the new logout route
router.post('logout', [AuthController, 'logout']).use(middleware.auth())
}).prefix('user')

As with the me route, we will tell it to use the authentication middleware with use(middleware.auth()) to access the auth object in our controller method.

If you wanna see the complete code go to my github repository.

Conclusion

You now know how to create an OAT authentication system with AdonisJS. Feel free to read the official documentation to learn more.

--

--