Supabase Auth: pourquoi user_metadata.role n'est pas fiable
Un role dans user_metadata peut sembler pratique. Pour des permissions serieuses, ce n'est pourtant pas une base fiable.
Mettre un role metier dans user_metadata parait pratique. En quelques lignes, vous stockez "admin", "manager" ou "client" et vous avancez. Le probleme, c'est que ce n'est pas un bon endroit pour de l'autorisation serieuse.
Avec Supabase, la distinction importante est la suivante :
rawusermeta_datapeut etre modifie par l'utilisateurrawappmeta_datane peut pas etre modifie par l'utilisateur
Cette difference change tout. Si une policy RLS ou une logique d'acces se base sur une information que l'utilisateur peut modifier lui-meme, ce n'est plus une vraie permission. C'est une porte que vous lui laissez entrouverte.
Pourquoi l'erreur est frequente
Le schema est tentant :
- on enregistre un utilisateur
- on lui ajoute un champ
roledansuser_metadata - on lit ce champ dans le front ou dans une policy
Sur le papier, c'est simple. En pratique, c'est fragile, parce que Supabase documente clairement que rawusermeta_data est modifiable par l'utilisateur avec supabase.auth.update().
Si vous utilisez ce champ comme preuve d'autorisation, vous melangez un champ pratique de profil avec une regle de securite.
Ce qu'il faut utiliser a la place
Pour des roles, droits ou statuts qui doivent avoir une valeur de securite, la bonne zone est rawappmeta_data.
Supabase l'explique aussi dans sa documentation RLS :
rawusermeta_datan'est pas un bon endroit pour les donnees d'autorisationrawappmeta_dataest le bon endroit pour ces donnees, car l'utilisateur ne peut pas le modifier lui-meme
Dit simplement :
user_metadata= donnees de profil, preferences, infos non sensiblesapp_metadata= donnees d'autorisation ou de contexte d'acces
Le vrai risque
Le risque n'est pas seulement theorique.
Imaginons une policy du type :
using ((auth.jwt() -> 'user_metadata' ->> 'role') = 'admin')
Si un utilisateur peut changer cette valeur, toute la logique devient douteuse. Vous croyez filtrer l'acces, mais vous basez ce filtre sur une source qui n'est pas fiable.
Le probleme est encore plus subtil quand l'application fonctionne "a peu pres". Vous pouvez avoir des tests qui passent, un admin qui voit ses bons ecrans, et pourtant une base qui repose sur une information editable par le mauvais acteur.
Une structure plus saine
La version solide ressemble plutot a ceci :
- vous gardez les roles metier dans
rawappmeta_dataou dans une table de roles interne - vous lisez ces informations dans vos policies ou vos controles serveur
- vous reservez
user_metadataaux champs pratiques, comme un nom, une langue ou un avatar
Exemple de policy plus saine :
using ((auth.jwt() -> 'app_metadata' ->> 'role') = 'admin')
Ou, mieux encore dans certains cas, vous passez par une table de jointure qui represente vos equipes, organisations ou niveaux d'acces. Cela demande un peu plus de schema, mais c'est plus lisible et plus evolutif.
Quand user_metadata reste utile
Ce n'est pas un champ "mauvais". Il est tres utile pour :
- afficher un prenom ou un nom public
- memoriser une preference de langue
- conserver un fuseau horaire
- stocker une valeur de confort qui n'a pas d'impact de securite
Le bon test est simple : si l'utilisateur change cette valeur, est-ce que cela doit pouvoir lui ouvrir un acces de plus ?
Si la reponse est non, alors user_metadata peut aller. Si la reponse est oui, il faut sortir cette logique de la.
Le piege des controles uniquement front-end
Beaucoup d'equipes font aussi une erreur voisine : elles cachent un bouton dans l'interface si user_metadata.role !== 'admin', puis elles pensent que la securite est reglee.
Ce n'est pas une securite. C'est juste une condition d'affichage.
Le front peut etre plus confortable avec ce genre de test. Mais le vrai controle doit vivre :
- dans la base avec la RLS
- ou dans du code serveur avec une verification fiable
Sinon, vous avez juste un joli rideau devant une porte mal fermee.
Une bonne strategie pour un portail client
Pour un projet concret, voici une bonne base :
user_metadatapour le nom, l'avatar, la langueapp_metadatapour le role global si vous avez un systeme simple- tables metier en base si vous avez des organisations, des permissions fines ou des acces par client
Exemple typique :
- un utilisateur appartient a une ou plusieurs organisations
- ses droits reels sont derives d'une table
memberships - le front peut afficher un badge ou un menu selon ces droits
- la base reste la source de verite
Cette approche evite de surcharger le JWT avec une logique qui devient vite trop rigide.
Comment auditer un projet existant
Si vous avez deja un projet en place, verifiez trois choses :
- Est-ce qu'une policy RLS lit
user_metadatapour une decision d'acces ? - Est-ce qu'un route handler ou un endpoint serveur traite
user_metadata.rolecomme une preuve suffisante ? - Est-ce que le front et la base utilisent la meme source de verite pour les droits ?
Si la reponse est oui au premier ou au deuxieme point, il y a probablement un nettoyage a faire.
Ce qu'il faut retenir
user_metadata est utile pour le profil. Il n'est pas fiable pour l'autorisation.
Si un role, un statut ou une permission a un impact de securite, basez-vous sur une source que l'utilisateur ne peut pas modifier, comme rawappmeta_data, ou mieux encore sur des tables metier et des policies claires.
La bonne question n'est pas "ou est-ce le plus simple a stocker ?". La bonne question est : qui peut changer cette valeur, et que se passe-t-il s'il la change ?
Sources officielles
Lire ensuite sur le meme sujet
Ces liens renforcent le cocon semantique du blog et aident Google a comprendre le sujet principal de l'article.
