J'ai essayé de résoudre un problème similaire. Mes utilisateurs doivent être authentifiés pour chaque demande qu'ils font. Je me suis concentré sur l'authentification des utilisateurs au moins une fois par l'application backend (validation du token JWT), mais après cela, j'ai décidé de ne plus avoir besoin du backend.
J'ai choisi d'éviter d'exiger tout plugin Nginx qui n'est pas inclus par défaut. Sinon, vous pouvez vérifier les scripts nginx-jwt ou Lua et ce seraient probablement d'excellentes solutions.
Adressage de l'authentification
Jusqu'à présent, j'ai fait ce qui suit:
Délégué l'authentification à Nginx à l'aide de auth_request
. Cela appelle un internal
emplacement qui transmet la demande à mon point de terminaison de validation de jeton d'arrière-plan. Cela ne résout pas à lui seul la question du traitement d'un nombre élevé de validations.
Le résultat de la validation du jeton est mis en cache à l'aide d'une proxy_cache_key "$cookie_token";
directive. Une fois la validation du jeton réussie, le backend ajoute une Cache-Control
directive qui indique à Nginx de ne mettre en cache le jeton que pendant 5 minutes maximum. À ce stade, tout jeton d'authentification validé une fois se trouve dans le cache, les demandes ultérieures du même utilisateur / jeton ne touchent plus le backend d'authentification!
Pour protéger mon application backend contre les inondations potentielles par des jetons invalides, je cache également les validations refusées, lorsque mon point de terminaison backend renvoie 401. Celles-ci ne sont mises en cache que pendant une courte durée pour éviter de potentiellement remplir le cache Nginx avec de telles demandes.
J'ai ajouté quelques améliorations supplémentaires telles qu'un point de terminaison de déconnexion qui invalide un jeton en renvoyant 401 (qui est également mis en cache par Nginx) afin que si l'utilisateur clique sur déconnexion, le jeton ne puisse plus être utilisé même s'il n'est pas expiré.
De plus, mon cache Nginx contient pour chaque jeton, l'utilisateur associé en tant qu'objet JSON, ce qui m'évite de le récupérer dans la base de données si j'ai besoin de ces informations; et me sauve également du décryptage du jeton.
À propos de la durée de vie des jetons et des jetons d'actualisation
Après 5 minutes, le jeton aura expiré dans le cache, donc le backend sera à nouveau interrogé. Il s'agit de garantir que vous pouvez invalider un jeton, car l'utilisateur se déconnecte, car il a été compromis, etc. Une telle revalidation périodique, avec une implémentation appropriée dans le backend, m'évite d'avoir à utiliser des jetons de rafraîchissement.
Traditionnellement, les jetons d'actualisation sont utilisés pour demander un nouveau jeton d'accès; ils seraient stockés dans votre backend et vous vérifieriez qu'une demande de jeton d'accès est faite avec un jeton d'actualisation qui correspond à celui que vous avez dans la base de données pour cet utilisateur spécifique. Si l'utilisateur se déconnecte ou si les jetons sont compromis, vous supprimez / invalidez le jeton d'actualisation dans votre base de données afin que la prochaine demande de nouveau jeton utilisant le jeton d'actualisation invalidé échoue.
En bref, les jetons de rafraîchissement ont généralement une longue validité et sont toujours vérifiés par rapport au backend. Ils sont utilisés pour générer des jetons d'accès qui ont une validité très courte (quelques minutes). Ces jetons d'accès atteignent normalement votre backend mais vous ne vérifiez que leur signature et leur date d'expiration.
Ici, dans ma configuration, nous utilisons des jetons avec une validité plus longue (pouvant être des heures ou un jour), qui ont le même rôle et les mêmes fonctionnalités qu'un jeton d'accès et un jeton d'actualisation. Parce que nous avons mis en cache leur validation et invalidation par Nginx, ils ne sont entièrement vérifiés par le backend qu'une fois toutes les 5 minutes. Nous conservons donc l'avantage d'utiliser des jetons d'actualisation (pouvoir invalider rapidement un jeton) sans la complexité supplémentaire. Et la validation simple n'atteint jamais votre backend qui est au moins 1 ordre de grandeur plus lent que le cache Nginx, même s'il n'est utilisé que pour la signature et la vérification de la date d'expiration.
Avec cette configuration, je pouvais désactiver l'authentification dans mon backend, car toutes les demandes entrantes atteignent la auth_request
directive Nginx avant de la toucher.
Cela ne résout pas complètement le problème si vous devez effectuer une autorisation par ressource, mais au moins vous avez enregistré la partie d'autorisation de base. Et vous pouvez même éviter de déchiffrer le jeton ou faire une recherche de base de données pour accéder aux données du jeton, car la réponse d'authentification en cache Nginx peut contenir des données et les transmettre au backend.
Maintenant, ma plus grande préoccupation est que je puisse briser quelque chose d'évident lié à la sécurité sans m'en rendre compte. Cela étant dit, tout jeton reçu est toujours validé au moins une fois avant d'être mis en cache par Nginx. Tout jeton tempéré serait différent et ne toucherait donc pas le cache puisque la clé de cache serait également différente.
En outre, il convient peut-être de mentionner qu'une authentification dans le monde réel lutterait contre le vol de jetons en générant (et en vérifiant) un Nonce supplémentaire ou quelque chose.
Voici un extrait simplifié de ma configuration Nginx pour mon application:
# Cache for internal auth checks
proxy_cache_path /usr/local/var/nginx/cache/auth levels=1:2 keys_zone=auth_cache:10m max_size=128m inactive=10m use_temp_path=off;
# Cache for content
proxy_cache_path /usr/local/var/nginx/cache/resx levels=1:2 keys_zone=content_cache:16m max_size=128m inactive=5m use_temp_path=off;
server {
listen 443 ssl http2;
server_name ........;
include /usr/local/etc/nginx/include-auth-internal.conf;
location /api/v1 {
# Auth magic happens here
auth_request /auth;
auth_request_set $user $upstream_http_X_User_Id;
auth_request_set $customer $upstream_http_X_Customer_Id;
auth_request_set $permissions $upstream_http_X_Permissions;
# The backend app, once Nginx has performed internal auth.
proxy_pass http://127.0.0.1:5000;
proxy_set_header X-User-Id $user;
proxy_set_header X-Customer-Id $customer;
proxy_set_header X-Permissions $permissions;
# Cache content
proxy_cache content_cache;
proxy_cache_key "$request_method-$request_uri";
}
location /api/v1/Logout {
auth_request /auth/logout;
}
}
Maintenant, voici l'extrait de configuration pour le /auth
point de terminaison interne , inclus ci-dessus comme /usr/local/etc/nginx/include-auth-internal.conf
:
# Called before every request to backend
location = /auth {
internal;
proxy_cache auth_cache;
proxy_cache_methods GET HEAD POST;
proxy_cache_key "$cookie_token";
# Valid tokens cache duration is set by backend returning a properly set Cache-Control header
# Invalid tokens are shortly cached to protect backend but not flood Nginx cache
proxy_cache_valid 401 30s;
# Valid tokens are cached for 5 minutes so we can get the backend to re-validate them from time to time
proxy_cache_valid 200 5m;
proxy_pass http://127.0.0.1:1234/auth/_Internal;
proxy_set_header Host ........;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header Accept application/json;
}
# To invalidate a not expired token, use a specific backend endpoint.
# Then we cache the token invalid/401 response itself.
location = /auth/logout {
internal;
proxy_cache auth_cache;
proxy_cache_key "$cookie_token";
# Proper caching duration (> token expire date) set by backend, which will override below default duration
proxy_cache_valid 401 30m;
# A Logout requests forces a cache refresh in order to store a 401 where there was previously a valid authorization
proxy_cache_bypass 1;
# This backend endpoint always returns 401, with a cache header set to the expire date of the token
proxy_pass http://127.0.0.1:1234/auth/_Internal/Logout;
proxy_set_header Host ........;
proxy_pass_request_body off;
}
.
Traitement de la diffusion de contenu
Maintenant, l'authentification est séparée des données. Puisque vous avez dit qu'il était identique pour chaque utilisateur, le contenu lui-même peut également être mis en cache par Nginx (dans mon exemple, dans la content_cache
zone).
Évolutivité
Ce scénario fonctionne très bien en supposant que vous avez un serveur Nginx. Dans un scénario réel, vous avez probablement une haute disponibilité, ce qui signifie plusieurs instances Nginx, potentiellement hébergeant également votre application dorsale (Laravel). Dans ce cas, toute demande de vos utilisateurs pourrait être envoyée à l'un de vos serveurs Nginx, et jusqu'à ce qu'ils aient tous mis en cache localement le jeton, ils continueront d'atteindre votre serveur pour le vérifier. Pour un petit nombre de serveurs, l'utilisation de cette solution apporterait tout de même de gros avantages.
Cependant, il est important de noter qu'avec plusieurs serveurs Nginx (et donc des caches), vous perdez la possibilité de vous déconnecter côté serveur car vous ne pouvez pas purger (en forçant une actualisation) le cache des jetons sur chacun d'eux, comme /auth/logout
fait dans mon exemple. Il ne vous reste plus que la durée du cache de jetons de 5 minutes qui forcera votre backend à être interrogé bientôt et indiquera à Nginx que la demande est refusée. Une solution de contournement partielle consiste à supprimer l'en-tête de jeton ou le cookie sur le client lors de la déconnexion.
Tout commentaire serait le bienvenu et apprécié!