Search code examples
c#ajaxasp.net-web-apiasp.net-core-2.0antiforgerytoken

How to use [Authorize] and antiforgery in a View-less WebAPI ASP.NET Core app?


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 use AntiForgery.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...


Solution

  • First and foremost, requests are idempotent: each is a unique thing and must contain all information necessary to service the request, including authorization. A traditional website uses cookie-based authentication, and the web browser, in concert, sends all cookies back to the server with each request, without user intervention. The cookie that's sent with the request serves to authorize the request, giving the semblance that the user is authenticated without having to continually pass a username and password.

    An API is a different beast. Since it's not typically an actual web browser making the request, adding the authorization is a manual affair. This is most normally handled via the Authorization request header, which includes something like a bearer token. If you fail to pass something like this with your request, then it's the same as if the user never authenticated, because effectively they haven't. The server doesn't uniquely identity where a particular request is coming from and doesn't know or care that a particular client issued a request before. The HTTP protocol is decentralized by design.

    Second, antiforgery tokens are a completely different thing. They're designed to prevent XSRF attacks where a malicious website might try to host a duplicate of a form from your site to capture information, but then post to your site so that the user is not aware that anything ontoward happened. For example, imagine I set up a site at http://faceboook.com (notice the extra o). Most users wouldn't notice this slight difference in a link or may even accidentally type it themselves in the address bar. My malicious site then creates an exact duplicate of the real Facebook home page, with a place to login. The user enters their Facebook login credentials into my form (which I log to use later to steal their accounts), but I then post those credentials to same same handler that the real Facebook site uses. The end result is that the user ends up at Facebook, logged in, and goes on about their business of posting cat videos and such. However, now I have their their login credentials.

    Antiforgery tokens prevent this. The token is encrypted such that I could not create a token at my malicious site that would be able to be validated successfully at the Facebook end. When validation fails, the request is rejected. Based on the problem antiforgery tokens exist to solve, they are mostly incompatible with APIs, as generally APIs are intended to be utilized by third parties. Any API endpoint protected by antiforgery tokens would not be able to be utilized by anything external to that app or domain. Regardless, they have nothing to do with authentication/authorization of requests, either way.