Maneira segura de trocar JWT no ASP.NET Core + SPA

A.

Entrada


A autenticação JWT (JSON Web Token) é um mecanismo de autenticação e autenticação bastante uniforme e consistente entre o servidor e os clientes. A vantagem do JWT é que ele permite gerenciar menos o estado e escalar bem. Não é de surpreender que a autorização e a autenticação com sua ajuda sejam cada vez mais usadas em aplicativos da web modernos.

Ao desenvolver aplicativos com o JWT, geralmente surge a pergunta: onde e como é recomendado armazenar o token? Se estamos desenvolvendo um aplicativo Web, temos duas das opções mais comuns:

  • Armazenamento na Web em HTML5 (localStorage ou sessionStorage)
  • Cookies

Comparando esses métodos, podemos dizer que ambos armazenam os valores no navegador do cliente, ambos são bastante fáceis de usar e representam um armazenamento regular de pares de valores-chave. A diferença está no ambiente de armazenamento.

O armazenamento na Web (localStorage / sessionStorage) pode ser acessado via JavaScript no mesmo domínio. Isso significa que qualquer código JavaScript no seu aplicativo tem acesso ao Armazenamento na Web e isso cria uma vulnerabilidade a ataques de script entre sites (XSS). Como um mecanismo de armazenamento, o Web Storage não oferece nenhuma maneira de proteger seus dados durante o armazenamento e o compartilhamento. Podemos usá-lo apenas para dados auxiliares que queremos manter ao atualizar (F5) ou ao fechar a guia: status e número da página, filtros, etc.

Os tokens também podem ser transmitidos através de cookies do navegador. Os cookies usados ​​com o sinalizador httpOnly não são afetados por XSS. httpOnly é um sinalizador para acesso à leitura, gravação e exclusão de cookies apenas no servidor. Eles não estarão acessíveis através de JavaScript no cliente, portanto, o cliente não saberá sobre o token e a autorização será totalmente processada no lado do servidor.

Também podemos definir um sinalizador seguro para garantir que os cookies sejam transmitidos apenas via HTTPS. Dadas essas vantagens, minha escolha recaiu sobre os cookies.

Este artigo descreve uma abordagem para implementar autenticação e autenticação usando cookies seguros httpOnly + JSON Web Token no ASP.NET Core Web Api em conjunto com o SPA. A opção é considerada na qual o servidor e o cliente estão em origem diferente.

Configurando seu ambiente de desenvolvimento local


Para configurar e depurar corretamente os relacionamentos cliente-servidor via HTTPS, recomendo fortemente que você configure imediatamente o ambiente de desenvolvimento local para que o cliente e o servidor tenham uma conexão HTTPS.

Se você não fizer isso imediatamente e tentar criar relacionamentos sem uma conexão HTTPS, no futuro surgirão muitos detalhes sem os quais cookies seguros e políticas seguras adicionais em produção com HTTPS não funcionarão corretamente.

Vou mostrar um exemplo de configuração de HTTPS no sistema operacional Windows 10, servidor - ASP.NET Core, SPA - React.

Você pode configurar o HTTPS no ASP.NET Core usando o sinalizador Configure for HTTPS ao criar um projeto ou, se não fizemos isso ao criar, ative a opção correspondente em Propriedades.

Para configurar o SPA, é necessário modificar o script para "iniciar" , configurando-o para "definir HTTPS = true" . Minha configuração é a seguinte:

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

Aconselho você a configurar o HTTPS para o ambiente de desenvolvimento em outros ambientes em create-react-app.dev/docs/using-https-in-development

Configurar o ASP.NET Core Server


Configuração JWT


Nesse caso, a implementação mais comum do JWT da documentação ou de qualquer artigo, com options.RequireHttpsMetadata = true; configurações adicionais.RequireHttpsMetadata options.RequireHttpsMetadata = true; como nosso ambiente de desenvolvimento usa 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 { //  .  }; }); 

Configurando uma Política CORS


Importante : a política do CORS deve conter AllowCredentials() . Isso é necessário para receber uma solicitação de XMLHttpRequest.withCredentials e enviar cookies de volta ao cliente. Mais sobre isso será escrito mais tarde. Outras opções são configuradas dependendo das necessidades do projeto.

Se o servidor e o cliente estiverem na mesma origem, toda a configuração abaixo não será necessária.

ConfigureServices

 services.AddCors(); 

Configurar

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

Definindo a política de cookies


Force a política de cookies a httpOnly e segura.

Se possível, defina MinimumSameSitePolicy = SameSiteMode.Strict; - Isso melhora a segurança do cookie para tipos de aplicativos que não dependem do processamento de solicitações de origem cruzada.

Configurar

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

A ideia de uma troca segura de tokens


Esta parte é um conceito. Nós vamos fazer duas coisas:

  1. Ative um token em uma solicitação HTTP usando httpOnly e sinalizadores seguros.
  2. Receba e valide tokens de aplicativos clientes de uma solicitação HTTP.

Para fazer isso, precisamos:

  • Escreva o token no cookie httpOnly ao efetuar login e exclua-o de lá ao fazer login.
  • Se houver um token nos cookies, substitua o token no cabeçalho HTTP de cada solicitação subsequente.
  • Se não houver token nos cookies, o cabeçalho ficará vazio e a solicitação não será autorizada.

Middleware


A idéia principal é implementar o Middleware Customizado para inserir um token em uma solicitação HTTP recebida. Após a autorização do usuário, salvamos o cookie em uma determinada chave, por exemplo: ".AspNetCore.Application.Id" . Eu recomendo definir um nome que não tenha nada a ver com autorização ou tokens - nesse caso, um cookie com um token parecerá algum tipo de constante não normal do sistema para o aplicativo AspNetCore. Portanto, há uma chance maior de que o invasor veja muitas variáveis ​​do sistema e, sem entender qual mecanismo de autorização é usado, irá além. Obviamente, se ele não ler este artigo e não procurar especificamente por essa constante.

Em seguida, precisamos inserir esse token em todas as solicitações HTTP recebidas subsequentes. Para fazer isso, escreveremos algumas linhas do código do Middleware. Isso não passa de um pipeline HTTP.



Configurar

 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(); 

Podemos levar essa lógica para um serviço Middleware separado, para não entupir o Startup.cs, a ideia não mudará.

Para escrever o valor nos cookies, basta adicionar a seguinte linha à lógica de autorização:

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

Usando nossas políticas de cookies, esses cookies serão enviados automaticamente como httpOnly e seguro. Não há necessidade de redefinir sua política nas opções de cookies.

Em CookieOptions, você pode definir o MaxAge para especificar uma vida útil. É útil especificar junto com o JWT Lifetime ao emitir o token, para que o cookie desapareça depois de um tempo. Outras propriedades de CookieOptions são configuradas dependendo dos requisitos do projeto.

Para maior segurança, recomendo adicionar os seguintes cabeçalhos ao 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"); 

  • O cabeçalho X-Content-Type-Options é usado para proteger contra vulnerabilidades de detecção de MIME. Essa vulnerabilidade pode ocorrer quando um site permite que os usuários baixem conteúdo, mas o usuário disfarça um determinado tipo de arquivo como outra coisa. Isso pode oferecer aos invasores a capacidade de executar scripts de script entre sites ou comprometer um site.
  • Todos os navegadores modernos possuem recursos de filtragem XSS integrados que tentam capturar vulnerabilidades XSS antes que a página seja totalmente exibida para nós. Por padrão, eles estão ativados no navegador, mas o usuário pode ser mais complicado e desativá-los. Usando o cabeçalho X-XSS-Protection , podemos dizer ao navegador para ignorar o que o usuário fez e aplicar o filtro interno.
  • O X-Frame-Options informa ao navegador que, se o site for colocado dentro de um quadro HTML, não será exibido nada. Isso é muito importante ao tentar se proteger de tentativas de clickjacking.

Eu descrevi longe de todas as manchetes. Existem muitas outras maneiras de obter maior segurança de aplicativos da web. Aconselho que você se concentre na lista de verificação de segurança do recurso securityheaders.com.

Configuração do cliente SPA


Quando o cliente e o servidor estão localizados em uma origem diferente, também é necessária uma configuração adicional no cliente. Você deve agrupar cada solicitação usando XMLHttpRequest.withCredentials .

Eu agrupei meus métodos da seguinte maneira:

 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; 

Podemos quebrar nossa configuração de solicitação de qualquer maneira, o principal é que, comCredentials = true, esteja lá .

A propriedade XMLHttpRequest.withCredentials determina se solicitações entre domínios devem ser geradas usando credenciais como cookies, cabeçalhos de autorização ou certificados TLS.

Esse sinalizador também é usado para determinar se os cookies enviados na resposta serão ignorados. XMLHttpRequest de outro domínio não pode definir um cookie em seu próprio domínio se o sinalizador withCredentials não estiver definido como true antes de criar esta solicitação.

Em outras palavras, se você não especificar esse atributo, nosso cookie não será salvo pelo navegador, ou seja, não poderemos enviar o cookie de volta ao servidor, e o servidor não encontrará o cookie desejado com a JWT e não assinará o token do portador em nosso pipeline HTTP.

Para que serve tudo isso?


Acima, descrevi uma maneira resistente de XSS de trocar tokens. Vamos examinar o resultado da funcionalidade implementada.

Se você acessa as Ferramentas do desenvolvedor, vemos os sinalizadores cobiçados httpOnly e secure :



Vamos fazer um teste de esmagamento, tentar tirar os cookies do cliente:



Estamos observando '', ou seja, os cookies não são acessíveis no espaço do documento, o que torna impossível lê-los com scripts.

Podemos tentar obter esses cookies com a ajuda de ferramentas ou extensões adicionais, mas todas as ferramentas que tentei chamaram a implementação nativa no espaço do documento.

Projeto de demonstração


As instruções de inicialização estão em README.MD


UPD: Proteção contra CSRF


Configurar o ASP.NET Core Server


Serviços de 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(); 


Configurar

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

Configuração do 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; 


Use


Para proteger nossos métodos de API, você deve adicionar o atributo [AutoValidateAntiforgeryToken] para o controlador ou [ValidateAntiForgeryToken] para o método.

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


All Articles