Structurer les autorisations : authentification, contexte, politique et application
Structurer les autorisations en quatre couches (authentification, contexte, politique, application) pour un contrôle d'accès cohérent et auditable.
Points clés
- L'autorisation consiste à décider si un acteur authentifié peut effectuer une action précise sur une ressource précise. Elle doit être conçue comme un système, et non dispersée en vérifications locales.
- Découpez-la en quatre couches : le contexte d'authentification (qui), le contexte d'autorisation (les faits pertinents), la décision de politique (autorisé ou non) et l'application (la manière dont le refus s'exprime).
- La plupart des failles au niveau objet (IDOR) viennent du fait qu'on considère « authentifié + identifiant valide » comme une permission. Le contrôle d'accès défaillant est le premier risque du Top 10 de l'OWASP.
- Refusez par défaut, nommez les politiques d'après les capacités qu'elles évaluent, et testez les autorisations comme une matrice d'acteurs, d'actions et de ressources.
Introduction
La gestion des autorisations devient difficile à maintenir lorsqu'elle est implémentée comme un ensemble de vérifications locales.
Un endpoint vérifie que l'utilisateur est actif. Un autre contrôle un rôle. Un traitement en arrière-plan vérifie la propriété. Un gestionnaire de ressource charge un objet à partir de son identifiant et suppose que l'authentification suffit.
Chaque décision, prise isolément, peut sembler raisonnable, mais le modèle d'autorisation global se retrouve réparti dans toute l'application. Avec le temps, le système devient difficile à auditer et le risque augmente qu'un chemin d'exécution oublie une condition importante.
Une conception plus robuste consiste à séparer l'autorisation en couches distinctes :
Contexte d'authentification : qui émet la requête ?
Contexte d'autorisation : quels faits pertinents connaît-on sur l'acteur et la ressource ?
Décision de politique : l'acteur a-t-il le droit d'effectuer cette action ?
Application (enforcement) : comment le refus se traduit-il dans le contexte d'exécution courant ?
Cette séparation ne vise pas l'abstraction pour elle-même. Elle garantit que les règles d'autorisation ont un responsable identifié, qu'elles sont évaluées de façon cohérente et qu'elles ne sont pas reconstruites indépendamment à travers toute l'application. C'est particulièrement important pour éviter les défauts d'autorisation au niveau objet, notamment les vulnérabilités IDOR.
Vue d'ensemble de l'architecture
Une opération protégée traverse généralement plusieurs étapes distinctes. L'application commence par authentifier l'acteur et établir qui émet la requête. Elle rassemble ensuite le contexte d'autorisation, c'est-à-dire tous les faits utiles à la décision : état du compte, rôles, appartenance à des organisations ou droits souscrits (entitlements). Si l'opération porte sur un objet précis, l'application identifie ou charge aussi cette ressource, avec les attributs nécessaires pour évaluer l'accès. La couche de politique (policy) détermine alors si l'acteur peut effectuer l'action demandée sur cette ressource. Enfin, la couche d'application de la décision met en œuvre le résultat : elle laisse l'opération se poursuivre, ou bien renvoie une erreur, redirige l'utilisateur ou interrompt la requête d'une autre manière.
Le principe de conception central est le suivant : les appelants ne doivent pas assembler les règles d'autorisation à partir de prédicats de bas niveau. Ils doivent demander une décision de politique. Par exemple :
canAccessSubscriberContent(actor, now)
canViewProject(actor, project)
canEditUser(actor, targetUser)
canReadInvoice(actor, invoice)Ces noms décrivent des capacités au niveau de l'application. Les détails d'implémentation restent dans la couche de politique.
L'authentification établit l'identité
L'authentification doit répondre à une seule question : qui émet cette requête ?
Une session, un jeton, un certificat ou tout autre mécanisme d'authentification peut établir une identité de confiance. Sa responsabilité s'arrête là.
L'authentification n'établit pas que l'acteur peut accéder à une ressource donnée.
Un acteur correctement authentifié peut très bien être :
- suspendu ;
- hors de sa période de droits ;
- non propriétaire de la ressource demandée ;
- dépourvu de la permission requise pour l'action demandée.
Cette distinction est fondamentale.
Une requête authentifiée n'est pas forcément une requête autorisée.
Un système qui considère une authentification réussie comme une protection suffisante peut encore exposer des données entre utilisateurs, organisations ou tenants.
Le contexte d'autorisation rassemble les faits
Une fois l'identité établie, l'application rassemble les faits nécessaires pour évaluer l'autorisation. Selon l'opération, il peut s'agir du statut du compte de l'acteur, de son rôle global, de son appartenance à des organisations ou à des tenants, de l'état de son abonnement, de ses droits, de la propriété de la ressource ciblée, du périmètre organisationnel de cette ressource et d'informations d'environnement comme l'heure courante. Le contexte d'autorisation est chargé de mettre ces faits sous une forme que la couche de politique peut évaluer, mais il ne décide pas si l'accès doit être accordé. La récupération des données reste ainsi séparée de la logique d'autorisation : la couche de contexte établit ce qui est vrai, tandis que la couche de politique détermine ce que ces faits signifient pour l'opération demandée. Dans un petit système, un seul contexte d'autorisation peut suffire, alors que des systèmes plus importants utiliseront des chargeurs de contexte plus ciblés pour l'accès administrateur, les abonnements, l'appartenance aux organisations ou les décisions propres à une ressource. L'implémentation peut évoluer avec l'application sans modifier la frontière architecturale sous-jacente.
Les politiques expriment les règles d'autorisation
Les politiques d'autorisation contiennent les règles métier qui déterminent si une opération est permise. Une politique d'accès abonné peut par exemple exiger que le compte soit actif et non supprimé, tout en réservant l'accès aux administrateurs ou aux utilisateurs disposant d'un droit actuellement valide. Une politique de projet peut exiger que l'acteur soit actif, appartienne à l'organisation propriétaire du projet et détienne la permission requise pour l'action demandée.
La propriété essentielle est que la règle complète est définie à un seul endroit. Sans couche de politique, les appelants ont tendance à reconstruire l'autorisation à partir de prédicats plus petits comme isActive, isAdmin, isOwner, hasEntitlement ou isOrganizationMember. Ces prédicats peuvent être corrects individuellement, mais chaque appelant doit encore savoir comment les combiner correctement.
C'est là qu'apparaissent les incohérences. Un chemin peut vérifier correctement l'état du compte, l'appartenance au tenant et le rôle, tandis qu'un autre omet l'une de ces conditions ou ne confirme pas que la ressource ciblée appartient au même tenant. Le problème n'est souvent pas un prédicat erroné, mais l'assemblage répété de la règle globale à différents endroits.
Une conception orientée politiques déplace cette composition dans une décision unique et nommée. Au lieu d'exiger que chaque appelant comprenne comment se calcule l'accès à un projet, l'application expose une politique telle que canViewProject(actor, project). Les appelants fournissent le contexte pertinent et appliquent le résultat, tandis que la règle sous-jacente peut évoluer sans qu'il faille réécrire ou réauditer chaque chemin d'exécution séparément.
Autorisation au niveau objet et IDOR
Les vulnérabilités IDOR (Insecure Direct Object Reference, ou référence directe non sécurisée à un objet) surviennent lorsqu'une application accepte une référence à un objet sans vérifier que l'acteur courant est autorisé à accéder à cet objet précis. Un utilisateur peut par exemple demander /invoices/1001, remplacer l'identifiant par /invoices/1002 et recevoir la seconde facture, parce que le serveur vérifie seulement que l'objet existe, et non que l'utilisateur a le droit d'y accéder.
Le problème de fond n'est pas l'usage d'un identifiant visible ou séquentiel. Des identifiants séquentiels facilitent l'énumération, mais les remplacer par des UUID ne règle pas le problème d'autorisation. Un identifiant de ressource indique à l'application quel objet est demandé ; il n'établit pas que le demandeur a la permission d'y accéder.
L'identification de l'objet et l'autorisation doivent donc être traitées comme deux préoccupations logiquement distinctes. L'application doit identifier la ressource ciblée, puis déterminer indépendamment si l'acteur courant peut effectuer l'action demandée sur celle-ci. La possession d'un identifiant valide ne doit jamais être considérée comme une preuve d'accès.
Pourquoi des politiques centralisées réduisent le risque d'IDOR
Un schéma propice aux IDOR consiste à authentifier l'acteur, charger un objet par son identifiant et le renvoyer sans évaluer la relation entre l'acteur et cet objet. La requête est authentifiée, mais l'accès à la ressource n'est pas autorisé.
Une conception plus sûre fait de l'autorisation au niveau objet une partie explicite de l'opération. Pour un projet, une décision comme canViewProject(actor, project) peut prendre en compte le statut de l'acteur, son rôle, son appartenance à une organisation, ainsi que le propriétaire du projet, son organisation ou son état courant.
Centraliser cette décision réduit le risque que différents endpoints, services ou tâches en arrière-plan appliquent des définitions différentes de l'accès. Cela ne rend pas les IDOR impossibles, car tout chemin de code qui contourne l'autorisation peut encore introduire une vulnérabilité. En revanche, l'autorisation au niveau objet devient une étape définie et attendue de l'accès aux ressources, et non une vérification facultative que chaque appelant implémente de son côté.
Accès aux ressources borné par l'autorisation
Pour des règles simples de propriété et de tenant, l'autorisation peut aussi être appliquée au moment du chargement de la ressource. Plutôt que de récupérer un projet uniquement par son identifiant et de vérifier l'accès ensuite, la requête peut être restreinte au périmètre autorisé de l'acteur. Une recherche peut exiger à la fois l'identifiant du projet demandé et l'appartenance à l'une des organisations autorisées de l'acteur, ou, dans un modèle de propriété plus simple, exiger que le ownerId du projet corresponde à l'acteur courant.
Cette approche a une propriété importante : les ressources situées hors du périmètre d'autorisation de l'acteur ne sont jamais renvoyées par la requête. Pour de nombreux schémas d'accès au niveau objet, c'est une défense solide contre les IDOR, car l'application ne charge pas d'abord un objet arbitraire pour s'en remettre entièrement à une vérification ultérieure.
Le filtrage des ressources et l'évaluation des politiques répondent toutefois à des besoins différents. Une requête filtrée par périmètre convient bien pour déterminer à quelles ressources un acteur peut accéder, tandis qu'une politique peut répondre à des questions propres à chaque action sur ce que l'acteur peut faire d'une ressource. Un utilisateur peut avoir le droit de consulter un projet sans pouvoir l'archiver, le transférer, le supprimer ou en gérer les membres.
C'est pourquoi les systèmes plus importants combinent souvent un chargement des ressources borné par l'autorisation et des politiques propres à chaque action, plutôt que de considérer l'un de ces mécanismes comme un remplacement complet de l'autre.
Acteur, action, ressource et environnement
Les décisions d'autorisation dépendent souvent de plus que l'identité ou le rôle de l'acteur. En pratique, l'application peut devoir prendre en compte ensemble les attributs de l'acteur, l'action demandée, la ressource ciblée et l'environnement. L'acteur peut apporter des informations comme l'état de son compte, son rôle, son appartenance à une organisation ou ses droits, tandis que la ressource apporte son propriétaire, son tenant, son organisation, son état courant, sa sensibilité ou sa visibilité. Des conditions d'environnement comme l'heure courante ou l'état opérationnel peuvent aussi influer sur le résultat.
C'est pourquoi de nombreux systèmes d'autorisation réels vont au-delà du simple contrôle d'accès basé sur les rôles (RBAC). Un rôle peut être une donnée d'entrée importante, mais ce n'est souvent qu'une partie de la décision. Un administrateur peut obtenir un accès étendu du fait de son rôle, alors qu'un membre ordinaire aura besoin d'un compte actif, d'une appartenance à la même organisation que la ressource et d'une permission suffisante pour l'action précise qu'il effectue.
L'objectif architectural est donc de modéliser la décision métier réelle plutôt que de réduire chaque règle d'autorisation à une comparaison de rôles. L'autorisation doit refléter la combinaison d'acteur, d'action, de ressource et de contexte pertinent qui détermine si l'opération est permise.
Les noms de politiques doivent décrire des capacités
Le nom d'une politique doit décrire la capacité évaluée plutôt que d'exposer seulement les conditions individuelles qui contribuent à la décision. Des noms comme canAccessAdmin, canViewProject, canEditProject ou canManageOrganizationMembers expriment la véritable question posée par l'application : cet acteur peut-il effectuer telle opération ?
Les prédicats de plus bas niveau comme isAdmin, isOwner, isActive ou hasEntitlement restent utiles à l'intérieur de la couche de politique, mais ils ne doivent pas devenir l'interface principale d'autorisation. Une vérification isOwner ne décrit qu'un seul fait sur la relation entre un acteur et une ressource. Une politique comme canEditProject, à l'inverse, représente la décision d'autorisation complète et peut intégrer la propriété avec l'état du compte, l'appartenance à l'organisation, le rôle, l'état de la ressource ou toute autre condition pertinente.
Cette distinction réduit la quantité de connaissances en matière d'autorisation exigée des appelants. Plutôt que de savoir quelle combinaison de prédicats constitue une permission, l'appelant pose une seule question au niveau du domaine et applique le résultat.
L'application de la décision dépend du contexte d'exécution
Les politiques doivent déterminer si une opération est permise, mais elles ne doivent pas décider de la façon dont un refus se manifeste. L'application de la décision dépend du contexte d'exécution. Un endpoint HTTP peut renvoyer 401 Unauthorized, 403 Forbidden ou 404 Not Found ; une interface utilisateur peut rediriger vers une page de connexion ou d'accès refusé ; un service peut rejeter une commande ; et un processus en arrière-plan peut interrompre l'opération et enregistrer un événement d'audit.
Garder ces préoccupations séparées évite que le comportement du transport, du framework ou de l'environnement d'exécution ne s'incruste dans la règle d'autorisation elle-même. Une politique comme canEditProject(actor, project) peut ainsi être réutilisée dans un endpoint HTTP, une méthode RPC, une tâche en arrière-plan, un workflow d'administration ou une commande pilotée par messages.
Chacun de ces appelants peut réagir différemment à un refus, mais la décision d'autorisation sous-jacente reste la même. La politique définit si l'action est permise ; l'appelant est chargé d'appliquer cette décision sous la forme adaptée à son environnement.
404 ou 403 pour l'accès aux objets
Pour les ressources au niveau objet, une application peut choisir de renvoyer 404 Not Found lorsque l'acteur n'est pas autorisé à accéder à l'objet demandé. Elle évite ainsi de révéler si une ressource existe hors du périmètre d'autorisation de l'acteur, ce qui est utile dans les systèmes où le simple fait d'exposer l'existence des ressources d'un autre utilisateur ou d'un autre tenant divulguerait déjà une information sensible.
L'accès aux ressources borné par l'autorisation se prête naturellement à ce modèle. Si l'application ne cherche que dans l'ensemble des ressources visibles par l'acteur courant, une recherche infructueuse peut signifier soit que l'objet n'existe pas, soit qu'il existe hors du périmètre autorisé de l'acteur. L'application n'a pas besoin de distinguer ces deux cas dans sa réponse externe.
Ce comportement ne convient pas à tous les systèmes, mais il est souvent utile pour les ressources appartenant à un utilisateur ou à un tenant, ou plus généralement sensibles, pour lesquelles confirmer l'existence d'un objet inaccessible fournirait une information superflue.
Autorisation dans les systèmes multi-tenant
La même séparation architecturale devient encore plus importante dans les systèmes multi-tenant (multilocataires), où une ressource appartient non seulement à un acteur individuel, mais aussi à une frontière organisationnelle. Un utilisateur d'une organisation ne doit pas pouvoir accéder à une ressource appartenant à une autre organisation simplement en fournissant un identifiant valide.
C'est pourquoi, dans un système multi-tenant, charger une ressource uniquement par son identifiant est souvent insuffisant. La frontière du tenant doit aussi faire partie de la décision d'autorisation, soit en limitant la recherche de la ressource aux organisations auxquelles l'acteur peut accéder, soit en évaluant une politique explicite comme canViewProject(actor, project) une fois la ressource identifiée.
L'invariant sous-jacent est le même que pour les autres formes d'autorisation au niveau objet : posséder un identifiant valide n'implique pas l'accès. Les défauts d'autorisation entre tenants sont particulièrement graves, car une seule vérification de frontière manquante peut exposer des données entre des organisations entières, et pas seulement entre des utilisateurs. Un modèle d'autorisation centralisé rend ces frontières explicites et permet d'appliquer les mêmes règles de façon cohérente partout où l'on accède à des ressources protégées.
Refus par défaut
Les politiques doivent en général suivre un modèle de refus par défaut (deny by default), dans lequel l'accès n'est accordé que si une règle explicite l'autorise. Un compte inactif ou supprimé, par exemple, doit être refusé, sauf si la politique prévoit une exception précise et délibérée. L'accès peut ensuite être accordé à un administrateur ou à un acteur qui remplit les conditions requises de droits, de propriété ou de tenant.
Le même principe s'applique à l'autorisation au niveau objet. Une politique peut d'abord refuser les acteurs inactifs, autoriser une exception administrative définie, puis évaluer si l'acteur entretient la relation requise avec la ressource. Si aucune de ces conditions n'est remplie, le résultat reste un refus.
Cette structure rend le comportement des politiques plus facile à raisonner, car des états inattendus ou non gérés ne produisent pas d'accès par accident. Une permission doit découler d'une règle connue, et non de l'absence d'une condition de refus.
Le temps comme contexte d'autorisation
Un accès qui dépend du temps doit être traité comme une donnée d'entrée explicite de la politique. Les règles d'abonnement et de droits dépendent souvent de la question de savoir si l'heure courante se situe dans un intervalle de validité défini, par exemple startAt <= now < endAt.
Passer l'heure courante à la politique rend la décision déterministe et plus facile à tester. Le système peut ainsi vérifier le comportement juste avant le début de l'accès, à l'instant exact du début, juste avant l'expiration, à la borne d'expiration et après la fin de l'accès.
La politique reste ainsi centrée sur l'interprétation des faits fournis, au lieu de mêler logique métier et état d'exécution caché. L'appelant fournit l'heure courante, et la politique évalue si le droit ou l'abonnement concerné est valide à cet instant.
Tester les autorisations sous forme de matrice
Les tests d'autorisation doivent évaluer des combinaisons d'acteur, de ressource et d'action, plutôt que de tester uniquement les accès réussis. Pour une politique au niveau objet, cela signifie couvrir à la fois les permissions attendues et les violations délibérées de frontière : un propriétaire qui accède à sa propre ressource, un utilisateur qui tente d'accéder à la ressource d'un autre, un membre qui franchit une frontière de tenant, un administrateur qui exerce un accès étendu, ou un acteur suspendu ou supprimé qui tente d'utiliser des permissions qu'il détiendrait autrement.
C'est particulièrement important pour prévenir les vulnérabilités IDOR. Une suite de tests ne doit pas seulement démontrer que les utilisateurs légitimes accèdent à des ressources valides ; elle doit aussi démontrer que des utilisateurs non autorisés ne peuvent pas accéder à des ressources valides appartenant à quelqu'un d'autre. Les défauts d'autorisation au niveau objet passent souvent les tests fonctionnels ordinaires, car la ressource existe, l'opération réussit et le demandeur est authentifié. Seuls des tests explicites entre utilisateurs et entre tenants révèlent si la frontière d'autorisation est réellement appliquée.
Organisation du code
L'arborescence exacte dépendra de l'application, du langage et de la taille du code, mais la responsabilité de chaque aspect de l'autorisation doit rester explicite. Un petit système peut regrouper l'authentification, le chargement du contexte, les politiques et l'application des décisions dans un seul module d'autorisation, tandis qu'un système plus important répartira la logique de contexte et de politique par domaine : utilisateurs, organisations, abonnements, projets ou facturation.
Les noms et la structure de dossiers comptent moins que la capacité à localiser rapidement chaque responsabilité. Un développeur doit pouvoir déterminer où l'identité est établie, où les faits d'autorisation sont rassemblés, où se définit l'accès à une ressource ou à une capacité donnée, et où le refus est traduit en comportement propre au transport ou à l'environnement d'exécution.
Si répondre à ces questions oblige à fouiller des dizaines d'endpoints, de services et de gestionnaires, le modèle d'autorisation est probablement trop dispersé. Une structure claire facilite la revue du système, car les règles qui régissent l'accès sont concentrées à des endroits connus au lieu d'être disséminées dans du code applicatif sans rapport.
Propriétés de sécurité de l'architecture
Cette organisation apporte plusieurs propriétés importantes en matière de sécurité et d'exploitation. Comme les décisions d'autorisation sont définies de manière centralisée, une même politique peut être réutilisée dans les gestionnaires HTTP, les services, les tâches en arrière-plan, les outils d'administration et les autres chemins d'exécution. Les appelants n'ont plus à reconstruire indépendamment des expressions d'autorisation complexes, ce qui réduit la duplication et le risque que différentes parties du système appliquent des règles légèrement différentes.
Le modèle renforce aussi l'autorisation au niveau objet en évaluant l'accès au regard à la fois de l'acteur et de la ressource ciblée, au lieu de déduire la permission de l'authentification ou de la possession d'un identifiant valide. Le modèle d'autorisation effectif devient plus facile à auditer, car les relecteurs peuvent examiner la couche de politique pour comprendre comment l'accès est déterminé, et plus facile à tester, car les politiques peuvent être évaluées indépendamment sur des combinaisons d'acteur, de ressource, d'action et de contexte d'environnement.
Dans les systèmes multi-tenant, la même structure rend les frontières entre tenants plus explicites. Les règles d'appartenance et de propriété peuvent être appliquées par un accès aux ressources borné par l'autorisation, par des politiques explicites au niveau objet, ou par les deux. Une approche de refus par défaut garantit en outre que des conditions inattendues ou non gérées aboutissent à un refus plutôt qu'à un accès accidentel.
Ces propriétés ne dispensent pas d'une implémentation sécurisée. Tout chemin d'exécution qui contourne la couche d'autorisation peut encore introduire une vulnérabilité. La valeur de cette architecture est d'établir un chemin clair et cohérent pour les opérations protégées, et de rendre les écarts par rapport à ce chemin plus faciles à repérer pendant le développement et la revue.
En résumé
L'autorisation doit être traitée comme un système de décisions plutôt que comme un ensemble de vérifications isolées. L'authentification établit l'identité, le contexte d'autorisation rassemble les faits nécessaires à la décision, le contexte de ressource identifie l'objet concerné, les politiques déterminent si l'acteur peut effectuer l'action demandée, et la couche d'application traduit cette décision en un comportement adapté à l'environnement d'exécution courant.
Pour l'accès au niveau objet, cette structure s'attaque directement aux conditions qui produisent les vulnérabilités IDOR. L'application ne considère plus un acteur authentifié muni d'un identifiant d'objet valide comme implicitement autorisé. L'accès est au contraire déterminé en évaluant ensemble l'acteur, la ressource ciblée, l'action demandée et tout autre contexte pertinent.
Lorsque c'est pertinent, l'accès aux ressources peut aussi être restreint à l'ensemble autorisé de l'acteur, de sorte que les objets inaccessibles ne soient même pas considérés comme des candidats valides. Combiné à des politiques propres à chaque action, cela donne un modèle d'autorisation plus explicite, plus testable et plus facile à relire.
L'objectif n'est pas d'introduire un framework de permissions générique avant que l'application en ait besoin. Il s'agit de faire de l'autorisation une partie définie de l'architecture du système, avec une responsabilité claire pour l'identité, le contexte, la politique, le périmètre des ressources et l'application des décisions. L'application réduit ainsi le risque de contrôles d'accès incohérents et de défauts d'autorisation au niveau objet, car la permission n'est plus déduite de l'état de la session, de l'accès à une route ou de la connaissance d'un identifiant de ressource. Elle est évaluée explicitement.
Nous avons appliqué cette structure sur une plateforme de création de sociétés multi-juridictions, où l'accès aux documents dépendait à la fois de l'identité, de l'état de la société et de la juridiction. Les autorisations font aussi partie des premiers points que nous examinons lors d'un audit technique (voir ce que comprend un audit), et les concevoir dès le départ fait partie de notre travail d'architecture logicielle.
Pour aller plus loin
- OWASP, Authorization Cheat Sheet (en anglais)
- OWASP, Insecure Direct Object Reference Prevention Cheat Sheet (en anglais)
- Top 10 de l'OWASP, A01: Broken Access Control (en anglais)
- NIST, SP 800-162: Guide to Attribute Based Access Control (ABAC), le guide de référence du contrôle d'accès basé sur les attributs (en anglais)
- Documentation d'Open Policy Agent (en anglais), pour les équipes qui veulent faire des politiques un service à part