asp.net-mvc-4 - example - asp.net core mvc
Problemas al implementar el atributo ValidatingAntiForgeryToken para la API web con MVC 4 RC (5)
Estoy haciendo solicitudes AJAX basadas en JSON y, con los controladores MVC, he estado muy agradecido a Phil Haack por su Preventing CSRF con AJAX y, el Anti-XSRF actualizado de Johan Driessen para MVC 4 RC . Pero, al hacer la transición de los controladores centrados en la API a la API web, estoy teniendo problemas donde la funcionalidad entre los dos enfoques es notablemente diferente y no puedo hacer la transición del código CSRF.
ScottS formuló una question similar recientemente que fue answered por Darin Dimitrov. La solución de Darin consiste en implementar un filtro de autorización que llame a AntiForgery.Validate. Desafortunadamente, este código no funciona para mí (ver el siguiente párrafo) y, honestamente, es demasiado avanzado para mí.
Como lo entiendo, la solución de Phil resuelve el problema con MVC AntiForgery al realizar solicitudes JSON en ausencia de un elemento de formulario; el elemento de formulario es asumido / esperado por el método AntiForgery.Validate. Creo que esta puede ser la razón por la que también tengo problemas con la solución de Darin. Recibo una HttpAntiForgeryException "El campo de formulario de anti-falsificación requerido ''__RequestVerificationToken'' no está presente". Estoy seguro de que el token está siendo POSTADO (aunque en el encabezado de la solución de Phil Haack). Aquí hay una instantánea de la llamada del cliente:
$token = $(''input[name=""__RequestVerificationToken""]'').val();
$.ajax({
url:/api/states",
type: "POST",
dataType: "json",
contentType: "application/json: charset=utf-8",
headers: { __RequestVerificationToken: $token }
}).done(function (json) {
...
});
Intenté un hackeo combinando la solución de Johan con Darin''s y pude hacer que las cosas funcionen, pero estoy introduciendo HttpContext.Current, no estoy seguro de si esto es apropiado / seguro y por qué no puedo usar el HttpActionContext proporcionado.
Aquí está mi mash-up poco elegante ... el cambio son las 2 líneas en el bloque try:
public Task<HttpResponseMessage> ExecuteAuthorizationFilterAsync(HttpActionContext actionContext, CancellationToken cancellationToken, Func<Task<HttpResponseMessage>> continuation)
{
try
{
var cookie = HttpContext.Current.Request.Cookies[AntiForgeryConfig.CookieName];
AntiForgery.Validate(cookie != null ? cookie.Value : null, HttpContext.Current.Request.Headers["__RequestVerificationToken"]);
}
catch
{
actionContext.Response = new HttpResponseMessage
{
StatusCode = HttpStatusCode.Forbidden,
RequestMessage = actionContext.ControllerContext.Request
};
return FromResult(actionContext.Response);
}
return continuation();
}
Mis preguntas son:
- ¿Tengo razón al pensar que la solución de Darin asume la existencia de un elemento de forma?
- ¿Cuál es una forma elegante de combinar el filtro de API web de Darin con el código MVC 4 RC de Johan?
¡Gracias por adelantado!
Método de extensión utilizando la respuesta de Darin, con una verificación de la presencia del encabezado. La verificación significa que el mensaje de error resultante es más indicativo de lo que está mal ("El campo de formulario de antifalsificación requerido" __RequestVerificationToken "no está presente") frente a "No se encontró el encabezado dado".
public static bool IsHeaderAntiForgeryTokenValid(this HttpRequestMessage request)
{
try
{
HttpRequestHeaders headers = request.Headers;
CookieState cookie = headers
.GetCookies()
.Select(c => c[AntiForgeryConfig.CookieName])
.FirstOrDefault();
var rvt = string.Empty;
if (headers.Any(x => x.Key == AntiForgeryConfig.CookieName))
rvt = headers.GetValues(AntiForgeryConfig.CookieName).FirstOrDefault();
AntiForgery.Validate(cookie != null ? cookie.Value : null, rvt);
}
catch (Exception ex)
{
LogHelper.LogError(ex);
return false;
}
return true;
}
Uso de ApiController:
public IHttpActionResult Get()
{
if (Request.IsHeaderAntiForgeryTokenValid())
return Ok();
else
return BadRequest();
}
Podrías intentar leer desde los encabezados:
var headers = actionContext.Request.Headers;
var cookie = headers
.GetCookies()
.Select(c => c[AntiForgeryConfig.CookieName])
.FirstOrDefault();
var rvt = headers.GetValues("__RequestVerificationToken").FirstOrDefault();
AntiForgery.Validate(cookie != null ? cookie.Value : null, rvt);
Nota: GetCookies
es un método de extensión que existe en la clase HttpRequestHeadersExtensions
que forma parte de System.Net.Http.Formatting.dll
. Lo más probable es que exista en C:/Program Files (x86)/Microsoft ASP.NET/ASP.NET MVC 4/Assemblies/System.Net.Http.Formatting.dll
Si ayuda a alguien, en .net core, el valor predeterminado del encabezado es en realidad "RequestVerificationToken", sin el "__". Así que si cambias la clave del encabezado a eso, funcionará.
También puede anular el nombre del encabezado si lo desea:
services.AddAntiforgery(o => o.HeaderName = "__RequestVerificationToken")
Solo quería agregar que este enfoque también funcionó para mí (.ajax que publica JSON en un punto final de la API web), aunque lo simplifiqué un poco al heredarlo de ActionFilterAttribute y sobreescribiendo el método OnActionExecuting.
public class ValidateJsonAntiForgeryTokenAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
try
{
var cookieName = AntiForgeryConfig.CookieName;
var headers = actionContext.Request.Headers;
var cookie = headers
.GetCookies()
.Select(c => c[AntiForgeryConfig.CookieName])
.FirstOrDefault();
var rvt = headers.GetValues("__RequestVerificationToken").FirstOrDefault();
AntiForgery.Validate(cookie != null ? cookie.Value : null, rvt);
}
catch
{
actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.Forbidden, "Unauthorized request.");
}
}
}
Una implementación usando AuthorizeAttribute:
using System;
using System.Linq;
using System.Net.Http;
using System.Web;
using System.Web.Helpers;
using System.Web.Http;
using System.Web.Http.Controllers;
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class ApiValidateAntiForgeryToken : AuthorizeAttribute {
public const string HeaderName = "X-RequestVerificationToken";
private static string CookieName => AntiForgeryConfig.CookieName;
public static string GenerateAntiForgeryTokenForHeader(HttpContext httpContext) {
if (httpContext == null) {
throw new ArgumentNullException(nameof(httpContext));
}
// check that if the cookie is set to require ssl then we must be using it
if (AntiForgeryConfig.RequireSsl && !httpContext.Request.IsSecureConnection) {
throw new InvalidOperationException("Cannot generate an Anti Forgery Token for a non secure context");
}
// try to find the old cookie token
string oldCookieToken = null;
try {
var token = httpContext.Request.Cookies[CookieName];
if (!string.IsNullOrEmpty(token?.Value)) {
oldCookieToken = token.Value;
}
}
catch {
// do nothing
}
string cookieToken, formToken;
AntiForgery.GetTokens(oldCookieToken, out cookieToken, out formToken);
// set the cookie on the response if we got a new one
if (cookieToken != null) {
var cookie = new HttpCookie(CookieName, cookieToken) {
HttpOnly = true,
};
// note: don''t set it directly since the default value is automatically populated from the <httpCookies> config element
if (AntiForgeryConfig.RequireSsl) {
cookie.Secure = AntiForgeryConfig.RequireSsl;
}
httpContext.Response.Cookies.Set(cookie);
}
return formToken;
}
protected override bool IsAuthorized(HttpActionContext actionContext) {
if (HttpContext.Current == null) {
// we need a context to be able to use AntiForgery
return false;
}
var headers = actionContext.Request.Headers;
var cookies = headers.GetCookies();
// check that if the cookie is set to require ssl then we must honor it
if (AntiForgeryConfig.RequireSsl && !HttpContext.Current.Request.IsSecureConnection) {
return false;
}
try {
string cookieToken = cookies.Select(c => c[CookieName]).FirstOrDefault()?.Value?.Trim(); // this throws if the cookie does not exist
string formToken = headers.GetValues(HeaderName).FirstOrDefault()?.Trim();
if (string.IsNullOrEmpty(cookieToken) || string.IsNullOrEmpty(formToken)) {
return false;
}
AntiForgery.Validate(cookieToken, formToken);
return base.IsAuthorized(actionContext);
}
catch {
return false;
}
}
}
Luego simplemente decora tu controlador o métodos con [ApiValidateAntiForgeryToken]
Y agrega al archivo de afeitar esto para generar tu token para javascript:
<script>
var antiForgeryToken = ''@ApiValidateAntiForgeryToken.GenerateAntiForgeryTokenForHeader(HttpContext.Current)'';
// your code here that uses such token, basically setting it as a ''X-RequestVerificationToken'' header for any AJAX calls
</script>