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