Hardening OAuth 2.0 flows for public clients with PKCE

I’ve built an SPA which uses RaiderIO APIs to fetch character data.

This is hosted using only static website hosting with no server-side components, it just uses fetch() with CORS. This is great:

  • my serving infrastructure is really simple, it’s basically an object storage bucket
  • my server never has access to users’ data (even temporarily), and a technically-adept user could verify this

I’m looking to switch to Battlenet WoW Profile APIs instead, and make sure that the server side still doesn’t have access to users’ data or need to build out infrastructure.

Battlenet authentication and Profile APIs don’t seem to apply any CORS restrictions, so once my SPA has an access token, it should all just work – the problem is getting that token.

While I could use an Authorization Code flow, the docs seem to assume the client secret will always be secret. There are scary warnings on this forum that it must always be secret – indicating that only confidential clients are considered.

Keeping this a secret is impossible with public clients, such as a client app or an SPA (where the authorization flow is completed by JavaScript running in the user’s browser). This leaves it vulnerable to hijacking issues outlined in RFC 7636 (the PKCE RFC).

This doesn’t mean public clients are “less secure” than confidential clients – they’re all well defined in the OAuth 2.0 RFCs.

This issue come up a few times before:

Blizzard reps’ (and others’) responses to date have been “make a server-side component and proxy it” (ie: switch to being a confidential client instead) and that public clients shouldn’t exist, but this would mean the server side would start handling user data, I’d need to build out more infrastructure to support it, and deal with all the privacy and data protection implications.

Because my SPA is entirely stateless and doesn’t use the access token for authentication or identity, where an access token came from doesn’t really matter. It’s only used by the SPA to call Profile APIs on the user’s behalf, so isn’t a security risk to any of its functionality.

If the SPA generated the code challenge on the client side and stashed it in session storage (not cookies), Battlenet supporting PKCE would prevent:

  • a malicious app on a user’s device from using an authorization code intended for my SPA (potentially allowing access to protected data) to access Battlenet APIs directly
  • someone that could read my web server’s request logs from using the authorization code (passed as a query parameter in the redirect response) to request an access token and access Battlenet APIs directly

Only the first issue would be addressed by switching to a confidential client model, but it would make the scope of the second issue far wider.

On that basis, me shipping the client secret in an SPA is the lesser of two evils. Though for others, your mileage may vary – it really depends how you’re using the APIs. :slight_smile:

PS: Even for confidential clients (server-side apps), PKCE is recommended in the OAuth 2.0 Security BCP, and is required in the current OAuth 2.1 drafts – so this doesn’t magically go away. :wink:

PPS: SimulationCraft has shipped binaries with an embedded Battlenet API client secret for many years. :wink:

1 Like

Supporting PKCE would sure be a nice addition for the API. However just adding PKCE support doesn’t really solve the problem you described (not having a backend). It is meant to prevent a token hijacking when some other app is trying to impersonate your app when using public clients. As stated in PKCE for OAuth 2.0

Note: Because PKCE is not a replacement for client authentication, it does not allow treating a public client as a confidential client.

However the API treat its clients as private clients. When you create an API client you are (currently) creating a private client, that will be tied to your application (and account) and will be used to measure the rate limiting of the API using the allowed quota. If you use it as a public client you have no control of how many requests are being made and you have no way to limit those requests, which could lead to your app not working properly if you start getting 429 errors when multiple users are actively making requests.
Ideally a public client would have token based quota instead of application based.

To be clear, I am not advocating against it, just pointing out that PKCE is not the full solution to the problem (SPA without any back-end), but sure would be nice to have an official recommendation and possibly some options for SPA as having a back-end is detrimental for some projects and as you said we do get quite a lot of requests for such a feature here in the forums and over the discord.

Yeah, this part of the problem.

Treating everything as a confidential client encourages either:

  • ignoring that intention and shipping the client ID/secret anyway (which is what SimulationCraft do)
  • proxying everything through a server-side component, caching it, rate limiting API calls, and handling all the privacy issues… so you end up with an expensive pile of infrastructure (which is what RaiderIO do)

The risk for all types of clients is that Battlenet doesn’t apply any restrictions on which flows may be used:

  • If a public client shipped a client ID/secret for use with an Authorization Code flow with redirect URLs set up, someone else could take those and use it for a Client Credentials flow.
  • A public client might also just use the client ID/secret with the Client Credentials flow (which is what SimulationCraft do)
  • A confidential client which normally uses the Authorization Code flow to sign in users could have that token used for a Client Credentials flow as well, giving it wider access than necessary.

You could mitigate it by periodically rotating the client secret, but for a public client, you’re always disclosing it.

The problem is that switching to a confidential client model (where Battlenet API access proxied through some server-side component I’d operate) to mitigate the issue means it’s handling user data, which comes security and privacy concerns. This is true for any type of public client (SPA or native app).

That’s a perverse outcome to have for something which is supposed to limit access to user data. The best way to ensure the privacy and security of user data is to avoid acquiring it in the first place. :wink:

Yeah, application-wide rate limits are an issue for both public and confidential clients. Some other APIs bucket these separately into:

  • global quota for a client
  • global quota for a user
  • quota for a user with a particular client (ie: with Authorization Code flow)
  • quota for a client not acting on behalf of a user (ie: with Client Credentials or Implicit Grant flows)

This gives much more flexibility for an API service, because they can detect abuse or usage spikes on a bunch of different dimensions.

There’s also no monitoring for this, short of manually making a request and seeing if you hit a rate limit, or Blizzard sending you a polite email. :wink:

I agree that supporting PKCE alone doesn’t fix the issue of supporting public clients. There are unfortunately only so many characters you can put in a subject line, and “PKCE” works better for SEO than “public client”. :wink:

If Battlenet:

  • required PKCE but still assumed use in a confidential client:

    • it would address the hijacking issue
    • allow you to build an SPA that avoids the web server being able to use the SPA’s access token
    • but the client ID/secret could still be used in a Client Credentials flow.
  • supported public clients without requiring PKCE:

    • it would be vulnerable to hijacking issues
    • a web server hosting an SPA could use the redirect response in an Authorization Code flow to request a token to act on a user’s behalf
    • but the client ID/secret couldn’t be used with a Client Credentials flow.

By supporting both:

  • the SPA itself would generate the code_challenge and code_verifier (RFC 7636 § 4.2) on the client side
  • the SPA only ever sends it to Battlenet (RFC 7636 § 4.3, 4.5)
  • Battlenet’s redirect doesn’t contain any of those values in clear-text form (RFC 7636 § 4.4) so the web server couldn’t use it
  • it would be clear from network logs if the access token or (cleartext) code_verifier is ever transmitted to anything but Battlenet APIs
  • the client ID and secret could only be used with an Authorization Code flow at an authorised URL

I recognise that rolling out PKCE requires clients to actually use it, not all OAuth 2.0 libraries support it, and very few public identity providers can actually require it (most make it optional and have no option to require it, and some don’t support it at all!). The rollout would need planning and a migration strategy for existing clients.

PS: I’ve mentioned SimulationCraft and RaiderIO here as a hopefully-familiar examples of applications which use Battlenet APIs, rather than to criticise what they’ve done. They’re working within the design constraints of Battlenet APIs and their own applications.