Venho vendo com uma certa frequência o problema de configuração de CORS em aplicações ASP.NET Core publicadas no IIS, por este motivo estou escrevendo este artigo.
Esse problema pode gerar sérios impactos, pois ele se torna um fator bloqueante de um projeto que está prestes a ir para ambiente de produção.
O objetivo aqui é explicar com detalhes os fundamentos das políticas de CORS e o porquê este problema pode acontecer, ensinar como identificar a sua causa raiz, e como resolvê-lo.
Política de Same-Origin (Mesma-Origem)
A maioria dos navegadores aplicam uma política conhecida como same-origin, cujo objetivo é bloquear as requisições HTTP que não são da mesma origem.
Por exemplo, imagine que um hacker desenvolveu um script malicioso e o publicou no domínio http://foo.com, e esse script irá fazer requisições ao servidor http://bar.com, por padrão o navegador irá bloquear essas requisições de origem cruzada.
Em alguns momentos essa restrição não é interessante. Em um cenário onde você desenvolveu uma aplicação para o front-end e outra para o back-end e publicou essas aplicações em domínios diferentes, nós precisamos permitir que o browser realize esse tipo de requisição.
Para este tipo de ocasião nós precisamos configurar o CORS.
CORS
Cross Origin Resource Sharing (CORS) é um padrão que permite um User-Agent acessar recursos de outra origem através de um mecanismo que usa a negociação de alguns cabeçalhos específicos entre as requisições.
Esses cabeçalhos vão aparecer tanto nas requisições definidas pelo cliente, quanto nas respostas definidas pelo servidor.
Cabeçalhos implementados pelo cliente(navegador):
- Origin: O navegador se responsabiliza em preencher com o domínio em que a requisição se originou.
- Access-Control-Request-Method: Contém o método HTTP da chamada.
- Access-Control-Request-Headers: Contém os cabeçalhos adicionais que não fazem parte da fetch specification.
Cabeçalhos implementados pelo servidor (ASP.NET Core):
- Access-Control-Allow-Origin.
- Access-Control-Expose-Headers.
- Access-Control-Max-Age.
- Access-Control-Allow-Credentials.
- Access-Control-Allow-Methods.
- Access-Control-Allow-Headers.
A especificação do CORS classifica as requisições em dois tipos:
- "Simple".
- "Preflighted".
Requisições do tipo "Simple"
Para uma requisição ser classificada como simple, ela precisa atender a esses critérios:
- O método HTTP precisa ser: HEAD, GET ou POST.
- A requisição pode conter apenas os cabeçalhos definidos pela fetch specification.
- O cabeçalho Content-Type só pode conter os valores: application/x-www-form-urlencoded, multipart/form-data, ou text/plain.
Um exemplo de uma requisição que atende aos critérios seria:
GET http://backend.com/resource HTTP/1.1
Host: backend.com
Connection: keep-alive
Origin: http://frontend.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36
Accept: */*
Referer: http://frontend.com/
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Neste exemplo uma resposta valida seria:
HTTP/1.1 200 OK
Content-Type: application/json
Last-Modified: Thu, 01 Feb 2018 21:51:05 GMT
Accept-Ranges: bytes
Access-Control-Allow-Origin: http://frontend.com
Date: Thu, 01 Feb 2018 22:19:13 GMT
Content-Length: 38
Essa seria uma negociação válida, pois o valor do cabeçalho Origin da requisição é igual ao valor do cabeçalho Access-Control-Allow-Origin, neste caso http://frontend.com. Caso o cabeçalho Access-Control-Allow-Origin não estivesse presente na resposta HTTP, ou se o valor não fosse igual ao do Origin, a requisição falharia por regra de CORS, e provavelmente você poderia observar uma mensagem de erro no console do browser como:
Access to XMLHttpRequest at 'http://backend.com/resource' from origin 'http://frontend.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
Requisições do tipo "Preflighted"
Qualquer requisição cross-origin que não atenda os critérios abordados anteriormente, será classificada como preflighted.
Imagine que você está tentando enviar uma requisição HTTP GET com um cabeçalho que não está definido na fetch specification para o servidor por exemplo ADDITIONAL-HEADER:
GET http://backend.com/resource HTTP/1.1
Host: backend.com
Connection: keep-alive
Origin: http://frontend.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36
ADDITIONAL-HEADER: addtional-header-value
Accept: */*
Referer: http://frontend.com
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Neste cenário antes do navegador enviar a requisição original ele a interceptará e enviará uma requisição preflight com o verbo HTTP OPTIONS:
OPTIONS http://backend.com/resource HTTP/1.1
Host: bar.com
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache
Access-Control-Request-Method: GET
Origin: http://frontend.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36
Access-Control-Request-Headers: additional-header
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Resposta HTTP:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: http://frontend.com
Access-Control-Allow-Headers: additional-header
Com base na resposta da requisição preflight o browser tomará a decisão de, enviar a requisição original HTTP GET ou interromper o fluxo e gerar um erro de CORS.
Ele tomará essa decisão com base nos cabeçalhos da requisição e da resposta, neste exemplo seriam:
Cabeçalhos da requisição preflight:
- Access-Control-Request-Method: GET.
- Access-Control-Request-Headers: additional-header.
- Origin: http://frontend.com.
Cabeçalhos da resposta:
- Access-Control-Allow-Origin: http://frontend.com.
- Access-Control-Allow-Headers: additional-header.
No nosso exemplo supomos que o servidor está devidamente configurado, portanto não haveria interrupções.
CORS no ASP.NET Core
No ASP.NET Core podemos apenas configurar o middleware do CORS, e ele se encarregará da implementação dos cabeçalhos nas respostas HTTP, para que o navegador possa inferir as políticas do CORS.
A forma mais comum de realizar essa configuração segue um padrão parecido com:
var builder = WebApplication.CreateBuilder(args);
...
builder.Services.AddCors((options) => options.AddDefaultPolicy(policy => {
policy.AllowAnyHeader().AllowAnyOrigin().AllowAnyMethod();
}));
...
app.UseCors();
...
app.Run();
O ideal é especificar as origens e os cabeçalhos que nossa aplicação receberá evitando utilização dos métodos AllowAnyOrigin, AllowAnyHeader e AllowAnyMethod. Recomendo a leitura da documentação oficial para mais detalhes: Habilitar solicitações entre origens (CORS) em ASP.NET Core.
Essa abordagem funciona quando rodamos a aplicação localmente ou quando a publicamos em um container. Porém no IIS a aplicação ela pode apresentar um comportamento diferente do esperado, resultando em erro de CORS na maioria das requisições, entenderemos esse comportamento com mais detalhes na próxima seção.
CORS no IIS
Para uma melhor compreensão dos conceitos usarei uma abordagem prática demonstrando o este comportamento em uma aplicação publicada no IIS com o middleware do CORS configurado.
Confira mais detalhes sobre como publicar uma aplicação ASP.NET Core no IIS em: Publicar um aplicativo ASP.NET Core no IIS.
Dentro de uma Máquina Virtual Windows no Azure publiquei uma Web Api em ASP.NET Core no IIS geara através do comando:
dotnet new webapi --name corsteste
Essa api pode ser acessada através da url https://localhost/corsteste. Ela irá conter apenas um endpoint weatherforescast (gerado por padrão no template do comando anterior), e a última modificação que fiz no projeto é a configuração do Middleware de CORS que mostrei no último parágrafo.
Ainda na mesma VM publiquei na url https://localhost:3000 uma aplicação React SPA criada através do comando:
npx create-react-app corstfront
Confira mais detalhes sobre como publicar uma aplicação SPA no IIS: Build a Static Website on IIS.
No arquivo App.Js adicionei o código:
useEffect(() => {
// Essa requisição será classificada como "Sample"
const client1 = axios.create({
baseURL: "https://localhost/corsteste"
});
client1.get("/weatherforecast").then(x => console.log(x));
// Essa requisição será classificada como "Preflighted".
const client2 = axios.create({
baseURL: "https://localhost/corsteste",
headers: {"Custom-Header": "custom"}
});
client2.get("/weatherforecast").then(x => console.log(x));
},[])
Agora vou abrir essa SPA no browser e inspecionar as requisições:
A primeira requisição classificada como simple funcionou normalmente.
Já na requisição preflight que o browser fez automaticamente o status code da resposta foi 404.
Mas por quê? No IIS podem existir alguns motivos para isso acontecer, para descobrir qual é a razão da requisição preflight retornar 404 podemos habilitar o mecanismo de Failed Request Tracing do IIS.
Siga o passo a passo do GIF abaixo para habilitar essa funcionalidade, depois reproduza o erro, e então abra o arquivo de log gerado pelo IIS:
Abra o arquivo depois de reproduzir o problema, este arquivo é salvo no formato XML e ele registra todos os eventos dos módulos da sua requisição, então certamente aqui conseguiremos entrar mais informações sobre o erro, um bom ponto de partida é analisar a primeira tag deste arquivo:
<failedRequest url="https://localhost:443/corsteste/weatherforecast"
siteId="1"
appPoolId="DefaultAppPool"
processId="9620"
verb="OPTIONS"
authenticationType="NOT_AVAILABLE" activityId="{4000007D-0000-FD00-B63F-84710C7967BB}"
failureReason="STATUS_CODE"
statusCode="404.6"
triggerStatusCode="404.6"
timeTaken="16"
xmlns:freb="http://schemas.microsoft.com/win/2006/06/iis/freb"
>
A única informação relevante que encontramos aqui é o statuscode 404.6.
Na documentação oficial do IIS esse status significa: 404.6 - Verbo negado.
Ainda com o arquivo aberto tente procurar mais aparições desse statuscode e você será capaz de entender claramente o porquê esse verbo foi negado, no meu caso:
<h3>HTTP Error 404.6 - Not Found</h3>
<h4>The request filtering module is configured to deny the HTTP verb.</h4>
A mensagem que aparece no corpo da resposta é a "The request filtering module is configured to deny the HTTP verb" que significa que o motivo deste erro é por conta da configuração do módulo de Request Filtering do IIS.
Ou seja, essa requisição HTTP OPTIONS se quer chegou na aplicação em si, nesse caso ela foi negada antes.
Outros módulos podem negar as suas requisições, o ideal é utilizar a técnica demonstrada aqui para entender o comportamento dos módulos do IIS.
A resolução para este cenário específico seria seguir o passo a passo do GIF abaixo:
CORS não funciona com o Windows Authentication
Se sua aplicação estiver usando o módulos de Autenticação do Windows no IIS a configuração do middleware de CORS no ASP.NET Core não irá funcionar, pois o mecanismo do CORS precisa ser executado antes do módulo de Autenticação do Windows, caso contrário o IIS irá negar as requisições, mesmo que ela for uma requisição preflight.
Entenda o pipeline de execução de módulos do IIS em: IIS Modules Overview.
Para resolver este caso existe um módulo de CORS que pode ser configurado diretamente no servidor IIS, assim sua aplicação sequer precisará configurar o Middleware de CORS.
Este módulo se chama IIS CORS Module e pode ser baixado em: [https://www.iis.net/downloads/microsoft/iis-cors-module].
Configurar o módulo IIS CORS
Com o módulo instalado, você pode configura-lo através do arquivo Web.config da sua aplicação, adicionando a TAG na seção , por exemplo:
<configuration>
<system.webServer>
<cors enabled="true" failUnlistedOrigins="true">
<add origin="https://*.microsoft.com"
allowCredentials="true"
maxAge="120">
<allowHeaders allowAllRequestedHeaders="true">
<add header="header1" />
<add header="header2" />
</allowHeaders>
<allowMethods>
<add method="DELETE" />
</allowMethods>
</add>
</cors>
</system.webServer>
</configuration>
Você também poderia o fazê-lo através de linha de comando, e configurar o módulo da sua aplicação diretamente no Web.config Global para evitar que a configuração tenha que ser feita novamente em um novo Deploy, para fazer a mesma configuração anterior, abra um windows terminal como administrador, depois rode os comandos:
cd \Windows\system32\inetsrv
appcmd set config "Default Web Site/CorsTeste" -section:system.webServer/cors /enabled:"True" /commit:"MACHINE/WEBROOT/APPHOST/Default Web Site"
appcmd set config "Default Web Site/CorsTeste" -section:system.webServer/cors /+"[origin='https://*.microsoft.com',allowCredentials='True',maxAge='120']" /commit:"MACHINE/WEBROOT/APPHOST/Default Web Site"
appcmd set config "Default Web Site/CorsTeste" -section:system.webServer/cors /[origin='https://*.microsoft.com',allowCredentials='True',maxAge='120'].allowHeaders.allowAllRequestedHeaders:"True" /commit:"MACHINE/WEBROOT/APPHOST/Default Web Site"
appcmd set config "Default Web Site/CorsTeste" -section:system.webServer/cors /+"[origin='https://*.microsoft.com',allowCredentials='True',maxAge='120'].allowHeaders.[header='header1']" /commit:"MACHINE/WEBROOT/APPHOST/Default Web Site"
appcmd set config "Default Web Site/CorsTeste" -section:system.webServer/cors /+"[origin='https://*.microsoft.com',allowCredentials='True',maxAge='120'].allowHeaders.[header='header2']" /commit:"MACHINE/WEBROOT/APPHOST/Default Web Site"
appcmd set config "Default Web Site/CorsTeste" -section:system.webServer/cors /+"[origin='https://*.microsoft.com',allowCredentials='True',maxAge='120'].allowMethods.[method='DELETE']" /commit:"MACHINE/WEBROOT/APPHOST/Default Web Site"
No fim através destes comandos estamos literalmente configurando o arquivo Web.config, praticamente da mesma forma que demonstrado anteriormente, a grande diferença está no último parâmetro de cada comando MACHINE/WEBROOT/APPHOST/Default Web Site, este parâmetro faz com que as configuração sejam aplicadas ao Web.config Global, o que evita a necessidade de configurar novamente a Web.config depois de um Deploy, isso acaba evitando erros operacionais que os desenvolvedores costumer cometer.
Conclusão
Ter visibilidade de como as coisas funcionam no seu fundamento sempre irá fazer com que sua visão para resolver os problemas seja mais pragmática e eficiente, meu objetivo com este artigo não foi passar uma receita de bolo para resolver o erro de CORS, mas sim, passar todos os fundamentos para que você entenda o que está acontecendo, e conheça as ferramentas para conseguir guiá-lo no processo de resolução de problemas.