ManiÚre sécurisée d'échanger JWT dans ASP.NET Core + SPA

A.

Entrée


L'authentification JWT (JSON Web Token) est un mécanisme d'authentification et d'authentification assez uniforme et cohérent entre le serveur et les clients. L'avantage de JWT est qu'il nous permet de gérer moins l'état et évolue bien. Il n'est pas surprenant que l'autorisation et l'authentification avec son aide soient de plus en plus utilisées dans les applications Web modernes.

Lors du dĂ©veloppement d'applications avec JWT, la question se pose souvent: oĂč et comment est-il recommandĂ© de stocker le jeton? Si nous dĂ©veloppons une application Web, nous avons deux des options les plus courantes:

  • Stockage Web HTML5 (localStorage ou sessionStorage)
  • Les cookies

En comparant ces méthodes, nous pouvons dire qu'elles stockent toutes les deux les valeurs dans le navigateur du client, qu'elles sont assez faciles à utiliser et représentent un stockage régulier de paires clé-valeur. La différence réside dans l'environnement de stockage.

Le stockage Web (localStorage / sessionStorage) est accessible via JavaScript dans le mĂȘme domaine. Cela signifie que tout code JavaScript dans votre application a accĂšs Ă  Web Storage, ce qui crĂ©e une vulnĂ©rabilitĂ© aux attaques de script intersite (XSS). En tant que moteur de stockage, Web Storage ne fournit aucun moyen de sĂ©curiser vos donnĂ©es pendant le stockage et le partage. Nous ne pouvons l'utiliser que pour les donnĂ©es auxiliaires que nous souhaitons conserver lors de la mise Ă  jour (F5) ou de la fermeture de l'onglet: Ă©tat et numĂ©ro de page, filtres, etc.

Les jetons peuvent Ă©galement ĂȘtre transmis via les cookies du navigateur. Les cookies utilisĂ©s avec le drapeau httpOnly ne sont pas affectĂ©s par XSS. httpOnly est un indicateur d'accĂšs Ă  la lecture, Ă  l'Ă©criture et Ă  la suppression des cookies uniquement sur le serveur. Ils ne seront pas accessibles via JavaScript sur le client, donc le client ne sera pas au courant du jeton et l'autorisation sera entiĂšrement traitĂ©e cĂŽtĂ© serveur.

Nous pouvons également définir un indicateur sécurisé pour garantir que les cookies ne sont transmis que via HTTPS. Compte tenu de ces avantages, mon choix s'est porté sur les cookies.

Cet article décrit une approche pour implémenter l'authentification et l'authentification à l'aide de cookies sécurisés httpOnly + jeton Web JSON dans ASP.NET Core Web Api en conjonction avec SPA. L'option est considérée dans laquelle le serveur et le client sont d'origine différente.

Configuration de votre environnement de développement local


Pour configurer et déboguer correctement les relations client-serveur via HTTPS, je vous recommande fortement de configurer immédiatement l'environnement de développement local afin que le client et le serveur disposent d'une connexion HTTPS.

Si vous ne le faites pas tout de suite et essayez de nouer des relations sans connexion HTTPS, à l'avenir, de nombreux détails apparaßtront sans lesquels les cookies sécurisés et les politiques de sécurité supplémentaires en production avec HTTPS ne fonctionneront pas correctement.

Je vais montrer un exemple de configuration de HTTPS sur OS Windows 10, serveur - ASP.NET Core, SPA - React.

Vous pouvez configurer HTTPS dans ASP.NET Core à l'aide de l'indicateur Configurer pour HTTPS lors de la création d'un projet ou, si nous ne l'avons pas fait lors de la création, activez l'option correspondante dans Propriétés.

Pour configurer SPA, vous devez modifier le script pour "démarrer" , en le définissant sur "set HTTPS = true" . Ma configuration est la suivante:

'start': 'set HTTPS=true&&rimraf ./build&&react-scripts start' 

Je vous conseille de regarder la configuration de HTTPS pour l'environnement de développement dans d'autres environnements à create-react-app.dev/docs/using-https-in-development

Configurer le serveur ASP.NET Core


Configuration JWT


Dans ce cas, l'implémentation la plus courante de JWT à partir de la documentation ou de n'importe quel article, avec des options.RequireHttpsMetadata = true; paramÚtres supplémentaires.RequireHttpsMetadata options.RequireHttpsMetadata = true; comme notre environnement de développement utilise HTTPS:

ConfigureServices

 services.AddAuthentication(options => { options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultSignInScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options => { options.RequireHttpsMetadata = true; options.SaveToken = true; options.TokenValidationParameters = new TokenValidationParameters { //  .  }; }); 

Configuration d'une stratégie CORS


Important : la stratégie AllowCredentials() doit contenir AllowCredentials() . Cela est nécessaire pour recevoir une demande de XMLHttpRequest.withCredentials et renvoyer des cookies au client. Plus d'informations à ce sujet seront écrites plus tard. D'autres options sont configurées en fonction des besoins du projet.

Si le serveur et le client sont sur la mĂȘme origine, alors la configuration complĂšte ci-dessous n'est pas nĂ©cessaire.

ConfigureServices

 services.AddCors(); 

Configurer

 app.UseCors(x => x .WithOrigins("https://localhost:3000") //    SPA  .AllowCredentials() .AllowAnyMethod() .AllowAnyHeader()); 

DĂ©finition d'une politique de cookies


Forcer la politique des cookies sur httpOnly et sécurisé.

Si possible, définissez MinimumSameSitePolicy = SameSiteMode.Strict; - Cela améliore la sécurité des cookies pour les types d'applications qui ne dépendent pas du traitement des demandes d'origine croisée.

Configurer

 app.UseCookiePolicy(new CookiePolicyOptions { MinimumSameSitePolicy = SameSiteMode.Strict, HttpOnly = HttpOnlyPolicy.Always, Secure = CookieSecurePolicy.Always }); 

L'idée d'un échange de jetons sécurisé


Cette partie est un concept. Nous allons faire deux choses:

  1. Jetez un jeton dans une demande HTTP en utilisant httpOnly et des indicateurs sécurisés.
  2. Recevez et validez les jetons d'application client Ă  partir d'une requĂȘte HTTP.

Pour ce faire, nous avons besoin de:

  • Écrivez le jeton dans le cookie httpOnly lors de la connexion et supprimez-le lors de la connexion.
  • S'il y a un jeton dans les cookies, remplacez-le dans l'en-tĂȘte HTTP de chaque demande suivante.
  • S'il n'y a pas de jeton dans les cookies, l'en-tĂȘte sera vide et la demande ne sera pas autorisĂ©e.

Middleware


L'idĂ©e principale est d'implĂ©menter un middleware personnalisĂ© pour insĂ©rer un jeton dans une requĂȘte HTTP entrante. AprĂšs autorisation de l'utilisateur, nous enregistrons le cookie sous une certaine clĂ©, par exemple: ".AspNetCore.Application.Id" . Je recommande de dĂ©finir un nom qui n'a rien Ă  voir avec l'autorisation ou les jetons - dans ce cas, un cookie avec un jeton ressemblera Ă  une sorte de constante systĂšme banale pour l'application AspNetCore. Il y a donc plus de chances que l'attaquant voit de nombreuses variables systĂšme et, sans comprendre quel mĂ©canisme d'autorisation est utilisĂ©, ira plus loin. Bien sĂ»r, s'il ne lit pas cet article et ne recherche pas spĂ©cifiquement une telle constante.

Ensuite, nous devons insĂ©rer ce jeton dans toutes les requĂȘtes HTTP entrantes suivantes. Pour ce faire, nous allons Ă©crire quelques lignes de code Middleware. Ce n'est rien d'autre qu'un pipeline HTTP.



Configurer

 app.Use(async (context, next) => { var token = context.Request.Cookies[".AspNetCore.Application.Id"]; if (!string.IsNullOrEmpty(token)) context.Request.Headers.Add("Authorization", "Bearer " + token); await next(); }); app.UseAuthentication(); 

Nous pouvons prendre cette logique dans un service Middleware séparé afin de ne pas obstruer Startup.cs, l'idée ne changera pas.

Afin d'Ă©crire la valeur dans les cookies, il suffit d'ajouter la ligne suivante Ă  la logique d'autorisation:

 if (result.Succeeded) HttpContext.Response.Cookies.Append(".AspNetCore.Application.Id", token, new CookieOptions { MaxAge = TimeSpan.FromMinutes(60) }); 

En utilisant nos politiques de cookies, ces cookies seront automatiquement envoyés en tant que httpOnly et sécurisés. Pas besoin de redéfinir leur politique dans les options de cookies.

Dans CookieOptions, vous pouvez définir MaxAge pour spécifier une durée de vie. Il est utile de spécifier avec JWT Lifetime lors de l'émission du jeton afin que le cookie disparaisse aprÚs un certain temps. D'autres propriétés de CookieOptions sont configurées en fonction des exigences du projet.

Pour plus de sĂ©curitĂ©, je recommande d'ajouter les en-tĂȘtes suivants au middleware:

 context.Response.Headers.Add("X-Content-Type-Options", "nosniff"); context.Response.Headers.Add("X-Xss-Protection", "1"); context.Response.Headers.Add("X-Frame-Options", "DENY"); 

  • L'en - tĂȘte X-Content-Type-Options est utilisĂ© pour se protĂ©ger contre les vulnĂ©rabilitĂ©s de reniflement MIME. Cette vulnĂ©rabilitĂ© peut se produire lorsqu'un site permet aux utilisateurs de tĂ©lĂ©charger du contenu, mais l'utilisateur dĂ©guise un certain type de fichier en quelque chose d'autre. Cela peut donner aux attaquants la possibilitĂ© d'exĂ©cuter des scripts de script intersite ou de compromettre un site Web.
  • Tous les navigateurs modernes ont des capacitĂ©s de filtrage XSS intĂ©grĂ©es qui tentent de dĂ©tecter les vulnĂ©rabilitĂ©s XSS avant que la page ne nous soit entiĂšrement affichĂ©e. Par dĂ©faut, ils sont activĂ©s dans le navigateur, mais l'utilisateur peut ĂȘtre plus dĂ©licat et les dĂ©sactiver. En utilisant l'en - tĂȘte X-XSS-Protection , nous pouvons en fait dire au navigateur d'ignorer ce que l'utilisateur a fait et d'appliquer le filtre intĂ©grĂ©.
  • X-Frame-Options indique au navigateur que si votre site est placĂ© dans un cadre HTML, alors n'affichez rien. Ceci est trĂšs important lorsque vous essayez de vous protĂ©ger contre les tentatives de dĂ©tournement de clics.

J'ai décrit loin de tous les titres. Il existe de nombreuses autres façons d'améliorer la sécurité des applications Web. Je vous conseille de vous concentrer sur la liste de contrÎle de sécurité de la ressource securityheaders.com.

Configuration du client SPA


Lorsque le client et le serveur sont situés sur une origine différente, une configuration supplémentaire est également requise sur le client. Vous devez encapsuler chaque demande à l'aide de XMLHttpRequest.withCredentials .

J'ai encapsulé mes méthodes comme suit:

 import axios from "axios"; const api = axios.create({ baseURL: process.env.REACT_APP_API_URL }); api.interceptors.request.use(request => requestInterceptor(request)) const requestInterceptor = (request) => { request.withCredentials = true; return request; } export default api; 

Nous pouvons envelopper notre configuration de requĂȘte de n'importe quelle maniĂšre, l'essentiel est que withCredentials = true soit lĂ  .

La propriĂ©tĂ© XMLHttpRequest.withCredentials dĂ©termine si les demandes interdomaines doivent ĂȘtre gĂ©nĂ©rĂ©es Ă  l'aide d'informations d'identification telles que les cookies, les en-tĂȘtes d'autorisation ou les certificats TLS.

Cet indicateur est également utilisé pour déterminer si les cookies envoyés dans la réponse seront ignorés. XMLHttpRequest d'un autre domaine ne peut pas définir un cookie sur son propre domaine si l'indicateur withCredentials n'est pas défini sur true avant de créer cette demande.

En d'autres termes, si vous ne spécifiez pas cet attribut, notre cookie ne sera pas enregistré par le navigateur, c'est-à-dire nous ne pourrons pas renvoyer le cookie au serveur, et le serveur ne trouvera pas le cookie souhaité avec JWT et ne signera pas le Token porteur dans notre pipeline HTTP.

À quoi tout cela sert-il?


Ci-dessus, j'ai décrit un moyen d'échange de jetons résistant aux XSS. Passons en revue le résultat de la fonctionnalité implémentée.

Si vous allez dans Developer Tools, nous voyons les drapeaux convoités httpOnly and secure :



Faisons un test d'Ă©crasement, essayons d'obtenir les cookies du client:



Nous observons '', c'est-Ă -dire les cookies ne sont pas accessibles depuis l'espace documentaire, ce qui rend impossible leur lecture avec des scripts.

Nous pouvons essayer d'obtenir ces cookies à l'aide d'outils ou d'extensions supplémentaires, mais tous les outils que j'ai essayés ont appelé l'implémentation native à partir de l'espace document.

Projet de démonstration


Les instructions de démarrage se trouvent dans README.MD


UPD: Protection contre CSRF


Configurer le serveur ASP.NET Core


Services middleware

XsrfProtectionMiddleware.cs
 public class XsrfProtectionMiddleware { private readonly IAntiforgery _antiforgery; private readonly RequestDelegate _next; public XsrfProtectionMiddleware(RequestDelegate next, IAntiforgery antiforgery) { _next = next; _antiforgery = antiforgery; } public async Task InvokeAsync(HttpContext context) { context.Response.Cookies.Append( ".AspNetCore.Xsrf", _antiforgery.GetAndStoreTokens(context).RequestToken, new CookieOptions {HttpOnly = false, Secure = true, MaxAge = TimeSpan.FromMinutes(60)}); await _next(context); } } 



MiddlewareExtensions.cs
 public static class MiddlewareExtensions { public static IApplicationBuilder UseXsrfProtection(this IApplicationBuilder builder, IAntiforgery antiforgery) => builder.UseMiddleware<XsrfProtectionMiddleware>(antiforgery); } 



ConfigureServices

 services.AddAntiforgery(options => { options.HeaderName = "x-xsrf-token"; }); services.AddMvc(); 


Configurer

 app.UseAuthentication(); app.UseXsrfProtection(antiforgery); 

Configuration du SPA


api.axios.js
 import axios from "axios"; import cookie from 'react-cookies'; const api = axios.create({ baseURL: process.env.REACT_APP_API_URL }); api.interceptors.request.use(request => requestInterceptor(request)) const requestInterceptor = (request) => { request.headers['x-xsrf-token'] = cookie.load('.AspNetCore.Xsrf') return request; } export default api; 


Utiliser


Pour protéger nos méthodes API, vous devez ajouter l'attribut [AutoValidateAntiforgeryToken] pour le contrÎleur ou [ValidateAntiForgeryToken] pour la méthode.

Source: https://habr.com/ru/post/fr468401/


All Articles