Utiliser Nuxt-auth-utils pour implémenter un login par lien magique

By Hugo LassiègeMay 18, 20245 min read

RssFeedPulse vient de sortir, c'est un SAAS que j'ai construit dans les deux dernières semaines permettant d'envoyer une newsletter basée sur un flux RSS. C'est bien évidemment l'outil que j'utilise pour la newsletter de ce blog ;)

Et bien sûr, qui dit SAAS, dit écran de login. Je ne vais pas vous mentir, c'est rarement la partie la plus agréable à faire. Des écrans de logins, j'en ai déjà réalisé des centaines et c'est plus long qu'intéressant.

Donc évidemment j'ai souhaité trouver une librairie qui fasse tout pour moi. J'ai même regardé Supabase ou Clerk, mais j'ai choisi de garder la main sur ma base utilisateur et donc ne pas utiliser ces services.
Le détail de la stack complète vous intéresse ? J'en parlais dans un post précédent.

Bref, pour ce SAAS, j'ai choisi d'utiliser nuxt-auth-utils, écrit par

qui est tout simplement le CEO de NuxtLabs, la société qui édite le Framework Nuxt.

le login par lien magique

Voici le formulaire de login en question :

Formulaire de login
Formulaire de login

qui au passage est légèrement différent du formulaire de signup, parce qu'une inscription utilisateur doit toujours rappeler la valeur du produit. Mais c'est un autre sujet.

Formulaire de signup
Formulaire de signup

Vous noterez que je ne propose que deux choix :

  • un signup/login social par Google
  • un signup/login par email, sans mot de passe !

Le second mode est ce qu'on appelle, un login par lien magique.

Un lien magique c'est tout simplement un lien que l'on reçoit dans sa boite email, permettant de se connecter à une application.
C'est une option qui permet d'éviter l'usage de mot de passe. On appelle donc aussi cela : passwordless authentication.

Je ne vais pas entrer dans le détail, mais c'est aujourd'hui très utilisé, par exemple sur medium, supabase, clerk, microsoft live et j'en passe.

Le gros avantage que moi j'y vois, c'est que je ne veux pas gérer les mots de passe des utilisateurs. Je ne souhaite pas coder l'ensemble des fonctionnalités nécessaires liées à la gestion du mot de passe : reset de mot de passe, modification etc...
Et surtout, je ne veux pas être responsable de votre mot de passe, de sa fiabilité, de son renouvellement, de la façon dont vous le sécurisez etc...

Bref, j'ai choisi de n'avoir que le login social et le login par lien magique.

Maintenant voilà, Nuxt-auth-utils, ça sert à gérer les logins sociaux. Donc il va falloir un peu d'huile de coude pour gérer ce cas précis.

L'envoi du lien magique

Voici basiquement, sans aucune fioritures, un formulaire de login avec Nuxt :

<template>
<form ref="sendMagicLinkForm">
    <label for="email">Enter the email address associated with your account, and we’ll send a magic link to your inbox.</label>
    <input
        v-model="email"
        type="email"
        name="email"
        placeholder="youremail@test.com"
        required=""
    >
    <button type="submit" @click.prevent="sendMagicLink">
            Sign in
    </button>
</form>
</template>

L'utilisateur doit entrer son email, puis clique sur le bouton qui déclenche sendMagicLink.

La fonction sendMagicLink se contente d'envoyer l'email au serveur qui va se charger d'envoyer l'email

const sendMagicLinkForm = ref<HTMLFormElement | null>(null)
const email = ref('')

async function sendMagicLink() {

    if (!sendMagicLinkForm.value?.checkValidity()) {
        sendMagicLinkForm.value?.reportValidity()
    }

    try {
        await $fetch('/api/auth/signin/magic-link', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: {
                email: email.value,
            }
        })
    } catch (error) {
        consola.error(error)
    }
}

Ici, vous pouvez utiliser une route serveur côté Nuxt. De mon côté j'utilise Spring Boot et Kotlin. Mais c'est assez simple de transposer dans un aute language.

Voici déjà la méthode qui va envoyer un email avec le lien magique

    @PostMapping("/api/account/signin/magic-link")
    fun signinViaMagicLink(email: String): ResponseEntity<Void> {
        val account = sendMagicLink(email)

        return ResponseEntity.ok().build()
    }
    
    fun sendMagicLink(email: String): Account {
        val account = accountRepository.findByEmail(email) ?: throw BadParameterException("Account not found")
        val magicLink = createMagicLink(account.id!!)
        val emailBody = emailContentBuilder.getMagicLinkContent(magicLink)
        emailSender.sendMagicLinkEmail(email, "Signin to RssFeedPulse", emailBody)
        return account
    }

Le code d'envoi de l'email n'est pas le plus important.
La création elle-même du lien magique peut utiliser des mécanismes d'UUID. Ce qui est important par contre, c'est veiller à ce que lien magique ne soit pas valable plus de x minutes. On peut utiliser un TTL si la base de données le permet, ou tout simplement vérifier la date de création du lien avant de le valider.

Parce que oui, c'est pas terminé. Il faut maintenant valider le lien ET créer la session utilisateur.

La validation du lien magique

L'utilisateur va recevoir un email avec ce lien :

Lien magique reçu par email
Lien magique reçu par email

L'utilisateur peut donc à cette étape cliquer sur le lien, ce qui l'enverra sur le site, authentifié.

La validation du lien magique côté Kotlin est relativement simple, on compare ici les deux tokens, celui en base de données, et celui du lien. À noter qu'on supprime le lien aussi une fois utilisé. Un lien ne doit servir qu'une seule fois.

    @PostMapping("/api/account/signin/magic-link/verify")
    fun confirmSigninViaMagicLink(token: String): ResponseEntity<Void> {
        val account = magicLinkService.verifyMagicLink(token)
        return ResponseEntity.ok().build()
    }
    @Transactional
    fun verifyMagicLink(token: String): Account {
        val magicLink = magicLinkRepository.findByToken(token) ?: throw BadParameterException("Magic link not found")
        val since = LocalDateTime.now().minusHours(2)
        if (magicLink.expirationDate.isBefore(since)) {
            throw BadParameterException("Magic link expired")
        }
        accountRepository.findById(magicLink.account.id!!).orElseThrow { BadParameterException("Account not here anymore") }
        magicLinkRepository.deleteByToken(token)
        return magicLink.account
    }

Mais, il faut maintenant créer la session utilisateur.

La création de la session utilisateur

En réalité, le lien de l'email ne renvoie pas directement vers le serveur kotlin, on va cette fois-ci utiliser une route serveur Nuxt

Voici à quoi ça ressemble :

export default defineEventHandler(async (event) => {

    try {
        const token = getRouterParam(event, 'token') as string
        await confirmSigninFromSigninLink(event, token)
        return sendRedirect(event, '/campaigns')
    } catch (error: any) {
        throw await errorManager(error, true)
    }
})
async function confirmSigninFromSigninLink(event: H3Event, token: string): Promise<AccountDTO> {
    try {
        // call to the kotlin backend        
        const account = await api.confirmSigninViaMagicLink({token})
        await setUserSession(event, {
            user: {
                login: account.name,
                email: account.email,
                loggedInAt: new Date().toISOString(),
            },
        })
        return account
    } catch (error: any) {
        throw await errorManager(error, false)
    }
}

Deux choses sont extrêmement importantes à comprendre ici :

  • l'usage de setUserSession qui permet de modifier la session courante de notre utilisateur
  • le fait de renvoyer la session côté client avec le redirect sendRedirect(event, '/campaigns')

A cette étape, le lien a été consommé et l'utilisateur redirigé, avec une session valide.

Voilà, c'est tout pour aujourd'hui. J'espère que ce petit tutoriel vous aura aidé à comprendre comment implémenter un login par lien magique avec Nuxt-auth-utils.


Share this:

Written by Hugo Lassiège

Software engineer, ex-freelance, ex-cofounder, ex-CTO. I love building things, sharing knowledge and helping others.

Copyright © 2024
 Eventuallycoding
  Powered by Bloggrify