Rôles, permissions & multi-tenant dans une API .NET / React

Après avoir parlé du choix JWT / cookies / OAuth2 puis des schémas d’architecture d’authentification, il manque une brique essentielle : qui a le droit de faire quoi une fois connecté.

Dans cet article, on voit comment gérer :

  • les rôles simples (Admin, User, Manager),
  • les permissions plus fines (feature flags / policies),
  • et un début de multi-tenant (plusieurs organisations dans la même app).

1) Rôles simples avec [Authorize] et React

Premier niveau : des rôles attachés à l’utilisateur (Admin, Editor, Student…).

+------------------+        JWT        +--------------------+        +-------------+
|  Front React     | <--------------> |  API .NET          | ---->  |   SQL / DB   |
|  (routes / UI)   |  roles, claims   |  [Authorize(Roles)] |        |   Users      |
+------------------+                   +--------------------+        +-------------+
        |                                                  ^
        |                   Claim "role" = "Admin"         |
        +--------------------------------------------------+

.NET – inclure le rôle dans le JWT

On part d’un login qui valide l’utilisateur et génère un JWT contenant un claim role.

// AuthController.cs (extrait)

[HttpPost("login")]
public async Task<IActionResult> Login(LoginRequest request)
{
    var user = await _userService.ValidateCredentialsAsync(
        request.Email, request.Password);

    if (user is null)
        return Unauthorized();

    var claims = new List<Claim>
    {
        new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
        new Claim(ClaimTypes.Email, user.Email),
        new Claim(ClaimTypes.Name, user.DisplayName),
        new Claim(ClaimTypes.Role, user.Role) // <-- "Admin", "User", ...
    };

    var token = _jwtFactory.CreateToken(claims);

    return Ok(new
    {
        accessToken = token,
        user = new { user.Id, user.Email, user.DisplayName, user.Role }
    });
}

.NET – sécuriser un contrôleur par rôle

Exemple d’endpoint accessible uniquement aux admins.

// AdminController.cs

[ApiController]
[Route("api/admin")]
[Authorize(Roles = "Admin")]
public class AdminController : ControllerBase
{
    [HttpGet("users")]
    public async Task<IActionResult> GetUsers()
    {
        var users = await _userService.GetAllAsync();
        return Ok(users);
    }
}

Tu peux combiner plusieurs rôles : [Authorize(Roles = "Admin,Manager")].

React – stocker le rôle côté front

On récupère le rôle à la connexion et on le garde dans un contexte.

// auth-context.tsx (simplifié)

import React, { createContext, useContext, useState } from "react";

type User = {
  id: string;
  email: string;
  displayName: string;
  role: "Admin" | "User" | "Manager";
};

type AuthContextType = {
  user: User | null;
  accessToken: string | null;
  login: (email: string, password: string) => Promise<void>;
  logout: () => void;
};

const AuthContext = createContext<AuthContextType | undefined>(undefined);

export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  const [user, setUser] = useState<User | null>(null);
  const [accessToken, setAccessToken] = useState<string | null>(null);

  const login = async (email: string, password: string) => {
    const res = await fetch("/api/auth/login", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ email, password }),
      credentials: "include"
    });

    if (!res.ok) throw new Error("Bad credentials");

    const data = await res.json();
    setAccessToken(data.accessToken);
    setUser(data.user);
  };

  const logout = () => {
    setUser(null);
    setAccessToken(null);
  };

  return (
    <AuthContext.Provider value={{ user, accessToken, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
};

export function useAuth() {
  const ctx = useContext(AuthContext);
  if (!ctx) throw new Error("useAuth must be used inside <AuthProvider>");
  return ctx;
}

React – composant <ProtectedRoute> par rôle

On protège certaines routes React Router selon un ou plusieurs rôles.

// ProtectedRoute.tsx

import { Navigate } from "react-router-dom";
import { useAuth } from "./auth-context";

type Props = {
  children: JSX.Element;
  roles?: string[]; // ex: ["Admin"] ou ["Admin", "Manager"]
};

export function ProtectedRoute({ children, roles }: Props) {
  const { user } = useAuth();

  if (!user) {
    return <Navigate to="/login" replace />;
  }

  if (roles && !roles.includes(user.role)) {
    return <Navigate to="/forbidden" replace />;
  }

  return children;
}

Utilisation avec React Router :

// AppRoutes.tsx

<Route
  path="/admin"
  element={
    <ProtectedRoute roles={["Admin"]}>
      <AdminDashboard />
    </ProtectedRoute>
  }
/>

2) Permissions fines avec policies et claims

Les rôles ne suffisent pas toujours : tu peux avoir deux admins, mais seulement un qui a le droit de valider les paiements ou de gérer les tenants. On passe alors à des policies.

+----------+   claim "perm"   +--------------------+
|  JWT     | -------------->  |  Policies .NET     |
|          |  "payments:read" |  [Authorize(...)]  |
+----------+   "payments:approve"                  |

.NET – définir des policies basées sur un claim

// Program.cs (extrait)

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("CanApprovePayments", policy =>
        policy.RequireClaim("perm", "payments:approve"));

    options.AddPolicy("CanManageTenants", policy =>
        policy.RequireClaim("perm", "tenants:manage"));
});

.NET – remplir les claims de permissions

Lors de la génération du token, on ajoute les permissions calculées à partir du rôle / profil de l’utilisateur.

// JwtFactory.cs (exemple rapide)

public string CreateToken(AppUser user)
{
    var claims = new List<Claim>
    {
        new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
        new Claim(ClaimTypes.Email, user.Email),
        new Claim(ClaimTypes.Role, user.Role)
    };

    // Permissions dérivées du rôle
    foreach (var perm in _permissionService.GetPermissionsForRole(user.Role))
    {
        claims.Add(new Claim("perm", perm)); // ex: "payments:approve"
    }

    // ... création du JWT comme d'habitude
}

.NET – utiliser les policies dans les contrôleurs

// PaymentsController.cs

[ApiController]
[Route("api/payments")]
public class PaymentsController : ControllerBase
{
    [HttpGet]
    [Authorize(Policy = "CanApprovePayments")]
    public IActionResult GetPending()
    {
        // ...
        return Ok(/* paiements en attente */);
    }

    [HttpPost("{id}/approve")]
    [Authorize(Policy = "CanApprovePayments")]
    public IActionResult Approve(Guid id)
    {
        // ...
        return NoContent();
    }
}

React – afficher/masquer des actions selon les permissions

Côté front, on peut décoder le JWT ou simplement récupérer la liste des permissions dans la réponse login.

// auth-context.tsx (ajout d'un champ permissions)

type User = {
  id: string;
  email: string;
  displayName: string;
  role: string;
  permissions: string[]; // ex: ["payments:approve", "tenants:manage"]
};

// ... dans login():
const data = await res.json();
setAccessToken(data.accessToken);
setUser({
  id: data.user.id,
  email: data.user.email,
  displayName: data.user.displayName,
  role: data.user.role,
  permissions: data.user.permissions
});

Petit helper pour tester une permission :

// usePermission.ts

import { useAuth } from "./auth-context";

export function usePermission(permission: string) {
  const { user } = useAuth();
  return !!user?.permissions?.includes(permission);
}

Utilisation dans un composant :

// PaymentsPage.tsx

import { usePermission } from "../auth/usePermission";

export function PaymentsPage() {
  const canApprove = usePermission("payments:approve");

  return (
    <div>
      <h2>Paiements en attente</h2>

      {/* ... tableau ... */}

      {canApprove && (
        <button>Valider la sélection</button>
      )}
    </div>
  );
}

3) Début de multi-tenant (tenantId dans le token)

Dernière brique : ton application sert plusieurs organisations / clients. Chaque requête doit être filtrée par TenantId pour éviter les fuites de données.

+-----------------+       JWT       +--------------------+
| React (tenant A)| ---- tenant=1 ->| API .NET           |
+-----------------+                 | filtre par tenant  |
                                    +--------------------+

.NET – ajouter le tenant dans les claims

Au moment du login, on associe l’utilisateur à un tenant.

// Lors de la création du token

claims.Add(new Claim("tenant", user.TenantId.ToString()));

.NET – service pour récupérer le TenantId courant

// CurrentTenant.cs

public interface ICurrentTenant
{
    Guid Id { get; }
}

public class CurrentTenant : ICurrentTenant
{
    public Guid Id { get; }

    public CurrentTenant(IHttpContextAccessor accessor)
    {
        var claim = accessor.HttpContext?.User.FindFirst("tenant")?.Value;
        if (Guid.TryParse(claim, out var id))
            Id = id;
    }
}

Enregistrement dans DI :

// Program.cs

builder.Services.AddHttpContextAccessor();
builder.Services.AddScoped<ICurrentTenant, CurrentTenant>();

.NET – filtrer les données par tenant

Exemple avec Entity Framework Core.

// OrdersRepository.cs (extrait)

public class OrdersRepository
{
    private readonly AppDbContext _db;
    private readonly ICurrentTenant _tenant;

    public OrdersRepository(AppDbContext db, ICurrentTenant tenant)
    {
        _db = db;
        _tenant = tenant;
    }

    public Task<List<Order>> GetForCurrentTenantAsync()
    {
        return _db.Orders
            .Where(o => o.TenantId == _tenant.Id)
            .ToListAsync();
    }
}

React – sélectionner le tenant (optionnel)

Côté front, tu peux permettre à un utilisateur multi-tenant (ex. support) de choisir sur quel tenant il travaille, et envoyer ce choix dans un header ou dans l’URL. Mais côté sécurité, c’est toujours le claim tenant du token qui fait foi.


Conclusion

  • Rôles : simples à mettre en place, bonne première étape.
  • Policies / permissions : idéales pour des règles métiers fines.
  • Multi-tenant : toujours filtrer les données à partir du TenantId dans le token.

Comme pour l’authentification, le but est que ton modèle d’autorisations reste expliquable en 2–3 minutes au tableau blanc. Si tu n’arrives plus à recommander qui a accès à quoi, c’est qu’il est temps de simplifier la matrice de rôles et permissions. 😉

← Retour au blog