SPAs would not benefit from PKCE. PKCE solves a different problem than the one you're describing.
First of all, for SPAs the current best practice is still to use the implicit flow, not the authorization code flow. With the implicit flow, the access token is included in the hash fragment (#) of the redirect URI instead of in a query component (?). Since the browser never includes the hash fragment portion of a URI when it makes a request, the token does not appear in browser history, web logs...
When it comes to native apps, rfc8252 section 6 says the following:
Public native app clients MUST implement the Proof Key for Code Exchange (PKCE RFC7636])
On a side note, notice that the PKCE requirement is for public native clients. You may be wondering how it can be possible for a native app to not be public. The answer is in section 8.4:
Except when using a mechanism like Dynamic Client Registration [RFC7591] to provision per-instance secrets, native apps are classified as public clients
I'm not exactly sure how to refer to these clients since I've never seen confidential native client mentioned anywhere. Maybe non-public native client works :)
To answer your question: So why is the authorization code flow with PKCE required for public native apps and not SPAs?
The logic is as follows:
- One of the main purposes of OAuth2 is to prevent the exposure of user credentials to clients.
- For native apps to meet this requirement, the user must not enter credentials anywhere that the native app has access. Therefore native login screens and webviews must be avoided.
- The solution is for native apps to launch an external user-agent (see rfc8252 appendix B) where the user will authenticate with the authorization server and authorize the application. Native apps have no access to the external user-agent, so the user's credentials are safe.
- However, a new complication has been introduced which did not exist with SPAs: The external user-agent must now communicate the authorization server's response back to the native app? The answer is inter-app communication (see section 5 and section 7)
- Unfortunately, several types of inter-app communication can be intercepted by malicious 3rd party apps! With the implicit flow, this means that the access token could be stolen by a malicious app, which would be a very bad thing. This is one of the main reasons why the implicit flow is not used for native apps.
- But even if the authorization code flow is used instead, the malicious 3rd party app can still intercept the authorization code, and use it to obtain an access token since there is no client secret. So it seems useless to use the authorization code flow instead of implicit flow for public native apps.
- This is where PKCE comes in. PKCE makes it so that even if a malicious app intercepts an authorization code, it will not be able to exchange it for an access token. This is accomplished by requiring the entity that is requesting the access token prove that it is the same entity that requested the authorization code in the first place.
Hopefully now you understand why:
- SPAs would not benefit at all from PKCE since with SPAs everything occurs
within the browser. PKCE is only useful when there is inter-app communication.
- Public native apps should not use the implicit flow because inter-app communication can sometimes be insecure.
- Public native apps should use the authorization code flow + PKCE
Here is a good blog post that can provide more info: