Skip to content

Commit

Permalink
feat: add PKCE to 3LO exchange
Browse files Browse the repository at this point in the history
  • Loading branch information
bajajneha27 committed Jan 29, 2024
1 parent d27eea5 commit 4ce195f
Show file tree
Hide file tree
Showing 4 changed files with 82 additions and 3 deletions.
50 changes: 49 additions & 1 deletion lib/googleauth/user_authorizer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
require "multi_json"
require "googleauth/signet"
require "googleauth/user_refresh"
require "securerandom"

module Google
module Auth
Expand Down Expand Up @@ -57,14 +58,19 @@ class UserAuthorizer
# @param [String] callback_uri
# URL (either absolute or relative) of the auth callback.
# Defaults to '/oauth2callback'
def initialize client_id, scope, token_store, callback_uri = nil
# @param [String] code_verifier
# Random string of 43-128 chars used to verify the key exchange using
# PKCE. Auto-generated if not provided
def initialize client_id, scope, token_store,
callback_uri = nil, code_verifier = nil
raise NIL_CLIENT_ID_ERROR if client_id.nil?
raise NIL_SCOPE_ERROR if scope.nil?

@client_id = client_id
@scope = Array(scope)
@token_store = token_store
@callback_uri = callback_uri || "/oauth2callback"
@code_verifier = code_verifier
end

# Build the URL for requesting authorization.
Expand All @@ -86,6 +92,18 @@ def initialize client_id, scope, token_store, callback_uri = nil
# Authorization url
def get_authorization_url options = {}
scope = options[:scope] || @scope

options[:additional_parameters] ||= {}

if @code_verifier
options[:additional_parameters].merge!(
{
code_challenge: generate_code_challenge(@code_verifier),
code_challenge_method: get_code_challenge_method
}
)
end

credentials = UserRefreshCredentials.new(
client_id: @client_id.id,
client_secret: @client_id.secret,
Expand Down Expand Up @@ -157,6 +175,8 @@ def get_credentials_from_code options = {}
code = options[:code]
scope = options[:scope] || @scope
base_url = options[:base_url]
options[:additional_parameters] ||= {}
options[:additional_parameters].merge!({"code_verifier": @code_verifier})
credentials = UserRefreshCredentials.new(
client_id: @client_id.id,
client_secret: @client_id.secret,
Expand Down Expand Up @@ -228,6 +248,25 @@ def store_credentials user_id, credentials
credentials
end

# A cryptographically random string that is used to correlate the
# authorization request to the token request.
# The code verifier for PKCE for OAuth 2.0. When set, the
# authorization URI will contain the Code Challenge and Code
# Challenge Method querystring parameters, and the token URI will
# contain the Code Verifier parameter.
# @param [String|nil] new_code_erifier
def code_verifier= new_code_verifier
@code_verifier = new_code_verifier
end

# Generate the code verifier needed to be sent while fetching
# authorization URL.
def generate_code_verifier
random_number = rand 32..96
res = SecureRandom.alphanumeric(random_number).to_str
res
end

private

# @private Fetch stored token with given user_id
Expand Down Expand Up @@ -272,6 +311,15 @@ def redirect_uri_for base_url
def uri_is_postmessage? uri
uri.to_s.casecmp("postmessage").zero?
end

def generate_code_challenge code_verifier
digest = Digest::SHA256.digest code_verifier
Base64.urlsafe_encode64 digest, padding: false
end

def get_code_challenge_method
"S256"
end
end
end
end
8 changes: 6 additions & 2 deletions lib/googleauth/web_user_authorizer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,12 @@ def self.handle_auth_callback_deferred request
# @param [String] callback_uri
# URL (either absolute or relative) of the auth callback. Defaults
# to '/oauth2callback'
def initialize client_id, scope, token_store, callback_uri = nil
super client_id, scope, token_store, callback_uri
# @param [String] code_verifier
# Random string of 43-128 chars used to verify the key exchange using
# PKCE. Auto-generated if not provided.
def initialize client_id, scope, token_store,
callback_uri = nil, code_verifier = nil
super client_id, scope, token_store, callback_uri, code_verifier
end

# Handle the result of the oauth callback. Exchanges the authorization
Expand Down
21 changes: 21 additions & 0 deletions spec/googleauth/user_authorizer_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@
it "should include the scope" do
expect(URI(uri).query).to match(/scope=email%20profile/)
end

it "should include code_challenge and code_challenge_method" do
expect(URI(uri).query).to match(/code_challenge=/)
expect(URI(uri).query).to match(/code_challenge_method=S256/)
end
end

context "when generating authorization URLs and callback_uri is 'postmessage'" do
Expand All @@ -98,6 +103,22 @@
end
end

context "when generating authorization URLs and code_verifier is manually passed" do
let(:code_verifier) { "IeJRY4uem0581Lcw6CiZ3fNwngg" }
let :authorizer do
Google::Auth::UserAuthorizer.new(client_id,
scope,
token_store,
callback_uri,
code_verifier)
end
let :uri do
authorizer.get_authorization_url
end

it_behaves_like "valid authorization url"
end

context "when generating authorization URLs with user ID & state" do
let :uri do
authorizer.get_authorization_url login_hint: "user1", state: "mystate"
Expand Down
6 changes: 6 additions & 0 deletions spec/googleauth/web_user_authorizer_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,12 @@
)
expect(url).to match(/login_hint=user@example.com/)
end

it "should include code_challenge and code_challenge_method" do
url = authorizer.get_authorization_url(request: request)
expect(url).to match(/code_challenge=/)
expect(url).to match(/code_challenge_method=S256/)
end
end

shared_examples "handles callback" do
Expand Down

0 comments on commit 4ce195f

Please sign in to comment.