Authentification avec Adonisjs v6 et access token (OAT)

Maxime
6 min readFeb 1, 2024

--

Introduction

Dans cet article je vais vous montrer comment setup une application Adonisjs version 6 et créer un système d’authentification par token OAT (Opaque Access Token).

Avant de commencer le tutoriel je vous invite à lire cette partie de la documentation pour savoir si c’est vraiment ce type d’authentification qui est le mieux pour votre application.

Voici le lien du repository qui contient le code du tutoriel, n’hésitez surtout pas à me donner une ⭐ ça m’aide vraiment !

🇬🇧 English version available here.

Installation

Avant toute chose vérifiez que vous utilisez bien node v22 ou au moins node v20.6.

node -v
# v22.x.x

Il existe 3 starter kit officiel différent pour ne pas commencer à développer son application à partir de rien.

Il y a le slim qui contient juste le core du framework et la structure par défaut des fichiers et dossier d’Adonisjs.

Puis viens le kit web qui vient avec plein de package d’Adonisjs comme lucid qui est l’orm du framework ou encore un template engine nommé edge. Le kit web est une bonne base pour créer une application qui va rendre des vues en html ou Alpine.js par exemple.

Pour finir le troisième kit et donc celui qu’on va utiliser c’est le kit api. Il permet de créer facilement des API qui rendent du JSON.

Ci-dessous la commande pour l’installer. Ici pour le flag -K nous allons choisir api et nous allons spécifier que nous voulons des token OAT avec ce flag: --auth-guard=access_tokens

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

Après avoir rentré les différentes informations que vous demande le CLI et être aller dans le nouveau dossier créé nous pouvons passer à la suite.

Migrations

Durant l’installation du kit on a pu voir que lucid, l’ORM d’Adonisjs a été configuré automatiquement pour utiliser sqlite comme base de donnée. Pour des raisons pratique nous allons garder cette base de donnée pour le reste du tutoriel. Le package d’authentification a lui aussi été configuré pour utiliser les OAT donc rien à faire de notre côté à ce niveau là.

Si on fait un node ace migration:status on peut voir que le kit nous a créé automatiquement deux migrations: une pour les token et une pour l’utilisateur. Il nous reste plus qu’à migrer la base de donnée avec la commande suivante.

node ace migration:run

Controller

Une fois que les tables ont été migrés il est temps de créer notre controller pour l’authentification et on lui donne le nom “auth”.

node ace make:controller auth

Le nouveau controller est situé dans app/controller

Création de la route register

Pour commencer nous allons créer la route register qui va nous permettre d’enregistrer des utilisateurs dans notre 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)
}
}

Pour valider les données qui vont nous être transmise nous allons créer un validator.

node ace make:validator auth

Rendez-vous dans app/validator/auth.ts

On va définir un objet qui aura 3 propriétés:

fullName qui devra avoir une longueur minimum de 3 caractère et maximum 64 et qui devra être de type string.

Email qui devra être de type string et d’être un email :) et qui surtout devra être unique dans la base de donnée.

Password, de type string avec une longueur minimum de 12 caractères jusqu’à 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),
})
)

Il nous reste une dernière étape avant de tester notre route register, rendez-vous dans start/routes.ts pour créer la route à partir de notre controller.

On va remplacer le code existant par le code ci-dessous:

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

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

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

On va importer notre controller et on va créer une route POST qui est lié à la méthode register de notre controller.

On peut voir que j’ai créé un router group avec un préfixe user, toutes les routes qu’on va mettre dans ce groupe auront pour préfixe user ça veut dire que notre route register n’a pas pour url /register mais /user/register .

Lancez le serveur de développement avec cette commande: node ace serve puis testez la route http://localhost:3333/user/register avec Postman ou un autre outil.

Exemple de body:

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

Si tout se passe bien le serveur devrait vous renvoyer un 201 created avec les infos du user.

Pour éviter que le serveur vous renvoie le hash du password quand vous lui demander de renvoyer l’utilisateur allez dans app/models/users.ts et rajoutez { serializeAs: null } dans l’argument du décorateur column de password.

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

Maintenant quand le serveur renvoie l’utilisateur il va changer la valeur du champ password par la valeur null . N’hésitez pas à aller ici pour en apprendre plus.

Création de la route login

On va rajouter un validator à notre fichier:

import vine from '@vinejs/vine'

// nouveau 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),
})
)

Ainsi que une méthode login pour notre controller:

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

export default class AuthController {
// nouvelle méthode login
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)
}
}

Et bien sûr rajouter la route login à notre 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')

Quand on teste la route elle nous renvoie l’utilisateur ainsi que le token.

Création d’une route protégée par token

Maintenant que nous avons nos routes login et register nous allons créer une route seulement accessible par un utilisateur enregistré.

Allez dans config/auth.ts et rajoutez accessToken:

const authConfig = defineConfig({
default: 'api',
guards: {
api: tokensGuard({
provider: tokensUserProvider({
tokens: 'accessTokens', // <---- rajoutez cette ligne
model: () => import('#models/user'),
}),
}),
},
})

Sans ça Adonisjs n’est pas en mesure de trouver notre propriété accessToken de notre modèle et donc vérifier si notre token est bon.

Pour finir ajoutez la route /me à notre router pour recevoir les informations de l’utilisateur connecté.

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')

// ajoutez cette 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())

La route va renvoyer les informations sur l’utilisateur. On voit qu’on a défini un middleware avec le guard api et qu’on a utiliser l’objet auth pour avoir accès à l’utilisateur. Pour tester la route mettez dans les headers la propriété Authorization la valeur du token (Bearer token).

Création de la route logout

Maintenant que nous avons nos routes pour se login et se register il nous manque plus que la route pour se déconnecter. Pour déconnecter un utilisateur nous devons simplement supprimer son token dans la base de donnée.

On va rajouter une méthode logout à notre controller d’authentification:

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' })
}
}

L’objet auth va nous permettre de récupérer l’utilisateur connecté puis on va récupérer son token. Ensuite va vérifier que ce token est pas indéfini (ça ne devrait pas arriver vu que le user s’est authentifié avec) puis on va supprimer le token et renvoyer au client que tout s’est bien passé.

Rajout de la route logout:

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

Comme pour la route me on va lui dire d’utiliser le middleware d’authentification avec use(middleware.auth()) pour avoir accès à l’objet auth dans notre méthode de notre controller.

Si vous voulez le code complet je vous invite a aller voir mon repository github.

Conclusion

Vous savez maintenant comment créer un système d’authentification par OAT avec Adonisjs. N’hésitez pas à lire la documentation officielle pour en apprendre d’avantage.

--

--