From c4bde552e648265d287384973aca3d5198697fe6 Mon Sep 17 00:00:00 2001 From: Tim Emiola Date: Mon, 23 Mar 2015 21:06:26 -0700 Subject: [PATCH] Promote ServiceAccountCredentials when scope is nil - if the ServiceAccountCredentials#apply is called when the scope is not set, authentication with the equivalent ServiceAccountJwtHeaderCredentials instance is attempted. --- lib/googleauth/service_account.rb | 26 ++++- spec/googleauth/apply_auth_examples.rb | 12 +- spec/googleauth/service_account_spec.rb | 146 +++++++++++++----------- 3 files changed, 109 insertions(+), 75 deletions(-) diff --git a/lib/googleauth/service_account.rb b/lib/googleauth/service_account.rb index 8018e2e..c74d3cc 100644 --- a/lib/googleauth/service_account.rb +++ b/lib/googleauth/service_account.rb @@ -31,6 +31,7 @@ require 'googleauth/signet' require 'googleauth/credentials_loader' require 'jwt' require 'multi_json' +require 'stringio' module Google # Module Auth provides classes that provide Google-specific authorization @@ -69,6 +70,29 @@ module Google issuer: client_email, signing_key: OpenSSL::PKey::RSA.new(private_key)) end + + # Extends the base class. + # + # If scope(s) is not set, it creates a transient + # ServiceAccountJwtHeaderCredentials instance and uses that to + # authenticate instead. + def apply!(a_hash, opts = {}) + # Use the base implementation if scopes are set + unless scope.nil? + super + return + end + + # Use the ServiceAccountJwtHeaderCredentials using the same cred values + # if no scopes are set. + cred_json = { + private_key: @signing_key.to_s, + client_email: @issuer + } + alt_clz = ServiceAccountJwtHeaderCredentials + alt = alt_clz.new(StringIO.new(MultiJson.dump(cred_json))) + alt.apply!(a_hash) + end end # Authenticates requests using Google's Service Account credentials via @@ -118,7 +142,7 @@ module Google @signing_key = OpenSSL::PKey::RSA.new(private_key) end - # Construct a jwt token if the WT_AUD_URI key is present in the input + # Construct a jwt token if the JWT_AUD_URI key is present in the input # hash. # # The jwt token is used as the value of a 'Bearer '. diff --git a/spec/googleauth/apply_auth_examples.rb b/spec/googleauth/apply_auth_examples.rb index 17d4036..a0eca97 100644 --- a/spec/googleauth/apply_auth_examples.rb +++ b/spec/googleauth/apply_auth_examples.rb @@ -47,7 +47,7 @@ def build_access_token_json(token) end shared_examples 'apply/apply! are OK' do - WANTED_AUTH_KEY = :Authorization + let(:auth_key) { :Authorization } # tests that use these examples need to define # @@ -79,7 +79,7 @@ shared_examples 'apply/apply! are OK' do md = { foo: 'bar' } @client.apply!(md, connection: c) - want = { :foo => 'bar', WANTED_AUTH_KEY => "Bearer #{token}" } + want = { :foo => 'bar', auth_key => "Bearer #{token}" } expect(md).to eq(want) stubs.verify_stubbed_calls end @@ -96,7 +96,7 @@ shared_examples 'apply/apply! are OK' do md = { foo: 'bar' } the_proc = @client.updater_proc got = the_proc.call(md, connection: c) - want = { :foo => 'bar', WANTED_AUTH_KEY => "Bearer #{token}" } + want = { :foo => 'bar', auth_key => "Bearer #{token}" } expect(got).to eq(want) stubs.verify_stubbed_calls end @@ -126,7 +126,7 @@ shared_examples 'apply/apply! are OK' do md = { foo: 'bar' } got = @client.apply(md, connection: c) - want = { :foo => 'bar', WANTED_AUTH_KEY => "Bearer #{token}" } + want = { :foo => 'bar', auth_key => "Bearer #{token}" } expect(got).to eq(want) stubs.verify_stubbed_calls end @@ -142,7 +142,7 @@ shared_examples 'apply/apply! are OK' do n.times do |_t| md = { foo: 'bar' } got = @client.apply(md, connection: c) - want = { :foo => 'bar', WANTED_AUTH_KEY => "Bearer #{token}" } + want = { :foo => 'bar', auth_key => "Bearer #{token}" } expect(got).to eq(want) end stubs.verify_stubbed_calls @@ -159,7 +159,7 @@ shared_examples 'apply/apply! are OK' do end md = { foo: 'bar' } got = @client.apply(md, connection: c) - want = { :foo => 'bar', WANTED_AUTH_KEY => "Bearer #{t}" } + want = { :foo => 'bar', auth_key => "Bearer #{t}" } expect(got).to eq(want) stubs.verify_stubbed_calls @client.expires_at -= 3601 # default is to expire in 1hr diff --git a/spec/googleauth/service_account_spec.rb b/spec/googleauth/service_account_spec.rb index ee0f535..7d5570a 100644 --- a/spec/googleauth/service_account_spec.rb +++ b/spec/googleauth/service_account_spec.rb @@ -40,10 +40,73 @@ require 'openssl' require 'spec_helper' require 'tmpdir' +include Google::Auth::CredentialsLoader + +shared_examples 'jwt header auth' do + context 'when jwt_aud_uri is present' do + let(:test_uri) { 'https://www.googleapis.com/myservice' } + let(:auth_prefix) { 'Bearer ' } + let(:auth_key) { ServiceAccountJwtHeaderCredentials::AUTH_METADATA_KEY } + let(:jwt_uri_key) { ServiceAccountJwtHeaderCredentials::JWT_AUD_URI_KEY } + + def expect_is_encoded_jwt(hdr) + expect(hdr).to_not be_nil + expect(hdr.start_with?(auth_prefix)).to be true + authorization = hdr[auth_prefix.length..-1] + payload, _ = JWT.decode(authorization, @key.public_key) + expect(payload['aud']).to eq(test_uri) + expect(payload['iss']).to eq(client_email) + end + + describe '#apply!' do + it 'should update the target hash with a jwt token' do + md = { foo: 'bar' } + md[jwt_uri_key] = test_uri + @client.apply!(md) + auth_header = md[auth_key] + expect_is_encoded_jwt(auth_header) + expect(md[jwt_uri_key]).to be_nil + end + end + + describe 'updater_proc' do + it 'should provide a proc that updates a hash with a jwt token' do + md = { foo: 'bar' } + md[jwt_uri_key] = test_uri + the_proc = @client.updater_proc + got = the_proc.call(md) + auth_header = got[auth_key] + expect_is_encoded_jwt(auth_header) + expect(got[jwt_uri_key]).to be_nil + expect(md[jwt_uri_key]).to_not be_nil + end + end + + describe '#apply' do + it 'should not update the original hash with a jwt token' do + md = { foo: 'bar' } + md[jwt_uri_key] = test_uri + the_proc = @client.updater_proc + got = the_proc.call(md) + auth_header = md[auth_key] + expect(auth_header).to be_nil + expect(got[jwt_uri_key]).to be_nil + expect(md[jwt_uri_key]).to_not be_nil + end + + it 'should add a jwt token to the returned hash' do + md = { foo: 'bar' } + md[jwt_uri_key] = test_uri + got = @client.apply(md) + auth_header = got[auth_key] + expect_is_encoded_jwt(auth_header) + end + end + end +end + describe Google::Auth::ServiceAccountCredentials do ServiceAccountCredentials = Google::Auth::ServiceAccountCredentials - CredentialsLoader = Google::Auth::CredentialsLoader - let(:client_email) { 'app@developer.gserviceaccount.com' } before(:example) do @@ -80,9 +143,17 @@ describe Google::Auth::ServiceAccountCredentials do it_behaves_like 'apply/apply! are OK' + context 'when scope is nil' do + before(:example) do + @client.scope = nil + end + + it_behaves_like 'jwt header auth' + end + describe '#from_env' do before(:example) do - @var_name = CredentialsLoader::ENV_VAR + @var_name = ENV_VAR @orig = ENV[@var_name] @scope = 'https://www.googleapis.com/auth/userinfo.profile' @clz = ServiceAccountCredentials @@ -122,7 +193,7 @@ describe Google::Auth::ServiceAccountCredentials do before(:example) do @home = ENV['HOME'] @scope = 'https://www.googleapis.com/auth/userinfo.profile' - @known_path = CredentialsLoader::WELL_KNOWN_PATH + @known_path = WELL_KNOWN_PATH @clz = ServiceAccountCredentials end @@ -148,7 +219,6 @@ describe Google::Auth::ServiceAccountCredentials do end describe Google::Auth::ServiceAccountJwtHeaderCredentials do - CredentialsLoader = Google::Auth::CredentialsLoader ServiceAccountJwtHeaderCredentials = Google::Auth::ServiceAccountJwtHeaderCredentials @@ -171,70 +241,11 @@ describe Google::Auth::ServiceAccountJwtHeaderCredentials do MultiJson.dump(cred_json) end - context 'when jwt_aud_uri is present' do - WANTED_AUTH_KEY = ServiceAccountJwtHeaderCredentials::AUTH_METADATA_KEY - JWT_AUD_URI_KEY = ServiceAccountJwtHeaderCredentials::JWT_AUD_URI_KEY - let(:test_uri) { 'https://www.googleapis.com/myservice' } - let(:auth_prefix) { 'Bearer ' } - - def expect_is_encoded_jwt(hdr) - expect(hdr).to_not be_nil - expect(hdr.start_with?(auth_prefix)).to be true - authorization = hdr[auth_prefix.length..-1] - payload, _ = JWT.decode(authorization, @key.public_key) - expect(payload['aud']).to eq(test_uri) - expect(payload['iss']).to eq(client_email) - end - - describe '#apply!' do - it 'should update the target hash with a jwt token' do - md = { foo: 'bar' } - md[JWT_AUD_URI_KEY] = test_uri - @client.apply!(md) - auth_header = md[WANTED_AUTH_KEY] - expect_is_encoded_jwt(auth_header) - expect(md[JWT_AUD_URI_KEY]).to be_nil - end - end - - describe 'updater_proc' do - it 'should provide a proc that updates a hash with a jwt token' do - md = { foo: 'bar' } - md[JWT_AUD_URI_KEY] = test_uri - the_proc = @client.updater_proc - got = the_proc.call(md) - auth_header = got[WANTED_AUTH_KEY] - expect_is_encoded_jwt(auth_header) - expect(got[JWT_AUD_URI_KEY]).to be_nil - expect(md[JWT_AUD_URI_KEY]).to_not be_nil - end - end - - describe '#apply' do - it 'should not update the original hash with a jwt token' do - md = { foo: 'bar' } - md[JWT_AUD_URI_KEY] = test_uri - the_proc = @client.updater_proc - got = the_proc.call(md) - auth_header = md[WANTED_AUTH_KEY] - expect(auth_header).to be_nil - expect(got[JWT_AUD_URI_KEY]).to be_nil - expect(md[JWT_AUD_URI_KEY]).to_not be_nil - end - - it 'should add a jwt token to the returned hash' do - md = { foo: 'bar' } - md[JWT_AUD_URI_KEY] = test_uri - got = @client.apply(md) - auth_header = got[WANTED_AUTH_KEY] - expect_is_encoded_jwt(auth_header) - end - end - end + it_behaves_like 'jwt header auth' describe '#from_env' do before(:example) do - @var_name = CredentialsLoader::ENV_VAR + @var_name = ENV_VAR @orig = ENV[@var_name] end @@ -271,7 +282,6 @@ describe Google::Auth::ServiceAccountJwtHeaderCredentials do describe '#from_well_known_path' do before(:example) do @home = ENV['HOME'] - @known_path = CredentialsLoader::WELL_KNOWN_PATH end after(:example) do @@ -285,7 +295,7 @@ describe Google::Auth::ServiceAccountJwtHeaderCredentials do it 'successfully loads the file when it is present' do Dir.mktmpdir do |dir| - key_path = File.join(dir, '.config', @known_path) + key_path = File.join(dir, '.config', WELL_KNOWN_PATH) FileUtils.mkdir_p(File.dirname(key_path)) File.write(key_path, cred_json_text) ENV['HOME'] = dir