Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auth Code flow with incorrect PKCE code_challenge works correctly on the 2nd attempt #815

Open
micolous opened this issue Feb 26, 2025 · 1 comment

Comments

@micolous
Copy link

micolous commented Feb 26, 2025

Edit (2025-03-21): I've discovered a mistake in my PKCE implementation: the code_challenge value included Base64 padding. Removing that padding fixes the issue, but it looks like mock-oauth2-server accepts incorrect code_challenge values on the 2nd attempt, and there are some usability improvements it could do.

Original report

I'm using mock-oauth2-server in its Docker container to test the server side of a Python application server which is a token-mediating backend + resource server hybrid for a browser-based single-page application.

When attempting to acquire a token with the Authorisation Code flow with PKCE (as a public client), mock-oauth2-server only works correctly on the 2nd attempt.

I've made a script which replicates a minimal subset of my server's test suite and auth logic to demonstrate the issue, which sends 5 identical token requests, but with their own cookie jars (to simulate multiple API calls): https://gist.github.com/micolous/b9c5bbd2964bd5488550a9bda12ece9c

Expected behaviour

The first (and some number of following) attempt(s) works, and all subsequent attempts (perhaps after a delay) fail.

Allowing more than one request to work correctly may be helpful (see additional notes).

Actual behaviour

On the 1st attempt, mock-oauth2-server returns:

{'error_description': 'invalid_pkce: code_verifier does not compute to code_challenge from request',
 'error': 'invalid_grant'}

The 2nd attempt works correctly, with all claims set as configured.

On the 3rd and following attempts, mock-oauth2-server returns a valid but incorrect access_token and id_token, where it:

  • sets sub to a random UUID, rather than the value supplied in the username parameter to the login form
  • drops any parameters passed via the claims parameter to the login form
  • drops any parameters passed via the tokenCallbacks[].requestMappings[].claims config option

I suspect mock-oauth2-server is stashing some state from the /authorize request, but then it can't fetch it on the 1st attempt, and then deletes it after the 2nd attempt.

Workarounds

I've made my server side retry any token request twice, and fail if id_token is missing claims.

Additional notes

I never noticed this issue when testing with the SPA, because React's strict mode causes useEffect to run twice (and thus, any API call in a useEffect would also trigger twice). So the first call it made to complete the auth process would fail, but then the second request would be fine.

When I worked around this by adding a retry to the server side, React was still making two attempts, and that's where I found that requesting a token from mock-oauth2-server a 3rd time returns a token without any of the claims I expected.

Environment

mock-oauth2-server 2.1.10 using the Docker container on linux/aarch64, with this config:

{
  "httpServer": {
    "type": "NettyWrapper",
    "ssl": {
      "keyPassword": "",
      "keystoreFile": "/run/secrets/server_p12",
      "keystoreType": "PKCS12",
      "keystorePassword": ""
    }
  }
}

The keystore file is provided via Docker Secrets.

The example script will accept any certificate it is given. There is a commented out option to validate certificates properly.

@micolous
Copy link
Author

micolous commented Mar 20, 2025

After testing with some other OAuth 2.0 providers, I've found a mistake in my PKCE implementation: the code_challenge includes Base64 padding. After removing the extra = from the end of the code_challenge, PKCE now succeeds on the 1st attempt only. So that's good. 😄

My suggestion here would be that the /authorize endpoint should try to decode Base64, and refuse the request if:

  • the Base64-encoded value contains padding
  • it was encoded with "regular" Base64 rather than URL-safe Base64
  • the value is not the correct length for the given code_challenge_method

It working on the 2nd try only seemed strange to me, so I updated my script to hard code code_challenge to a fixed value (without padding), while still generating a random code_verifier – which should always fail PKCE checks.

However, mock-oauth2-server seems to accept the token request on the second attempt only. That's not good – and if it was in a real OAuth 2.0 provider, that'd be a major security hole.

I don't know enough about this code base to tell whether this caused by something mock-oauth2-server is doing, or there is a bug in the Nimbus SDK (which would be a security hole if used in a real OAuth 2.0 server).

New test script, with more options, which helped me discover this bug, #825 and #826: https://gist.github.com/micolous/e54b84dec86fcc45754c5c429ed834c4

@micolous micolous changed the title Auth Code flow with PKCE only works correctly on the 2nd attempt (not the 1st, 3rd, 4th, etc.) Auth Code flow with incorrect PKCE code_challenge works correctly on the 2nd attempt (not the 1st, 3rd, 4th, etc.) Mar 20, 2025
@micolous micolous changed the title Auth Code flow with incorrect PKCE code_challenge works correctly on the 2nd attempt (not the 1st, 3rd, 4th, etc.) Auth Code flow with incorrect PKCE code_challenge works correctly on the 2nd attempt Mar 20, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant