BLOG | NGINX

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

NGINX-Partie-de-F5-horiz-black-type-RGB
Miniature de Liam Crilly
Liam Crilly
Publié le 13 mai 2019

 

Image reproduite avec l'aimable autorisation de John T. sur unsplash.com

Il existe de nombreuses options pour authentifier les appels API, des certificats clients X.509 à l'authentification HTTP de base. Ces dernières années, cependant, une norme de facto est apparue sous la forme des jetons d’accès OAuth 2.0. Il s’agit d’informations d’identification d’authentification transmises du client au serveur API et généralement transportées sous forme d’en-tête HTTP.

OAuth 2.0 est cependant un labyrinthe 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 associées. Au moment de la rédaction de cet article, il existe huit normes OAuth 2.0 , et les jetons d'accès en sont un bon exemple, car la spécification principale OAuth 2.0 ( RFC 6749 ) ne spécifie pas de format pour les jetons d'accès. Dans le monde réel, il existe deux formats couramment utilisés :

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

Après authentification, un client présente son jeton d'accès à chaque requête HTTP pour accéder aux ressources protégées. La validation du jeton d’accès est nécessaire pour garantir qu’il a bien été émis par un fournisseur d’identité de confiance (IdP) et qu’il n’a pas expiré. Étant donné que les IdP signent de manière cryptographique les JWT qu’ils émettent, les JWT peuvent être validés « hors ligne » sans dépendance d’exécution sur l’IdP. En règle générale, un JWT inclut également une date d’expiration qui peut également être vérifiée. Le module NGINX Plus auth_jwt effectue la validation JWT hors ligne.

Les jetons opaques, en revanche, doivent être validés en les renvoyant à l'IdP qui les a émis. Cependant, cela présente l’avantage que de tels jetons peuvent être révoqués par l’IdP, par exemple dans le cadre d’une opération de déconnexion globale, sans laisser les sessions précédemment connectées toujours actives. La déconnexion globale peut également rendre nécessaire la validation des JWT auprès de l'IdP.

Dans ce blog, nous décrivons comment NGINX et NGINX Plus peuvent agir en tant que partie de confiance OAuth 2.0, en envoyant des jetons d'accès à l'IdP pour validation et en proxy uniquement les demandes qui passent le processus de validation. Nous discutons des différents avantages de l’utilisation de NGINX et NGINX Plus pour cette tâche, et de la manière dont l’expérience utilisateur peut être améliorée en mettant en cache les réponses de validation pendant une courte période. Pour NGINX Plus, nous montrons également comment le cache peut être distribué sur un cluster d’instances NGINX Plus, en mettant à jour le magasin clé-valeur avec le 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 symbolique

La méthode standard de validation des jetons d’accès avec un IdP est appelée introspection de jetons . RFC 7662 , OAuth 2.0 Token Introspection , est désormais une norme largement prise en charge qui décrit une interface JSON/REST qu'une partie de confiance utilise pour présenter un jeton à l'IdP et décrit la structure de la réponse. Il est pris en charge par de nombreux fournisseurs d'IdP et de cloud de premier plan.

Quel que soit le format de jeton utilisé, l'exécution de la validation au niveau de chaque service ou application back-end génère beaucoup de code dupliqué et un traitement inutile. Différentes conditions d’erreur et cas limites doivent être pris en compte, et le faire dans chaque service back-end est une recette pour une incohérence dans la mise en œuvre et, par conséquent, une expérience utilisateur imprévisible. Réfléchissez à la manière dont chaque service back-end peut 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 présentés
  • Décalage d'horloge entre les services back-end
Applications back-end 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 au nom des services backend. Cela présente un certain nombre d’avantages :

  • Les requêtes parviennent aux services backend uniquement lorsque le client a présenté un jeton valide
  • Les services back-end existants peuvent être protégés avec des jetons d'accès, sans nécessiter de modifications de code
  • Seule l'instance NGINX (pas toutes les applications) doit être enregistrée auprès de l'IdP
  • Le comportement est cohérent pour chaque condition d'erreur, y compris les jetons manquants ou non valides
NGINX effectue la validation des jetons en tant que proxy inverse

Avec NGINX agissant comme un proxy inverse pour une ou plusieurs applications, nous pouvons utiliser le module auth_request pour déclencher un appel API vers un IdP avant de transmettre une requête au backend. Comme nous le verrons dans un instant, la solution suivante présente un défaut fondamental, mais elle introduit le fonctionnement de base du module auth_request , que nous développerons dans les sections ultérieures.

 

La directive auth_request (ligne 5) spécifie l'emplacement de gestion des appels API. Le proxy vers le backend (ligne 6) se produit uniquement si la réponse auth_request est réussie. L'emplacement auth_request est défini à la ligne 9. Il est marqué comme interne pour empêcher les clients externes d'y accéder directement.

Les lignes 11 à 14 définissent divers attributs de la demande afin qu’elle soit conforme au format de demande d’introspection de jeton. Notez que le jeton d’accès envoyé dans la demande d’introspection est un composant 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 demande 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 variable NGINX différente.

Extension de auth_request avec le module JavaScript NGINX

Comme mentionné, l’utilisation du module auth_request de cette manière n’est pas une solution complète. Le module auth_request utilise des codes d'état HTTP pour déterminer le succès ( 2xx = bien, 4xx = mauvais). Cependant, les réponses d'introspection du jeton OAuth 2.0 codent 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 de jeton pour un jeton valide

Ce dont nous avons besoin, c'est 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 JSON est une tâche triviale pour le module JavaScript NGINX (njs). Ainsi, au lieu de définir un bloc d’emplacement pour exécuter la demande d’introspection du jeton, nous demandons au module auth_request d’appeler une fonction JavaScript.

[ Éditeur – Cet article est l’un des nombreux articles qui explorent les cas d’utilisation du module JavaScript NGINX. Pour une liste complète, voir Cas d'utilisation du module JavaScript NGINX .

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

Note: Cette solution nécessite que le module JavaScript soit chargé en tant que module dynamique avec la directive load_module dans nginx.conf . Pour obtenir des instructions, consultez le Guide d'administration NGINX Plus .

 

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

 

Notez que la fonction introspectAccessToken effectue une sous-requête HTTP (ligne 2) vers un autre emplacement ( /oauth2_send_request ) qui est 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 actif . Les jetons valides (actifs) renvoient HTTP204 (Aucun contenu) (mais succès) et les jetons non valides renvoient HTTP403 (Interdit) . Les conditions d'erreur renvoient HTTP401 (Non autorisé) afin que les erreurs puissent être distinguées des jetons non valides.

Note: Ce code est fourni uniquement à titre de preuve de concept et n'est pas de qualité de production. Une solution complète avec une gestion et une journalisation complètes des erreurs est fournie ci-dessous .

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

 

Toute la configuration pour construire la demande d’introspection de jeton est contenue 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 du 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 demandes d'introspection de jetons de cette instance NGINX. La spécification OAuth 2.0 Token Introspection impose l'authentification, mais ne spécifie pas la méthode. Dans cet exemple, nous utilisons un jeton porteur dans l’en-tête d’autorisation .

Avec cette configuration en place, lorsque NGINX reçoit une demande, il la transmet au module JavaScript, qui effectue une demande d'introspection de jeton contre l'IdP. La réponse de l'IdP est inspectée et l'authentification est considérée comme réussie lorsque le champ actif est vrai . Cette solution est un moyen compact et efficace d’effectuer l’introspection des jetons OAuth 2.0 avec NGINX, et peut facilement être adaptée à d’autres API d’authentification.

Mais nous n’avons pas encore tout à fait terminé. Le plus gros défi avec l’introspection des jetons en général est qu’elle ajoute de la latence à chaque requête HTTP. Cela peut devenir un problème important lorsque l’IdP en question est une solution hébergée ou un fournisseur de cloud. NGINX et NGINX Plus peuvent proposer des optimisations à cet inconvénient en mettant en cache les réponses d’introspection.

Optimisation 1 : Mise en cache par NGINX

L'introspection du jeton OAuth 2.0 est fournie par l'IdP à un point de terminaison JSON/REST, et donc la réponse standard est un corps JSON avec un statut HTTP200 . Lorsque cette réponse est associée au jeton d'accès, elle devient hautement cachable.

Réponse d'introspection de jeton complète 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 afin que la prochaine fois que le même jeton d'accès est présenté, NGINX serve la réponse d'introspection mise en cache au lieu d'effectuer un appel API à l'IdP. Cela améliore considérablement la latence globale pour les requêtes ultérieures. Nous pouvons contrôler la durée d’utilisation des réponses mises en cache, afin d’atténuer le risque d’accepter un jeton d’accès expiré ou récemment révoqué. Par exemple, si un client API effectue généralement une série de plusieurs appels API sur une courte période, une validité de cache de 10 secondes peut être suffisante pour apporter une amélioration mesurable de l'expérience utilisateur.

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

 

La directive proxy_cache_path alloue le 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. Il est configuré dans le contexte http et apparaît donc en dehors des blocs serveur et emplacement . La mise en cache elle-même est ensuite activée à l'intérieur du bloc d'emplacement où les réponses d'introspection du jeton sont traitées :

 

La mise en cache est activée pour cet emplacement avec la directive proxy_cache (ligne 26). Par défaut, NGINX met en cache la base de l'URI, mais dans notre cas, nous souhaitons mettre en cache la réponse en fonction du jeton d'accès présenté dans l'en-tête de la demande 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 rempli le cache avant de répondre aux autres. La directive proxy_cache_valid (ligne 29) indique à NGINX combien de temps mettre en cache la réponse d'introspection. Sans cette directive, NGINX détermine le temps de mise en cache à partir des en-têtes de contrôle de cache envoyés par l'IdP ; cependant, ceux-ci ne sont pas toujours fiables, c'est pourquoi nous demandons également à NGINX d' ignorer les en-têtes qui affecteraient autrement la façon dont nous mettons en cache les réponses (ligne 30).

La mise en cache étant désormais activée, un client présentant un jeton d'accès subit uniquement le coût de latence lié à la réalisation de la demande d'introspection du jeton une fois toutes les 10 secondes.

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

La combinaison de la mise en cache de contenu avec l’introspection des jetons est un moyen très efficace d’améliorer les performances globales des applications avec un impact négligeable sur la sécurité. Toutefois, si NGINX est déployé de manière distribuée (par exemple sur plusieurs centres de données, plates-formes cloud ou cluster actif-actif), les réponses d'introspection de jetons mis en cache ne sont disponibles que pour l'instance NGINX qui a effectué la demande d'introspection.

Avec NGINX Plus, nous pouvons utiliser le module keyval (un magasin de clés-valeurs en mémoire) pour mettre en cache les réponses d’introspection des jetons. De plus, nous pouvons également synchroniser ces réponses sur un cluster d’instances NGINX Plus en utilisant le module zone_sync . Cela signifie que quelle que soit l'instance NGINX Plus qui a effectué la demande d'introspection de jeton, la réponse est disponible sur toutes les instances NGINX Plus du cluster.

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

Dans NGINX Plus R18 et 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, cela permet de renseigner les réponses d’introspection dans le magasin clé-valeur pendant le traitement de la réponse.

Comme le cache du système de fichiers NGINX, le magasin clé-valeur est activé en spécifiant son stockage, dans ce cas une zone mémoire qui stocke la clé (jeton d'accès) et la valeur (réponse d'introspection).

 

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

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

 

La ligne 2 teste s’il existe déjà une entrée de magasin clé-valeur pour ce jeton d’accès. Étant donné qu’il existe deux chemins par lesquels une réponse d’introspection peut être obtenue (à partir du 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 :

 

Désormais, chaque réponse d’introspection de jeton est enregistrée dans le magasin clé-valeur et synchronisée sur tous les autres membres du cluster NGINX Plus. L'exemple suivant montre une requête HTTP simple avec un jeton d'accès valide, suivie d'une requête à 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: Mercredi 24 avril 2019 17:41:34 GMT Type de contenu : application/json Longueur du contenu : 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, de sorte que la réponse d'introspection du jeton applique automatiquement un échappement aux guillemets.

Optimisation 3 : Extraction des attributs de la réponse d'introspection

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

Réponse d'introspection de jeton avec attributs de jeton

Ces informations complémentaires peuvent être très utiles. Il peut être enregistré, utilisé pour mettre en œuvre des politiques de contrôle d’accès précises ou fourni aux applications back-end. 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 connexion HTTP réussie.204 ) réponse.

 

Nous parcourons chaque attribut de la réponse d’introspection (ligne 23) et le renvoyons au module auth_request en tant qu’en-tête de réponse. Chaque nom d'en-tête est préfixé par Token- pour éviter les conflits 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 le cadre d'une configuration standard.

 

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 dans le contexte de la requête en cours. L'en-tête de réponse pour chaque attribut (ajouté par le code JavaScript) est disponible sous la forme de l'attribut $sent_http_token_ . La ligne 12 inclut ensuite la valeur de $username comme en-tête de requête transmis par proxy au backend. Nous pouvons répéter cette configuration pour n’importe lequel 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 proxy

Configuration de production

Les exemples de code et de configuration ci-dessus sont fonctionnels et adaptés aux tests de preuve de concept ou à la personnalisation pour un cas d'utilisation spécifique. Pour une utilisation en production, nous recommandons fortement une gestion des erreurs supplémentaire, une journalisation et une configuration flexible. Vous pouvez trouver une implémentation plus robuste et plus détaillée pour NGINX et NGINX Plus dans notre dépôt GitHub :

Résumé

Dans ce blog, nous avons montré comment utiliser le module NGINX auth_request en conjonction avec le module JavaScript pour effectuer l'introspection du jeton OAuth 2.0 sur les demandes des clients. De plus, nous avons étendu cette solution avec la mise en cache et extrait des attributs de la réponse d'introspection pour les utiliser dans la configuration NGINX.

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

Essayez vous-même l'introspection des jetons OAuth 2.0 avec NGINX Plus – démarrez votre essai gratuit de 30 jours dès aujourd'hui ou contactez-nous pour discuter de vos cas d'utilisation .


« Cet article de blog peut faire référence à des produits qui ne sont plus disponibles et/ou qui ne sont plus pris en charge. Pour obtenir les informations les plus récentes sur les produits et solutions F5 NGINX disponibles, explorez notre famille de produits NGINX . NGINX fait désormais partie de F5. Tous les liens NGINX.com précédents redirigeront vers un contenu NGINX similaire sur F5.com."