Skip to content

Commit

Permalink
OAuth2 - PKCE
Browse files Browse the repository at this point in the history
* Add a `pkce` option to the oauth2 strategy that defaults
  to `false`.
* When the option is true, the client will authorize with the
  provider using PKCE (proof key for code exchange) [1]. This
  enhances the security footprint of the interaction and is now
  recommended by the IETF for all OAuth2 code grant interactions.
* At a high level, PKCE works as follows:
  1. Generate a new random code verifier string value with a
     minimum length of 43 characters and a maximum length of
     128 characters.
  2. Take the SHA256 hash value of the code verifier string and
     perform a URL-safe Base64 encode of the result as defined
     in [2].
  3. Pass `code_challenge={Base64(SHA256(code_verifier)}`
     and `code_challenge_method=S256` query parameters with
     the client OAuth2 authorize request.
  4. In the callback_phase, pass the `code_verifier` in plaintext
     to the provider as a query parameter to the OAuth2 token
     endpoint. This provides strong guarantees to the OAuth provider
     that the client is the same entity that requested authorization.

[1]: https://tools.ietf.org/html/rfc7636
[2]: https://tools.ietf.org/html/rfc7636#appendix-A
  • Loading branch information
jessedoyle committed Jun 23, 2020
1 parent 35bc27b commit e53f2cb
Show file tree
Hide file tree
Showing 2 changed files with 38 additions and 4 deletions.
28 changes: 24 additions & 4 deletions lib/omniauth/strategies/oauth2.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ def self.inherited(subclass)
option :token_options, []
option :auth_token_params, {}
option :provider_ignores_state, false
option :pkce, false

attr_accessor :access_token

Expand All @@ -49,18 +50,33 @@ def request_phase
end

def authorize_params
verifier = SecureRandom.hex(64)

if options.pkce
# NOTE: see https://tools.ietf.org/html/rfc7636#appendix-A
challenge = Base64
.urlsafe_encode64(Digest::SHA2.digest(verifier))
.split("=")
.first
options.authorize_params[:code_challenge] = challenge
options.authorize_params[:code_challenge_method] = "S256"
end

options.authorize_params[:state] = SecureRandom.hex(24)
params = options.authorize_params.merge(options_for("authorize"))

if OmniAuth.config.test_mode
@env ||= {}
@env["rack.session"] ||= {}
end

session["omniauth.pkce.verifier"] = verifier if options.pkce
session["omniauth.state"] = params[:state]
params
end

def token_params
options.token_params.merge(options_for("token"))
options.token_params.merge(options_for("token")).merge(pkce_token_params)
end

def callback_phase # rubocop:disable AbcSize, CyclomaticComplexity, MethodLength, PerceivedComplexity
Expand All @@ -84,17 +100,21 @@ def callback_phase # rubocop:disable AbcSize, CyclomaticComplexity, MethodLength

protected

def pkce_token_params
return {} unless options.pkce

{ code_verifier: session.delete("omniauth.pkce.verifier") }
end

def build_access_token
verifier = request.params["code"]
client.auth_code.get_token(verifier, {:redirect_uri => callback_url}.merge(token_params.to_hash(:symbolize_keys => true)), deep_symbolize(options.auth_token_params))
end

def deep_symbolize(options)
hash = {}
options.each do |key, value|
options.each_with_object({}) do |(key, value), hash|
hash[key.to_sym] = value.is_a?(Hash) ? deep_symbolize(value) : value
end
hash
end

def options_for(option)
Expand Down
14 changes: 14 additions & 0 deletions spec/omniauth/strategies/oauth2_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,13 @@ def app
expect(instance.authorize_params.keys).to eq(["state"])
expect(instance.session["omniauth.state"]).to eq("qux")
end

it "includes PKCE parameters if enabled" do
instance = subject.new("abc", "def", pkce: true)
expect(instance.authorize_params[:code_challenge]).to be_a(String)
expect(instance.authorize_params[:code_challenge_method]).to eq("S256")
expect(instance.session["omniauth.pkce.verifier"]).to be_a(String)
end
end

describe "#token_params" do
Expand All @@ -80,6 +87,13 @@ def app
instance = subject.new("abc", "def", :token_options => %i[scope foo], :scope => "bar", :foo => "baz")
expect(instance.token_params).to eq("scope" => "bar", "foo" => "baz")
end

it "includes the PKCE code_verifier if enabled" do
instance = subject.new("abc", "def", pkce: true)
# setup session
instance.authorize_params
expect(instance.token_params[:code_verifier]).to be_a(String)
end
end

describe "#callback_phase" do
Expand Down

0 comments on commit e53f2cb

Please sign in to comment.