googleauth 0.14.0 → 0.16.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/.github/renovate.json +6 -0
  3. data/.github/sync-repo-settings.yaml +18 -0
  4. data/.github/workflows/ci.yml +55 -0
  5. data/.github/workflows/release-please.yml +39 -0
  6. data/.gitignore +3 -0
  7. data/.kokoro/populate-secrets.sh +76 -0
  8. data/.kokoro/release.cfg +7 -49
  9. data/.kokoro/release.sh +18 -0
  10. data/.kokoro/trampoline_v2.sh +489 -0
  11. data/.rubocop.yml +0 -2
  12. data/.toys/.toys.rb +45 -0
  13. data/.toys/ci.rb +43 -0
  14. data/.toys/kokoro/.toys.rb +66 -0
  15. data/.toys/kokoro/publish-docs.rb +67 -0
  16. data/.toys/kokoro/publish-gem.rb +53 -0
  17. data/.toys/linkinator.rb +43 -0
  18. data/.trampolinerc +48 -0
  19. data/CHANGELOG.md +69 -27
  20. data/Gemfile +2 -7
  21. data/README.md +9 -7
  22. data/googleauth.gemspec +2 -1
  23. data/lib/googleauth/compute_engine.rb +6 -5
  24. data/lib/googleauth/credentials.rb +167 -48
  25. data/lib/googleauth/credentials_loader.rb +1 -1
  26. data/lib/googleauth/iam.rb +1 -1
  27. data/lib/googleauth/id_tokens/key_sources.rb +7 -5
  28. data/lib/googleauth/id_tokens/verifier.rb +7 -9
  29. data/lib/googleauth/scope_util.rb +1 -1
  30. data/lib/googleauth/service_account.rb +35 -23
  31. data/lib/googleauth/signet.rb +1 -1
  32. data/lib/googleauth/stores/file_token_store.rb +1 -0
  33. data/lib/googleauth/stores/redis_token_store.rb +1 -0
  34. data/lib/googleauth/version.rb +1 -1
  35. data/lib/googleauth/web_user_authorizer.rb +4 -7
  36. data/spec/googleauth/compute_engine_spec.rb +18 -0
  37. data/spec/googleauth/credentials_spec.rb +228 -106
  38. data/spec/googleauth/service_account_spec.rb +8 -0
  39. metadata +18 -22
  40. data/.kokoro/build.bat +0 -16
  41. data/.kokoro/build.sh +0 -4
  42. data/.kokoro/continuous/common.cfg +0 -24
  43. data/.kokoro/continuous/linux.cfg +0 -25
  44. data/.kokoro/continuous/osx.cfg +0 -8
  45. data/.kokoro/continuous/post.cfg +0 -30
  46. data/.kokoro/continuous/windows.cfg +0 -29
  47. data/.kokoro/osx.sh +0 -4
  48. data/.kokoro/presubmit/common.cfg +0 -24
  49. data/.kokoro/presubmit/linux.cfg +0 -24
  50. data/.kokoro/presubmit/osx.cfg +0 -8
  51. data/.kokoro/presubmit/windows.cfg +0 -29
  52. data/.kokoro/trampoline.bat +0 -10
  53. data/.kokoro/trampoline.sh +0 -4
  54. data/Rakefile +0 -132
  55. data/rakelib/devsite_builder.rb +0 -45
  56. data/rakelib/link_checker.rb +0 -64
  57. data/rakelib/repo_metadata.rb +0 -59
@@ -103,7 +103,7 @@ module Google
103
103
  return make_creds options.merge(json_key_io: f)
104
104
  end
105
105
  elsif service_account_env_vars? || authorized_user_env_vars?
106
- return make_creds options
106
+ make_creds options
107
107
  end
108
108
  rescue StandardError => e
109
109
  raise "#{NOT_FOUND_ERROR}: #{e}"
@@ -68,7 +68,7 @@ module Google
68
68
  # Returns a reference to the #apply method, suitable for passing as
69
69
  # a closure
70
70
  def updater_proc
71
- lambda(&method(:apply))
71
+ proc { |a_hash, _opts = {}| apply a_hash }
72
72
  end
73
73
  end
74
74
  end
@@ -171,7 +171,9 @@ module Google
171
171
  curve_name = CURVE_NAME_MAP[jwk[:crv]]
172
172
  raise KeySourceError, "Unsupported EC curve #{jwk[:crv]}" unless curve_name
173
173
  group = OpenSSL::PKey::EC::Group.new curve_name
174
- bn = OpenSSL::BN.new ["04" + x_data.unpack1("H*") + y_data.unpack1("H*")].pack("H*"), 2
174
+ x_hex = x_data.unpack1 "H*"
175
+ y_hex = y_data.unpack1 "H*"
176
+ bn = OpenSSL::BN.new ["04#{x_hex}#{y_hex}"].pack("H*"), 2
175
177
  key = OpenSSL::PKey::EC.new curve_name
176
178
  key.public_key = OpenSSL::PKey::EC::Point.new group, bn
177
179
  key
@@ -284,10 +286,10 @@ module Google
284
286
  raise KeySourceError, "Unable to retrieve data from #{uri}" unless response.is_a? Net::HTTPSuccess
285
287
 
286
288
  data = begin
287
- JSON.parse response.body
288
- rescue JSON::ParserError
289
- raise KeySourceError, "Unable to parse JSON"
290
- end
289
+ JSON.parse response.body
290
+ rescue JSON::ParserError
291
+ raise KeySourceError, "Unable to parse JSON"
292
+ end
291
293
 
292
294
  @current_keys = Array(interpret_json(data))
293
295
  end
@@ -105,15 +105,13 @@ module Google
105
105
  def decode_token token, keys, aud, azp, iss
106
106
  payload = nil
107
107
  keys.find do |key|
108
- begin
109
- options = { algorithms: key.algorithm }
110
- decoded_token = JWT.decode token, key.key, true, options
111
- payload = decoded_token.first
112
- rescue JWT::ExpiredSignature
113
- raise ExpiredTokenError, "Token signature is expired"
114
- rescue JWT::DecodeError
115
- nil # Try the next key
116
- end
108
+ options = { algorithms: key.algorithm }
109
+ decoded_token = JWT.decode token, key.key, true, options
110
+ payload = decoded_token.first
111
+ rescue JWT::ExpiredSignature
112
+ raise ExpiredTokenError, "Token signature is expired"
113
+ rescue JWT::DecodeError
114
+ nil # Try the next key
117
115
  end
118
116
 
119
117
  normalize_and_verify_payload payload, aud, azp, iss
@@ -51,7 +51,7 @@ module Google
51
51
  when Array
52
52
  scope
53
53
  when String
54
- scope.split " "
54
+ scope.split
55
55
  else
56
56
  raise "Invalid scope value. Must be string or array"
57
57
  end
@@ -53,12 +53,18 @@ module Google
53
53
  attr_reader :project_id
54
54
  attr_reader :quota_project_id
55
55
 
56
+ def enable_self_signed_jwt?
57
+ @enable_self_signed_jwt
58
+ end
59
+
56
60
  # Creates a ServiceAccountCredentials.
57
61
  #
58
62
  # @param json_key_io [IO] an IO from which the JSON key can be read
59
63
  # @param scope [string|array|nil] the scope(s) to access
60
64
  def self.make_creds options = {}
61
- json_key_io, scope, target_audience = options.values_at :json_key_io, :scope, :target_audience
65
+ json_key_io, scope, enable_self_signed_jwt, target_audience, audience, token_credential_uri =
66
+ options.values_at :json_key_io, :scope, :enable_self_signed_jwt, :target_audience,
67
+ :audience, :token_credential_uri
62
68
  raise ArgumentError, "Cannot specify both scope and target_audience" if scope && target_audience
63
69
 
64
70
  if json_key_io
@@ -71,14 +77,15 @@ module Google
71
77
  end
72
78
  project_id ||= CredentialsLoader.load_gcloud_project_id
73
79
 
74
- new(token_credential_uri: TOKEN_CRED_URI,
75
- audience: TOKEN_CRED_URI,
76
- scope: scope,
77
- target_audience: target_audience,
78
- issuer: client_email,
79
- signing_key: OpenSSL::PKey::RSA.new(private_key),
80
- project_id: project_id,
81
- quota_project_id: quota_project_id)
80
+ new(token_credential_uri: token_credential_uri || TOKEN_CRED_URI,
81
+ audience: audience || TOKEN_CRED_URI,
82
+ scope: scope,
83
+ enable_self_signed_jwt: enable_self_signed_jwt,
84
+ target_audience: target_audience,
85
+ issuer: client_email,
86
+ signing_key: OpenSSL::PKey::RSA.new(private_key),
87
+ project_id: project_id,
88
+ quota_project_id: quota_project_id)
82
89
  .configure_connection(options)
83
90
  end
84
91
 
@@ -94,30 +101,35 @@ module Google
94
101
  def initialize options = {}
95
102
  @project_id = options[:project_id]
96
103
  @quota_project_id = options[:quota_project_id]
104
+ @enable_self_signed_jwt = options[:enable_self_signed_jwt] ? true : false
97
105
  super options
98
106
  end
99
107
 
100
- # Extends the base class.
101
- #
102
- # If scope(s) is not set, it creates a transient
103
- # ServiceAccountJwtHeaderCredentials instance and uses that to
104
- # authenticate instead.
108
+ # Extends the base class to use a transient
109
+ # ServiceAccountJwtHeaderCredentials for certain cases.
105
110
  def apply! a_hash, opts = {}
106
- # Use the base implementation if scopes are set
107
- unless scope.nil? && target_audience.nil?
111
+ # Use a self-singed JWT if there's no information that can be used to
112
+ # obtain an OAuth token, OR if there are scopes but also an assertion
113
+ # that they are default scopes that shouldn't be used to fetch a token.
114
+ if target_audience.nil? && (scope.nil? || enable_self_signed_jwt?)
115
+ apply_self_signed_jwt! a_hash
116
+ else
108
117
  super
109
- return
110
118
  end
119
+ end
120
+
121
+ private
111
122
 
123
+ def apply_self_signed_jwt! a_hash
112
124
  # Use the ServiceAccountJwtHeaderCredentials using the same cred values
113
- # if no scopes are set.
114
125
  cred_json = {
115
- private_key: @signing_key.to_s,
116
- client_email: @issuer
126
+ private_key: @signing_key.to_s,
127
+ client_email: @issuer,
128
+ project_id: @project_id,
129
+ quota_project_id: @quota_project_id
117
130
  }
118
- alt_clz = ServiceAccountJwtHeaderCredentials
119
131
  key_io = StringIO.new MultiJson.dump(cred_json)
120
- alt = alt_clz.make_creds json_key_io: key_io
132
+ alt = ServiceAccountJwtHeaderCredentials.make_creds json_key_io: key_io
121
133
  alt.apply! a_hash
122
134
  end
123
135
  end
@@ -193,7 +205,7 @@ module Google
193
205
  # Returns a reference to the #apply method, suitable for passing as
194
206
  # a closure
195
207
  def updater_proc
196
- lambda(&method(:apply))
208
+ proc { |a_hash, opts = {}| apply a_hash, opts }
197
209
  end
198
210
 
199
211
  protected
@@ -63,7 +63,7 @@ module Signet
63
63
  # Returns a reference to the #apply method, suitable for passing as
64
64
  # a closure
65
65
  def updater_proc
66
- lambda(&method(:apply))
66
+ proc { |a_hash, opts = {}| apply a_hash, opts }
67
67
  end
68
68
 
69
69
  def on_refresh &block
@@ -40,6 +40,7 @@ module Google
40
40
  # @param [String, File] file
41
41
  # Path to storage file
42
42
  def initialize options = {}
43
+ super()
43
44
  path = options[:file]
44
45
  @store = YAML::Store.new path
45
46
  end
@@ -49,6 +49,7 @@ module Google
49
49
  # the options passed through. You may include any other keys accepted
50
50
  # by `Redis.new`
51
51
  def initialize options = {}
52
+ super()
52
53
  redis = options.delete :redis
53
54
  prefix = options.delete :prefix
54
55
  @redis = case redis
@@ -31,6 +31,6 @@ module Google
31
31
  # Module Auth provides classes that provide Google-specific authorization
32
32
  # used to access Google APIs.
33
33
  module Auth
34
- VERSION = "0.14.0".freeze
34
+ VERSION = "0.16.2".freeze
35
35
  end
36
36
  end
@@ -58,12 +58,9 @@ module Google
58
58
  # end
59
59
  #
60
60
  # Instead of implementing the callback directly, applications are
61
- # encouraged to use {Google::Auth::Web::AuthCallbackApp} instead.
61
+ # encouraged to use {Google::Auth::WebUserAuthorizer::CallbackApp} instead.
62
62
  #
63
- # For rails apps, see {Google::Auth::ControllerHelpers}
64
- #
65
- # @see {Google::Auth::AuthCallbackApp}
66
- # @see {Google::Auth::ControllerHelpers}
63
+ # @see CallbackApp
67
64
  # @note Requires sessions are enabled
68
65
  class WebUserAuthorizer < Google::Auth::UserAuthorizer
69
66
  STATE_PARAM = "state".freeze
@@ -192,7 +189,7 @@ module Google
192
189
  # May raise an error if an authorization code is present in the session
193
190
  # and exchange of the code fails
194
191
  def get_credentials user_id, request = nil, scope = nil
195
- if request && request.session.key?(CALLBACK_STATE_KEY)
192
+ if request&.session&.key? CALLBACK_STATE_KEY
196
193
  # Note - in theory, no need to check required scope as this is
197
194
  # expected to be called immediately after a return from authorization
198
195
  state_json = request.session.delete CALLBACK_STATE_KEY
@@ -261,7 +258,7 @@ module Google
261
258
  # Google::Auth::WebUserAuthorizer::CallbackApp.call(env)
262
259
  # end
263
260
  #
264
- # @see {Google::Auth::WebUserAuthorizer}
261
+ # @see Google::Auth::WebUserAuthorizer
265
262
  class CallbackApp
266
263
  LOCATION_HEADER = "Location".freeze
267
264
  REDIR_STATUS = 302
@@ -90,6 +90,24 @@ describe Google::Auth::GCECredentials do
90
90
  expect(stub).to have_been_requested
91
91
  end
92
92
 
93
+ it "should fail if the metadata request returns a 403" do
94
+ stub = stub_request(:get, MD_ACCESS_URI)
95
+ .to_return(status: 403,
96
+ headers: { "Metadata-Flavor" => "Google" })
97
+ expect { @client.fetch_access_token! }
98
+ .to raise_error Signet::AuthorizationError
99
+ expect(stub).to have_been_requested.times(6)
100
+ end
101
+
102
+ it "should fail if the metadata request returns a 500" do
103
+ stub = stub_request(:get, MD_ACCESS_URI)
104
+ .to_return(status: 500,
105
+ headers: { "Metadata-Flavor" => "Google" })
106
+ expect { @client.fetch_access_token! }
107
+ .to raise_error Signet::AuthorizationError
108
+ expect(stub).to have_been_requested.times(6)
109
+ end
110
+
93
111
  it "should fail if the metadata request returns an unexpected code" do
94
112
  stub = stub_request(:get, MD_ACCESS_URI)
95
113
  .to_return(status: 503,
@@ -46,42 +46,47 @@ describe Google::Auth::Credentials, :private do
46
46
  }
47
47
  end
48
48
 
49
- it "uses a default scope" do
49
+ def mock_signet
50
50
  mocked_signet = double "Signet::OAuth2::Client"
51
51
  allow(mocked_signet).to receive(:configure_connection).and_return(mocked_signet)
52
52
  allow(mocked_signet).to receive(:fetch_access_token!).and_return(true)
53
53
  allow(mocked_signet).to receive(:client_id)
54
54
  allow(Signet::OAuth2::Client).to receive(:new) do |options|
55
+ yield options if block_given?
56
+ mocked_signet
57
+ end
58
+ mocked_signet
59
+ end
60
+
61
+ it "uses a default scope" do
62
+ mock_signet do |options|
55
63
  expect(options[:token_credential_uri]).to eq("https://oauth2.googleapis.com/token")
56
64
  expect(options[:audience]).to eq("https://oauth2.googleapis.com/token")
57
65
  expect(options[:scope]).to eq([])
58
66
  expect(options[:issuer]).to eq(default_keyfile_hash["client_email"])
59
67
  expect(options[:signing_key]).to be_a_kind_of(OpenSSL::PKey::RSA)
60
-
61
- mocked_signet
62
68
  end
63
69
 
64
70
  Google::Auth::Credentials.new default_keyfile_hash
65
71
  end
66
72
 
67
73
  it "uses a custom scope" do
68
- mocked_signet = double "Signet::OAuth2::Client"
69
- allow(mocked_signet).to receive(:configure_connection).and_return(mocked_signet)
70
- allow(mocked_signet).to receive(:fetch_access_token!).and_return(true)
71
- allow(mocked_signet).to receive(:client_id)
72
- allow(Signet::OAuth2::Client).to receive(:new) do |options|
74
+ mock_signet do |options|
73
75
  expect(options[:token_credential_uri]).to eq("https://oauth2.googleapis.com/token")
74
76
  expect(options[:audience]).to eq("https://oauth2.googleapis.com/token")
75
77
  expect(options[:scope]).to eq(["http://example.com/scope"])
76
78
  expect(options[:issuer]).to eq(default_keyfile_hash["client_email"])
77
79
  expect(options[:signing_key]).to be_a_kind_of(OpenSSL::PKey::RSA)
78
-
79
- mocked_signet
80
80
  end
81
81
 
82
82
  Google::Auth::Credentials.new default_keyfile_hash, scope: "http://example.com/scope"
83
83
  end
84
84
 
85
+ it "uses empty paths and env_vars by default" do
86
+ expect(Google::Auth::Credentials.paths).to eq([])
87
+ expect(Google::Auth::Credentials.env_vars).to eq([])
88
+ end
89
+
85
90
  describe "using CONSTANTS" do
86
91
  it "can be subclassed to pass in other env paths" do
87
92
  test_path_env_val = "/unknown/path/to/file.txt".freeze
@@ -101,21 +106,22 @@ describe Google::Auth::Credentials, :private do
101
106
  allow(::File).to receive(:file?).with(test_path_env_val) { false }
102
107
  allow(::File).to receive(:file?).with(test_json_env_val) { false }
103
108
 
104
- mocked_signet = double "Signet::OAuth2::Client"
105
- allow(mocked_signet).to receive(:configure_connection).and_return(mocked_signet)
106
- allow(mocked_signet).to receive(:fetch_access_token!).and_return(true)
107
- allow(mocked_signet).to receive(:client_id)
108
- allow(Signet::OAuth2::Client).to receive(:new) do |options|
109
+ mocked_signet = mock_signet
110
+
111
+ allow(Google::Auth::ServiceAccountCredentials).to receive(:make_creds) do |options|
109
112
  expect(options[:token_credential_uri]).to eq("https://example.com/token")
110
113
  expect(options[:audience]).to eq("https://example.com/audience")
111
114
  expect(options[:scope]).to eq(["http://example.com/scope"])
112
- expect(options[:issuer]).to eq(default_keyfile_hash["client_email"])
113
- expect(options[:signing_key]).to be_a_kind_of(OpenSSL::PKey::RSA)
115
+ expect(options[:enable_self_signed_jwt]).to eq(true)
116
+ expect(options[:target_audience]).to be_nil
117
+ expect(options[:json_key_io].read).to eq(test_json_env_val)
114
118
 
115
- mocked_signet
119
+ # This should really be a Signet::OAuth2::Client object,
120
+ # but mocking is making that difficult, so return a valid hash instead.
121
+ default_keyfile_hash
116
122
  end
117
123
 
118
- creds = TestCredentials1.default
124
+ creds = TestCredentials1.default enable_self_signed_jwt: true
119
125
  expect(creds).to be_a_kind_of(TestCredentials1)
120
126
  expect(creds.client).to eq(mocked_signet)
121
127
  expect(creds.project_id).to eq(default_keyfile_hash["project_id"])
@@ -130,25 +136,28 @@ describe Google::Auth::Credentials, :private do
130
136
  DEFAULT_PATHS = ["~/default/path/to/file.txt"].freeze
131
137
  end
132
138
 
139
+ json_content = JSON.generate default_keyfile_hash
140
+
133
141
  allow(::ENV).to receive(:[]).with("GOOGLE_AUTH_SUPPRESS_CREDENTIALS_WARNINGS") { "true" }
134
142
  allow(::ENV).to receive(:[]).with("PATH_ENV_DUMMY") { "/fake/path/to/file.txt" }
135
143
  allow(::File).to receive(:file?).with("/fake/path/to/file.txt") { false }
136
144
  allow(::ENV).to receive(:[]).with("PATH_ENV_TEST") { "/unknown/path/to/file.txt" }
137
145
  allow(::File).to receive(:file?).with("/unknown/path/to/file.txt") { true }
138
- allow(::File).to receive(:read).with("/unknown/path/to/file.txt") { JSON.generate default_keyfile_hash }
146
+ allow(::File).to receive(:read).with("/unknown/path/to/file.txt") { json_content }
139
147
 
140
- mocked_signet = double "Signet::OAuth2::Client"
141
- allow(mocked_signet).to receive(:configure_connection).and_return(mocked_signet)
142
- allow(mocked_signet).to receive(:fetch_access_token!).and_return(true)
143
- allow(mocked_signet).to receive(:client_id)
144
- allow(Signet::OAuth2::Client).to receive(:new) do |options|
148
+ mocked_signet = mock_signet
149
+
150
+ allow(Google::Auth::ServiceAccountCredentials).to receive(:make_creds) do |options|
145
151
  expect(options[:token_credential_uri]).to eq("https://oauth2.googleapis.com/token")
146
152
  expect(options[:audience]).to eq("https://oauth2.googleapis.com/token")
147
153
  expect(options[:scope]).to eq(["http://example.com/scope"])
148
- expect(options[:issuer]).to eq(default_keyfile_hash["client_email"])
149
- expect(options[:signing_key]).to be_a_kind_of(OpenSSL::PKey::RSA)
154
+ expect(options[:enable_self_signed_jwt]).to be_nil
155
+ expect(options[:target_audience]).to be_nil
156
+ expect(options[:json_key_io].read).to eq(json_content)
150
157
 
151
- mocked_signet
158
+ # This should really be a Signet::OAuth2::Client object,
159
+ # but mocking is making that difficult, so return a valid hash instead.
160
+ default_keyfile_hash
152
161
  end
153
162
 
154
163
  creds = TestCredentials2.default
@@ -175,18 +184,19 @@ describe Google::Auth::Credentials, :private do
175
184
  allow(::ENV).to receive(:[]).with("JSON_ENV_DUMMY") { nil }
176
185
  allow(::ENV).to receive(:[]).with("JSON_ENV_TEST") { test_json_env_val }
177
186
 
178
- mocked_signet = double "Signet::OAuth2::Client"
179
- allow(mocked_signet).to receive(:configure_connection).and_return(mocked_signet)
180
- allow(mocked_signet).to receive(:fetch_access_token!).and_return(true)
181
- allow(mocked_signet).to receive(:client_id)
182
- allow(Signet::OAuth2::Client).to receive(:new) do |options|
187
+ mocked_signet = mock_signet
188
+
189
+ allow(Google::Auth::ServiceAccountCredentials).to receive(:make_creds) do |options|
183
190
  expect(options[:token_credential_uri]).to eq("https://oauth2.googleapis.com/token")
184
191
  expect(options[:audience]).to eq("https://oauth2.googleapis.com/token")
185
192
  expect(options[:scope]).to eq(["http://example.com/scope"])
186
- expect(options[:issuer]).to eq(default_keyfile_hash["client_email"])
187
- expect(options[:signing_key]).to be_a_kind_of(OpenSSL::PKey::RSA)
193
+ expect(options[:enable_self_signed_jwt]).to be_nil
194
+ expect(options[:target_audience]).to be_nil
195
+ expect(options[:json_key_io].read).to eq(test_json_env_val)
188
196
 
189
- mocked_signet
197
+ # This should really be a Signet::OAuth2::Client object,
198
+ # but mocking is making that difficult, so return a valid hash instead.
199
+ default_keyfile_hash
190
200
  end
191
201
 
192
202
  creds = TestCredentials3.default
@@ -204,25 +214,28 @@ describe Google::Auth::Credentials, :private do
204
214
  DEFAULT_PATHS = ["~/default/path/to/file.txt"].freeze
205
215
  end
206
216
 
217
+ json_content = JSON.generate default_keyfile_hash
218
+
207
219
  allow(::ENV).to receive(:[]).with("GOOGLE_AUTH_SUPPRESS_CREDENTIALS_WARNINGS") { "true" }
208
220
  allow(::ENV).to receive(:[]).with("PATH_ENV_DUMMY") { "/fake/path/to/file.txt" }
209
221
  allow(::File).to receive(:file?).with("/fake/path/to/file.txt") { false }
210
222
  allow(::ENV).to receive(:[]).with("JSON_ENV_DUMMY") { nil }
211
223
  allow(::File).to receive(:file?).with("~/default/path/to/file.txt") { true }
212
- allow(::File).to receive(:read).with("~/default/path/to/file.txt") { JSON.generate default_keyfile_hash }
224
+ allow(::File).to receive(:read).with("~/default/path/to/file.txt") { json_content }
225
+
226
+ mocked_signet = mock_signet
213
227
 
214
- mocked_signet = double "Signet::OAuth2::Client"
215
- allow(mocked_signet).to receive(:configure_connection).and_return(mocked_signet)
216
- allow(mocked_signet).to receive(:fetch_access_token!).and_return(true)
217
- allow(mocked_signet).to receive(:client_id)
218
- allow(Signet::OAuth2::Client).to receive(:new) do |options|
228
+ allow(Google::Auth::ServiceAccountCredentials).to receive(:make_creds) do |options|
219
229
  expect(options[:token_credential_uri]).to eq("https://oauth2.googleapis.com/token")
220
230
  expect(options[:audience]).to eq("https://oauth2.googleapis.com/token")
221
231
  expect(options[:scope]).to eq(["http://example.com/scope"])
222
- expect(options[:issuer]).to eq(default_keyfile_hash["client_email"])
223
- expect(options[:signing_key]).to be_a_kind_of(OpenSSL::PKey::RSA)
232
+ expect(options[:enable_self_signed_jwt]).to be_nil
233
+ expect(options[:target_audience]).to be_nil
234
+ expect(options[:json_key_io].read).to eq(json_content)
224
235
 
225
- mocked_signet
236
+ # This should really be a Signet::OAuth2::Client object,
237
+ # but mocking is making that difficult, so return a valid hash instead.
238
+ default_keyfile_hash
226
239
  end
227
240
 
228
241
  creds = TestCredentials4.default
@@ -246,26 +259,18 @@ describe Google::Auth::Credentials, :private do
246
259
  allow(::ENV).to receive(:[]).with("JSON_ENV_DUMMY") { nil }
247
260
  allow(::File).to receive(:file?).with("~/default/path/to/file.txt") { false }
248
261
 
249
- mocked_signet = double "Signet::OAuth2::Client"
250
- allow(mocked_signet).to receive(:configure_connection).and_return(mocked_signet)
251
- allow(mocked_signet).to receive(:fetch_access_token!).and_return(true)
252
- allow(mocked_signet).to receive(:client_id)
253
- allow(Google::Auth).to receive(:get_application_default) do |scope|
262
+ mocked_signet = mock_signet
263
+
264
+ allow(Google::Auth).to receive(:get_application_default) do |scope, options|
254
265
  expect(scope).to eq([TestCredentials5::SCOPE])
266
+ expect(options[:enable_self_signed_jwt]).to be_nil
267
+ expect(options[:token_credential_uri]).to eq("https://oauth2.googleapis.com/token")
268
+ expect(options[:audience]).to eq("https://oauth2.googleapis.com/token")
255
269
 
256
270
  # This should really be a Signet::OAuth2::Client object,
257
271
  # but mocking is making that difficult, so return a valid hash instead.
258
272
  default_keyfile_hash
259
273
  end
260
- allow(Signet::OAuth2::Client).to receive(:new) do |options|
261
- expect(options[:token_credential_uri]).to eq("https://oauth2.googleapis.com/token")
262
- expect(options[:audience]).to eq("https://oauth2.googleapis.com/token")
263
- expect(options[:scope]).to eq(["http://example.com/scope"])
264
- expect(options[:issuer]).to eq(default_keyfile_hash["client_email"])
265
- expect(options[:signing_key]).to be_a_kind_of(OpenSSL::PKey::RSA)
266
-
267
- mocked_signet
268
- end
269
274
 
270
275
  creds = TestCredentials5.default
271
276
  expect(creds).to be_a_kind_of(TestCredentials5)
@@ -273,6 +278,31 @@ describe Google::Auth::Credentials, :private do
273
278
  expect(creds.project_id).to eq(default_keyfile_hash["project_id"])
274
279
  expect(creds.quota_project_id).to eq(default_keyfile_hash["quota_project_id"])
275
280
  end
281
+
282
+ it "can be subclassed to pass in other env paths" do
283
+ class TestCredentials6 < Google::Auth::Credentials
284
+ TOKEN_CREDENTIAL_URI = "https://example.com/token".freeze
285
+ AUDIENCE = "https://example.com/audience".freeze
286
+ SCOPE = "http://example.com/scope".freeze
287
+ PATH_ENV_VARS = ["TEST_PATH"].freeze
288
+ JSON_ENV_VARS = ["TEST_JSON_VARS"].freeze
289
+ DEFAULT_PATHS = ["~/default/path/to/file.txt"]
290
+ end
291
+
292
+ class TestCredentials7 < TestCredentials6
293
+ end
294
+
295
+ expect(TestCredentials7.token_credential_uri).to eq("https://example.com/token")
296
+ expect(TestCredentials7.audience).to eq("https://example.com/audience")
297
+ expect(TestCredentials7.scope).to eq(["http://example.com/scope"])
298
+ expect(TestCredentials7.env_vars).to eq(["TEST_PATH", "TEST_JSON_VARS"])
299
+ expect(TestCredentials7.paths).to eq(["~/default/path/to/file.txt"])
300
+
301
+ TestCredentials7::TOKEN_CREDENTIAL_URI = "https://example.com/token2"
302
+ expect(TestCredentials7.token_credential_uri).to eq("https://example.com/token2")
303
+ TestCredentials7::AUDIENCE = nil
304
+ expect(TestCredentials7.audience).to eq("https://example.com/audience")
305
+ end
276
306
  end
277
307
 
278
308
  describe "using class methods" do
@@ -293,18 +323,19 @@ describe Google::Auth::Credentials, :private do
293
323
  allow(::File).to receive(:file?).with(test_path_env_val) { false }
294
324
  allow(::File).to receive(:file?).with(test_json_env_val) { false }
295
325
 
296
- mocked_signet = double "Signet::OAuth2::Client"
297
- allow(mocked_signet).to receive(:configure_connection).and_return(mocked_signet)
298
- allow(mocked_signet).to receive(:fetch_access_token!).and_return(true)
299
- allow(mocked_signet).to receive(:client_id)
300
- allow(Signet::OAuth2::Client).to receive(:new) do |options|
326
+ mocked_signet = mock_signet
327
+
328
+ allow(Google::Auth::ServiceAccountCredentials).to receive(:make_creds) do |options|
301
329
  expect(options[:token_credential_uri]).to eq("https://example.com/token")
302
330
  expect(options[:audience]).to eq("https://example.com/audience")
303
331
  expect(options[:scope]).to eq(["http://example.com/scope"])
304
- expect(options[:issuer]).to eq(default_keyfile_hash["client_email"])
305
- expect(options[:signing_key]).to be_a_kind_of(OpenSSL::PKey::RSA)
332
+ expect(options[:enable_self_signed_jwt]).to be_nil
333
+ expect(options[:target_audience]).to be_nil
334
+ expect(options[:json_key_io].read).to eq(test_json_env_val)
306
335
 
307
- mocked_signet
336
+ # This should really be a Signet::OAuth2::Client object,
337
+ # but mocking is making that difficult, so return a valid hash instead.
338
+ default_keyfile_hash
308
339
  end
309
340
 
310
341
  creds = TestCredentials11.default
@@ -321,25 +352,28 @@ describe Google::Auth::Credentials, :private do
321
352
  self.paths = ["~/default/path/to/file.txt"]
322
353
  end
323
354
 
355
+ json_content = JSON.generate default_keyfile_hash
356
+
324
357
  allow(::ENV).to receive(:[]).with("GOOGLE_AUTH_SUPPRESS_CREDENTIALS_WARNINGS") { "true" }
325
358
  allow(::ENV).to receive(:[]).with("PATH_ENV_DUMMY") { "/fake/path/to/file.txt" }
326
359
  allow(::File).to receive(:file?).with("/fake/path/to/file.txt") { false }
327
360
  allow(::ENV).to receive(:[]).with("PATH_ENV_TEST") { "/unknown/path/to/file.txt" }
328
361
  allow(::File).to receive(:file?).with("/unknown/path/to/file.txt") { true }
329
- allow(::File).to receive(:read).with("/unknown/path/to/file.txt") { JSON.generate default_keyfile_hash }
362
+ allow(::File).to receive(:read).with("/unknown/path/to/file.txt") { json_content }
363
+
364
+ mocked_signet = mock_signet
330
365
 
331
- mocked_signet = double "Signet::OAuth2::Client"
332
- allow(mocked_signet).to receive(:configure_connection).and_return(mocked_signet)
333
- allow(mocked_signet).to receive(:fetch_access_token!).and_return(true)
334
- allow(mocked_signet).to receive(:client_id)
335
- allow(Signet::OAuth2::Client).to receive(:new) do |options|
366
+ allow(Google::Auth::ServiceAccountCredentials).to receive(:make_creds) do |options|
336
367
  expect(options[:token_credential_uri]).to eq("https://oauth2.googleapis.com/token")
337
368
  expect(options[:audience]).to eq("https://oauth2.googleapis.com/token")
338
369
  expect(options[:scope]).to eq(["http://example.com/scope"])
339
- expect(options[:issuer]).to eq(default_keyfile_hash["client_email"])
340
- expect(options[:signing_key]).to be_a_kind_of(OpenSSL::PKey::RSA)
370
+ expect(options[:enable_self_signed_jwt]).to be_nil
371
+ expect(options[:target_audience]).to be_nil
372
+ expect(options[:json_key_io].read).to eq(json_content)
341
373
 
342
- mocked_signet
374
+ # This should really be a Signet::OAuth2::Client object,
375
+ # but mocking is making that difficult, so return a valid hash instead.
376
+ default_keyfile_hash
343
377
  end
344
378
 
345
379
  creds = TestCredentials12.default
@@ -365,18 +399,19 @@ describe Google::Auth::Credentials, :private do
365
399
  allow(::ENV).to receive(:[]).with("JSON_ENV_DUMMY") { nil }
366
400
  allow(::ENV).to receive(:[]).with("JSON_ENV_TEST") { test_json_env_val }
367
401
 
368
- mocked_signet = double "Signet::OAuth2::Client"
369
- allow(mocked_signet).to receive(:configure_connection).and_return(mocked_signet)
370
- allow(mocked_signet).to receive(:fetch_access_token!).and_return(true)
371
- allow(mocked_signet).to receive(:client_id)
372
- allow(Signet::OAuth2::Client).to receive(:new) do |options|
402
+ mocked_signet = mock_signet
403
+
404
+ allow(Google::Auth::ServiceAccountCredentials).to receive(:make_creds) do |options|
373
405
  expect(options[:token_credential_uri]).to eq("https://oauth2.googleapis.com/token")
374
406
  expect(options[:audience]).to eq("https://oauth2.googleapis.com/token")
375
407
  expect(options[:scope]).to eq(["http://example.com/scope"])
376
- expect(options[:issuer]).to eq(default_keyfile_hash["client_email"])
377
- expect(options[:signing_key]).to be_a_kind_of(OpenSSL::PKey::RSA)
408
+ expect(options[:enable_self_signed_jwt]).to be_nil
409
+ expect(options[:target_audience]).to be_nil
410
+ expect(options[:json_key_io].read).to eq(test_json_env_val)
378
411
 
379
- mocked_signet
412
+ # This should really be a Signet::OAuth2::Client object,
413
+ # but mocking is making that difficult, so return a valid hash instead.
414
+ default_keyfile_hash
380
415
  end
381
416
 
382
417
  creds = TestCredentials13.default
@@ -393,25 +428,28 @@ describe Google::Auth::Credentials, :private do
393
428
  self.paths = ["~/default/path/to/file.txt"]
394
429
  end
395
430
 
431
+ json_content = JSON.generate default_keyfile_hash
432
+
396
433
  allow(::ENV).to receive(:[]).with("GOOGLE_AUTH_SUPPRESS_CREDENTIALS_WARNINGS") { "true" }
397
434
  allow(::ENV).to receive(:[]).with("PATH_ENV_DUMMY") { "/fake/path/to/file.txt" }
398
435
  allow(::File).to receive(:file?).with("/fake/path/to/file.txt") { false }
399
436
  allow(::ENV).to receive(:[]).with("JSON_ENV_DUMMY") { nil }
400
437
  allow(::File).to receive(:file?).with("~/default/path/to/file.txt") { true }
401
- allow(::File).to receive(:read).with("~/default/path/to/file.txt") { JSON.generate default_keyfile_hash }
438
+ allow(::File).to receive(:read).with("~/default/path/to/file.txt") { json_content }
439
+
440
+ mocked_signet = mock_signet
402
441
 
403
- mocked_signet = double "Signet::OAuth2::Client"
404
- allow(mocked_signet).to receive(:configure_connection).and_return(mocked_signet)
405
- allow(mocked_signet).to receive(:fetch_access_token!).and_return(true)
406
- allow(mocked_signet).to receive(:client_id)
407
- allow(Signet::OAuth2::Client).to receive(:new) do |options|
442
+ allow(Google::Auth::ServiceAccountCredentials).to receive(:make_creds) do |options|
408
443
  expect(options[:token_credential_uri]).to eq("https://oauth2.googleapis.com/token")
409
444
  expect(options[:audience]).to eq("https://oauth2.googleapis.com/token")
410
445
  expect(options[:scope]).to eq(["http://example.com/scope"])
411
- expect(options[:issuer]).to eq(default_keyfile_hash["client_email"])
412
- expect(options[:signing_key]).to be_a_kind_of(OpenSSL::PKey::RSA)
446
+ expect(options[:enable_self_signed_jwt]).to be_nil
447
+ expect(options[:target_audience]).to be_nil
448
+ expect(options[:json_key_io].read).to eq(json_content)
413
449
 
414
- mocked_signet
450
+ # This should really be a Signet::OAuth2::Client object,
451
+ # but mocking is making that difficult, so return a valid hash instead.
452
+ default_keyfile_hash
415
453
  end
416
454
 
417
455
  creds = TestCredentials14.default
@@ -421,7 +459,7 @@ describe Google::Auth::Credentials, :private do
421
459
  expect(creds.quota_project_id).to eq(default_keyfile_hash["quota_project_id"])
422
460
  end
423
461
 
424
- it "subclasses that find no matches default to Google::Auth.get_application_default" do
462
+ it "subclasses that find no matches default to Google::Auth.get_application_default with self-signed jwt enabled" do
425
463
  class TestCredentials15 < Google::Auth::Credentials
426
464
  self.scope = "http://example.com/scope"
427
465
  self.env_vars = %w[PATH_ENV_DUMMY JSON_ENV_DUMMY]
@@ -434,33 +472,117 @@ describe Google::Auth::Credentials, :private do
434
472
  allow(::ENV).to receive(:[]).with("JSON_ENV_DUMMY") { nil }
435
473
  allow(::File).to receive(:file?).with("~/default/path/to/file.txt") { false }
436
474
 
437
- mocked_signet = double "Signet::OAuth2::Client"
438
- allow(mocked_signet).to receive(:configure_connection).and_return(mocked_signet)
439
- allow(mocked_signet).to receive(:fetch_access_token!).and_return(true)
440
- allow(mocked_signet).to receive(:client_id)
441
- allow(Google::Auth).to receive(:get_application_default) do |scope|
475
+ mocked_signet = mock_signet
476
+
477
+ allow(Google::Auth).to receive(:get_application_default) do |scope, options|
442
478
  expect(scope).to eq(TestCredentials15.scope)
479
+ expect(options[:enable_self_signed_jwt]).to eq(true)
480
+ expect(options[:token_credential_uri]).to eq("https://oauth2.googleapis.com/token")
481
+ expect(options[:audience]).to eq("https://oauth2.googleapis.com/token")
443
482
 
444
483
  # This should really be a Signet::OAuth2::Client object,
445
484
  # but mocking is making that difficult, so return a valid hash instead.
446
485
  default_keyfile_hash
447
486
  end
448
- allow(Signet::OAuth2::Client).to receive(:new) do |options|
487
+
488
+ creds = TestCredentials15.default enable_self_signed_jwt: true
489
+ expect(creds).to be_a_kind_of(TestCredentials15)
490
+ expect(creds.client).to eq(mocked_signet)
491
+ expect(creds.project_id).to eq(default_keyfile_hash["project_id"])
492
+ expect(creds.quota_project_id).to eq(default_keyfile_hash["quota_project_id"])
493
+ end
494
+
495
+ it "subclasses that find no matches default to Google::Auth.get_application_default with self-signed jwt disabled" do
496
+ class TestCredentials16 < Google::Auth::Credentials
497
+ self.scope = "http://example.com/scope"
498
+ self.env_vars = %w[PATH_ENV_DUMMY JSON_ENV_DUMMY]
499
+ self.paths = ["~/default/path/to/file.txt"]
500
+ end
501
+
502
+ allow(::ENV).to receive(:[]).with("GOOGLE_AUTH_SUPPRESS_CREDENTIALS_WARNINGS") { "true" }
503
+ allow(::ENV).to receive(:[]).with("PATH_ENV_DUMMY") { "/fake/path/to/file.txt" }
504
+ allow(::File).to receive(:file?).with("/fake/path/to/file.txt") { false }
505
+ allow(::ENV).to receive(:[]).with("JSON_ENV_DUMMY") { nil }
506
+ allow(::File).to receive(:file?).with("~/default/path/to/file.txt") { false }
507
+
508
+ mocked_signet = mock_signet
509
+
510
+ allow(Google::Auth).to receive(:get_application_default) do |scope, options|
511
+ expect(scope).to eq(TestCredentials16.scope)
512
+ expect(options[:enable_self_signed_jwt]).to be_nil
449
513
  expect(options[:token_credential_uri]).to eq("https://oauth2.googleapis.com/token")
450
514
  expect(options[:audience]).to eq("https://oauth2.googleapis.com/token")
451
- expect(options[:scope]).to eq(["http://example.com/scope"])
452
- expect(options[:issuer]).to eq(default_keyfile_hash["client_email"])
453
- expect(options[:signing_key]).to be_a_kind_of(OpenSSL::PKey::RSA)
454
515
 
455
- mocked_signet
516
+ # This should really be a Signet::OAuth2::Client object,
517
+ # but mocking is making that difficult, so return a valid hash instead.
518
+ default_keyfile_hash
456
519
  end
457
520
 
458
- creds = TestCredentials15.default
459
- expect(creds).to be_a_kind_of(TestCredentials15)
521
+ creds = TestCredentials16.default
522
+ expect(creds).to be_a_kind_of(TestCredentials16)
460
523
  expect(creds.client).to eq(mocked_signet)
461
524
  expect(creds.project_id).to eq(default_keyfile_hash["project_id"])
462
525
  expect(creds.quota_project_id).to eq(default_keyfile_hash["quota_project_id"])
463
526
  end
527
+
528
+ it "subclasses that find no matches default to Google::Auth.get_application_default with custom values" do
529
+ scope2 = "http://example.com/scope2"
530
+
531
+ class TestCredentials17 < Google::Auth::Credentials
532
+ self.scope = "http://example.com/scope"
533
+ self.env_vars = %w[PATH_ENV_DUMMY JSON_ENV_DUMMY]
534
+ self.paths = ["~/default/path/to/file.txt"]
535
+ self.token_credential_uri = "https://example.com/token2"
536
+ self.audience = "https://example.com/token3"
537
+ end
538
+
539
+ allow(::ENV).to receive(:[]).with("GOOGLE_AUTH_SUPPRESS_CREDENTIALS_WARNINGS") { "true" }
540
+ allow(::ENV).to receive(:[]).with("PATH_ENV_DUMMY") { "/fake/path/to/file.txt" }
541
+ allow(::File).to receive(:file?).with("/fake/path/to/file.txt") { false }
542
+ allow(::ENV).to receive(:[]).with("JSON_ENV_DUMMY") { nil }
543
+ allow(::File).to receive(:file?).with("~/default/path/to/file.txt") { false }
544
+
545
+ mocked_signet = mock_signet
546
+
547
+ allow(Google::Auth).to receive(:get_application_default) do |scope, options|
548
+ expect(scope).to eq(scope2)
549
+ expect(options[:enable_self_signed_jwt]).to eq(false)
550
+ expect(options[:token_credential_uri]).to eq("https://example.com/token2")
551
+ expect(options[:audience]).to eq("https://example.com/token3")
552
+
553
+ # This should really be a Signet::OAuth2::Client object,
554
+ # but mocking is making that difficult, so return a valid hash instead.
555
+ default_keyfile_hash
556
+ end
557
+
558
+ creds = TestCredentials17.default scope: scope2, enable_self_signed_jwt: true
559
+ expect(creds).to be_a_kind_of(TestCredentials17)
560
+ expect(creds.client).to eq(mocked_signet)
561
+ expect(creds.project_id).to eq(default_keyfile_hash["project_id"])
562
+ expect(creds.quota_project_id).to eq(default_keyfile_hash["quota_project_id"])
563
+ end
564
+
565
+ it "subclasses delegate up the class hierarchy" do
566
+ class TestCredentials18 < Google::Auth::Credentials
567
+ self.scope = "http://example.com/scope"
568
+ self.target_audience = "https://example.com/target_audience"
569
+ self.env_vars = ["TEST_PATH", "TEST_JSON_VARS"]
570
+ self.paths = ["~/default/path/to/file.txt"]
571
+ end
572
+
573
+ class TestCredentials19 < TestCredentials18
574
+ end
575
+
576
+ expect(TestCredentials19.scope).to eq(["http://example.com/scope"])
577
+ expect(TestCredentials19.target_audience).to eq("https://example.com/target_audience")
578
+ expect(TestCredentials19.env_vars).to eq(["TEST_PATH", "TEST_JSON_VARS"])
579
+ expect(TestCredentials19.paths).to eq(["~/default/path/to/file.txt"])
580
+
581
+ TestCredentials19.token_credential_uri = "https://example.com/token2"
582
+ expect(TestCredentials19.token_credential_uri).to eq("https://example.com/token2")
583
+ TestCredentials19.token_credential_uri = nil
584
+ expect(TestCredentials19.token_credential_uri).to eq("https://oauth2.googleapis.com/token")
585
+ end
464
586
  end
465
587
 
466
588
  it "warns when cloud sdk credentials are used" do