Utilisation de nuxt-auth-utils pour gérer des tokens d'API

By Hugo LassiègeMar 17, 20246 min read

Hey, j'ai pas mal joué avec nuxt récemment et notamment pour commencer à créer une application de suivi de ma chaine Youtube

Le studio youtube est pas mal, mais je trouve qu'il manque certaines analyses. Voici une partie de ce que j'ai pu faire jusqu'ici :

Affichage d'un écran de mon side project pour Youtube
Affichage d'un écran de mon side project pour Youtube

Pour cette application, j'ai dû utiliser l'API Youtube et elle nécessite donc de mettre en place une authent et une récupération de token d'api. Je vous propose de voir comment mettre ça en place.

Le flow d'authentification

Mon flow d'authentification doit être le suivant :

  • l'utilisateur se loggue
  • il arrive ensuite sur une page qui lui demande de sélectionner un channel youtube
  • je stocke les tokens d'accès pour ensuite appeler l'API Youtube

C'est un process relativement classique pour une API. Dans de très nombreux cas, on vous file directement une clé d'API et c'est terminé.

Avec Youtube, vous allez plutôt utiliser le protocole Oauth. Ce protocole permet à votre utilisateur d'aller se logger via un "Authorization server" externe. Cet authorization server va vous autoriser l'accès et vous fournir un access token en retour. Ce token d'accès pourra être utilisé dans tous les appels d'API suivant.

Affichage d'un écran de mon side project pour Youtube, issu de Wikipedia
Affichage d'un écran de mon side project pour Youtube, issu de Wikipedia

Voyons en détail l'implémentation.

Nuxt-auth-utils

Avec Nuxt, j'ai testé ces options là :

  • Nuxt-auth en principe le module officiel, mais il n'est pas encore prêt pour Nuxt 3
  • Nuxt-auth-sidebase, je n'ai pas réussi à l'utiliser pour récupérer les access token.
  • Nuxt-supabase, je n'ai pas réussi à l'utiliser pour récupérer les access token non plus. Et j'étais un peu embêté de devoir utiliser un SAAS pour la gestion de mes utilisateurs.

Malheureusement, je n'ai pas abouti avec ces 3 pistes. Et puis

m'a redirigé vers nuxt-auth-utils.

Et, cette fois-ci, c'est bon. Donc c'est cette solution que je vous propose de découvrir.

Mise en place

(la doc est déjà écrite sur le github du projet, mais je vous en remets une partie ici).

La première chose à faire est bien sûr d'installer le package :

npm install --save-dev nuxt-auth-utils

puis d'ajouter le module

export default defineNuxtConfig({
  modules: [
    'nuxt-auth-utils'
  ]
})

Ensuite, il vous faut créer un identifiant d'API sur la console Google. Je m'épargne cette partie et je vous renvoie vers la doc officielle

TIP

la page de doc concerne l'api google ads, mais on s'en fout, c'est le même principe, prenez bien soin d'activer l'api qui vous intéresse et pas celle de google ads si elle ne vous intéresse pas.

Suite à cette étape, vous devriez avoir en votre possession un client_id et un client_secret. Ces deux valeurs doivent être mis dans un fichier .env à la racine de votre projet

NUXT_SESSION_PASSWORD=password-with-at-least-32-characters

NUXT_OAUTH_GOOGLE_CLIENT_ID=YOUR_CLIENT_ID
NUXT_OAUTH_GOOGLE_CLIENT_SECRET=YOUR_CLIENT_SECRET

Configurer la fenêtre de login

Une fois que tout est en place, vous pouvez créer une page : index.vue qui contient la page de login.

<template>
    <div v-if="!loggedIn">
        <main >
            <a href="/api/auth/googlelogin" type="button">
                Sign in with Google
            </a>
        </main>
    </div>
</template>
<script setup lang="ts">
// Get the user session from nuxt-auth-utils 
const { loggedIn } = useUserSession()

// if the user is already logged, redirect the user to /dashboard 
if (loggedIn.value) {
    navigateTo('/dashboard')
}
</script>

Le lien va déclencher le flow d'authentification. Evidemment pour ça, il faut avoir créé un handler.

Il s'agit du fichier server/api/auth/googlelogin.get.ts

import {setUserSession, oauth} from "#imports";

export default oauth.googleEventHandler({
    async onSuccess(event, { user, tokens }) {

        // Optionally : find if the user with this email already exist in your database
        // if not, create the user
        // ..... 

        await setUserSession(event, {
            user: {
                login: user.name,
                email: user.email,
                loggedInAt: new Date().toISOString(),
            },
        })
        return sendRedirect(event, '/channel')
    },
    onError(event, error) {
        return sendRedirect(event, '/')
    },
})

En cas de succès, l'utilisateur sera redirigé vers la page channel. Dans mon cas, on va demander à l'utilisateur de choisir le channel Youtube à connecter à son compte.

<template>

    <a href="/api/auth/google" type="button">
        Choose channel to join
    </a>

</template>
<script setup lang="ts">
// this page is protected
definePageMeta({
        middleware: 'auth',
    }
)

</script>

Vous noterez ici que ma page utilise un middleware. Je ne souhaite pas qu'on puisse accéder à cette page sans être loggé au préalable. Dans cet exemple précis, c'est peu important mais mon application va un peu plus loin que l'exemple que je vous montre ici.

Voici le code de ce middleware, qui n'a vraiment rien d'extraordinaire.

import {navigateTo, defineNuxtRouteMiddleware, useUserSession} from "#imports";

export default defineNuxtRouteMiddleware(async () => {
    const { loggedIn } = useUserSession()
    if (!loggedIn.value) {
        return navigateTo("/");
    }
})

Et enfin, voyons le code qui nous intéresse beaucoup, c'est le second handler de login :

import {setUserSession, oauth} from '#imports'

export default oauth.googleEventHandler({ 
    config: {
        scope: ['openid', 'email', 'profile', 'https://www.googleapis.com/auth/youtube'],
        authorizationParams: {
            access_type: 'offline',
        }
    },
    async onSuccess(event, {user, tokens}) {
        const session = await requireUserSession(event)

        // Optional, store the token in database 
        // ...
        // Tokens contains access_token AND refresh_token

        await setUserSession(event, {
            user: {
                login: session.user.login,
                email: session.user.email,
                loggedInAt: session.user.loggedInAt
            },
        })
        return sendRedirect(event, '/dashboard')
    },
    // Optional, will return a json error and 401 status code by default
    onError(event, error) {
        console.error('Google OAuth error:', error)
        return sendRedirect(event, '/')
    },
})

Ce code est intéressant à plus d'un titre :

    config: {
        scope: ['openid', 'email', 'profile', 'https://www.googleapis.com/auth/youtube'],
        authorizationParams: {
            access_type: 'offline',
        }
    },

Ce bloc configure les scopes que vous allez demander lors de l'échange avec l'IDP de Google. Vous noterez 'https://www.googleapis.com/auth/youtube' qui va déclencher l'écran de sélection d'un channel youtube. Mais pourriez avoir besoin de l'API d'accès au drive, ou toute autre API de Google.

Enfin, très important, on demande un accès offline. C'est-à-dire qu'on va recevoir non seulement un access_token, mais aussi un refresh_token.

Le refresh token

Il faut comprendre que lors du retour de l'authent google. Le onSuccess vous renvoie des tokens mais, ces tokens ont une durée de vie limitée. C'est-à-dire que même si votre session nuxt est toujours valable, l'access token, lui, il peut avoir expiré. Un token Google expire souvent au bout d'une heure.

A ce moment là, si vous appelez une API avec ce token, vous obtiendrez une 401.

Donc, il faut utiliser le refresh token pour demander un nouveau access token s'il n'est plus valide.

async function getAccessToken(channelId: string) {

    const token = await getTokenFromDb(channelId)

    if (token && token.expiresAt && new Date(token.expiresAt) > new Date()) {
        return token.accessToken
    }
    
    // else, we have to renew the token 
    else {
        const refreshToken = token.refreshToken
        const response = await fetch('https://oauth2.googleapis.com/token', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded'
            },
            body: `client_id=${process.env.NUXT_OAUTH_GOOGLE_CLIENT_ID}&client_secret=${process.env.NUXT_OAUTH_GOOGLE_CLIENT_SECRET}&refresh_token=${refreshToken}&grant_type=refresh_token`
        })
        const tokens = await response.json() as { access_token: string, refresh_token: string }
        await updateToken(tokens.access_token, tokens.refresh_token, channelId)
        return tokens.access_token
    }
}

L'appel à getAccessToken va donc aller lire le token stocké en base de donnée, et sa date d'expiration. La date d'expiration a été positionné à 1h moins 5 minutes pour anticiper sa péremption.

Si le token est expiré, on va redemander un nouveau token AVEC le refresh_token et on peut mettre à jour la base de données avec ce nouveau token.

Et voilà, c'est tout pour aujourd'hui. N'hésitez pas à me poser des questions en commentaires ou me dire si vous avez des retours sur cette implémentation.


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