googleauth 0.14.0 → 0.16.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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