Sécuriser des WebApi avec OAuth2 et Client Credentials?

J’ai déjà eu à implémenter des applications clientes compatibles OAuth (1 et 2), c’est-à-dire côté consommateur de ressources protégées, mais ce n’est que récemment que je me suis intéressé à la mise en place d’une solution de sécurité pour un ensemble d’applications. J’ai logiquement étudié ce qui se fait avec OAuth 2 et OpenID Connect (OIDC), et ce qui est couramment considéré comme l’état de l’art à l’heure actuelle.

Pourquoi cet article ?

Le sujet est complexe, et on trouve beaucoup d’informations sur Internet, pas toujours de première qualité. La première raison est qu’OAuth contient beaucoup de concepts dont le niveau d’abstraction est relativement élevé. Une deuxième raison est due au nombre d’alternatives proposées par OAuth 2 qui font qu’une approche ne conviendra pas forcément à votre cas. Ce tri, à savoir identifier les choix à votre disposition pour votre problème, est déjà un gros travail en soi. Enfin, il est notoirement connu que tout ce qui touche à la sécurité en informatique est compliqué, là encore à cause du nombre de vecteurs d’attaque possibles, des contraintes du système (ce qui est envisageable et ce qui ne l’est pas), et de nos besoins (il y a souvent un choix à faire entre sécurité et fonctionnalités).

J’ai donc pensé écrire sur ma propre expérience, en me limitant au cas pratique qui m’a occupé: sécuriser des API déployées sous forme de micro-services. Les cookies de session n’étaient pas une option (à eux seuls). L’important à retenir dans cette définition est qu’il n’y a pas d’utilisateur impliqué dans cette sécurisation. Cela permet de grandement simplifier cet article en évitant les concepts d’OAuth 2 que nous n’utilisons pas.

OAuth 2: un objectif de standardisation

“Standardiser” l’authentification et les autorisations entre plusieurs applications web, c’est l’objectif d’OAuth 2 qui fournit avant tout un moyen de déléguer le processus d’authentification à une application tierce. Cependant, comme il ne s’agit pas d’un protocole mais plus de “guidelines”, cela ne va pas jusqu’à rendre inter-opérable n’importe quelles applications entre elles. Le standard est simplement censé faciliter ce travail en fournissant un cadre très ouvert.

OAuth 2: autorisations basées sur un Token

La finalité d’OAuth 2 est l’obtention et l’utilisation d’un “token” (jeton) pour accéder à une ressource.

Par exemple, une application A (appelée Client) donne comme preuve d’accès un token à une application B (appelée Resource Server), ceci à chaque appel d’API. L’application B valide le token et autorise ou non l’accès à la ressource.

Le token est simplement une chaîne, généralement opaque pour son utilisateur. La forme du token n’est pas spécifiée par OAuth 2. Le plus généralement utilisé est probablement le JSON Web Token (JWT). Il s’agit d’un objet JSON qui contient les informations permettant d’autoriser ou non l’accès à une ressource, ainsi qu’une signature qui empêche de falsifier ces informations.

La signature du token est appliquée grâce à un algorithme asymétrique: le serveur qui délivre le token signe celui-ci avec une clé privée. Les applications qui doivent valider le token utilisent la clé publique. Clé privée et clé publique sont généralement mises en oeuvre avec un certificat installé sur le serveur de délivrance du token. La clé publique est généralement exposée publiquement par le serveur de délivrance.

Du point de vue de la requête HTTP, le token est appelé “Bearer Token”.

Deux aspects importants:

  • Le token se suffit à lui seul pour valider un accès: c’est-à-dire que son authenticité peut être vérifiée par l’application qui le reçoit pour servir une ressource.
  • La conséquence de ce qui vient d’être dit est que le token n’est pas révocable. C’est la raison pour laquelle il a une durée d’expiration après laquelle un nouveau token doit être obtenu.

En conclusion, une fois qu’un token est détenu, la consommation d’une API protégée est extrêmement simple. N’importe quel détenteur du token peut alors l’inclure dans ses requêtes pour accéder aux ressources qu’il protège. Autrement dit, le token est une clé temporaire.

Voici un exemple de requête HTTP vers une API, avec l’outil curl, accédée grâce à un token (pour le rendu de cette page, l’access token illustré est plus court qu’un véritable):

curl -H "Authorization: Bearer AbCdEf123456" http://www.example.com/my-api

OAuth 2: obtention du Token

La complication se trouve dans l’obtention de ce token. OAuth2 propose plusieurs méthodes, nommées “flows” ou parfois “authorization grant” (les deux termes semblent interchangeables).

Une chose importante à appréhender est que le choix du flow n’est pas libre. Il dépend surtout de ce que vous voulez faire.

La première contrainte qui guidera votre choix est celle-ci: voulez-vous autoriser l’accès à une ressource détenue par un utilisateur ou par une application ? Autrement dit, un utilisateur est-il impliqué dans le processus d’autorisation d’accès ? Selon la réponse, les flows disponibles sont réduits à ceux-ci:

Flows avec un utilisateur:

  • Authorization Code
  • Implicit
  • Resource Owner Password Credentials

Flows avec une application:

  • Client Credentials

Je ne décrirai pas chaque flow mais vous l’aurez compris: dans le cas qui m’intéresse pour cet article, à savoir sécuriser la consommation d’API entre applications, il n’y a pas vraiment de choix: Client Credentials est le seul qui n’implique pas un utilisateur.

OpenId Connect (OIDC) est une extension d’OAuth 2 qui se concentre sur l’identification d’un utilisateur. Ce n’est donc pas non plus l’objet de cet article.

Client Credentials flow

Etudions donc plus en détail ce qu’implique cette méthode. Dans celle-ci, le token est appelé un “access token”.

Les acteurs

  • L’application A qui consomme une API distante est appelée “Client”.
  • L’application B qui expose une API est appelée “Resource Server”.
  • Le serveur qui délivre les tokens est appelé “Authorization Server”. Son API est nommée “Token Endpoint”.

Le workflow

Diagramme de séquence

Le Client (l’application A) ne détient pas d’access token. Il fait une demande à l’Authorization Server avec les éléments suivants:

  • Un couple “Client Id” / “Client Secret”, comparable aux login/mot de passe qui identifient l’application consommatrice.
  • Un ou plusieurs “resource scopes”, qui identifient les API à autoriser.

L’Authorization Server valide le Secret, vérifie que le Client spécifié a bien accès aux API identifiées par leur “scope”, et retourne le cas échéant un access token temporaire.

Le Client vérifie que le token obtenu lui est bien destiné (en vérifiant le “ClientId” contenu dans le token). Cela réduit le risque d’une substitution de token (le Client pourrait être trompé pour utiliser un token valide destiné à un autre Client avec des droits différents).

Le Client peut consommer les API du Resource Server avec son access token. Lorsque le token expirera, il lui faudra refaire une demande à l’Authorization Server.

Le Resource Server, lorsqu’il reçoit des requêtes, valide le token inclus dans la requête et délivre les ressources demandées si la validation réussie. Cette validation n’implique pas directement l’Authorization Server. En effet, du moment que le Resource Server connait la clé publique du certificat qui a signé le token, il est en mesure de vérifier l’authenticité du token.

Cette souplesse d’utilisation du token est tout l’objet d’OAuth 2: être scalable. Un token peut être utilisé avec un très grand nombre de Resource Servers, qui peuvent servir un très grand nombre de clients (et donc d’utilisateurs). La validation de chaque requête ne requiert pas un appel à l’Authorization Server (qui a délivré le token). Il n’y a plus de problématique d’affinité de session comme c’est presque toujours le cas avec les cookies de session.

Un point déterminant est que le Resource Server ne peut pas vérifier la légitimité du détenteur du token: seulement son authenticité

Cela veut dire que si un acteur illicite découvre ce token, il peut l’utiliser librement pour accéder à des ressources protégées. Cette notion de légitimité de détention est appelée “proof of possession” (PoP, preuve de détention). Elle implique la signature de chaque requête par le Client. Cette signature établit une relation entre la requête et le token. Cette fonctionnalité qui était présente dans OAuth 1 ne l’est plus dans OAuth 2 (ce n’est pas entièrement vrai car la spécification en parle brièvement mais en pratique, elle n’est généralement pas implémentée).

La spécification décrit clairement cette limitation (section 10.3):

This specification does not provide any methods for the resource
server to ensure that an access token presented to it by a given
client was issued to that client by the authorization server.

Contraintes et limitation du Client Credential flow

Toute communication du Client Secret ou du Token doit être sécurisée, typiquement en TLS (https)

La première évidence est que le Client Secret doit être gardé confidentiel car c’est la clé permanente. Par conséquent, cette information ne peut être stockée dans un client non fiable, tel qu’un navigateur web ou une application déployée sur un ordinateur tiers (application mobile sur un téléphone ou même une application sur un ordinateur inconnu). Ces deux types d’applications sont délivrées à un utilisateur final, qui est libre d’y rechercher (assez facilement) ce type d’information confidentielle.

La deuxième évidence est que l’Access Token est presqu’aussi confidentiel que le Client Secret, à ceci près que sa durée de vie est limitée.

La sécurité du Client Credentials flow repose donc entièrement une mise en oeuvre correcte du protocole TLS.

Les Clients doivent être des applications de confiance

Etant donné que nous parlons d’information confidentielle, le Client Secret ne peut être donné qu’à des applications de confiance (appelés “Confidential Clients” par opposition aux “Public Clients”).

C’est une limitation majeure si, par exemple, vous exposez des API à des clients applicatifs que vous ne maîtrisez pas. Si vous utilisez cette méthode, vous faites aveuglément confiance aux développeurs de ces applications sur le respect de ces contraintes. Autrement dit, c’est le plus souvent un voeux pieux.

La spécification insiste plusieurs fois sur ce point, par exemple à la section 4.4:

The client credentials grant type MUST only be used by confidential
clients.

Conseils pratiques d’implémentation avec une Single Page Application (SPA) et un serveur ASP.NET

Je ne proposerai pas d’exemple de code car il y en a foison sur Internet et il est rare que les exemples que vous trouviez correspondent précisément à votre cas, tellement il y a d’alternatives possibles.

Framework Identity Server 3

ASP.NET inclut un support partiel pour OAuth 2 (voir par exemple le package Microsoft.Owin.Security.OAuth) mais je vous conseille vivement le projet open source Identity Server 3 dont le support de OAuth 2 et OpenId Connect est beaucoup plus étendu. Il est actuellement dans sa version 2. Une refonte est en cours pour .NET Core mais elle n’est pas encore mature.

Pour l’avoir testé, le framework semble être d’une qualité exemplaire, tant dans les fonctionnalités proposées, dans sa fiabilité et dans sa documentation.

La première tâche est de créer l’Authorization Server. Vous n’aurez rien d’autre à faire que créer le projet hôte, que ce soit un site web hébergé sur IIS ou un self host (service Windows ou programme console). Le framework propose un middleware Owin qui se charge du reste (package IdentityServer3).

Ensuite, côté Resource Servers (API), la librairie IdentityServer3.AccessTokenValidation propose un middleware Owin qui gère la validation du token dans chaque requête. Ce middleware va alimenter IPrincipal du contexte de la requête à partir du token éventuellement validé. Le reste fonctionne sur le même principe que les autorisations traditionnelles dans ASP.NET à ceci près qu’on ne parle plus de rôles mais de Claims. Dans le cadre du Client Credentials flow, les Claims associées à l’utilisateur authentifié contiendront les Scopes autorisés, ainsi que le ClientId qui a obtenu le token. Il suffit de vérifier la présence du scope de l’API pour autoriser ou non son accès.

Quelques recommandations

  • Forcer l’accès à l’Authorization Server et à toute API protégée en TLS (https): option IdentityServerOptions.RequireSsl (activée par défaut).
  • Déployer le certificat X509 dans le Personal Store de l’ordinateur qui héberge l’Authorization Server et s’assurer que seul ce dernier peut accéder à sa clé privée (ne pas inclure ce certificat dans les fichiers de l’application, comme c’est souvent le cas dans les exemples trouvés sur Internet).
  • Configurer l’Authorization Server pour qu’il valide le certificat utilisé. Un certificat non validé n’a aucune valeur: il facilite les tests mais offre à peu près le même niveau de protection que l’absence de certificat. En effet, la validation consiste à garantir que le certificat provient d’une autorité de confiance (grâce à un “root certificate”, on appelle cela la chaîne de confiance, ou chain trust).
  • L’authentification du client avec le couple Client Id / Client Secret doit être fait côté serveur (ASP.NET) et non côté client (HTML/Javascript).
  • Idéalement, mettre en place une supervision constante des accès, grâce à des traces détaillées et leur surveillance effective, pour réagir rapidement en cas de découverte d’une faille.

Pour plus de détails sur l’utilisation du certificat, consultez cet article MSDN destiné à WCF mais qui est largement transposable ici.

Conclusion

Peut-on considérer OAuth 2 comme une solution pour sécuriser des API ? Oui avec beaucoup de “si”.

Quand on parle de sécurité, il est important de clairement définir ses objectifs. Souhaite-t-on donner une impression de sécurité ou souhaite-t-on réellement protéger des ressources ?
Et de quelles menaces souhaite-t-on les protéger ? Par exemple, des attaquants de type étatiques ont des moyens très perfectionnés; des hackers privés ont des moyens plus limités; et des “utilisateurs avancés” sont encore plus limités (à l’utilisation d’outils accessibles librement sans réelle compétence technique et sans infrastructures à leur disposition). Je pense qu’OAuth 2, bien implémenté, est efficace pour ce dernier degré de menace.

OAuth 2 est mieux que rien. Son danger principal est qu’il donne l’illusion d’une sécurité plus importante que celle qu’il ne fournit en pratique.

Un grand nombre de choix importants ne sont pas définis par la spécification d’OAuth 2, laissant de ce fait beaucoup de liberté, et donc de responsabilités, aux implémentations. Pour vous donner un exemple illustratif de ce que l’on peut lire tout au long de la spécification, voici un extrait de la section 1.4 concernant l’Access Token (qui est tout de même la pierre angulaire de cette solution):

Access tokens can have different formats, structures, and methods of
utilization (e.g. cryptographic properties) based on the resource
server security requirements. Access token attributes and the
methods used to access protected resources are beyond the scope of
this specification and are defined by companion specifications.

OAuth 2 est compliqué à implémenter (mais facile à consommer): cette complication est autant de points de fragilité. Et il y a finalement assez peu de gardes-fous. Au moins pour le flow Client Credentials, tout repose sur un transport sécurisé (TLS) et sur la confidentialité des clients (notamment de leur Client Secret). Et aussi sur l’implémentation choisie, puisque celle-ci a beaucoup de libertés.

On trouve sur Internet beaucoup de critiques sur OAuth2, certains parlent de “Security Theater”, autrement dit de poudre aux yeux davantage destinée à donner un sentiment de sécurité qu’à réellement sécuriser. Je partage ce point de vue, bien que je reconnaisse l’utilité de ce standard et que je l’utilise à défaut de meilleure solution à la fois pratique à mettre en oeuvre et largement reconnue par la communauté. OAuth 2 reste un moyen simple de restreindre l’accès à une API, en particulier si celle-ci est de type REST. Il est tellement facile de répéter ce type de requête dans un navigateur que le simple fait de rendre son utilisation plus contraignante est une forme de restriction. En revanche, il ne faut pas se leurrer en supposant que l’on sécurise, dans l’absolu, les ressources exposées. Je préfère parler de restriction plus que de sécurité.

Une analogie est possible avec les degrés de confidentialité utilisés par le gouvernement, notamment l’armée: chaque document officiel non destiné au public porte une mention de diffusion. Le premier degré est “Diffusion Restreinte”, le deuxième “Confidentiel Défense”, puis “Secret Défense” et enfin “Très Secret Défense” (avec des sous-classifications). La mention “Diffusion Restreinte” n’est pas une classification reconnue au niveau pénal, la première “vraie” classification de confidentialité est “Confidentiel Défense”. La mention “Diffusion Restreinte” n’a qu’une valeur informative destinée à limiter la diffusion d’un document que pourrait faire son utilisateur. C’est pour cela que l’on peut trouver assez couramment des documents « Diffusion Restreinte » sur Internet, mais en aucun cas « Confidentiel Defense » (hormis ceux qui ont fuité sur des canaux tels que wikileaks). OAuth 2, pour moi, correspond bien à ce premier degré: il “restreint” certes l’accès à une ressource, mais son niveau de protection est limité.

À beaucoup d’égards, on peut considérer qu’OAuth 2 est un échec (partiel) par rapport aux promesses de départ. Pour le lecteur curieux, voici même une thèse sur le sujet: Simple But Not Secure: An Empirical Security Analysis of OAuth 2.0-Based Single
Sign-On Systems
.

Enfin, Eran Hammer, l’un des créateurs des deux spécifications OAuth 1, puis 2 (dont il a préféré retirer son nom), a créé une librairie javascript nommée Oz (https://github.com/hueniverse/oz). Celle-ci fournirait une meilleure protection que OAuth 2 (elle est probablement plus proche de OAuth 1). Je ne l’ai pas testé mais c’est une alternative qui peut être intéressante selon vos besoins.

References

OAuth 2 (site officiel)
OpenId Connect (site officiel)
OAuth 2.0 and the Road to Hell (Eran Hammer)
Auth to See the Wizard (or, I wrote an OAuth Replacement), Eran Hammer
Defending against your own stupidity (Ben Adida)
Simple But Not Secure: An Empirical Security Analysis of OAuth 2.0-Based Single Sign-On Systems (thèse de San-Tsai Sun)
Working with Certificates (MSDN)

Ajouté le 27 aout 2016:
OAuth 2.0 (without Signatures) is Bad for the Web (Eran Hammer)
Article très intéressant sur les coulisses des spécifications d’OAuth 1 et 2.