Skip to content

feat: support PKCE code_verifier in exchange_auth_code_for_tokens - #791

Open
gagalago wants to merge 1 commit into
auth0:masterfrom
gagalago:feat/pkce-code-verifier
Open

feat: support PKCE code_verifier in exchange_auth_code_for_tokens#791
gagalago wants to merge 1 commit into
auth0:masterfrom
gagalago:feat/pkce-code-verifier

Conversation

@gagalago

Copy link
Copy Markdown

Changes

Adds an optional code_verifier keyword to Auth0::Api::AuthenticationEndpoints#exchange_auth_code_for_tokens, so the Authorization Code Flow with PKCE can be completed through the SDK.

def exchange_auth_code_for_tokens(
  code,
  redirect_uri: nil,
  client_id: @client_id,
  client_secret: @client_secret,
  code_verifier: nil
)

The parameter is sent only when provided:

request_params[:code_verifier] = code_verifier unless code_verifier.nil?

No endpoint is added, removed or changed. POST /oauth/token is already the target; this only allows one more documented body parameter to reach it. The keyword is appended last and defaults to nil, so every existing call site behaves exactly as before — when code_verifier is omitted the request body is byte-for-byte what it is today.

Usage, completing the flow that authorization_url can already start via additional_parameters:

code_verifier = Base64.urlsafe_encode64(SecureRandom.hex(32), padding: false)
code_challenge = Base64.urlsafe_encode64(Digest::SHA256.digest(code_verifier), padding: false)

client = Auth0Client.new(
  client_id: client_id,
  client_secret: client_secret,
  domain: domain,
  additional_parameters: {
    code_challenge: code_challenge,
    code_challenge_method: "S256"
  }
)

# Send the user to client.authorization_url(redirect_uri), then exchange the code:
tokens = client.exchange_auth_code_for_tokens(
  code,
  redirect_uri: redirect_uri,
  code_verifier: code_verifier
)

References

In #305 a maintainer wrote that "code_verifier specifically should be a first-class parameter on that method, and should be optional", which is the shape implemented here. That thread also documented a workaround that reaches request_with_retry directly; it still works, but it depends on a method outside the documented public API.

code_verifier does not currently appear anywhere in the repository, so as of v6.1.0 there is no supported way to perform a PKCE exchange. For reference, node-auth0 exposes this as authorizationCodeGrantWithPKCE, which requires code and code_verifier.

Testing

Two cases added to test/unit/authentication_endpoints_test.rb, next to the existing exchange_auth_code_for_tokens tests and using the same WebMock body-matching style:

  • test_exchange_auth_code_for_tokens_with_code_verifier — asserts code_verifier is present in the request body when passed
  • test_exchange_auth_code_for_tokens_omits_code_verifier_when_not_provided — asserts the key is absent from the body when it is not, which is what protects existing callers

To run them:

bundle exec rake test TEST=test/unit/authentication_endpoints_test.rb TESTOPTS="--name=/code_verifier/"

I checked that the first test actually fails if the one-line implementation is removed, so it is not a test that passes regardless.

Full suite on Ruby 3.3.9, before and after: 554 runs, 4582 assertions, 0 failures, 0 errors.

Unrelated heads-up while testing on Ruby 4.0.6: the full suite reports one error there, NoMethodError: undefined method 'parse' for class CGI, from CGI.parse at test/unit/authentication_endpoints_test.rb:480 on master. CGI.parse was removed in Ruby 4.0. It reproduces on a clean checkout of master without this branch, and nothing in this change touches CGI — flagging it only because required_ruby_version is >= 3.3.0 with no upper bound, so Ruby 4 users will hit it. Happy to open a separate issue or pull request for that if useful.

One note on style: lib/auth0/api/authentication_endpoints.rb uses single-quoted strings throughout and is listed under AllCops.Exclude in .rubocop.yml ("Files ported verbatim from the legacy ruby-auth0 SDK... Excluded to preserve exact compatibility with the original source"), so I followed the surrounding convention rather than the double_quotes style enforced elsewhere. bundle exec rubocop reports the same 134 offences with and without this branch — it introduces none.

  • This change adds unit test coverage
  • This change adds integration test coverage
  • This change has been tested on the latest version of Ruby

The two new tests pass on both Ruby 3.3.9 and Ruby 4.0.6 (2 runs, 4 assertions, 0 failures).

Checklist

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

Successfully merging this pull request may close these issues.

1 participant