From e3e33e2c380ed87c6da3e4ee20f3580b30c8c20e Mon Sep 17 00:00:00 2001 From: Tim Emiola Date: Fri, 6 Mar 2015 19:52:08 -0800 Subject: [PATCH 1/3] Adds an implementation of JWT signing to ServiceAccountCredentials --- .rubocop_todo.yml | 8 +-- lib/googleauth/service_account.rb | 31 ++++++++++++ spec/googleauth/apply_auth_examples.rb | 4 +- spec/googleauth/service_account_spec.rb | 65 ++++++++++++++++++++++++- 4 files changed, 101 insertions(+), 7 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 543d13f..8c99036 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,15 +1,15 @@ # This configuration was generated by `rubocop --auto-gen-config` -# on 2015-02-25 04:34:33 -0800 using RuboCop version 0.28.0. +# on 2015-03-06 19:51:00 -0800 using RuboCop version 0.28.0. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. -# Offense count: 1 +# Offense count: 3 Metrics/AbcSize: - Max: 16 + Max: 24 -# Offense count: 1 +# Offense count: 3 # Configuration parameters: CountComments. Metrics/MethodLength: Max: 11 diff --git a/lib/googleauth/service_account.rb b/lib/googleauth/service_account.rb index 66d7be6..076f5b1 100644 --- a/lib/googleauth/service_account.rb +++ b/lib/googleauth/service_account.rb @@ -29,6 +29,7 @@ require 'googleauth/signet' require 'googleauth/credentials_loader' +require 'jwt' require 'multi_json' module Google @@ -43,6 +44,8 @@ module Google # # cf [Application Default Credentials](http://goo.gl/mkAHpZ) class ServiceAccountCredentials < Signet::OAuth2::Client + JWT_AUD_URI_KEY = :jwt_aud_uri + AUTH_METADATA_KEY = Signet::OAuth2::AUTH_METADATA_KEY TOKEN_CRED_URI = 'https://www.googleapis.com/oauth2/v3/token' extend CredentialsLoader @@ -67,6 +70,34 @@ module Google issuer: client_email, signing_key: OpenSSL::PKey::RSA.new(private_key)) end + + # Extends the superclass behaviour to construct a jwt token if the + # google jwt uri key is present in the input hash. + # + # The jwt is used as the authentication token. + def apply!(a_hash, opts = {}) + jwt_aud_uri = a_hash.delete(JWT_AUD_URI_KEY) + unless jwt_aud_uri.nil? + jwt_token = new_jwt_token(jwt_aud_uri, opts) + a_hash[AUTH_METADATA_KEY] = "Bearer #{jwt_token}" + return a_hash + end + super + end + + # Creates a jwt uri token. + def new_jwt_token(jwt_aud_uri, options = {}) + now = Time.new + skew = options[:skew] || 60 + assertion = { + 'iss' => issuer, + 'sub' => issuer, + 'aud' => jwt_aud_uri, + 'exp' => (now + expiry).to_i, + 'iat' => (now - skew).to_i + } + JWT.encode(assertion, signing_key, signing_algorithm) + end end end end diff --git a/spec/googleauth/apply_auth_examples.rb b/spec/googleauth/apply_auth_examples.rb index fcafa9e..17d4036 100644 --- a/spec/googleauth/apply_auth_examples.rb +++ b/spec/googleauth/apply_auth_examples.rb @@ -46,9 +46,9 @@ def build_access_token_json(token) 'expires_in' => 3600) end -WANTED_AUTH_KEY = :Authorization - shared_examples 'apply/apply! are OK' do + WANTED_AUTH_KEY = :Authorization + # tests that use these examples need to define # # @client which should be an auth client diff --git a/spec/googleauth/service_account_spec.rb b/spec/googleauth/service_account_spec.rb index 108e2b0..6992b36 100644 --- a/spec/googleauth/service_account_spec.rb +++ b/spec/googleauth/service_account_spec.rb @@ -44,6 +44,8 @@ describe Google::Auth::ServiceAccountCredentials do ServiceAccountCredentials = Google::Auth::ServiceAccountCredentials CredentialsLoader = Google::Auth::CredentialsLoader + let(:client_email) { 'app@developer.gserviceaccount.com' } + before(:example) do @key = OpenSSL::PKey::RSA.new(2048) @client = ServiceAccountCredentials.new( @@ -69,7 +71,7 @@ describe Google::Auth::ServiceAccountCredentials do cred_json = { private_key_id: 'a_private_key_id', private_key: @key.to_pem, - client_email: 'app@developer.gserviceaccount.com', + client_email: client_email, client_id: 'app.apps.googleusercontent.com', type: 'service_account' } @@ -78,6 +80,67 @@ describe Google::Auth::ServiceAccountCredentials do it_behaves_like 'apply/apply! are OK' + context 'when jwt_aud_uri is present' do + WANTED_AUTH_KEY = ServiceAccountCredentials::AUTH_METADATA_KEY + JWT_AUD_URI_KEY = ServiceAccountCredentials::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 + describe '#from_env' do before(:example) do @var_name = CredentialsLoader::ENV_VAR From 53ec6fce3bde1741d6858e6cd0a52648400a204f Mon Sep 17 00:00:00 2001 From: Tim Emiola Date: Tue, 17 Mar 2015 18:26:40 -0700 Subject: [PATCH 2/3] Splits the JWTHeader creds into its own class --- lib/googleauth/service_account.rb | 92 +++++++++-- spec/googleauth/service_account_spec.rb | 209 +++++++++++++++++------- 2 files changed, 224 insertions(+), 77 deletions(-) diff --git a/lib/googleauth/service_account.rb b/lib/googleauth/service_account.rb index 076f5b1..8018e2e 100644 --- a/lib/googleauth/service_account.rb +++ b/lib/googleauth/service_account.rb @@ -36,7 +36,8 @@ module Google # Module Auth provides classes that provide Google-specific authorization # used to access Google APIs. module Auth - # Authenticates requests using Google's Service Account credentials. + # Authenticates requests using Google's Service Account credentials via an + # OAuth access token. # # This class allows authorizing requests for service accounts directly # from credentials from a json key file downloaded from the developer @@ -44,8 +45,6 @@ module Google # # cf [Application Default Credentials](http://goo.gl/mkAHpZ) class ServiceAccountCredentials < Signet::OAuth2::Client - JWT_AUD_URI_KEY = :jwt_aud_uri - AUTH_METADATA_KEY = Signet::OAuth2::AUTH_METADATA_KEY TOKEN_CRED_URI = 'https://www.googleapis.com/oauth2/v3/token' extend CredentialsLoader @@ -70,33 +69,94 @@ module Google issuer: client_email, signing_key: OpenSSL::PKey::RSA.new(private_key)) end + end - # Extends the superclass behaviour to construct a jwt token if the - # google jwt uri key is present in the input hash. + # Authenticates requests using Google's Service Account credentials via + # JWT Header. + # + # This class allows authorizing requests for service accounts directly + # from credentials from a json key file downloaded from the developer + # console (via 'Generate new Json Key'). It is not part of any OAuth2 + # flow, rather it creates a JWT and sends that as a credential. + # + # cf [Application Default Credentials](http://goo.gl/mkAHpZ) + class ServiceAccountJwtHeaderCredentials + JWT_AUD_URI_KEY = :jwt_aud_uri + AUTH_METADATA_KEY = Signet::OAuth2::AUTH_METADATA_KEY + TOKEN_CRED_URI = 'https://www.googleapis.com/oauth2/v3/token' + SIGNING_ALGORITHM = 'RS256' + EXPIRY = 60 + extend CredentialsLoader + + # make_creds proxies the construction of a credentials instance # - # The jwt is used as the authentication token. + # make_creds is used by the methods in CredentialsLoader. + # + # By default, it calls #new with 2 args, the second one being an + # optional scope. Here's the constructor only has one param, so + # we modify make_creds to reflect this. + def self.make_creds(*args) + new(args[0]) + end + + # Reads the private key and client email fields from the service account + # JSON key. + def self.read_json_key(json_key_io) + json_key = MultiJson.load(json_key_io.read) + fail 'missing client_email' unless json_key.key?('client_email') + fail 'missing private_key' unless json_key.key?('private_key') + [json_key['private_key'], json_key['client_email']] + end + + # Initializes a ServiceAccountJwtHeaderCredentials. + # + # @param json_key_io [IO] an IO from which the JSON key can be read + def initialize(json_key_io) + private_key, client_email = self.class.read_json_key(json_key_io) + @private_key = private_key + @issuer = client_email + @signing_key = OpenSSL::PKey::RSA.new(private_key) + end + + # Construct a jwt token if the WT_AUD_URI key is present in the input + # hash. + # + # The jwt token is used as the value of a 'Bearer '. def apply!(a_hash, opts = {}) jwt_aud_uri = a_hash.delete(JWT_AUD_URI_KEY) - unless jwt_aud_uri.nil? - jwt_token = new_jwt_token(jwt_aud_uri, opts) - a_hash[AUTH_METADATA_KEY] = "Bearer #{jwt_token}" - return a_hash - end - super + return a_hash if jwt_aud_uri.nil? + jwt_token = new_jwt_token(jwt_aud_uri, opts) + a_hash[AUTH_METADATA_KEY] = "Bearer #{jwt_token}" + a_hash end + # Returns a clone of a_hash updated with the authoriation header + def apply(a_hash, opts = {}) + a_copy = a_hash.clone + apply!(a_copy, opts) + a_copy + end + + # Returns a reference to the #apply method, suitable for passing as + # a closure + def updater_proc + lambda(&method(:apply)) + end + + protected + # Creates a jwt uri token. def new_jwt_token(jwt_aud_uri, options = {}) now = Time.new skew = options[:skew] || 60 assertion = { - 'iss' => issuer, - 'sub' => issuer, + 'iss' => @issuer, + 'sub' => @issuer, 'aud' => jwt_aud_uri, - 'exp' => (now + expiry).to_i, + 'exp' => (now + EXPIRY).to_i, 'iat' => (now - skew).to_i } - JWT.encode(assertion, signing_key, signing_algorithm) + JWT.encode(assertion, @signing_key, SIGNING_ALGORITHM) end end end diff --git a/spec/googleauth/service_account_spec.rb b/spec/googleauth/service_account_spec.rb index 6992b36..ee0f535 100644 --- a/spec/googleauth/service_account_spec.rb +++ b/spec/googleauth/service_account_spec.rb @@ -80,67 +80,6 @@ describe Google::Auth::ServiceAccountCredentials do it_behaves_like 'apply/apply! are OK' - context 'when jwt_aud_uri is present' do - WANTED_AUTH_KEY = ServiceAccountCredentials::AUTH_METADATA_KEY - JWT_AUD_URI_KEY = ServiceAccountCredentials::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 - describe '#from_env' do before(:example) do @var_name = CredentialsLoader::ENV_VAR @@ -207,3 +146,151 @@ describe Google::Auth::ServiceAccountCredentials do end end end + +describe Google::Auth::ServiceAccountJwtHeaderCredentials do + CredentialsLoader = Google::Auth::CredentialsLoader + ServiceAccountJwtHeaderCredentials = + Google::Auth::ServiceAccountJwtHeaderCredentials + + let(:client_email) { 'app@developer.gserviceaccount.com' } + let(:clz) { Google::Auth::ServiceAccountJwtHeaderCredentials } + + before(:example) do + @key = OpenSSL::PKey::RSA.new(2048) + @client = clz.new(StringIO.new(cred_json_text)) + end + + def cred_json_text + cred_json = { + private_key_id: 'a_private_key_id', + private_key: @key.to_pem, + client_email: client_email, + client_id: 'app.apps.googleusercontent.com', + type: 'service_account' + } + 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 + + describe '#from_env' do + before(:example) do + @var_name = CredentialsLoader::ENV_VAR + @orig = ENV[@var_name] + end + + after(:example) do + ENV[@var_name] = @orig unless @orig.nil? + end + + it 'returns nil if the GOOGLE_APPLICATION_CREDENTIALS is unset' do + ENV.delete(@var_name) unless ENV[@var_name].nil? + expect(clz.from_env).to be_nil + end + + it 'fails if the GOOGLE_APPLICATION_CREDENTIALS path does not exist' do + ENV.delete(@var_name) unless ENV[@var_name].nil? + expect(clz.from_env).to be_nil + Dir.mktmpdir do |dir| + key_path = File.join(dir, 'does-not-exist') + ENV[@var_name] = key_path + expect { clz.from_env }.to raise_error + end + end + + it 'succeeds when the GOOGLE_APPLICATION_CREDENTIALS file is valid' do + Dir.mktmpdir do |dir| + key_path = File.join(dir, 'my_cert_file') + FileUtils.mkdir_p(File.dirname(key_path)) + File.write(key_path, cred_json_text) + ENV[@var_name] = key_path + expect(clz.from_env).to_not be_nil + end + end + end + + describe '#from_well_known_path' do + before(:example) do + @home = ENV['HOME'] + @known_path = CredentialsLoader::WELL_KNOWN_PATH + end + + after(:example) do + ENV['HOME'] = @home unless @home == ENV['HOME'] + end + + it 'is nil if no file exists' do + ENV['HOME'] = File.dirname(__FILE__) + expect(clz.from_well_known_path).to be_nil + end + + it 'successfully loads the file when it is present' do + Dir.mktmpdir do |dir| + key_path = File.join(dir, '.config', @known_path) + FileUtils.mkdir_p(File.dirname(key_path)) + File.write(key_path, cred_json_text) + ENV['HOME'] = dir + expect(clz.from_well_known_path).to_not be_nil + end + end + end +end From c4bde552e648265d287384973aca3d5198697fe6 Mon Sep 17 00:00:00 2001 From: Tim Emiola Date: Mon, 23 Mar 2015 21:06:26 -0700 Subject: [PATCH 3/3] 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