diff --git a/src/ruby/grpc.gemspec b/src/ruby/grpc.gemspec
index 0eb8f48d8431316790c3d1a309a4bcc7f67ebf8f..2ce242dd0b11de1035dbfdc29f09f9236e54de52 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 0000000000000000000000000000000000000000..35b5cbfe2de1ff5eff7ee66719ddc586cf661988
--- /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 9cc51b7b3c5ab3b12f743ef9aef02250b29b1110..b46af1696afb75e6f1eb5f50beaadc0b152b4ebd 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 0000000000000000000000000000000000000000..af1f6df04ae4ba7dacc66c86d8ddd3992651d016
--- /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 0000000000000000000000000000000000000000..cbc6a73ac20c245ecb98d8096e94820487a55fcd
--- /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 d658e5c5448314ad85b89ad0c4798b79012cd037..1712edf29614061a25fc15faa2b1e3ea45ef75d9 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