Temido problema do CORS com WebAPI e token
Eu juro que isso aconteceu tantas vezes comigo que eu realmenteódio CORS. Acabei de dividir meu aplicativo em dois, para que um lide apenas com o lado da API e o outro lide com o lado do cliente. Eu já fiz isso antes, então sabia que precisava garantir que o CORS estivesse ativado e permitido tudo, então configurei isso emWebApiConfig.cs
public static void Register(HttpConfiguration config)
{
// Enable CORS
config.EnableCors(new EnableCorsAttribute("*", "*", "*"));
// Web API configuration and services
var formatters = config.Formatters;
var jsonFormatter = formatters.JsonFormatter;
var serializerSettings = jsonFormatter.SerializerSettings;
// Remove XML formatting
formatters.Remove(config.Formatters.XmlFormatter);
jsonFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/json"));
// Configure our JSON output
serializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
serializerSettings.Formatting = Formatting.Indented;
serializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore;
serializerSettings.PreserveReferencesHandling = Newtonsoft.Json.PreserveReferencesHandling.None;
// Configure the API route
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
Como você pode ver, minha primeira linha habilita o CORS, portanto deve funcionar. Se eu abrir meu aplicativo cliente e consultar a API, ele realmente funcionará (sem os EnableCors, recebo o erro CORS esperado. O problema é meu/símbolo ainda está recebendo um erro de CORS. Agora estou ciente de que o ponto de extremidade / token não faz parte da WebAPI, então criei meu próprio OAuthProvider (que devo salientar que está sendo usado em outros lugares muito bem) e que se parece com isso:
public class OAuthProvider<TUser> : OAuthAuthorizationServerProvider
where TUser : class, IUser
{
private readonly string publicClientId;
private readonly UserService<TUser> userService;
public OAuthProvider(string publicClientId, UserService<TUser> userService)
{
if (publicClientId == null)
throw new ArgumentNullException("publicClientId");
if (userService == null)
throw new ArgumentNullException("userService");
this.publicClientId = publicClientId;
this.userService = userService;
}
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { "*" });
var user = await this.userService.FindByUserNameAsync(context.UserName, context.Password);
if (user == null)
{
context.SetError("invalid_grant", "The user name or password is incorrect.");
return;
}
var oAuthIdentity = this.userService.CreateIdentity(user, context.Options.AuthenticationType);
var cookiesIdentity = this.userService.CreateIdentity(user, CookieAuthenticationDefaults.AuthenticationType);
var properties = CreateProperties(user.UserName);
var ticket = new AuthenticationTicket(oAuthIdentity, properties);
context.Validated(ticket);
context.Request.Context.Authentication.SignIn(cookiesIdentity);
}
public override Task TokenEndpoint(OAuthTokenEndpointContext context)
{
foreach (KeyValuePair<string, string> property in context.Properties.Dictionary)
context.AdditionalResponseParameters.Add(property.Key, property.Value);
return Task.FromResult<object>(null);
}
public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
// Resource owner password credentials does not provide a client ID.
if (context.ClientId == null)
{
context.Validated();
}
return Task.FromResult<object>(null);
}
public override Task ValidateClientRedirectUri(OAuthValidateClientRedirectUriContext context)
{
if (context.ClientId == this.publicClientId)
{
var redirectUri = new Uri(context.RedirectUri);
var expectedRootUri = new Uri(context.Request.Uri, redirectUri.PathAndQuery);
if (expectedRootUri.AbsoluteUri == redirectUri.AbsoluteUri)
context.Validated();
}
return Task.FromResult<object>(null);
}
public static AuthenticationProperties CreateProperties(string userName)
{
IDictionary<string, string> data = new Dictionary<string, string>
{
{ "userName", userName }
};
return new AuthenticationProperties(data);
}
}
Como você pode ver, noGrantResourceOwnerCredentials método, habilito o acesso do CORS a tudo novamente. Isso deve funcionar para todas as solicitações de / token, mas não. Quando tento fazer login no meu aplicativo cliente, recebo um erro CORS. O Chrome mostra isso:
XMLHttpRequest não pode carregarhttp: // localhost: 62605 / token. A resposta à solicitação de comprovação não passa na verificação do controle de acesso: Nenhum cabeçalho 'Access-Control-Allow-Origin' está presente no recurso solicitado. Origemhttp: // localhost: 50098'não é, portanto, permitido o acesso. A resposta tinha o código de status HTTP 400.
e o Firefox mostra isso:
Solicitação de origem cruzada bloqueada: a mesma política de origem não permite a leitura do recurso remoto emhttp: // localhost: 62605 / token. (Motivo: cabeçalho do CORS 'Access-Control-Allow-Origin' ausente). Solicitação de origem cruzada bloqueada: a mesma política de origem não permite a leitura do recurso remoto emhttp: // localhost: 62605 / token. (Motivo: solicitação do CORS falhou).
Para fins de teste, decidi usar o violinista para ver se havia alguma outra coisa que me desse uma pista do que está acontecendo. Quando tento fazer login, o FIddler mostra um código de resposta como 400 e, se eu olhar a resposta bruta, posso ver o erro:
{"error":"unsupported_grant_type"}
o que é estranho, porque os dados que estou enviando não foram alterados e estavam funcionando bem antes da divisão. Decidi usar o Composer no violinista e repliquei como seria a solicitação do POST. Quando o executo, ele funciona bem e recebo um código de resposta 200.
Alguém tem alguma idéia de por que isso pode estar acontecendo?
Atualização 1Apenas para referência, a solicitação do meu aplicativo cliente é assim:
OPTIONS http://localhost:62605/token HTTP/1.1
Host: localhost:62605
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache
Access-Control-Request-Method: POST
Origin: http://localhost:50098
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.71 Safari/537.36
Access-Control-Request-Headers: accept, authorization, content-type
Accept: */*
Referer: http://localhost:50098/account/signin
Accept-Encoding: gzip, deflate, sdch
Accept-Language: en-US,en;q=0.8
do compositor, fica assim:
POST http://localhost:62605/token HTTP/1.1
User-Agent: Fiddler
Content-Type: 'application/x-www-form-urlencoded'
Host: localhost:62605
Content-Length: 67
grant_type=password&userName=foo&password=bar