aboutsummaryrefslogtreecommitdiffhomepage
path: root/src
diff options
context:
space:
mode:
authorGravatar Tim Emiola <temiola@google.com>2015-01-30 17:56:25 -0800
committerGravatar Tim Emiola <temiola@google.com>2015-01-31 17:55:24 -0800
commit720bc81c899cc9c75f73984a8e2a2498b31a1604 (patch)
tree70e144ce28745398337a4e255faaee9368cacd28 /src
parent5d6dfd59b324b1193a2c644963062c759775615d (diff)
Adds a signet based service_account creds implementation
Diffstat (limited to 'src')
-rwxr-xr-xsrc/ruby/grpc.gemspec1
-rw-r--r--src/ruby/lib/grpc/auth/service_account.rb68
-rw-r--r--src/ruby/lib/grpc/auth/signet.rb5
-rw-r--r--src/ruby/spec/auth/apply_auth_examples.rb147
-rw-r--r--src/ruby/spec/auth/service_account_spec.rb75
-rw-r--r--src/ruby/spec/auth/signet_spec.rb145
6 files changed, 316 insertions, 125 deletions
diff --git a/src/ruby/grpc.gemspec b/src/ruby/grpc.gemspec
index 0eb8f48d84..2ce242dd0b 100755
--- a/src/ruby/grpc.gemspec
+++ b/src/ruby/grpc.gemspec
@@ -25,6 +25,7 @@ Gem::Specification.new do |s|
s.add_dependency 'logging', '~> 1.8'
s.add_dependency 'jwt', '~> 1.2.1'
s.add_dependency 'minitest', '~> 5.4' # reqd for interop tests
+ s.add_dependency 'multijson', '1.10.1'
s.add_dependency 'signet', '~> 0.6.0'
s.add_dependency 'xray', '~> 1.1'
diff --git a/src/ruby/lib/grpc/auth/service_account.rb b/src/ruby/lib/grpc/auth/service_account.rb
new file mode 100644
index 0000000000..35b5cbfe2d
--- /dev/null
+++ b/src/ruby/lib/grpc/auth/service_account.rb
@@ -0,0 +1,68 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+require 'grpc/auth/signet'
+require 'multi_json'
+require 'openssl'
+
+# Reads the private key and client email fields from service account JSON key.
+def 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
+
+module Google
+ module RPC
+ # Module Auth provides classes that provide Google-specific authentication
+ # used to access Google gRPC services.
+ module Auth
+ # Authenticates requests using Google's Service Account credentials.
+ # (cf https://developers.google.com/accounts/docs/OAuth2ServiceAccount)
+ class ServiceAccountCredentials < Signet::OAuth2::Client
+ TOKEN_CRED_URI = 'https://www.googleapis.com/oauth2/v3/token'
+ AUDIENCE = TOKEN_CRED_URI
+
+ # Initializes a ServiceAccountCredentials.
+ #
+ # @param scope [string|array] the scope(s) to access
+ # @param json_key_io [IO] an IO from which the JSON key can be read
+ def initialize(scope, json_key_io)
+ private_key, client_email = read_json_key(json_key_io)
+ super(token_credential_uri: TOKEN_CRED_URI,
+ audience: AUDIENCE,
+ scope: scope,
+ issuer: client_email,
+ signing_key: OpenSSL::PKey::RSA.new(private_key))
+ end
+ end
+ end
+ end
+end
diff --git a/src/ruby/lib/grpc/auth/signet.rb b/src/ruby/lib/grpc/auth/signet.rb
index 9cc51b7b3c..b46af1696a 100644
--- a/src/ruby/lib/grpc/auth/signet.rb
+++ b/src/ruby/lib/grpc/auth/signet.rb
@@ -31,7 +31,8 @@ require 'signet/oauth_2/client'
module Signet
module OAuth2
- # Google::RPC creates an OAuth2 client
+ AUTH_METADATA_KEY = :Authorization
+ # Signet::OAuth2::Client creates an OAuth2 client
#
# Here client is re-opened to add the #apply and #apply! methods which
# update a hash map with the fetched authentication token
@@ -45,7 +46,7 @@ module Signet
# fetch the access token there is currently not one, or if the client
# has expired
fetch_access_token!(opts) if access_token.nil? || expired?
- a_hash['auth'] = access_token
+ a_hash[AUTH_METADATA_KEY] = "Bearer: #{access_token}"
end
# Returns a clone of a_hash updated with the authentication token
diff --git a/src/ruby/spec/auth/apply_auth_examples.rb b/src/ruby/spec/auth/apply_auth_examples.rb
new file mode 100644
index 0000000000..af1f6df04a
--- /dev/null
+++ b/src/ruby/spec/auth/apply_auth_examples.rb
@@ -0,0 +1,147 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+spec_dir = File.expand_path(File.join(File.dirname(__FILE__)))
+$LOAD_PATH.unshift(spec_dir)
+$LOAD_PATH.uniq!
+
+require 'faraday'
+require 'spec_helper'
+
+def build_json_response(payload)
+ [200,
+ { 'Content-Type' => 'application/json; charset=utf-8' },
+ MultiJson.dump(payload)]
+end
+
+WANTED_AUTH_KEY = :Authorization
+
+shared_examples 'apply/apply! are OK' do
+
+ # tests that use these examples need to define
+ #
+ # @client which should be an auth client
+ #
+ # @make_auth_stubs, which should stub out the expected http behaviour of the
+ # auth client
+ describe '#fetch_access_token' do
+ it 'should set access_token to the fetched value' do
+ token = '1/abcdef1234567890'
+ stubs = make_auth_stubs with_access_token: token
+ c = Faraday.new do |b|
+ b.adapter(:test, stubs)
+ end
+
+ @client.fetch_access_token!(connection: c)
+ expect(@client.access_token).to eq(token)
+ stubs.verify_stubbed_calls
+ end
+ end
+
+ describe '#apply!' do
+ it 'should update the target hash with fetched access token' do
+ token = '1/abcdef1234567890'
+ stubs = make_auth_stubs with_access_token: token
+ c = Faraday.new do |b|
+ b.adapter(:test, stubs)
+ end
+
+ md = { foo: 'bar' }
+ @client.apply!(md, connection: c)
+ want = { :foo => 'bar', WANTED_AUTH_KEY => "Bearer: #{token}" }
+ expect(md).to eq(want)
+ stubs.verify_stubbed_calls
+ end
+ end
+
+ describe '#apply' do
+ it 'should not update the original hash with the access token' do
+ token = '1/abcdef1234567890'
+ stubs = make_auth_stubs with_access_token: token
+ c = Faraday.new do |b|
+ b.adapter(:test, stubs)
+ end
+
+ md = { foo: 'bar' }
+ @client.apply(md, connection: c)
+ want = { foo: 'bar' }
+ expect(md).to eq(want)
+ stubs.verify_stubbed_calls
+ end
+
+ it 'should add the token to the returned hash' do
+ token = '1/abcdef1234567890'
+ stubs = make_auth_stubs with_access_token: token
+ c = Faraday.new do |b|
+ b.adapter(:test, stubs)
+ end
+
+ md = { foo: 'bar' }
+ got = @client.apply(md, connection: c)
+ want = { :foo => 'bar', WANTED_AUTH_KEY => "Bearer: #{token}" }
+ expect(got).to eq(want)
+ stubs.verify_stubbed_calls
+ end
+
+ it 'should not fetch a new token if the current is not expired' do
+ token = '1/abcdef1234567890'
+ stubs = make_auth_stubs with_access_token: token
+ c = Faraday.new do |b|
+ b.adapter(:test, stubs)
+ end
+
+ n = 5 # arbitrary
+ n.times do |_t|
+ md = { foo: 'bar' }
+ got = @client.apply(md, connection: c)
+ want = { :foo => 'bar', WANTED_AUTH_KEY => "Bearer: #{token}" }
+ expect(got).to eq(want)
+ end
+ stubs.verify_stubbed_calls
+ end
+
+ it 'should fetch a new token if the current one is expired' do
+ token_1 = '1/abcdef1234567890'
+ token_2 = '2/abcdef1234567890'
+
+ [token_1, token_2].each do |t|
+ stubs = make_auth_stubs with_access_token: t
+ c = Faraday.new do |b|
+ b.adapter(:test, stubs)
+ end
+ md = { foo: 'bar' }
+ got = @client.apply(md, connection: c)
+ want = { :foo => 'bar', WANTED_AUTH_KEY => "Bearer: #{t}" }
+ expect(got).to eq(want)
+ stubs.verify_stubbed_calls
+ @client.expires_at -= 3601 # default is to expire in 1hr
+ end
+ end
+ end
+end
diff --git a/src/ruby/spec/auth/service_account_spec.rb b/src/ruby/spec/auth/service_account_spec.rb
new file mode 100644
index 0000000000..cbc6a73ac2
--- /dev/null
+++ b/src/ruby/spec/auth/service_account_spec.rb
@@ -0,0 +1,75 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+spec_dir = File.expand_path(File.join(File.dirname(__FILE__)))
+$LOAD_PATH.unshift(spec_dir)
+$LOAD_PATH.uniq!
+
+require 'apply_auth_examples'
+require 'grpc/auth/service_account'
+require 'jwt'
+require 'multi_json'
+require 'openssl'
+require 'spec_helper'
+
+describe Google::RPC::Auth::ServiceAccountCredentials do
+ before(:example) do
+ @key = OpenSSL::PKey::RSA.new(2048)
+ cred_json = {
+ private_key_id: 'a_private_key_id',
+ private_key: @key.to_pem,
+ client_email: 'app@developer.gserviceaccount.com',
+ client_id: 'app.apps.googleusercontent.com',
+ type: 'service_account'
+ }
+ cred_json_text = MultiJson.dump(cred_json)
+ @client = Google::RPC::Auth::ServiceAccountCredentials.new(
+ 'https://www.googleapis.com/auth/userinfo.profile',
+ StringIO.new(cred_json_text))
+ end
+
+ def make_auth_stubs(with_access_token: '')
+ Faraday::Adapter::Test::Stubs.new do |stub|
+ stub.post('/oauth2/v3/token') do |env|
+ params = Addressable::URI.form_unencode(env[:body])
+ _claim, _header = JWT.decode(params.assoc('assertion').last,
+ @key.public_key)
+ want = ['grant_type', 'urn:ietf:params:oauth:grant-type:jwt-bearer']
+ expect(params.assoc('grant_type')).to eq(want)
+ build_json_response(
+ 'access_token' => with_access_token,
+ 'token_type' => 'Bearer',
+ 'expires_in' => 3600
+ )
+ end
+ end
+ end
+
+ it_behaves_like 'apply/apply! are OK'
+end
diff --git a/src/ruby/spec/auth/signet_spec.rb b/src/ruby/spec/auth/signet_spec.rb
index d658e5c544..1712edf296 100644
--- a/src/ruby/spec/auth/signet_spec.rb
+++ b/src/ruby/spec/auth/signet_spec.rb
@@ -31,141 +31,40 @@ spec_dir = File.expand_path(File.join(File.dirname(__FILE__)))
$LOAD_PATH.unshift(spec_dir)
$LOAD_PATH.uniq!
-require 'spec_helper'
-
+require 'apply_auth_examples'
require 'grpc/auth/signet'
-require 'openssl'
require 'jwt'
-
-def build_json_response(payload)
- [200,
- { 'Content-Type' => 'application/json; charset=utf-8' },
- MultiJson.dump(payload)]
-end
+require 'openssl'
+require 'spec_helper'
describe Signet::OAuth2::Client do
- describe 'when using RSA keys' do
- before do
- @key = OpenSSL::PKey::RSA.new(2048)
- @client = Signet::OAuth2::Client.new(
+ before(:example) do
+ @key = OpenSSL::PKey::RSA.new(2048)
+ @client = Signet::OAuth2::Client.new(
token_credential_uri: 'https://accounts.google.com/o/oauth2/token',
scope: 'https://www.googleapis.com/auth/userinfo.profile',
issuer: 'app@example.com',
audience: 'https://accounts.google.com/o/oauth2/token',
signing_key: @key
)
- end
-
- def make_oauth_stubs(with_access_token: '')
- Faraday::Adapter::Test::Stubs.new do |stub|
- stub.post('/o/oauth2/token') do |env|
- params = Addressable::URI.form_unencode(env[:body])
- _claim, _header = JWT.decode(params.assoc('assertion').last,
- @key.public_key)
- want = ['grant_type', 'urn:ietf:params:oauth:grant-type:jwt-bearer']
- expect(params.assoc('grant_type')).to eq(want)
- build_json_response(
- 'access_token' => with_access_token,
- 'token_type' => 'Bearer',
- 'expires_in' => 3600
- )
- end
- end
- end
-
- describe '#fetch_access_token' do
- it 'should set access_token to the fetched value' do
- token = '1/abcdef1234567890'
- stubs = make_oauth_stubs with_access_token: token
- c = Faraday.new(url: 'https://www.google.com') do |b|
- b.adapter(:test, stubs)
- end
-
- @client.fetch_access_token!(connection: c)
- expect(@client.access_token).to eq(token)
- stubs.verify_stubbed_calls
- end
- end
-
- describe '#apply!' do
- it 'should update the target hash with fetched access token' do
- token = '1/abcdef1234567890'
- stubs = make_oauth_stubs with_access_token: token
- c = Faraday.new(url: 'https://www.google.com') do |b|
- b.adapter(:test, stubs)
- end
-
- md = { foo: 'bar' }
- @client.apply!(md, connection: c)
- want = { :foo => 'bar', 'auth' => token }
- expect(md).to eq(want)
- stubs.verify_stubbed_calls
- end
- end
-
- describe '#apply' do
- it 'should not update the original hash with the access token' do
- token = '1/abcdef1234567890'
- stubs = make_oauth_stubs with_access_token: token
- c = Faraday.new(url: 'https://www.google.com') do |b|
- b.adapter(:test, stubs)
- end
-
- md = { foo: 'bar' }
- @client.apply(md, connection: c)
- want = { foo: 'bar' }
- expect(md).to eq(want)
- stubs.verify_stubbed_calls
- end
-
- it 'should add the token to the returned hash' do
- token = '1/abcdef1234567890'
- stubs = make_oauth_stubs with_access_token: token
- c = Faraday.new(url: 'https://www.google.com') do |b|
- b.adapter(:test, stubs)
- end
-
- md = { foo: 'bar' }
- got = @client.apply(md, connection: c)
- want = { :foo => 'bar', 'auth' => token }
- expect(got).to eq(want)
- stubs.verify_stubbed_calls
- end
-
- it 'should not fetch a new token if the current is not expired' do
- token = '1/abcdef1234567890'
- stubs = make_oauth_stubs with_access_token: token
- c = Faraday.new(url: 'https://www.google.com') do |b|
- b.adapter(:test, stubs)
- end
-
- n = 5 # arbitrary
- n.times do |_t|
- md = { foo: 'bar' }
- got = @client.apply(md, connection: c)
- want = { :foo => 'bar', 'auth' => token }
- expect(got).to eq(want)
- end
- stubs.verify_stubbed_calls
- end
-
- it 'should fetch a new token if the current one is expired' do
- token_1 = '1/abcdef1234567890'
- token_2 = '2/abcdef1234567890'
+ end
- [token_1, token_2].each do |t|
- stubs = make_oauth_stubs with_access_token: t
- c = Faraday.new(url: 'https://www.google.com') do |b|
- b.adapter(:test, stubs)
- end
- md = { foo: 'bar' }
- got = @client.apply(md, connection: c)
- want = { :foo => 'bar', 'auth' => t }
- expect(got).to eq(want)
- stubs.verify_stubbed_calls
- @client.expires_at -= 3601 # default is to expire in 1hr
- end
+ def make_auth_stubs(with_access_token: '')
+ Faraday::Adapter::Test::Stubs.new do |stub|
+ stub.post('/o/oauth2/token') do |env|
+ params = Addressable::URI.form_unencode(env[:body])
+ _claim, _header = JWT.decode(params.assoc('assertion').last,
+ @key.public_key)
+ want = ['grant_type', 'urn:ietf:params:oauth:grant-type:jwt-bearer']
+ expect(params.assoc('grant_type')).to eq(want)
+ build_json_response(
+ 'access_token' => with_access_token,
+ 'token_type' => 'Bearer',
+ 'expires_in' => 3600
+ )
end
end
end
+
+ it_behaves_like 'apply/apply! are OK'
end