I'm having trouble using [Authorize] annotations in a strict (ie, Viewless) ASP.NET Core WebAPI project when I can't guarantee what platform the client will use. That is, the app needs to be a true API that doesn't require a specific platform to access.
Note: When I say, "strict WebAPI", my project actually started life as an MVC project generated by...
dotnet new mvc --auth Individual... from which I immediately deleted all the views, etc, and changed the routing preferences to match WebAPI conventions.
What I'm trying
When I access a standard login function (stripped down to the essentials in this paste, below) via AJAX, I get a JSON payload and one cookie returned.
[HttpPost("apiTest")]
[AllowAnonymous]
public async Task<IActionResult> ApiLoginTest([FromBody] LoginViewModel model, string returnUrl = null)
{
object ret = new { Error = "Generic Error" };
if (ModelState.IsValid)
{
var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: false);
if (result.Succeeded)
ret = new { Success = true };
else
ret = new { Error = "Invalid login attempt" };
}
return new ObjectResult(ret);
}
On success, that returns a cookie similar to the following:
.AspNetCore.Identity.Application=CfDJ8Ge9E-[many characters removed]; path=/; domain=localhost; HttpOnly; Expires=Fri, 16 Mar 2018 16:27:47 GMT;
Issue
After a seemingly successful login, I try to access two API endpoints that do exactly the same thing, one annotated AllowAnonymous and one Authorized:
private IActionResult _getStatus()
{
object ret = new { Error = "Generic Error" };
var isSignedIn = _signInManager.IsSignedIn(User);
var userName = _userManager.GetUserName(User);
return new ObjectResult(
new {
SignedIn = isSignedIn,
Name = userName
}
);
}
[HttpGet("authorizedTest")]
[Authorize]
public IActionResult GetCurrentLoginInfo2()
{
return _getStatus();
}
[HttpGet("anonymousTest")]
[AllowAnonymous]
public IActionResult GetCurrentLoginInfo()
{
return _getStatus();
}
The anonymousTest endpoint is accessible before and after login, though it tells me I'm not logged in (SignedIn is false) even after login. The authorizedTest endpoint is never accessible.
My guess is that the single cookie is not enough to get past an [Authorized] tag. I believe I also need an antiforgery value, either from a hidden value in a form generated by @Html.AntiForgeryToken() or from a second cookie that Views seem to send by default. That cookie looks like this...
.AspNetCore.Antiforgery.0g4CU0eoNew=CfDJ8Ge9E-[many characters removed]; path=/; domain=localhost; HttpOnly; Expires=Tue, 19 Jan 2038 03:14:07 GMT;
Failed solutions
I've seen lots of answers for how to use pure AJAX that basically say "get the anti-forgery from the hidden form" or "read it from the headers", but I don't have a View; there's no hidden form. Nor do I really want to kludge sending down a partial view for the clients to scrape.
The best answer I've seen is this one talking about using iOS. The situation seems analogous:
Because we're not delivering HTML to the client, we can't use the standard
@Html.AntiForgeryToken(), so instead we have to useAntiForgery.GetTokensto acquire and distribute the tokens to our clients.
But even though my Intellisense "sees" AntiForgery.GetTokens, it won't compile, even after grabbing what seems to be the right nuget package:
dotnet add package microsoft-web-helpers --version 2.1.20710.2
My question, then, is: How do I use Antiforgery outside of Razor but using ASP.NET Core to create an Identity-restricted WebAPI-style project?
Why I think it's an anti-forgery issue
For a while, I couldn't figure out why my code didn't work in a strict WebAPI project, but did when transplanted into the standard identity ASP.NET Core project's (one created from dotnet new mvc --auth Individual) AccountController.
The key? options.LoginPath. When I'm in WebAPI land, I forward to an API endpoint to log in:
options.LoginPath = "/api/accounts/requestLogin";
In the stock project, it's a View by default, which provides the antiforgery cookie on load:
options.LoginPath = "/Account/Login"; // As soon as Login.cshtml loads, BAM. .AspNetCore.Antiforgery cookie.
Strangely, sometimes I can delete the second, AspNetCore.Antiforgery cookie within Postman and still access an [Authorize]-annotated method, so I'm not 100% sure I'm not barking up the wrong tree here, but this is my best lead so far...