the client backend finally gives the code flow's access token in the URL fragment to the client front end
This should never happen; I think there still is (or at least was) some confusion here. I think you're referring to a backend-for-frontend architecture here, and in that case, the "client backend" - which is itself a server - should not ever send a redirect back to the "client frontend". For sure, doing so is not part of either the implicit or code grant flow specifications.
What a client backend would typically use to authenticate a client frontend would be a session cookie, as demonstrated here. The exchange of the token for resources would then typically be done with a resource server, directly from the "client backend" - so the frontend would never see the token. Some would argue that this setup is more secure than the client having a token e.g. because the token is stateless and can't be invalidated easily server side if compromised. But that's not really relevant to this question.
To keep things simple, let's forget about a "backend-for-frontend" architecture. We can show that even in the case of a static frontend (i.e. one without any "backend client"), the code grant flow - when used correctly with PKCE - does not result in tokens going to the browser history via a redirect. Sure, the tokens go to the client code running in the browser, but that's different. The key point is outlined in this OKTA article:
The response is returned on the same channel (as opposed to a redirect response with the Implicit flow) and, as such, that response will not be in your browser history. [Bold formatting mine]
If it's still not clear, let me attempt to explain, with steps, what happens with the implicit vs. code grant with PKCE flows, for a static website.
Implicit Flow
- User requests page from CDN
- Page loaded to user's browser
- Javascript (JS) on the page determines the user is not logged in & redirects the browser to the authorization server. As part of that redirect URL, it includes a
redirect
URL parameter (yes, a redirect parameter within a redirect URL - sometimes referred to as the callback URL maybe to avoid confusion) specifying where the auth server should redirect to after it's done. In this case it will be the same URL as the frontend page.
- Authorization server notes the redirect URL for later
- Authorization server sends login page as response
- User sends over credentials
- Authorization server validates the credentials and responds with the redirect URL it was given earlier and an access token in the URL fragment
- Browser gets the redirect response and it loads into the browser's address bar and history
- Browser redirects back to the client app
- Client app extracts the token from the URL fragment and stores it e.g. in local storage or else in memory.
- Client can now use that token in requests to the resource server.
The key vulnerability here, in boldface above, is that the access token went through the browser's history.
Code Grant Flow with PKCE
It starts off quite similar to the implicit flow, but diverges after a few steps.
- User requests page from CDN
- Page loaded to user's browser
- Before redirecting, a random value is created in memory. Let's call this
k
. This is saved somewhere e.g. session storage. Here the flows have diverged.
- Client JS now redirects the user to the authorization server, with a redirect (a.k.a. callback) URL to itself, just like it did in the implicit flow. But it also sends the hash of key, let's say
h(k)
as the code_challenge
parameter.
- Authorization server notes the redirect URL for later
- Authorization server notes
h(k)
- Authorization server sends login page as response
- User sends over credentials
- Authorization server validates the credentials and responds with the redirect URL it was given earlier plus a code that it just generated. Just like the token, the code indeed still goes to the browser history, as it's given via a redirect. So it's just as likely someone will get their hands on the code... BUT - see the following steps for why the code alone is not enough.
- Browser gets the redirect response and it loads into the browser's address bar and history
- Browser redirects back to the client app
- Client app extracts the code from the URL fragment
- Client extracts
k
from storage. A key point (no pun): k
was never sent anywhere, so it's a secret that only the client can have.
- Client now directly sends
code+k
to the authorization server's token granting endpoint. This is done over a secure channel (HTTPS).
- Authorization server compares
code
to the code it generated and k
to h(k)
and if both match, it then returns a token as a response. Not as a browser redirect. This is probably the most crucial point in relation to the above misunderstanding.
- Client JS can now process the response and store it e.g. in local storage, session storage or in memory. There are still vulnerabilities here e.g. if it's being stored in local storage, an XSS attack on the frontend page would be able to access that storage. But discussing that is beyond the scope of this answer, whose sole purpose is to demonstrate a vulnerability that differentiates code grant from implicit.
- Client can now use that token in requests to the resource server.
Note, to avoid over-complicating matters, I did not talk here about another optional request parameter called state
which can and probably should be used also in the above case, to prevent CSRF attacks. See here for an approach that also uses a state
.
I will augment this answer with some diagrams. Like many diagrams you will find when Googling this subject, they are abstracted somewhat. However, I believe many of the other diagrams you'll find will abstract a little too much detail from what's happening client side. The main difference with these diagrams, to demonstrate the issue with the implicit flow, is the differentiation between the "browser" and the "webpage". The "browser" is the computer program Chrome/Firefox etc. running on your OS. Its code is predefined and unchangeable (presumably). The "webpage" is the Javascript and HTML that gets loaded in the browser. While yes, it runs in the browser, we must think of it as a separate process for the purpose of this demonstration. Also, we can think of the "browser" abstractly as a machine that can be asked to go to some URL, either by the webpage or the user, and after doing so, it will make a request to that URL and remember that request in its history. With that in mind, here are the flows.
Implicit
Notice the vulnerability, highlighted by the red square:
Code Grant
Remarks
Arguably, my diagrams are themselves too abstract. You could say, sure, the token ends up in browser history. So what? How does that make it more vulnerable? To answer such questions, further explanation would be needed to detail how exactly such a vulnerability could be exploited. For me, I'm reasonably happy enough to accept, at least for now, various high-level explanations of why having a token in browser history is a bad thing. But a more thorough answer would also give a concrete example of this vulnerability being exploited in the real world. Perhaps an interested reader will find such an example and contribute it to this thread some day.
Also, the examples I have given are just one implementation of these protocols. The protocols themselves are abstract enough to allow multiple implementations. For example, there is nothing in the code grant flow dictating that a key needs to be stored in a browser's local storage - that is just an implementation detail I've added to help make this concrete.
Let me briefly return to the notion of a backend-for-frontend (BFF) since it was mentioned by the OP. With such an architecture, using code grant, it is possible to use a "client secret" instead of the "proof key" that was used in PKCE. This is possible because the server, being on the backend, can keep secrets, while a static webpage, being served publicly, cannot. However, the BFF could also use PKCE server side. Another choice with this architecture is whether you want the tokens sent to the client browser or not. For example, in this SO blog, the tokens are sent by the BFF to the frontend client (on the browser) as cookies & are used directly by the frontend client thereafter. But as I mentioned earlier you could also have the BFF securely store those tokens and then keep a session alive with the client frontend; all requests would then need to go through the BFF.
I will also note that even without PKCE, or a client secret in the case of the BFF architecture, the code grant is arguably more secure. In fact, as pointed out in the OKTA article:
It’s very short lived (60 seconds with Okta)
So think about an attacker program that tries to use it more than a minute later e.g. another malicious app that is only opened by the user hours later. The code, even if left unused for some reason, is no longer a vulnerability in this case.
and:
It can only be used once to be exchanged for tokens
This means that any attacker is forced to steal and use your code before you do so. This makes their attack extra tricky because they need to race with you or else try to cut you off.
That article is well worth the read.