An alternative way to secure SPAs (with ASP.NET Core, OpenID Connect, OAuth 2.0 and ProxyKit)

You might have noticed the recent public discussions around how to securely build SPAs – and especially about the “weak security properties” of the OAuth 2.0 Implicit Flow. Brock has written up a good summary here.

The whole implicit vs code flow discussion isn’t particularly new – and my stance was always that, yes – getting rid of the tokens on the URL is nice – but the main problem isn’t how the tokens are transported to the browser, but rather how they are stored in the browser afterwards.

Actually, we had this discussion already back in May 2015 on the OAuth 2.0 email list, e.g.:

My conjecture is that it does not matter >>at all<< where you store tokens in relation to XSS. There is no secure place to store data in a browser in ways that cannot be abused by XSS. One XSS is complete compromise of the client.

And XSS resistant apps are illusive.

Jim Manico – here

What about CSP?

Content Security Policy was created to mitigate XSS attacks in the browser. But to be honest, I see it rarely being used because it is hard to retro-fit into an existing application and interferes with some of the libraries that are being used. Even in brand new applications it is often an afterthought, and the longer you wait, the harder it becomes to enable it.

And btw – getting CSP right might be harder than you think – check out this video about bypassing CSP.

Screenshot 2019-01-18 08.38.39.png

In addition, JS front-end developers typically use highly complex frameworks these days where they do not only need to know the basics of JavaScript/browser security, but also the framework specific features, quirks and vulnerabilities. Let alone the quality of known and unknown dependencies pulled into those applications.

Oh and btw – the fact that the new guidance around SPAs and Authorization Code Flow potentially allows refresh tokens in the browser, the token storage problem becomes even more interesting.

..so the conclusion turned out to be:

SPA may turn out to be impossible to completely secure.

John Bradley – here

Just like there is IETF guidance for native apps, we always thought there should be a similar document that talks about SPAs. But it seemed everyone was trying to avoid this.

Finally, the work on such a document has started now, and you can track it here.

What about Token Binding?

For me, the most promising technology to protect tokens stored in the browser was token binding (and especially the OAuth 2.0/OpenID Connect extensions for it). This would have allowed to tie the tokens to a browser/TLS connection, and even if an attack would be able to exfiltrate the tokens, it could not be used outside the context of this original connection.

Unfortunately, Google decided against implementing token binding, and if the standard is not implemented in all browsers, it’s pretty much useless.

What other options do we have?

The above mentioned IETF document mentions two alternative architecture styles that result in no access tokens at all in the browser. Let’s have a look.

Screenshot 2019-01-17 17.57.42.png

Apps Served from the Same Domain as the API

Quoting 6.1

For simple system architectures, such as when the JavaScript application is served from the same domain as the API (resource server) being accessed, it is likely a better decision to avoid using OAuth entirely, and just use session authentication to communicate with the API.

Some notes about this:

  • This is indeed a very simple scenario. Most applications I review also use APIs from other domains, or need to share their APIs between multiple clients (see next section).
  • This is also not new. Especially “legacy” application often had local “API endpoints” to support the AJAX calls they sprinkled over their “multi-pages” over the years.
  • When traditional session authentication (aka cookies) is used, you need to protect against CSRF. Implementing anti-forgery for APIs is extra work, and while well-understood I often found it missing when doing code reviews.

The new kid on the block: SameSite cookies

SameSite cookie are a relatively new (but standardised) feature that prohibits cross-origin usage of cookies – and thus effectively stops CSRF attacks (well at least for cookies – but that’s what we care about here). As you can see, it is implemented in all current major browsers:

Screenshot 2019-01-18 09.33.49.png

ASP.NET Core by default sets the SameSite mode to Lax – which means that cross-origin POSTs don’t send a cookie anymore – but GETs do (which pretty much resembles that standard anti-forgery approach in MVC). You can also set the mode to Strict – which also would prohibit GETs.

This can act as a replacement for anti-forgery protection, but is relatively new. So you decide.

Bringing it together

OK – so let’s create a POC for this scenario, the building blocks are:

  • ASP.NET Core on the server side for authentication and session management as well as servicing our static content
  • Local or OpenID Connect authentication handled on the server-side
  • Cookies with HttpOnly and Lax or Strict SameSite mode for session management (see Brock’s blog post on how to enable Strict for remote authentication)
  • ASP.NET Core Web APIs as a private back-end for the SPA front-end

That’s it. This way all authentication is happening on the server, session management is done via a SameSite cookie that is not reachable from JavaScript, and since there are no tokens stored in the browser, all the client-side APIs calls don’t need to deal with tokens or token lifetime management.

The full solution can be found here.

Browser-Based App with a Backend Component

Quoting 6.2.:

To avoid the risks inherent in handling OAuth access tokens from a purely browser-based application, implementations may wish to move the authorization code exchange and handling of access and refresh tokens into a backend component.

The backend component essentially becomes a new authorization server for the code running in the browser, issuing its own tokens (e.g. a session cookie).  Security of the connection between code running in the browser and this backend component is assumed to utilize browser-level protection mechanisms.

In this scenario, the backend component may be a confidential client which is issued its own client secret.

This is a much more common scenario and some people call that a BFF architecture (back-end for front-end). In this scenario, there is a dedicated back-end for the SPA that provides the necessary API endpoints. These endpoints might have a local implementation or in turn contact other APIs to get the job done. These other APIs might be shared with other front-ends and might also require a user-context – IOW the BFF must delegate the user’s identity.

The good news is, that technically this is a really easy extension to the previous scenario. Since we already used OpenID Connect to authenticate the user, we simply ask for an additional access token to be able to communicate with the shared APIs from the back-end.

The ASP.NET Core authentication session management will store the access token in an encrypted and signed cookie and all token lifetime management can be automated by plugging-in the component I described in my last blog post. This allows the BFF to use the access token to call back-end APIs on behalf of the logged-on user.

One thing I noticed is, that you often end up duplicating the back-end API endpoints in the BFF to make them available to the front-end, which is a bit tedious. If all you want is passing through the API calls from the BFF to the back-end while attaching that precious access token on the way, you might want to use a light-weight reverse proxy: enter ProxyKit.

A toolkit to create HTTP proxies hosted in ASP.NET Core as middleware. This allows focused code-first proxies that can be embedded in existing ASP.NET Core applications or deployed as a standalone server.

While ProxyKit is very powerful and has plenty of powerful features like e.g. load balancing, I use it for a very simple case: If a request comes in via a certain route (e.g. /api), proxy that request to a back-end API while attaching the current access token. Job done.

app.Map("/api", api =>
{
    api.RunProxy(async context =>
    {
        var forwardContext = context.ForwardTo("http://localhost:5001");
 
        var token = await context.GetTokenAsync("access_token");
        forwardContext.UpstreamRequest.Headers.Add("Authorization""Bearer " + token);
 
        return await forwardContext.Execute();
    });
});

I think it is compelling, that combining server-side OpenID Connect, SameSite, automatic token management and ProxyKit, your SPA can focus on the actual functionality and is not cluttered with login logic, session and token management. And since no access tokens are stored in the browser itself, we mitigated at least this specific XSS problem.

Again, the full sample can be found here.

Some closing thoughts

Of course, this is not the silver-bullet. XSS can still be used to attack your front- and back-end.

Screenshot 2019-01-17 17.58.23.png

But it is a different threat-model, and this might be easier for you to handle.

Also you are doubling the number of round-trips and you might not find this very efficient. Also keep in mind that if you are using the reverse-proxy mechanism you are not really lowering the attack surface of your back-end APIs.

But regardless if you are using OAuth 2.0 in the browser directly or the BFF approach, XSS is still the main problem.

Update

This article was quoted or questioned like “this approach is better than tokens”. This was not the point and I apologize if I wasn’t clear enough. It is an alternative approach to the “pure” OAuth 2 approach – and I am not saying it is better or worse. Both approaches have a different threat model and you might be more comfortable with one or the other.

Maybe you also need to evaluate your architecture based on the fact where the APIs live that you want to call. Would you bother with OAuth if all APIs are same-domain? Where is the tipping point? If all APIs are cross-domain then it certainly is questionable if they should proxied.

Anyways – time will tell and SameSite cookies are still very new but certainly give you a new interesting option.

This entry was posted in ASP.NET Core, OAuth, OpenID Connect, WebAPI. Bookmark the permalink.

15 Responses to An alternative way to secure SPAs (with ASP.NET Core, OpenID Connect, OAuth 2.0 and ProxyKit)

  1. Sri says:

    Thank you for everything you do for the open source community. Just a thought about proxying, instead of proxy, you could secure a GetToken end point (via client credentials or other means) and leave it to the APIs to get the access tokens? Do you see any security issues with this approach?

  2. Michael says:

    Interesting post. Although I have to admit I fail to see the added value of this architecture. XSS is indeed still the main problem. The argument ‘this allows for behavioral analysis type defense in one central location’ is a bit vague, in my opinion.

    It seems related to a discussion from the past (http://www.redotheweb.com/2015/11/09/api-security.html, see the comment by ‘Sabine’), although it is different since in the BFF architecture you could easily use the new same-site attribute like you mention. So with regards to CSRF, this BFF architecture seems better, however CSRF was not the issue in a traditional architecture without an extra back-end component.

    Could you give concrete security measures or threats that are better protected using a BFF architecture compared to a traditional architecture without an extra back-end?

    • I wasn’t happy with my conclusion either – so I added an update to it:

      This article was quoted or questioned like “this approach is better than tokens”. This was not the point and I apologize if I wasn’t clear enough. It is an alternative approach to the “pure” OAuth 2 approach – and I am not saying it is better or worse. Both approaches have a different threat model and you might be more comfortable with one or the other.

      Maybe you also need to evaluate your architecture based on the fact where the APIs live that you want to call. Would you bother with OAuth if all APIs are same-domain? Where is the tipping point? If all APIs are cross-domain then it certainly is questionable if they should proxied.

      Anyways – time will tell and SameSite cookies are still very new but certainly give you a new interesting option.

      • Michael says:

        I agree about the same-domain scenario and I certainly could follow the reasoning of not using OAuth there. Where I was a bit lost was in the cross-domain scenario: I’m not entirely convinced that BFF would solve the problem in a better manner compared to ‘traditional OAuth’. And I indeed thought that was the point of the article.

        Thanks for the clarification and the nuance! I guess indeed time will tell.

  3. andyliazon says:

    Really like this approach, but I’m confused about something though: for those calls that aren’t simply passed thru the proxy, how would I protect the *public* API that resides on my SPA server using user claims? The ClaimsPrincipal doesn’t have any of the access_token claims on it when it hits the SPA api. I can implement a policy and within it call
    httpContextAccessor.HttpContext.GetAccessTokenAsync(),
    but that’s doing it manually and doesn’t seem quite right. What am I missing?

    • You can put whatever claims into the principal that you want. The SPA (and the SPA local APIs) have to be treated as “the client”. So I guess if you need claims from the token service, they have to go to the id_token or userinfo endpoint.

      But this sounds to me like you want to distribute authorization information. Which I wouldn’t recommend.

      • andyliazon says:

        By “distribute authorization information” I assume you mean I’m enforcing authorization in the SPA tier instead of each back-end service? The thought here is:

        1.) To consolidate the authorization at the app tier, so that it’s easier for the app-tier devs to manage, not all spread out over the multiple back-end services.
        2.) Some SPA APIs have to be protected directly because they’re orchestrating multiple back-end calls.
        3.) Seemed like a better practice to block at the earliest possible point – the SPA server tier – as opposed to letting calls through into the backend that haven’t been fully checked.

        These might be flawed, but that was the reasoning. You think it’s better to leave all authorization on each service?

        Thanks!

  4. All I am saying is that tokens are not a great mechanism to distribute authorization rules. Your comment about the claims principal seems to imply that this is what you are trying to do…

  5. Andrew Alderson says:

    As someone that has spent a lot of time over the last 20 years writing a lot of client side code to handle authentication I really like this approach and have tried it out. It certainly makes my code a lot cleaner. One thing I am wondering about though is Progressive Web Apps. Do you see any issues with using this approach with a PWA? I am assuming that once a PWA is installed it will work the same as a native app and send the same site cookie.

  6. Robert Bramhall says:

    Soooo, after reading this and everything else I’ve been reading about all of the security issues, is it best to just create a single site that does it all? As long as there’s no Session state, it should be able to scale right?

    I merged several of the tutorials, and using the HybridAndClientCredentials have an MVC Authorization site with IdentityServer4 and is hosting the unsecured public-facing front end and out of the box registration and password management. Once registered I send the user to a second site the App site, which handles the root content for the SPA application and token handling, and provides the access token to the SPA application via WebService request and any other User information and is simply stored as a JavaScript variable (so not secure I guess due to XSS) and then 95% of the SPA calls on a third site, the API utilizing that access token.

    It works generally… I still have issues even though my session is supposed to be 8 hours long, every 20-60 minutes the access token times out and my code causes the SPA page to refresh and sometimes it updates the token, most of the time it seems to bounce back to the MVC Auth site and then bounces back to the page I’m on and is ok, not great.

    I’ve got code that uses a TokenFilter : ActionFillter that is supposed to renew the AccessToken, and it’s using the HttpClient and GetDiscoveryDocumentAsync so not getting the DiscoveryClient deprecated error, but also there is the CookieAuthenticationEvents OnValidatePrincipal in the Startup.cs of the APP site that is using the old DiscoveryClient and looking at it now, they look very similar… Maybe I can take the TokenFilter code and use it here… I don’t know. Been dizzy spinning in circles trying to figure this stuff all out! :) I’ve got both… seems redundant.

    It’s all ASP.NET Core 2.2, EF Core, SQL Server backend, all up on Azure. Not using any JavaScript libraries besides jQuery and not using the oidc-client.js, couldn’t figure out how to use it with the App handling the initial token provisioning.

    Also been having issues trying to get the clientside JavaScript to detect that the token has expired and detect the redirect and then try to hit the APP site and let it do the access token updating. With XHR wasn’t able to do it, but now have been playing with Fetch and it allows me to detect the redirect and stop it.

    Sooo, back to the first sentence, maybe I just need to smash them all into one site and call it a day! I really like the separation of the sites and concepts but trying to get it to work perfectly is nuts and painful. We do want the API interface to be secured and accessible by third parties in the future. Our SPA is the first of many… Or when that occurs we can THEN just refactor it out into a publicly accessible API. But since we have the IdentityServer4 would like to use it to manage the security on the API.

    Done rambling!
    Rob

  7. Peter C. says:

    I’ve tested the code provided and it works fine. I’ve also tried to get it working with ADFS 2016 but was not successful so far. Is it possible to create this workflow with ADFS 2106?

    • Don’t see a reason why it wouldn’t – it’s standard code flow..

      • Peter C. says:

        I have the suspicion that additional properties have to be set for ADFS in the client code (like callback for instance). My main issue with ADFS is the documentation. There isn’t a lot and many of the examples just do not work or are outdated.

Leave a comment