L'autorisation est l'étape qui décide ce qu'une identité authentifiée a le droit de faire sur une API. C'est la source principale de vulnérabilités API en 2026 : BOLA, BFLA et BOPLA - les trois premiers de l'OWASP API Top 10 - sont tous des échecs d'autorisation. Ce guide couvre les fondamentaux : distinctions avec l'authentification, modèles (RBAC/ABAC/ReBAC/PBAC), couches d'autorisation (object/function/property), implémentations concrètes (middleware, decorators, policy engines OPA/Cedar/OpenFGA), pièges fréquents et tests.
1. Authentification vs autorisation
1.1 Distinction fondamentale
Deux fonctions distinctes, souvent confondues :
- Authentification (authN) : "qui es-tu ?" → vérification d'identité.
- Autorisation (authZ) : "que peux-tu faire ?" → vérification des droits.
Exemple :
- Alice présente un token JWT valide → authentification OK, elle est bien Alice.
- Alice essaie de supprimer la facture de Bob → autorisation DOIT ÉCHOUER, même si elle est authentifiée.
Les deux fonctions sont indépendantes et séquentielles : d'abord authentifier, ensuite autoriser.
1.2 Une erreur classique
Beaucoup de développeurs vérifient l'authentification via middleware global et oublient l'autorisation au niveau de l'endpoint. Résultat : tout utilisateur authentifié accède à toutes les ressources. C'est la cause racine de la majorité des incidents API.
1.3 Trois couches d'autorisation
Une autorisation complète couvre trois niveaux :
- Function-level : ai-je le droit d'appeler cet endpoint / cette fonction ?
- Object-level : ai-je le droit d'accéder à cette ressource spécifique (cet objet identifié par son ID) ?
- Property-level : quels champs de l'objet ai-je le droit de lire/modifier ?
Oublier l'un des trois niveaux = vulnérabilité. Respectivement : BFLA (API5), BOLA (API1), BOPLA (API3) dans l'OWASP API Top 10.
2. Les modèles d'autorisation - RBAC, ABAC, ReBAC, PBAC
2.1 RBAC - Role-Based Access Control
Chaque utilisateur a un ou plusieurs rôles (admin, editor, viewer). Chaque rôle a un ensemble de permissions (listes d'actions et de ressources).
Exemple :
| Rôle | Permissions |
|---|---|
| Viewer | documents.read, comments.read |
| Editor | documents.read, documents.write, comments.create |
| Admin | documents.*, users.*, settings.* |
Avantages : simple à comprendre, facile à gérer à petite échelle, largement supporté par les frameworks.
Limites :
- Explosion combinatoire : besoin fréquent de rôles fins (editor-region-1, editor-region-2...). Rapidement inmanageable.
- Pas de contexte dynamique : "editor mais uniquement pendant les heures ouvrées" est impossible à exprimer purement en RBAC.
- Mapping imparfait au métier : un utilisateur peut avoir des droits dérivés de relations (membre d'un projet, propriétaire d'un document) mal modélisés en rôles.
RBAC reste l'entrée de gamme appropriée pour la majorité des applications. Quand il atteint ses limites, on passe à ABAC ou ReBAC.
2.2 ABAC - Attribute-Based Access Control
Les décisions d'autorisation utilisent des attributs de l'utilisateur, de la ressource, de l'action, et du contexte.
Exemple de règle ABAC :
"Un utilisateur du département Finance peut consulter les documents classifiés 'Financial' uniquement depuis une IP interne, pendant les heures ouvrées, sur un device compliant."
Attributs utilisés :
- Sujet : département = Finance, role = employee, ancienneté = 5 ans.
- Ressource : type = document, classification = Financial, owner = Finance.
- Action : read.
- Contexte : source_ip = 10.0.0.0/8, time = 08:00-18:00, device_compliant = true.
Avantages : très flexible, exprime des règles complexes proches du métier.
Limites :
- Plus complexe à implémenter et tester.
- Plus coûteux en runtime (évaluation des attributs à chaque requête).
- Nécessite une source fiable pour chaque attribut (IdP, device management, HR).
2.3 ReBAC - Relationship-Based Access Control
Les permissions sont dérivées des relations entre entités (user, groups, resources). Popularisé par Google Zanzibar (2019), adopté par Figma, Netflix, Airbnb.
Exemple :
- Alice est membre du team
engineering. - Team
engineeringowne le dossierprojects/alpha. - Dossier
projects/alphacontient le documentdoc-123. - Donc Alice peut éditer
doc-123(transitivement).
Les permissions sont un graphe parcouru à la décision.
Avantages : modélise naturellement les produits de partage (Google Drive, Notion, Figma, GitHub).
Limites : plus complexe à mettre en place, nécessite outils dédiés (OpenFGA, SpiceDB, Warrant).
2.4 PBAC - Policy-Based Access Control
Les règles d'autorisation sont exprimées comme des policies en code, évaluées par un moteur dédié. Super-ensemble qui peut recouvrir RBAC, ABAC, ReBAC selon l'expression des policies.
Outils de référence : Open Policy Agent (OPA) avec langage Rego, AWS Cedar, Oso.
Exemple OPA/Rego :
package authz
default allow = false
allow if {
input.action == "read"
input.resource.type == "document"
input.user.org_id == input.resource.org_id
input.user.role in {"viewer", "editor", "admin"}
}
allow if {
input.action in {"edit", "delete"}
input.resource.type == "document"
input.user.org_id == input.resource.org_id
input.user.role in {"editor", "admin"}
}
allow if {
input.action == "read"
input.resource.type == "document"
input.user.id in input.resource.shared_with
}Avantages :
- Policies versionnées en Git, testées en CI, revues en PR.
- Externalisées de l'application : cohérence entre services.
- Évolution sans redéploiement applicatif.
- Audit structuré des décisions.
Limites :
- Courbe d'apprentissage du langage (Rego, Cedar).
- Infrastructure à maintenir (sidecar OPA, API Cedar).
- Latence additionnelle (minimisable avec bundles locaux).
2.5 Lequel choisir
| Besoin | Modèle |
|---|---|
| App simple, moins de 50 rôles différents | RBAC |
| Décisions dépendant du contexte runtime | ABAC |
| App avec partage granulaire entre users (Drive, Figma) | ReBAC |
| Plusieurs apps/services, cohérence policies critique | PBAC (OPA, Cedar) |
| Combinaison de plusieurs modèles | PBAC avec policies qui composent |
La plupart des applications matures utilisent un mix : RBAC simple pour les rôles de base + OPA/Cedar pour les règles business complexes + ReBAC pour les scénarios de partage spécifiques.
3. Object-level authorization - prévenir BOLA
L'autorisation au niveau de l'objet est la plus difficile à implémenter correctement et la source n°1 de vulnérabilités API.
3.1 Le piège ORM
Un pattern classique en ORM :
def get_invoice(invoice_id: int, current_user: User):
invoice = Invoice.objects.get(id=invoice_id)
return invoiceCe code authentifie (current_user non null) mais n'autorise pas : il renvoie l'invoice peu importe son propriétaire. BOLA direct.
3.2 Pattern recommandé - filter by user
Toujours filtrer la requête par l'utilisateur courant :
def get_invoice(invoice_id: int, current_user: User):
invoice = Invoice.objects.filter(
id=invoice_id,
customer_id=current_user.id
).first()
if not invoice:
raise HTTPException(status_code=404, detail="Not found")
return invoiceAvantages :
- Si l'invoice n'existe pas ou n'appartient pas à l'user, même réponse 404.
- Pas de fuite d'existence (on ne révèle pas que l'invoice 78902 existe pour quelqu'un d'autre).
- Logique d'autorisation dans la requête, plus difficile à oublier.
3.3 Pattern alternatif - check explicite
Si le filter direct n'est pas possible (relations complexes) :
def get_invoice(invoice_id: int, current_user: User):
invoice = Invoice.objects.get(id=invoice_id)
if not current_user.can_access(invoice):
raise HTTPException(status_code=404, detail="Not found")
return invoiceInconvénient : le code "oublie la vérification" est facile à écrire si la méthode can_access n'existe pas déjà.
3.4 Row-Level Security au niveau DB
Pour des garanties plus fortes, utiliser le Row-Level Security (RLS) de la base :
ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;
CREATE POLICY user_invoices ON invoices
FOR ALL TO api_service
USING (customer_id = current_setting('app.current_user_id')::int);L'application exécute SET app.current_user_id = <user_id> en début de transaction. Ensuite, impossible d'accéder aux données d'un autre user, même via requête SQL manuelle ou injection. Filet de sécurité en profondeur.
Supporté par : PostgreSQL, SQL Server, Oracle.
3.5 UUIDs vs integer IDs
Utiliser des UUIDs au lieu d'integer auto-increment pour les IDs exposés :
- Empêche l'énumération triviale (pas de
/invoices/1, /invoices/2, ...). - Ne remplace pas la vérification d'ownership : un attaquant qui connaît un UUID (fuité, partagé, scrapé) peut toujours accéder si l'authz est absente.
UUIDs = défense en profondeur, pas mitigation primaire.
4. Function-level authorization - prévenir BFLA
Vérifier que l'utilisateur a le rôle requis pour appeler la fonction/endpoint.
4.1 Middleware par rôle
Pattern FastAPI :
from fastapi import Depends, HTTPException
def require_role(role: str):
def checker(current_user: User = Depends(get_current_user)):
if role not in current_user.roles:
raise HTTPException(status_code=403, detail="Forbidden")
return current_user
return checker
@app.post("/api/admin/users/{user_id}/promote")
async def promote_user(
user_id: int,
admin: User = Depends(require_role("admin"))
):
...Le décorateur require_role("admin") est obligatoire sur tous les endpoints admin. Oublier = BFLA.
4.2 Séparation des routers
Pour éviter l'oubli, séparer les routes :
admin_router = APIRouter(
prefix="/api/admin",
dependencies=[Depends(require_role("admin"))]
)
user_router = APIRouter(
prefix="/api",
dependencies=[Depends(get_current_user)]
)Tout endpoint sous admin_router hérite automatiquement de la vérification admin. Pas besoin de l'ajouter individuellement.
4.3 Policy-as-Code pour fonction-level
Avec OPA/Cedar, exprimer les policies au niveau fonction :
package authz.api
default allow = false
# Endpoint admin : role admin requis
allow if {
startswith(input.path, "/api/admin/")
"admin" in input.user.roles
}
# Endpoint user standard : authentifié suffit
allow if {
startswith(input.path, "/api/users/me")
input.user.id != null
}Avantage : l'autorisation fonction-level devient une policy versionnée, testable, auditable.
5. Property-level authorization - prévenir BOPLA
5.1 Excessive Data Exposure - mitigation
Ne pas renvoyer l'objet complet mais un DTO limité :
from pydantic import BaseModel
class UserPublicDTO(BaseModel):
id: int
name: str
email: str
created_at: datetime
# pas de ssn, password_hash, is_admin, stripe_customer_id
class UserPrivateDTO(UserPublicDTO):
phone: str | None
address: str | None
# champs visibles par le propriétaire uniquement
class UserAdminDTO(UserPrivateDTO):
is_admin: bool
stripe_customer_id: str
# champs visibles par l'admin uniquementHandler :
@app.get("/api/users/{user_id}")
async def get_user(user_id: int, current_user: User = Depends(...)):
user = User.objects.get(id=user_id)
if current_user.id == user.id:
return UserPrivateDTO.from_orm(user)
if "admin" in current_user.roles:
return UserAdminDTO.from_orm(user)
return UserPublicDTO.from_orm(user)Chaque rôle a son schéma de sortie. Aucun champ sensible leaked par défaut.
5.2 Mass Assignment - mitigation
Utiliser un DTO strict en input avec extra = "forbid" :
from pydantic import BaseModel, ConfigDict
class UpdateProfileDTO(BaseModel):
model_config = ConfigDict(extra="forbid")
name: str | None = None
phone: str | None = None
# pas de role, is_admin, is_verifiedHandler :
@app.put("/api/users/me")
async def update_profile(
body: UpdateProfileDTO,
current_user: User = Depends(...)
):
user = User.objects.get(id=current_user.id)
for key, value in body.model_dump(exclude_unset=True).items():
setattr(user, key, value)
user.save()
return UserPrivateDTO.from_orm(user)Tout champ envoyé en plus dans le body fait échouer la validation Pydantic. Impossible de devenir admin par injection.
5.3 GraphQL et property-level
GraphQL a l'avantage naturel que seuls les champs demandés sont retournés. Mais l'autorisation par champ reste nécessaire :
type User {
id: ID!
name: String!
email: String!
ssn: String @auth(requires: [SELF, ADMIN])
isAdmin: Boolean @auth(requires: ADMIN)
}Directives GraphQL (@auth) permettent de déclarer les exigences par champ. Middleware serveur vérifie à l'exécution.
6. Patterns d'implémentation
6.1 Middleware global + décorateurs locaux
Architecture classique :
- Middleware global : vérifie l'authentification, attache
current_userau contexte. - Décorateurs/dépendances sur chaque endpoint : vérifient les rôles requis.
- Dans le handler : vérification d'ownership via filter ou check explicit.
Pattern Express.js + TypeScript :
app.use(authenticate); // middleware global
router.get(
"/api/invoices/:id",
requireRole("user"), // décorateur
async (req, res) => {
const invoice = await Invoice.findOne({
id: req.params.id,
customerId: req.user.id, // ownership check
});
if (!invoice) return res.status(404).json({ error: "Not found" });
res.json(InvoiceDTO.fromModel(invoice));
}
);6.2 Policy engine externe (OPA sidecar)
Pour des architectures microservices ou des policies complexes :
Application → middleware authN → OPA sidecar → décision allow/deny → handler
L'application n'exprime plus la logique d'autorisation en code : elle demande à OPA "ce user peut-il faire cette action sur cette ressource ?" et OPA répond basé sur les policies versionnées.
Avantages :
- Cohérence multi-services.
- Policies hot-reloadables.
- Audit centralisé.
Inconvénients :
- Latence (quelques ms) - minimisée via bundle local.
- Infrastructure à opérer.
6.3 OpenFGA / SpiceDB pour ReBAC
Pour des scénarios de partage granulaire :
# Définir la relation
authz_client.write(
user="user:alice",
relation="editor",
object="document:doc-123"
)
# Vérifier
if authz_client.check(
user="user:alice",
relation="editor",
object="document:doc-123"
):
# autoriserOpenFGA (Auth0), SpiceDB (AuthZed), Warrant, Google Zanzibar (fermé) offrent un graph d'autorisation haute performance.
6.4 Cedar (AWS)
AWS Cedar est orienté sécurité formelle - les policies sont analysables mathématiquement.
permit(
principal in Role::"editor",
action in [Action::"read", Action::"write"],
resource is Document
) when {
principal.organization == resource.organization
};Implémenté dans AWS Verified Permissions, ou utilisable en standalone via SDK Cedar.
7. Pièges classiques à éviter
7.1 Vérifier authz seulement côté UI
"Si le bouton admin n'est pas affiché, l'utilisateur ne peut pas l'appeler." Faux. L'attaquant appelle directement l'API, court-circuitant l'UI. L'autorisation doit être appliquée côté serveur, pas seulement côté client.
7.2 Vérifier l'user ID du body au lieu du token
Code vulnérable :
@app.get("/api/users/{user_id}")
async def get_user(user_id: int, claimed_user_id: int = Body()):
if user_id == claimed_user_id:
return User.objects.get(id=user_id)
raise HTTPException(403)L'attaquant envoie user_id=123 et claimed_user_id=123 dans le body même s'il est en réalité user 456. Le token JWT authentifie user 456 mais le code fait confiance au body.
Règle : toujours utiliser l'ID issu du token, jamais de l'input utilisateur.
7.3 Trust the URL
Code vulnérable :
@app.get("/api/users/{user_id}/private")
async def get_private(user_id: int, current_user: User = Depends(...)):
return PrivateData.objects.filter(user_id=user_id).first()Même si current_user.id est attaché au token, le code ne le vérifie pas contre user_id dans l'URL. Alice peut accéder aux private data de Bob via /api/users/456/private.
Mitigation : comparaison explicite.
if user_id != current_user.id:
raise HTTPException(403)7.4 Vérifier l'ownership après avoir chargé l'objet sensible
Code qui révèle de l'information par timing :
invoice = Invoice.objects.get(id=invoice_id)
# à ce stade, on a loggé "found invoice X"
if invoice.customer_id != current_user.id:
raise HTTPException(403)Le simple fait que la requête réussisse à charger l'objet (sans erreur) peut révéler son existence via timing. Mitigation : filter directement par user dans la requête DB.
7.5 Oublier une permission lors d'un refactor
Un refactor ajoute un nouveau endpoint similaire à un ancien, mais oublie le décorateur require_role. Mitigation : linting qui détecte les endpoints sans décorateur d'autorisation.
7.6 Logs trop fins sur les decisions d'authz
Loguer chaque decision avec tous les attributs y compris sensibles (SSN, password_hash) peut créer une fuite par logs. Mitigation : logs structurés avec masking des champs sensibles.
7.7 Race conditions sur permissions
Un user qui vient d'être révoqué mais dont le token JWT est encore valide (non expiré) peut continuer à appeler l'API. Mitigation : token courte durée + refresh token + possibilité de blacklister jti en cas d'urgence.
7.8 Pas de test d'authorization
Tests unitaires couvrent le path heureux (propriétaire accède à sa propre ressource). Pas de test BOLA/BFLA systématique. Mitigation : CI avec tests explicit "user A ne peut pas accéder aux données user B".
8. Tester l'autorisation
8.1 Tests unitaires
Pour chaque endpoint :
- Accès par le propriétaire : 200.
- Accès par un autre user : 404 ou 403.
- Accès par un admin : 200.
- Accès sans authentification : 401.
def test_get_invoice_owner():
response = client.get("/api/invoices/1", headers=auth_header(owner))
assert response.status_code == 200
def test_get_invoice_other_user():
response = client.get("/api/invoices/1", headers=auth_header(other))
assert response.status_code == 404 # not exposed existence
def test_get_invoice_admin():
response = client.get("/api/invoices/1", headers=auth_header(admin))
assert response.status_code == 200
def test_get_invoice_unauthenticated():
response = client.get("/api/invoices/1")
assert response.status_code == 4018.2 Tests BOLA automatisés
Pour chaque endpoint qui accepte un ID :
- Créer 2 users (A et B).
- Créer une ressource pour chacun.
- Tester l'accès de A aux ressources de B → doit échouer.
- Répéter pour toutes les méthodes (GET, PUT, DELETE).
Frameworks spécialisés : Akto, APIsec, Burp Suite avec extensions.
8.3 Tests BFLA automatisés
Pour chaque endpoint admin :
- Appeler avec un user standard.
- Vérifier que la réponse est 403 ou 404.
8.4 Policy testing (OPA/Cedar)
Les policies déclaratives sont testables comme du code :
package authz_test
test_editor_can_edit_own_doc if {
allow with input as {
"user": {"id": "alice", "role": "editor"},
"action": "edit",
"resource": {"type": "document", "owner_id": "alice", "org_id": "acme"},
}
}
test_editor_cannot_edit_other_org if {
not allow with input as {
"user": {"id": "alice", "role": "editor", "org_id": "acme"},
"action": "edit",
"resource": {"type": "document", "owner_id": "bob", "org_id": "other"},
}
}Exécutés en CI, les tests garantissent la cohérence des policies.
9. Outils et frameworks
9.1 Par langage / framework
| Stack | Solutions authorization |
|---|---|
| Node.js / Express | CASL, Casbin, AccessControl, middleware custom |
| Python / FastAPI, Django | FastAPI dependencies, Django guardian, Pundit-style |
| Ruby on Rails | Pundit, CanCanCan |
| Java / Spring | Spring Security (roles + methodSecurity + expressions) |
| Go | Casbin, OPA SDK, custom |
| .NET | Policy-based authorization natif, Casbin |
9.2 Policy engines cross-language
- OPA (Open Policy Agent) : Rego, sidecar, bundles local. Le plus adopté.
- AWS Cedar : DSL déclaratif, orienté sécurité formelle.
- Oso : library avec langage Polar, embeddable.
- Casbin : light, supporte RBAC/ABAC/ACL.
9.3 ReBAC dedicated
- OpenFGA : open source Auth0, inspiré Zanzibar.
- SpiceDB (AuthZed) : commercial + open source core.
- Warrant : acquis par WorkOS, embeddable.
9.4 API-specific
- 42Crunch : schema-based security avec validation authz.
- Permit.io : authorization-as-a-service, ReBAC + RBAC + ABAC.
- Aserto : PDP/PEP packaged.
10. Plan d'adoption - partir d'une app sans autorisation
Semaines 1-2 - inventaire
- Lister tous les endpoints.
- Classifier par niveau de sensibilité.
- Identifier les endpoints sans check d'autorisation.
- Créer une matrice "endpoint x rôle" attendue.
Semaines 3-6 - instrumentation
- Ajouter middleware global d'authentification si absent.
- Implémenter
require_roleet l'appliquer aux endpoints admin. - Ajouter ownership checks sur tous les endpoints
/{id}. - Tests BOLA/BFLA pour chaque endpoint critique.
Mois 2-3 - DTO et BOPLA
- Migrer vers DTO explicites en sortie (fin des objects ORM directs).
- Activer
extra = "forbid"sur les DTO d'input. - Audit de chaque endpoint pour fields leaked.
Mois 4-6 - maturité
- Évaluer policy engine (OPA, Cedar) selon complexité des règles métier.
- Externaliser la logique d'autorisation si multi-services.
- Automatiser les tests d'autorisation en CI.
- Red team exercise : tentative d'escalade via BOLA/BFLA.
11. Verdict et posture Zeroday
L'autorisation est la compétence API security la plus sous-estimée et celle qui produit le plus d'incidents en 2026. Les développeurs maîtrisent l'authentification (JWT, OAuth, sessions) beaucoup mieux que l'autorisation granulaire. Cette asymétrie est exploitée systématiquement.
Pour un développeur : maîtriser les trois couches d'autorisation (object, function, property) et les appliquer systématiquement est la compétence différenciante qui évite les accidents. Tests d'autorisation dédiés en CI = minimum vital.
Pour un AppSec : l'investissement dans un policy engine mature (OPA ou Cedar) est rentable dès qu'on a 3+ services qui partagent des règles. La cohérence et la testabilité apportées valent largement la complexité opérationnelle.
Pour une organisation : audit trimestriel des implémentations d'autorisation, particulièrement sur les endpoints nouveaux et les refactors. La majorité des incidents BOLA sont introduits par un refactor qui casse une vérification historique.
Pour approfondir : OWASP API Security Top 10 expliqué pour voir chaque item en détail, pourquoi les API sont attaquées pour le contexte, broken access control pour le parent conceptuel commun à BOLA/BFLA, least privilege en pratique pour l'articulation avec la politique IAM globale, SSO : définition pour l'authentification qui précède l'autorisation.







