Start here

Arrows to move, enter to open, escape to close.

Ship/Now

Ce qui est branché · 3 min

Authentification

Liens magiques et OAuth sur Supabase Auth, et les deux pannes qui coûtent une journée chacune quand on les découvre.

Les sessions vivent dans des cookies que le serveur peut lire, et c'est ce qui permet à requireUser() de fonctionner dans un Server Component sans état de chargement.

import { requireUser, currentUser } from "@/lib/auth";

// Dans une page qui n'a aucun sens déconnecté. Redirige s'il n'y a pas de session.
const user = await requireUser();

// Là où être déconnecté est un état normal, la navigation par exemple.
const maybe = await currentUser();

currentUser est enveloppé dans le cache de React, donc un layout et la page qu'il contient partagent un seul appel au serveur d'authentification au lieu d'en faire deux.

Le piège du lien magique

Les modèles d'e-mail par défaut de Supabase utilisent {{ .ConfirmationURL }}, qui renvoie la session dans un fragment d'URL. Un fragment n'est jamais envoyé au serveur. Votre route de callback voit une query string vide, ne crée aucune session, et renvoie l'utilisateur vers la page de connexion sans erreur à lui montrer. On dirait que le lien est cassé. Il ne l'est pas : la session est partie vers le navigateur et personne ne l'a lue.

Réécrivez les trois modèles, confirmation d'inscription, lien magique et changement d'adresse, pour envoyer un token hash vers votre propre callback :

{{ .SiteURL }}/auth/callback?token_hash={{ .TokenHash }}&type=magiclink

Dans un route handler, modifier le store de cookies de next/headers ne suit pas sur un NextResponse.redirect. Vous vérifiez le token, vous posez la session, vous redirigez, et la session a disparu.

Liez plutôt le client Supabase à l'objet de réponse :

let response = NextResponse.redirect(`${origin}${target}`);

const supabase = createServerClient(url, key, {
  cookies: {
    getAll: () => request.cookies.getAll(),
    setAll: (list) => {
      list.forEach(({ name, value, options }) =>
        response.cookies.set(name, value, options));
    },
  },
});

await supabase.auth.verifyOtp({ type, token_hash: tokenHash });
return response;

C'est ce que fait src/app/auth/callback/route.ts, et le commentaire au-dessus le dit, parce que c'est exactement le code que quelqu'un simplifiera dans six mois.

Les profils

Une ligne dans auth.users ne vous appartient pas. Le schéma crée une ligne public.profiles correspondante depuis un trigger :

create trigger on_auth_user_created
  after insert on auth.users
  for each row execute function public.handle_new_user();

Un trigger, et pas un insert dans le handler d'inscription, parce qu'un utilisateur peut arriver par OAuth, par une invitation ou par le tableau de bord, et seule la base voit les trois. Faites-le côté application et vous aurez un jour un utilisateur sans profil et une page qui plante sur profile.full_name.

Ajouter un fournisseur

Activez-le dans le tableau de bord Supabase, ajoutez l'URL de redirection, et ajoutez un bouton. L'appel client tient en une ligne :

await supabaseBrowser().auth.signInWithOAuth({
  provider: "github",
  options: { redirectTo: `${location.origin}/auth/callback?next=/app` },
});

Aucune nouvelle route. Le même callback s'en charge, puisqu'il lit déjà ?code= aussi bien que ?token_hash=.

Une erreur ou un manque sur cette page ? Dites-le-nous.