2

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

ruffin
  • 16,507
  • 9
  • 88
  • 138

1 Answers1

10

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.

Chris Pratt
  • 232,153
  • 36
  • 385
  • 444
  • I get the idempotency -- that's why `AntiForgery.GetTokens` was an appealing solution. Send down a unique token as part of the login payload, and the client uses that value, OAuth-style, to validate actions from there on out. I believe you're implying that **you can't coerce ASP.NET Identity to work with true API projects**. Is that right? The only reason I care about antiforgery is b/c, after too much Postman-ing, it appears Identity is forcing me to. What's the MS replacement for Identity & its roles in a WebAPI app? Home-spun logins & auth tokens? Am I rolling my own API auth key generator? – ruffin Mar 02 '18 at 19:09
  • Not at all. Identity easily can handle any auth scenario. The point is that you need to actually set it up for something like token-based auth, and then actually pass those tokens back. Antiforgery tokens are not a replacement for this. In particular, they're single use, so you'd have to pass a new one back to the client with each request and they'd have to send that new one with the next request. That puts quite a strain on the client and is really no better than authenticating each request again and again. – Chris Pratt Mar 02 '18 at 20:08
  • You might also consider IdentityServer4 which can plug into Identity and as well as decentralizing your auth, also handles complex workflows like OAuth – Chris Pratt Mar 02 '18 at 20:09
  • I'd be a *little* surprised if you had to rope in a 3rd party lib to accomplish API auth -- seems like proper bearer token auth would be just as important to Microsoft for WebAPI as Identity is for MVC. I ended up essentially writing custom annotations to ensure certain controller actions checked for legit auth tokens in request headers, and gave out those bearer tokens during a co-opted Identity login. But that's essentially just gutting MS Identity into basic authentication for bearer tokens. I'll check out IdentityServer, but was hoping there was an MSDN-documented solution I was missing. – ruffin Mar 20 '18 at 22:39
  • You don't have to, but it's easier with IdentityServer. While it's technically third-party, it's supported and used by Microsoft. – Chris Pratt Mar 21 '18 at 01:16
  • Good deal. Identity tutorials for MVC are *everywhere*. That there's no analog (tutorial-wise on MSDN) for WebAPI surprised me. Figured I had to be missing something. Guess yes... and no! Thanks again. – ruffin Mar 21 '18 at 15:25
  • @ChrisPratt maybe I missed something, but after a serious research I concluded that to have bearer token auth is not out of the box, I need 3rd party like OpenIddict, etc, or write a simple one from the statch. – g.pickardou Oct 16 '20 at 03:45
  • 1
    ASP.NET Core supports token auth out of the box, but like all things, it remains agnostic about how you choose to implement that. In other words, you still need to handle the process of obtaining a token. That's where things like OpenIddict, IdentityServer, etc. come in – Chris Pratt Oct 16 '20 at 11:11