Skip to content

Commit e53f2cb

Browse files
committed
OAuth2 - PKCE
* 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
1 parent 35bc27b commit e53f2cb

File tree

2 files changed

+38
-4
lines changed

2 files changed

+38
-4
lines changed

lib/omniauth/strategies/oauth2.rb

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ def self.inherited(subclass)
2929
option :token_options, []
3030
option :auth_token_params, {}
3131
option :provider_ignores_state, false
32+
option :pkce, false
3233

3334
attr_accessor :access_token
3435

@@ -49,18 +50,33 @@ def request_phase
4950
end
5051

5152
def authorize_params
53+
verifier = SecureRandom.hex(64)
54+
55+
if options.pkce
56+
# NOTE: see https://tools.ietf.org/html/rfc7636#appendix-A
57+
challenge = Base64
58+
.urlsafe_encode64(Digest::SHA2.digest(verifier))
59+
.split("=")
60+
.first
61+
options.authorize_params[:code_challenge] = challenge
62+
options.authorize_params[:code_challenge_method] = "S256"
63+
end
64+
5265
options.authorize_params[:state] = SecureRandom.hex(24)
5366
params = options.authorize_params.merge(options_for("authorize"))
67+
5468
if OmniAuth.config.test_mode
5569
@env ||= {}
5670
@env["rack.session"] ||= {}
5771
end
72+
73+
session["omniauth.pkce.verifier"] = verifier if options.pkce
5874
session["omniauth.state"] = params[:state]
5975
params
6076
end
6177

6278
def token_params
63-
options.token_params.merge(options_for("token"))
79+
options.token_params.merge(options_for("token")).merge(pkce_token_params)
6480
end
6581

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

85101
protected
86102

103+
def pkce_token_params
104+
return {} unless options.pkce
105+
106+
{ code_verifier: session.delete("omniauth.pkce.verifier") }
107+
end
108+
87109
def build_access_token
88110
verifier = request.params["code"]
89111
client.auth_code.get_token(verifier, {:redirect_uri => callback_url}.merge(token_params.to_hash(:symbolize_keys => true)), deep_symbolize(options.auth_token_params))
90112
end
91113

92114
def deep_symbolize(options)
93-
hash = {}
94-
options.each do |key, value|
115+
options.each_with_object({}) do |(key, value), hash|
95116
hash[key.to_sym] = value.is_a?(Hash) ? deep_symbolize(value) : value
96117
end
97-
hash
98118
end
99119

100120
def options_for(option)

spec/omniauth/strategies/oauth2_spec.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,13 @@ def app
6666
expect(instance.authorize_params.keys).to eq(["state"])
6767
expect(instance.session["omniauth.state"]).to eq("qux")
6868
end
69+
70+
it "includes PKCE parameters if enabled" do
71+
instance = subject.new("abc", "def", pkce: true)
72+
expect(instance.authorize_params[:code_challenge]).to be_a(String)
73+
expect(instance.authorize_params[:code_challenge_method]).to eq("S256")
74+
expect(instance.session["omniauth.pkce.verifier"]).to be_a(String)
75+
end
6976
end
7077

7178
describe "#token_params" do
@@ -80,6 +87,13 @@ def app
8087
instance = subject.new("abc", "def", :token_options => %i[scope foo], :scope => "bar", :foo => "baz")
8188
expect(instance.token_params).to eq("scope" => "bar", "foo" => "baz")
8289
end
90+
91+
it "includes the PKCE code_verifier if enabled" do
92+
instance = subject.new("abc", "def", pkce: true)
93+
# setup session
94+
instance.authorize_params
95+
expect(instance.token_params[:code_verifier]).to be_a(String)
96+
end
8397
end
8498

8599
describe "#callback_phase" do

0 commit comments

Comments
 (0)