aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorGravatar Tim Emiola <temiola@google.com>2015-01-30 13:43:28 -0800
committerGravatar Tim Emiola <temiola@google.com>2015-01-31 17:55:24 -0800
commit5d6dfd59b324b1193a2c644963062c759775615d (patch)
treeac27c34a0f37266e971d8b48b2150e5c8ab59982
parent36066537cf6e6929c03cf3689d0bd633a6325c5e (diff)
Extends signet with an apply/apply! methods
apply/apply! update a hash with an OAuth2 authorization token
-rwxr-xr-xsrc/ruby/grpc.gemspec1
-rw-r--r--src/ruby/lib/grpc/auth/signet.rb59
-rw-r--r--src/ruby/spec/auth/signet_spec.rb171
-rw-r--r--src/ruby/spec/spec_helper.rb12
4 files changed, 243 insertions, 0 deletions
diff --git a/src/ruby/grpc.gemspec b/src/ruby/grpc.gemspec
index 3938e0c436..0eb8f48d84 100755
--- a/src/ruby/grpc.gemspec
+++ b/src/ruby/grpc.gemspec
@@ -23,6 +23,7 @@ Gem::Specification.new do |s|
s.add_dependency 'faraday', '~> 0.9'
s.add_dependency 'google-protobuf', '~> 3.0.0alpha.1.1'
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 'signet', '~> 0.6.0'
s.add_dependency 'xray', '~> 1.1'
diff --git a/src/ruby/lib/grpc/auth/signet.rb b/src/ruby/lib/grpc/auth/signet.rb
new file mode 100644
index 0000000000..9cc51b7b3c
--- /dev/null
+++ b/src/ruby/lib/grpc/auth/signet.rb
@@ -0,0 +1,59 @@
+# 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 'signet/oauth_2/client'
+
+module Signet
+ module OAuth2
+ # Google::RPC 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
+ #
+ # Eventually, this change may be merged into signet itself, or some other
+ # package that provides Google-specific auth via signet, and this extension
+ # will be unnecessary.
+ class Client
+ # Updates a_hash updated with the authentication token
+ def apply!(a_hash, opts = {})
+ # 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
+ end
+
+ # Returns a clone of a_hash updated with the authentication token
+ def apply(a_hash, opts = {})
+ a_copy = a_hash.clone
+ apply!(a_copy, opts)
+ a_copy
+ end
+ end
+ end
+end
diff --git a/src/ruby/spec/auth/signet_spec.rb b/src/ruby/spec/auth/signet_spec.rb
new file mode 100644
index 0000000000..d658e5c544
--- /dev/null
+++ b/src/ruby/spec/auth/signet_spec.rb
@@ -0,0 +1,171 @@
+# 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 'spec_helper'
+
+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
+
+describe Signet::OAuth2::Client do
+ describe 'when using RSA keys' do
+ before 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'
+
+ [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
+ end
+ end
+ end
+end
diff --git a/src/ruby/spec/spec_helper.rb b/src/ruby/spec/spec_helper.rb
index 3322674e97..ea0a256713 100644
--- a/src/ruby/spec/spec_helper.rb
+++ b/src/ruby/spec/spec_helper.rb
@@ -27,10 +27,22 @@
# (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.dirname(__FILE__))
+root_dir = File.expand_path(File.join(spec_dir, '..'))
+lib_dir = File.expand_path(File.join(root_dir, 'lib'))
+
+$LOAD_PATH.unshift(spec_dir)
+$LOAD_PATH.unshift(lib_dir)
+$LOAD_PATH.uniq!
+
+require 'faraday'
require 'rspec'
require 'logging'
require 'rspec/logging_helper'
+# Allow Faraday to support test stubs
+Faraday::Adapter.load_middleware(:test)
+
# Configure RSpec to capture log messages for each test. The output from the
# logs will be stored in the @log_output variable. It is a StringIO instance.
RSpec.configure do |config|