I'm having trouble using [Authorize]
annotations in a strict (ie, View
less) 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 View
s 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.GetTokens
to 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...