Validation des jetons d’accès OAuth 2.0 avec NGINX et NGINX Plus

NGINX | 13 mai 2019

Photo publiée avec l’aimable autorisation de John T. sur unsplash.com

Il existe de nombreuses options pour authentifier les appels d’API, des certificats clients X.509 à l’authentification HTTP Basic. Ces dernières années, toutefois, une norme de fait s’est imposée sous la forme des jetons d’accès OAuth 2.0. Il s’agit d’identifiants d’authentification transmis du client au serveur d’API, généralement dans un en-tête HTTP.

OAuth 2.0 est toutefois un véritable dédale de normes interconnectées. Les processus d’émission, de présentation et de validation d’un flux d’authentification OAuth 2.0 reposent souvent sur plusieurs normes connexes. À l’heure où nous écrivons ces lignes, il existe huit normes OAuth 2.0, et les jetons d’accès en sont un parfait exemple, puisque la spécification principale d’OAuth 2.0 (RFC 6749) ne définit aucun format pour les jetons d’accès. Dans la pratique, deux formats sont couramment utilisés :

  • JSON Web Token (JWT), tel que défini par RFC 7519
  • Jetons opaques qui ne sont guère plus qu’un identifiant unique pour un client authentifié

Après l’authentification, un client présente son jeton d’accès avec chaque requête HTTP afin d’accéder aux ressources protégées. Le jeton d’accès doit être validé pour vérifier qu’il a bien été émis par un fournisseur d’identité (IdP) de confiance et qu’il n’a pas expiré. Comme les IdP signent cryptographiquement les JWT qu’ils émettent, ceux-ci peuvent être validés « hors ligne », sans dépendance à l’IdP lors de l’exécution. En règle générale, un JWT comporte également une date d’expiration qui peut elle aussi être vérifiée. Le module auth_jwt de NGINX Plus effectue la validation hors ligne des JWT.

En revanche, les jetons opaques doivent être validés en les renvoyant au fournisseur d’identité (IdP) qui les a émis. Cette méthode présente toutefois l’avantage de permettre à l’IdP de révoquer ces jetons, par exemple dans le cadre d’une opération de déconnexion globale, sans que les sessions précédemment ouvertes restent actives. Une déconnexion globale peut également nécessiter la validation des JWT auprès de l’IdP.

Dans cet article de blog, nous expliquons comment NGINX et NGINX Plus peuvent agir en tant que partie utilisatrice OAuth 2.0, en envoyant les jetons d’accès à l’IdP pour validation et en ne transmettant que les requêtes qui réussissent le processus de validation. Nous présentons les différents avantages de l’utilisation de NGINX et de NGINX Plus pour cette tâche, ainsi que la manière d’améliorer l’expérience utilisateur en mettant brièvement en cache les réponses de validation. Pour NGINX Plus, nous montrons également comment répartir le cache entre plusieurs instances NGINX Plus d’un cluster, en mettant à jour le magasin clé-valeur à l’aide du module JavaScript, comme introduit dans NGINX Plus R18.

Sauf indication contraire, les informations contenues dans ce blog s’appliquent à la fois à NGINX Open Source et à NGINX Plus. Les références à NGINX Plus s’appliquent uniquement à ce produit.

Introspection des jetons

La méthode standard de validation des jetons d’accès auprès d’un IdP est appelée « introspection de jeton ». La norme RFC 7662, « Introspection de jeton OAuth 2.0 », est désormais largement prise en charge. Elle décrit une interface JSON/REST qu’une partie utilisatrice emploie pour présenter un jeton à l’IdP, ainsi que la structure de la réponse. Elle est prise en charge par de nombreux fournisseurs d’IdP et de services cloud de premier plan.

Quel que soit le format de jeton utilisé, effectuer la validation au niveau de chaque service backend ou de chaque application entraîne une importante duplication du code et des traitements inutiles. Il faut tenir compte de diverses conditions d’erreur et de différents cas limites. Le faire dans chaque service backend risque d’entraîner des incohérences dans l’implémentation et, par conséquent, une expérience utilisateur imprévisible. Voyons comment chaque service backend pourrait gérer les conditions d’erreur suivantes :

  • Jeton d’accès manquant
  • Jeton d’accès extrêmement volumineux
  • Caractères non valides ou inattendus dans le jeton d’accès
  • Plusieurs jetons d’accès ont été présentés
  • Décalage d’horloge entre les services backend

Applications backend effectuant la validation des jetons

Utilisation du module NGINX « auth_request » pour valider les jetons

Pour éviter la duplication de code et les problèmes qui en découlent, nous pouvons utiliser NGINX pour valider les jetons d’accès pour le compte des services backend. Cette approche présente plusieurs avantages :

  • Les requêtes n’atteignent les services backend que lorsque le client a présenté un jeton valide
  • Les services backend existants peuvent être protégés à l’aide de jetons d’accès, sans qu’aucune modification du code ne soit nécessaire
  • Seule l’instance NGINX (et non chaque application) doit être enregistrée auprès du fournisseur d’identité
  • Le comportement est cohérent pour toutes les conditions d’erreur, y compris en cas de jetons manquants ou non valides

NGINX effectuant la validation des jetons en tant que proxy inverse

Lorsque NGINX fait office de proxy inverse pour une ou plusieurs applications, nous pouvons utiliser le module auth_request module pour déclencher un appel d’API vers un fournisseur d’identité avant de transmettre une requête au serveur backend. Comme nous le verrons dans un instant, la solution suivante présente une faille fondamentale, mais elle permet d’introduire le fonctionnement de base du module auth_request, que nous approfondirons dans les sections suivantes.

Chargement de l’exemple de code…

La directive « auth_request » (ligne 5) indique l’emplacement chargé de traiter les appels d’API. Le transfert vers le backend (ligne 6) n’a lieu que si la réponse de « auth_request » indique une réussite. L’emplacement « auth_request » est défini à la ligne 9. Il est marqué comme « internal » afin d’empêcher les clients externes d’y accéder directement.

Les lignes 11 à 14 définissent différents attributs de la requête afin qu’elle soit conforme au format d’une requête d’introspection de jeton. Notez que le jeton d’accès envoyé dans la requête d’introspection fait partie du corps défini à la ligne 14. Ici, token=$http_apikey indique que le client doit fournir le jeton d’accès dans l’en-tête de requête apikey. Bien entendu, le jeton d’accès peut être fourni dans n’importe quel attribut de la requête, auquel cas nous utilisons une autre variable NGINX.

Extension de « auth_request » avec le module JavaScript de NGINX

Comme indiqué précédemment, l’utilisation du module auth_request de cette manière ne constitue pas une solution complète. Le module auth_request utilise les codes d’état HTTP pour déterminer si l’opération a réussi (2xx = réussite, 4xx = échec). Cependant, les réponses d’introspection de jeton OAuth 2.0 indiquent la réussite ou l’échec dans un objet JSON et renvoient le code d’état HTTP 200 (OK) dans les deux cas.

Format JSON de la réponse d’introspection pour un jeton valide

Nous avons besoin d’un analyseur JSON pour convertir la réponse d’introspection de l’IdP en code d’état HTTP approprié afin que le module auth_request puisse interpréter correctement cette réponse.

Heureusement, l’analyse du JSON est une tâche triviale pour le module JavaScript de NGINX (njs). Ainsi, au lieu de définir un bloc location pour effectuer la requête d’introspection du jeton, nous indiquons au module auth_request d’appeler une fonction JavaScript.

[Éditeur – Cet article fait partie d’une série consacrée aux cas d’utilisation du module JavaScript de NGINX. Pour consulter la liste complète, voir « Cas d’utilisation du module JavaScript de NGINX ».

Le code de cette section a été mis à jour pour utiliser la directive « js_import », qui remplace la directive « js_include » dans NGINX Plus R23 et les versions ultérieures. Pour plus d’informations, consultez la documentation de référence du module JavaScript NGINX : la section « Exemple de configuration » indique la syntaxe correcte pour les fichiers de configuration NGINX et les fichiers JavaScript.]

Remarque : Cette solution nécessite que le module JavaScript soit chargé en tant que module dynamique à l’aide de la directive load_module dans le fichier nginx.conf. Pour obtenir des instructions, consultez le guide d’administration de NGINX Plus.

Chargement de l’exemple de code…

La directive « js_content » à la ligne 13 spécifie une fonction JavaScript, « introspectAccessToken », comme gestionnaire de auth_request. La fonction de gestion est définie dans oauth2.js :

Chargement de l’exemple de code…

Notez que la fonction introspectAccessToken effectue une sous-requête HTTP (ligne 2) vers un autre emplacement (/oauth2_send_request), défini dans l’extrait de configuration ci-dessous. Le code JavaScript analyse ensuite la réponse (ligne 5) et renvoie le code d’état approprié au module auth_request en fonction de la valeur du champ active. Les jetons valides (actifs) renvoient le code HTTP 204 (No Content) (indiquant une réussite), tandis que les jetons non valides renvoient le code HTTP 403 (Forbidden). En cas d’erreur, le code HTTP 401 (Unauthorized) est renvoyé afin de distinguer les erreurs des jetons non valides.

Remarque : Ce code est fourni uniquement à titre de démonstration et n’est pas adapté à une utilisation en production. Une solution complète, avec une gestion exhaustive des erreurs et une journalisation, est proposée ci-dessous.

L’emplacement cible de la sous-requête défini à la ligne 2 ressemble beaucoup à notre configuration auth_request d’origine.

Chargement de l’exemple de code…

Toute la configuration nécessaire à la création de la requête d’introspection de jeton se trouve dans l’emplacement /_oauth2_send_request. L’authentification (ligne 19), le jeton d’accès lui-même (ligne 21) et l’URL du point de terminaison d’introspection de jeton (ligne 22) sont généralement les seuls éléments de configuration nécessaires. L’authentification est requise pour que l’IdP accepte les requêtes d’introspection de jeton provenant de cette instance NGINX. La spécification Introspection de jeton OAuth 2.0 impose une authentification, mais n’en précise pas la méthode. Dans cet exemple, nous utilisons un jeton porteur dans l’en-tête Authorization.

Avec cette configuration, lorsque NGINX reçoit une requête, il la transmet au module JavaScript, qui envoie une requête d’introspection de jeton au fournisseur d’identité. La réponse de ce dernier est examinée et l’authentification est considérée comme réussie lorsque le champ active a pour valeur true. Cette solution permet d’effectuer l’introspection de jetons OAuth 2.0 avec NGINX de manière compacte et efficace, et peut facilement être adaptée à d’autres API d’authentification.

Mais ce n’est pas tout. Le principal défi de l’introspection des jetons réside dans le fait qu’elle augmente la latence de chaque requête HTTP. Cela peut devenir un problème majeur lorsque l’IdP concerné est une solution hébergée ou un fournisseur de services cloud. NGINX et NGINX Plus permettent d’atténuer cet inconvénient en mettant en cache les réponses d’introspection.

Optimisation 1 : mise en cache par NGINX

L’introspection des jetons OAuth 2.0 est fournie par l’IdP via un point de terminaison JSON/REST. La réponse standard est donc un corps JSON avec le statut HTTP 200. Lorsque cette réponse est indexée à l’aide du jeton d’accès, elle peut être mise en cache très efficacement.

Réponse complète d’introspection de jeton pour un jeton valide

NGINX peut être configuré pour mettre en cache une copie de la réponse d’introspection pour chaque jeton d’accès. Ainsi, lorsque le même jeton d’accès est présenté à nouveau, NGINX renvoie la réponse d’introspection mise en cache au lieu d’effectuer un appel d’API auprès de l’IdP. Cela réduit considérablement la latence globale des requêtes suivantes. Il est possible de définir la durée d’utilisation des réponses mises en cache afin de limiter le risque d’accepter un jeton d’accès arrivé à expiration ou récemment révoqué. Par exemple, si un client d’API effectue généralement plusieurs appels d’API en rafale sur une courte période, une durée de validité du cache de 10 secondes peut suffire à améliorer sensiblement l’expérience utilisateur.

La mise en cache est activée en spécifiant son espace de stockage : un répertoire sur le disque pour le cache (réponses d’introspection) et une zone de mémoire partagée pour les clés (jetons d’accès).

Chargement de l’exemple de code…

La directive « proxy_cache_path » alloue l’espace de stockage nécessaire : /var/cache/nginx/oauth pour les réponses d’introspection et une zone mémoire appelée « token_responses » pour les clés. Elle est configurée dans le contexte http et apparaît donc en dehors des blocs server et location. La mise en cache est ensuite activée dans le bloc location où les réponses d’introspection des jetons sont traitées :

Chargement de l’exemple de code…

La mise en cache est activée pour cet emplacement à l’aide de la directive « proxy_cache » (ligne 26). Par défaut, NGINX utilise l’URI comme clé de cache, mais dans notre cas, nous souhaitons mettre en cache la réponse en fonction du jeton d’accès fourni dans l’en-tête de requête « apikey » (ligne 27).

À la ligne 28, nous utilisons la directive proxy_cache_lock pour indiquer à NGINX que, si des requêtes simultanées arrivent avec la même clé de cache, il doit attendre que la première requête ait alimenté le cache avant de répondre aux autres. La directive proxy_cache_valid (ligne 29) indique à NGINX la durée de mise en cache de la réponse d’introspection. Sans cette directive, NGINX détermine la durée de mise en cache à partir des en-têtes Cache-Control envoyés par l’IdP. Toutefois, ceux-ci ne sont pas toujours fiables, c’est pourquoi nous indiquons également à NGINX d’ignorer les en-têtes qui, autrement, influeraient sur la manière dont les réponses sont mises en cache (ligne 30).

La mise en cache étant désormais activée, un client présentant un jeton d’accès ne subit la latence liée à la requête d’introspection du jeton qu’une fois toutes les 10 secondes.

Optimisation 2 : mise en cache distribuée avec NGINX Plus

L’association de la mise en cache du contenu et de l’introspection des jetons constitue un moyen très efficace d’améliorer les performances globales de l’application, avec un impact négligeable sur la sécurité. Toutefois, si NGINX est déployé de manière distribuée – par exemple, dans plusieurs centres de données, sur plusieurs plateformes cloud ou au sein d’un cluster actif-actif –, les réponses d’introspection des jetons mises en cache ne sont accessibles qu’à l’instance NGINX qui a effectué la requête d’introspection.

Avec NGINX Plus, nous pouvons utiliser le module keyval module – un magasin clé-valeur en mémoire – pour mettre en cache les réponses d’introspection des jetons. Nous pouvons également synchroniser ces réponses au sein d’un cluster d’instances NGINX Plus à l’aide du module zone_sync module. Ainsi, quelle que soit l’instance NGINX Plus ayant effectué la requête d’introspection du jeton, la réponse est disponible sur toutes les instances NGINX Plus du cluster.

Remarque : La configuration du module zone_sync pour le partage de l’état d’exécution n’entre pas dans le cadre de cet article. Pour plus d’informations sur le partage d’état dans un cluster NGINX Plus, consultez le Guide d’administration de NGINX Plus.

Dans NGINX Plus R18 et les versions ultérieures, le magasin clé-valeur peut être mis à jour en modifiant la variable déclarée dans la directive keyval. Comme le module JavaScript a accès à toutes les variables NGINX, les réponses d’introspection peuvent ainsi être ajoutées au magasin clé-valeur lors de leur traitement.

À l’instar du cache du système de fichiers NGINX, le magasin clé-valeur est activé en spécifiant son espace de stockage, en l’occurrence une zone mémoire qui stocke la clé (jeton d’accès) et la valeur (réponse d’introspection).

Chargement de l’exemple de code…

Notez qu’avec le paramètre « timeout » de la directive « keyval_zone », nous définissons pour les réponses mises en cache la même durée de validité de 10 secondes qu’à la ligne 29 du fichier auth_request_cache.conf, afin que chaque membre du cluster NGINX Plus supprime indépendamment la réponse lorsqu’elle expire. La ligne 2 définit la paire clé-valeur de chaque entrée : la clé correspond au jeton d’accès fourni dans l’en-tête de requête « apikey », et la valeur à la réponse d’introspection telle qu’évaluée par la variable « $token_data ».

Désormais, pour chaque requête comportant un en-tête de requête « apikey », la variable « $token_data » est renseignée avec la réponse précédente d’introspection du jeton, le cas échéant. Nous mettons donc à jour le code JavaScript afin de vérifier si nous disposons déjà d’une réponse d’introspection du jeton.

Chargement de l’exemple de code…

La ligne 2 vérifie s’il existe déjà une entrée dans le magasin clé-valeur pour ce jeton d’accès. Comme une réponse d’introspection peut être obtenue de deux manières (depuis le magasin clé-valeur ou à partir d’une réponse d’introspection), nous déplaçons la logique de validation dans la fonction distincte suivante, « tokenResult » :

Chargement de l’exemple de code…

Désormais, chaque réponse d’introspection de jeton est enregistrée dans le magasin clé-valeur et synchronisée avec tous les autres membres du cluster NGINX Plus. L’exemple suivant présente une simple requête HTTP avec un jeton d’accès valide, suivie d’une requête adressée à l’API NGINX Plus pour afficher le contenu du magasin clé-valeur.

$ curl -IH "apikey: tQ7AfuEFvI1yI-XNPNhjT38vg_reGkpDFA" http://localhost/HTTP/1.1 200 OK
Date: Wed, 24 Apr 2019 17:41:34 GMT
Content-Type: application/json
Content-Length: 612

$ curl http://localhost/api/4/http/keyvals/access_tokens
{"tQ7AfuEFvI1yI-XNPNhjT38vg_reGkpDFA":"{\"active\":true}"}

Notez que le magasin clé-valeur utilise lui-même le format JSON. Les guillemets de la réponse d’introspection du jeton sont donc automatiquement échappés.

Optimisation 3 : extraction des attributs de la réponse d’introspection

Une fonctionnalité utile de l’introspection des jetons OAuth 2.0 est que la réponse peut contenir des informations sur le jeton en plus de son statut actif. Ces informations comprennent la date d’expiration du jeton et les attributs de l’utilisateur associé : nom d’utilisateur, adresse e-mail, etc.

Réponse d’introspection du jeton avec les attributs du jeton

Ces informations supplémentaires peuvent être très utiles. Elles peuvent être consignées dans des journaux, utilisées pour mettre en œuvre des politiques de contrôle d’accès précises ou transmises aux applications backend. Nous pouvons exporter chacun de ces attributs vers le module auth_request en les envoyant sous forme d’en-têtes de réponse supplémentaires avec une réponse positive (HTTP 204).

Chargement de l’exemple de code…

Nous parcourons chaque attribut de la réponse d’introspection (ligne 23) et le renvoyons au module « auth_request » sous la forme d’un en-tête de réponse. Le nom de chaque en-tête est précédé du préfixe Token- afin d’éviter tout conflit avec les en-têtes de réponse standard (ligne 26). Ces en-têtes de réponse peuvent désormais être convertis en variables NGINX et utilisés dans une configuration classique.

Chargement de l’exemple de code…

Dans cet exemple, nous convertissons l’attribut username en une nouvelle variable, $username (ligne 11). La directive auth_request_set nous permet d’exporter le contexte de la réponse d’introspection du jeton vers celui de la requête actuelle. L’en-tête de réponse de chaque attribut (ajouté par le code JavaScript) est disponible sous la forme $sent_http_token_attribute. La ligne 12 inclut ensuite la valeur de $username en tant qu’en-tête de requête transmis au serveur backend. Nous pouvons répéter cette configuration pour chacun des attributs renvoyés dans la réponse d’introspection du jeton.

Exportation des attributs de la réponse d’introspection du jeton vers la requête transmise au serveur mandataire

Configuration de production

Les exemples de code et de configuration ci-dessus sont fonctionnels et adaptés aux tests de validation de concept ou à la personnalisation pour un cas d’utilisation spécifique. Pour une utilisation en production, nous recommandons vivement d’ajouter une gestion des erreurs et une journalisation plus poussées, ainsi qu’une configuration flexible. Vous trouverez une implémentation plus robuste et détaillée pour NGINX et NGINX Plus dans notre dépôt GitHub :

Résumé

Dans cet article, nous avons montré comment utiliser le module auth_request de NGINX conjointement avec le module JavaScript afin d’effectuer l’introspection des jetons OAuth 2.0 dans les requêtes des clients. Nous avons également enrichi cette solution en y ajoutant la mise en cache et extrait des attributs de la réponse d’introspection afin de les utiliser dans la configuration de NGINX.

Nous avons également expliqué comment le magasin clé-valeur de NGINX Plus peut être utilisé comme cache distribué pour les réponses d’introspection, adapté aux déploiements en production sur un cluster d’instances NGINX Plus.

Testez vous-même l’introspection des jetons OAuth 2.0 avec NGINX Plus : commencez votre essai gratuit de 30 jours dès aujourd’hui ou contactez-nous pour discuter de vos cas d’utilisation.


Partager
Mots-clés : F5 NGINX, Tech, 2019

À propos de l’auteur

Liam Crilly
Liam CrillyDirecteur senior de la gestion des produits

Autres articles de Liam Crilly

Articles de blog associés

Tendances actuelles des technologies cloud natives, de l’ingénierie de plateforme et de l’IA
NGINX | 13/10/2025

Tendances actuelles des technologies cloud natives, de l’ingénierie de plateforme et de l’IA

Découvrez les principaux enseignements tirés des réponses à l’enquête annuelle F5 NGINX sur les dernières tendances en matière de technologies cloud natives, de sécurité, d’ingénierie de plateforme et d’IA.

Quels contrôleurs Ingress NGINX sont affectés par les vulnérabilités CVE-2022-4886, CVE-2023-5043 et CVE-2023-5044 ?
NGINX | 03/11/2023

Quels contrôleurs Ingress NGINX sont affectés par les vulnérabilités CVE-2022-4886, CVE-2023-5043 et CVE-2023-5044 ?

Lorsque de nouvelles CVE sont signalées, il est facile de ne plus savoir si elles concernent un outil de contrôleur Ingress NGINX particulier, car il existe plusieurs projets. Cet article présente les CVE et explique comment distinguer les différents projets de contrôleurs Ingress afin de dissiper toute confusion.

Votre API est-elle en temps réel ? Testez sa latence avec l’outil rtapi de NGINX
NGINX | 11/03/2020

Votre API est-elle en temps réel ? Testez sa latence avec l’outil rtapi de NGINX

Votre API répond-elle aux requêtes en moins de 30 ms ? Si ce n’est pas le cas, il ne s’agit pas d’une API en temps réel offrant la faible latence exigée par la grande majorité des organisations. Mesurez la latence de votre API avec l’outil rtapi de NGINX et découvrez comment NGINX peut l’aider à offrir une réactivité en temps réel.

Un testeur d’expressions régulières pour NGINX et NGINX Plus
NGINX | 11/06/2019

Un testeur d’expressions régulières pour NGINX et NGINX Plus

Le testeur d’expressions régulières (regex) pour NGINX et NGINX Plus vous évite de tâtonner en vous indiquant si une expression régulière utilisée dans un bloc location ou map correspond aux valeurs comme prévu. Il génère la configuration appropriée et affiche les valeurs des groupes de capture.

Caches partagés avec les clusters de cache NGINX Plus, partie 1
NGINX | 24/01/2017

Caches partagés avec les clusters de cache NGINX Plus, partie 1

Optimisez la capacité de votre cache web en le répartissant entre plusieurs serveurs de cache web NGINX ou NGINX Plus. Découvrez comment procéder dans cet article.

Introduction aux microservices
NGINX | 19/05/2015

Introduction aux microservices

Les microservices suscitent actuellement beaucoup d’intérêt. Cet article de blog est le premier d’une série de sept consacrée à la conception, au développement et au déploiement de microservices.