MVC 4 application does not go to default page after login - asp.net-mvc-4

I have created an MVC 4 application which targets .NET 4.0. After deploying to my production server, it will show the login page but will not redirect to the default page. However, when I add debugging, I can see that the authentication process works but then the error I am getting is an error that says it can't find my View for my Error Page and then shows my Error Page. It just seems that it will not go to my "Home/Index" page - even when I remove the authorize attribute. Of course the application works in development. Additionally, it will not go to my register page or forgot login page.
My Login Controller looks like this:
[HttpPost]
[AllowAnonymous]
[ValidateAntiforgeryToken]
public ActionResult Login(LoginViewModel model, string returnUrl)
{
if(ModelStat.IsValid && _userService.Login(model.UserId, model.Password))
{
var user = _userService.GetUser(model.UserId);
var loggedInUser = new LoggedInUser
{
// Build the user for custom IPrincipal
};
var userData = JsonConvert.SerializeObject(loggedInUser);
var compressData = StringCompression.Compress(userData);
var authTicket = new FormsAuthenticationTicket(
1,
user.UserId,
DateTime.Now,
DateTime.Now.AddHours(1),
false,
compressData);
var encTicket = FormsAuthentication.Encrypt(authTicket);
if(encTicket != null)
{
var faCookie = new HttpCookie(FormsAuthentication.FormsCookieName, encTicket)
{
HttpOnly = true
};
Response.Cookies.Add(faCookie);
}
user.LastActivityDate = DateTime.Now;
user.LastLoginDate = DateTime.Now;
_userService.UpdateUser(user);
_uow.Commit();
return Url.IsLocalUrl(returnUrl) ? (ActionResult)Redirect(returnUrl) : RedirectToAction("Index", "Home");
}
return View(model);
and in my Global.asax:
protected void Application_PostAuthenticateRequest(object sender, EventArgs e)
{
var authCookie = Request.Cookies[FormsAuthentication.FormsCookieName];
if(authCookie != null)
{
var decompressedData = StringCompression.Decompress(authTicket.UserData);
var loggedInUser = JsonConvert.DesrializeObject<LoggedInUser>(decompressedData);
var currrentUser = new CustomPrincipal(authTicket.Name)
{
// Build the CustomPrincipal from the loggedInUser
};
if(HttpContext.Current.User.Identity.IsAuthenticated)
{
HttpContext.Current.User = currentUser;
}
}
}
I hope that this is enough to give someone an idea of what I may be doing wrong. Somehow I feel that it is something small that I am missing. Thanks in advance.
~InDireStraits
Update:
After more troubleshooting, it would seem that the issue may have something to do with the fact that I am using a BaseController for specifying permissions but I am still baffled as to why the application works as intended in my development environment but not in production. To verify my IIS settings I installed the default MVC4 App to production which does not have .NET 4.5, and it runs. I am using VS 2012 so I do have 4.5. Could I somehow be introducing .NET 4.5 classes or functionality even if this targets .NET 4.0? At any rate, here is my BaseController code:
public class BaseController: Controller
{
private string _actionKey;
private const string PermisisionList = "permissionList";
private Dictionary<string, string> _requiredActionPermissions;
private static readonly IControllerActionService<ControllerAction> _actionService;
protected new CustomPrincipal User
{
get
{
return HttpContext.User as CustomPrincipal;
}
}
public BaseController(IControllerActionService<ControllerAction> actionService)
{
_actionService = actionService;
}
protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
// Check to see if the PermissionList is loaded and load if necessary
if(!CacheLayer.Exists(PermissionList))
{
_requiredActionPermissions = _actionService.GetControllerActionDictionary();
CacheLayer.Add(_requiredActionPermissions, PermissionList);
}
else
{
_requiredActionPermission = CacheLayer.Get<Dictionary<string, string>>(PermissionList);
}
// Get the Controller/Action of the current request
_actionKey = string.Format("{0}-{1}", filterContext.ActionDescriptor.ControllerDescriptor.ControllerName, filterContext.ActionDescriptor.ActionName);
// If the user is authenticated, grab the permissions
if(filterContext.HttpContext.User.Identity.IsAuthenticated)
{
var userPermissions = User.Permissions;
if(!_requiredActionPermissions.Values.Any(a=>a.Equals(_actionKey, StringComparison.OrdinalIgnoreCase)))
{
return;
}
if(userPermissions.Contains(_requiredActionsPermissions.FirstOrDefault(x=>x.Value == _actionKey).Key))
{
return;
}
filterContext.Result = new RedirectResult("~/Error/ErrorUnauthorized");
return;
}
if(!filterContext.HttpContext.User.Identity.IsAuthenticated)
{
if(!_requiredActionPermissions.Values.Any(a=>a.Equals(_actionKey, StringComparison.OrdinalIgnoreCase)))
{
return;
}
}
if(filterContext.HttpContext.Request.Url == null)
{
return;
}
if(filterContext.HttpContext.Request.Url.AbsolutePath == FormsAuthentication.LoginUrl)
{
return;
}
var redirectUrl = string.Format("?returnUrl={0}", filterContext.HttpContext.Request.Url.PathAndQuery);
filterContext.HttpContext.Response.Redirect(FormsAuthentication.LoginUrl + redirectUrl, true);
}
UPDATE 2: Installed .NET 4.52 on Staging Server and the application now works as intended. The problem is that I will not be able to install this on the production server. I don't understand what it is that 4.5 is fixing that 4.0 does not seem to facilitate. HELLLLLLPPP!!!!!!

The answer can be found here. To summarize, I added an extra parameter to my route config that worked in 4.5 but not in 4.0. Will follow up on the linked question. Thanks

Related

Setup HttpContext.GetOpenIddictServerRequest() for Controller Unit Test

I am using MS Tests to write Unit tests for controller that authorizes the user using OAuth. I understand it is not a great idea to Moq HttpContext. Can I get help with setting up GetOpenIddictServerRequest().
The Controller End point is
public async Task<IActionResult> Authorize()
{
var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
// If prompt=login was specified by the client application,
// immediately return the user agent to the login page.
if (request.HasPrompt(Prompts.Login))
{
// To avoid endless login -> authorization redirects, the prompt=login flag
// is removed from the authorization request payload before redirecting the user.
var prompt = string.Join(" ", request.GetPrompts().Remove(Prompts.Login));
var parameters = Request.HasFormContentType ?
Request.Form.Where(parameter => parameter.Key != Parameters.Prompt).ToList() :
Request.Query.Where(parameter => parameter.Key != Parameters.Prompt).ToList();
parameters.Add(KeyValuePair.Create(Parameters.Prompt, new StringValues(prompt)));
return Challenge(
authenticationSchemes: IdentityConstants.ApplicationScheme,
properties: new AuthenticationProperties
{
RedirectUri = Request.PathBase + Request.Path + QueryString.Create(parameters)
});
}
```
The code Snipped looks like
public static OpenIddictRequest? GetOpenIddictServerRequest(this HttpContext context)
{
if (context == null)
{
throw new ArgumentNullException("context");
}
return context.Features.Get<OpenIddictServerAspNetCoreFeature>()?.Transaction?.Request;
}
I tried setting up HttpContext in my TestMethod as:
_authorizationController.ControllerContext = new ControllerContext() { HttpContext = new DefaultHttpContext() { } };

OnAuthenticationCompleted in ASP.NET 5 RC

I have an ASP.NET 5 beta 8 application which integrates with Azure Active Directory using OpenIdConnect. I tried updating the application to RC1 and changed the openid nuget package to "Microsoft.AspNet.Authentication.OpenIdConnect": "1.0.0-rc1-final". Everything seems to be correct, but the OnAuthenticationComplete method which I used to add roles to my ClaimsIdentity is no longer in the Events object, and I can't find the alternative. How can I add claims to my Identity with the new version?
UPDATE: Changing to OnAuthenticationValidated still not works for me. I must be doing something wrong in my event's code:
OnAuthenticationValidated = async notification =>
{
var claimsIdentity = notification.AuthenticationTicket.Principal.Identity as ClaimsIdentity;
var userRepository=new UserRepository(Configuration);
var userId = claimsIdentity?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var user = await userRepository.FindAsync(userId);
if (user == null)
{
await userRepository.AddAsync(new UserDto
{
UserId = userId,
Username = claimsIdentity?.FindFirst(ClaimTypes.Name)?.Value,
DisplayName = claimsIdentity?.FindFirst(ClaimTypes.Name)?.Value
});
}
claimsIdentity?.AddClaim(new Claim(ClaimTypes.Role, "super"));
}
Also, the code I use to do the login is:
[HttpGet]
public IActionResult Login()
{
if (HttpContext.User == null || !HttpContext.User.Identity.IsAuthenticated)
return new ChallengeResult(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties { RedirectUri = "/" });
return RedirectToAction("Index", "Home");
}
The AuthenticationCompleted event has been renamed to AuthenticationValidated.
You can find more information on this ticket: https://github.com/aspnet/Security/pull/442.

Basic Authentication Middleware with OWIN and ASP.NET WEB API

I created an ASP.NET WEB API 2.2 project. I used the Windows Identity Foundation based template for individual accounts available in visual studio see it here.
The web client (written in angularJS) uses OAUTH implementation with web browser cookies to store the token and the refresh token. We benefit from the helpful UserManager and RoleManager classes for managing users and their roles.
Everything works fine with OAUTH and the web browser client.
However, for some retro-compatibility concerns with desktop based clients I also need to support Basic authentication. Ideally, I would like the [Authorize], [Authorize(Role = "administrators")] etc. attributes to work with both OAUTH and Basic authentication scheme.
Thus, following the code from LeastPrivilege I created an OWIN BasicAuthenticationMiddleware that inherits from AuthenticationMiddleware.
I came to the following implementation. For the BasicAuthenticationMiddleWare only the Handler has changed compared to the Leastprivilege's code. Actually we use ClaimsIdentity rather than a series of Claim.
class BasicAuthenticationHandler: AuthenticationHandler<BasicAuthenticationOptions>
{
private readonly string _challenge;
public BasicAuthenticationHandler(BasicAuthenticationOptions options)
{
_challenge = "Basic realm=" + options.Realm;
}
protected override async Task<AuthenticationTicket> AuthenticateCoreAsync()
{
var authzValue = Request.Headers.Get("Authorization");
if (string.IsNullOrEmpty(authzValue) || !authzValue.StartsWith("Basic ", StringComparison.OrdinalIgnoreCase))
{
return null;
}
var token = authzValue.Substring("Basic ".Length).Trim();
var claimsIdentity = await TryGetPrincipalFromBasicCredentials(token, Options.CredentialValidationFunction);
if (claimsIdentity == null)
{
return null;
}
else
{
Request.User = new ClaimsPrincipal(claimsIdentity);
return new AuthenticationTicket(claimsIdentity, new AuthenticationProperties());
}
}
protected override Task ApplyResponseChallengeAsync()
{
if (Response.StatusCode == 401)
{
var challenge = Helper.LookupChallenge(Options.AuthenticationType, Options.AuthenticationMode);
if (challenge != null)
{
Response.Headers.AppendValues("WWW-Authenticate", _challenge);
}
}
return Task.FromResult<object>(null);
}
async Task<ClaimsIdentity> TryGetPrincipalFromBasicCredentials(string credentials,
BasicAuthenticationMiddleware.CredentialValidationFunction validate)
{
string pair;
try
{
pair = Encoding.UTF8.GetString(
Convert.FromBase64String(credentials));
}
catch (FormatException)
{
return null;
}
catch (ArgumentException)
{
return null;
}
var ix = pair.IndexOf(':');
if (ix == -1)
{
return null;
}
var username = pair.Substring(0, ix);
var pw = pair.Substring(ix + 1);
return await validate(username, pw);
}
Then in Startup.Auth I declare the following delegate for validating authentication (simply checks if the user exists and if the password is right and generates the associated ClaimsIdentity)
public void ConfigureAuth(IAppBuilder app)
{
app.CreatePerOwinContext(DbContext.Create);
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
Func<string, string, Task<ClaimsIdentity>> validationCallback = (string userName, string password) =>
{
using (DbContext dbContext = new DbContext())
using(UserStore<ApplicationUser> userStore = new UserStore<ApplicationUser>(dbContext))
using(ApplicationUserManager userManager = new ApplicationUserManager(userStore))
{
var user = userManager.FindByName(userName);
if (user == null)
{
return null;
}
bool ok = userManager.CheckPassword(user, password);
if (!ok)
{
return null;
}
ClaimsIdentity claimsIdentity = userManager.CreateIdentity(user, DefaultAuthenticationTypes.ApplicationCookie);
return Task.FromResult(claimsIdentity);
}
};
var basicAuthOptions = new BasicAuthenticationOptions("KMailWebManager", new BasicAuthenticationMiddleware.CredentialValidationFunction(validationCallback));
app.UseBasicAuthentication(basicAuthOptions);
// Configure the application for OAuth based flow
PublicClientId = "self";
OAuthOptions = new OAuthAuthorizationServerOptions
{
TokenEndpointPath = new PathString("/Token"),
Provider = new ApplicationOAuthProvider(PublicClientId),
//If the AccessTokenExpireTimeSpan is changed, also change the ExpiresUtc in the RefreshTokenProvider.cs.
AccessTokenExpireTimeSpan = TimeSpan.FromHours(2),
AllowInsecureHttp = true,
RefreshTokenProvider = new RefreshTokenProvider()
};
// Enable the application to use bearer tokens to authenticate users
app.UseOAuthBearerTokens(OAuthOptions);
}
However, even with settings the Request.User in Handler's AuthenticationAsyncCore method the [Authorize] attribute does not work as expected: responding with error 401 unauthorized every time I try to use the Basic Authentication scheme.
Any idea on what is going wrong?
I found out the culprit, in the WebApiConfig.cs file the 'individual user' template inserted the following lines.
//// Web API configuration and services
//// Configure Web API to use only bearer token authentication.
config.SuppressDefaultHostAuthentication();
config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));
Thus we also have to register our BasicAuthenticationMiddleware
config.SuppressDefaultHostAuthentication();
config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));
config.Filters.Add(new HostAuthenticationFilter(BasicAuthenticationOptions.BasicAuthenticationType));
where BasicAuthenticationType is the constant string "Basic" that is passed to the base constructor of BasicAuthenticationOptions
public class BasicAuthenticationOptions : AuthenticationOptions
{
public const string BasicAuthenticationType = "Basic";
public BasicAuthenticationMiddleware.CredentialValidationFunction CredentialValidationFunction { get; private set; }
public BasicAuthenticationOptions( BasicAuthenticationMiddleware.CredentialValidationFunction validationFunction)
: base(BasicAuthenticationType)
{
CredentialValidationFunction = validationFunction;
}
}

Windows 8 app, getting data on db change

I´m developing a windows 8 store app in c# and xaml for a website(the website is in .net mvc 4) .In the web site i can create, edit, and delete products (i´m using sql server 2008 r2).
In my w8 app i call a wcf service to get the data and populate the items.
I wanted to know if there is a way to notify or update the data in the w8 app after someone create or edit a product through the website.
One of the posible solution that i thought, was call the wcf service every time that the user click to see the list of products but i dont think is a good practice.
Here is my code:
//App.xaml.cs
protected override async void OnLaunched(LaunchActivatedEventArgs args)
{
ServiceProduct serviceData = (ServiceProduct)App.Current.Resources["serviceProducts"];
if (serviceData != null)
{
if (serviceData.ListaIos.Count == 0)
{
try
{
await serviceData.GetListProductsAsync();
}
catch(ServiceException)
{
var messageDialog = new Windows.UI.Popups.MessageDialog("Error");
var result = messageDialog.ShowAsync();
}
}
}
*** code ***
}
//Items page
protected override void LoadState(Object navigationParameter, Dictionary<String, Object> pageState)
{
// TODO: Assign a bindable collection of items to this.DefaultViewModel["Items"]
ServiceProduct serviceData = (ServiceProduct)App.Current.Resources["serviceProducts"];
if (serviceData != null)
{
this.DefaultViewModel["Items"] = serviceData.GetProducts;
}
}
public async Task GetListProductsAsync()
{
ProductServiceClient service = new ProductServiceClient();
Product product;
try
{
var wcfCall = await service.GetListaAsync();
foreach (var item in wcfCall.List)
{
map product
this.ListaIos.Add(product);
}
}
catch(Exception)
{
throw new NotImplementedException();
}
}

Problems implementing ValidatingAntiForgeryToken attribute for Web API with MVC 4 RC

I'm making JSON-based AJAX requests and, with MVC controllers have been very grateful to Phil Haack for his Preventing CSRF with AJAX and, Johan Driessen's Updated Anti-XSRF for MVC 4 RC. But, as I transition API-centric controllers to Web API, I'm hitting issues where the functionality between the two approaches is markedly different and I'm unable to transition the CSRF code.
ScottS raised a similar question recently which was answered by Darin Dimitrov. Darin's solution involves implementing an authorization filter which calls AntiForgery.Validate. Unfortunately, this code does not work for me (see next paragraph) and - honestly - is too advanced for me.
As I understand it, Phil's solution overcomes the problem with MVC AntiForgery when making JSON requests in the absence of a form element; the form element is assumed/expected by the AntiForgery.Validate method. I believe that this may be why I'm having problems with Darin's solution too. I receive an HttpAntiForgeryException "The required anti-forgery form field '__RequestVerificationToken' is not present". I am certain that the token is being POSTed (albeit in the header per Phil Haack's solution). Here's a snapshot of the client's call:
$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) {
...
});
I tried a hack by mashing together Johan's solution with Darin's and was able to get things working but am introducing HttpContext.Current, unsure whether this is appropriate/secure and why I can't use the provided HttpActionContext.
Here's my inelegant mash-up.. the change is the 2 lines in the try block:
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();
}
My questions are:
Am I correct in thinking that Darin's solution assumes the existence of a form element?
What's an elegant way to mash-up Darin's Web API filter with Johan's MVC 4 RC code?
Thanks in advance!
You could try reading from the headers:
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);
Note: GetCookies is an extension method that exists in the class HttpRequestHeadersExtensions which is part of System.Net.Http.Formatting.dll. It will most likely exist in C:\Program Files (x86)\Microsoft ASP.NET\ASP.NET MVC 4\Assemblies\System.Net.Http.Formatting.dll
Just wanted to add that this approach worked for me also (.ajax posting JSON to a Web API endpoint), although I simplified it a bit by inheriting from ActionFilterAttribute and overriding the OnActionExecuting method.
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.");
}
}
}
Extension method using Darin's answer, with a check for the presence of the header. The check means that the resulting error message is more indicative of what's wrong ("The required anti-forgery form field "__RequestVerificationToken" is not present.") versus "The given header was not found."
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;
}
ApiController Usage:
public IHttpActionResult Get()
{
if (Request.IsHeaderAntiForgeryTokenValid())
return Ok();
else
return BadRequest();
}
An implementation using 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;
}
}
}
Then just decorate your controller or methods with [ApiValidateAntiForgeryToken]
And add to the razor file this to generate your token for 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>
If it helps anyone, in .net core, the header's default value is actually just "RequestVerificationToken", without the "__". So if you change the header's key to that instead, it'll work.
You can also override the header name if you like:
services.AddAntiforgery(o => o.HeaderName = "__RequestVerificationToken")