My understanding of the difference between Oauth2.0 implicit and OIDC authorization code flow is that with implicit, the access token is returned in the URL response from the /authorize call, but with auth code flow the access token is retrieved by a subsequent POST to a /token endpoint. The latter is far more secure and it also separates authentication from authorization.
I thought the auth code flow requires a shared secret between the client and server, which subsequently requires that the client be able to store a secret, which is not possible in a client-side JavaScript application.
My client-side SPA is being authorized with Auth0 via “code” type. I assume this is authorization code flow; indeed, I have deselected implicit in the control panel so it can’t be implicit! Also, I see a code being transmitted in the call to the /authorization endpoint, and my access token is being retrieved via a POST to the /token endpoint.
So that’s great… but clearly there is a gap in my understanding and I would really love to fill it. How does the above work without me setting a secret key (the code part of auth code flow)?
Your understanding of the difference between Implicit and Authorization Code (AC) is spot on. Note that Implicit is not that insecure, but the AC flow is just cleaner.
The client secret is used to authenticate a confidential client (i.e., a client living in a secure environment). That is mainly used to authenticate a backend service acting as a client on behalf of the user.
Since a frontend application (browser, mobile, …) does not run in a secure environment, it cannot handle a secret. Anyone able to read the source code would be able to extract that secret. That’s why you cannot use the secret in SPAs.
For mobile applications, not using the secret makes the authorization code vulnerable. It would mean that a malicious app that intercepts the code could exchange it for tokens, which is a problem. That is why they added PKCE for mobile applications, which uses a one-time secret for the flow. Because of PKCE, only the app starting the flow will be able to obtain the tokens, thus preventing abuse of the authorizatio ncode.
Given the widespread support for PKCE, it has become a best practice in every instance of the AC flow. It does not cost much, and it improves security. Note that the AC flow with PKCE is recommended for SPAs (and supported by the Auth0 SDK), but that authorization code theft in SPAs is less of an issue than in mobile apps.
Hey Philippe, thanks for the reply. It led me to the Auth0 doco that I had seen, but not quite understood how it fits into the picture.
Taking a quick step back - we have used AC in our .NET MVC apps, using a pre-defined client secret (stored server side and with the auth provider). I believe this is the gold standard for federated authentication.
It seems that for AC + PKCE, the Auth0 client SDK JavaScript generates a code challenge secret, which acts pretty much like the client secret on our server-side apps.
Is this right? If so, they are closer than I thought. Is it fair to say that the difference in security is just that with PKCE there is the theoretical chance of someone generating a valid code challenge? That in practice, AC and AC+PKSE are as good as each other?
You can think of PKCE as a “one time password” for a public client. It starts during the init phase, where the client generates a secret value (the code verifier). The code challenge is the hash of that secret, typically a SHA256.
The client sends the code challenge in step 1, so that the authorization server can keep track of it. The code is returned like a normal AC flow, nothing special there. When the client exchanges the code for tokens, it now provides the code verifier. With that verifier, the authorization server can also calculate the SHA256 hash and compare that to the challenge it received during the initialization. If these match, the authorization server now knows that it is dealing with the same client instance as during the initialization of the flow.
The security here depends on two things. First, the client keeps the generated code verifier a secret (storing it locally suffices). Second, hashing functions like SHA256 are irreversible. This means that an attacker that sees a challenge cannot determine what verifier was used to generate that challenge. This implies that even if the attacker obtains a valid authorization code, they will not be able to provide the correct verifier, so the token exchange will fail.
OK I get it. The server authorizes the client, and takes a promise in the form of a hash. In the POST to the /oauth/token endpoint, the client provides the source of the hash and if accepted gets its tokens.
Brilliant, elegant design.
This really helped me to understand, thanks again Phillippe.