googleauth 0.10.0 → 0.14.0

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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/.github/CODEOWNERS +7 -0
  3. data/.kokoro/continuous/linux.cfg +2 -2
  4. data/.kokoro/continuous/post.cfg +30 -0
  5. data/.kokoro/presubmit/linux.cfg +1 -1
  6. data/.kokoro/release.cfg +1 -1
  7. data/.repo-metadata.json +5 -0
  8. data/.rubocop.yml +5 -4
  9. data/CHANGELOG.md +27 -0
  10. data/Gemfile +5 -2
  11. data/{COPYING → LICENSE} +0 -0
  12. data/README.md +4 -5
  13. data/Rakefile +45 -3
  14. data/googleauth.gemspec +5 -3
  15. data/integration/helper.rb +31 -0
  16. data/integration/id_tokens/key_source_test.rb +74 -0
  17. data/lib/googleauth.rb +1 -0
  18. data/lib/googleauth/application_default.rb +2 -2
  19. data/lib/googleauth/compute_engine.rb +36 -6
  20. data/lib/googleauth/credentials.rb +89 -22
  21. data/lib/googleauth/id_tokens.rb +233 -0
  22. data/lib/googleauth/id_tokens/errors.rb +71 -0
  23. data/lib/googleauth/id_tokens/key_sources.rb +394 -0
  24. data/lib/googleauth/id_tokens/verifier.rb +144 -0
  25. data/lib/googleauth/json_key_reader.rb +6 -2
  26. data/lib/googleauth/service_account.rb +16 -7
  27. data/lib/googleauth/signet.rb +3 -2
  28. data/lib/googleauth/user_authorizer.rb +6 -1
  29. data/lib/googleauth/user_refresh.rb +1 -1
  30. data/lib/googleauth/version.rb +1 -1
  31. data/rakelib/devsite_builder.rb +45 -0
  32. data/rakelib/link_checker.rb +64 -0
  33. data/rakelib/repo_metadata.rb +59 -0
  34. data/spec/googleauth/apply_auth_examples.rb +28 -5
  35. data/spec/googleauth/compute_engine_spec.rb +48 -13
  36. data/spec/googleauth/credentials_spec.rb +17 -6
  37. data/spec/googleauth/service_account_spec.rb +23 -16
  38. data/spec/googleauth/signet_spec.rb +15 -7
  39. data/spec/googleauth/user_authorizer_spec.rb +21 -1
  40. data/spec/googleauth/user_refresh_spec.rb +1 -1
  41. data/test/helper.rb +33 -0
  42. data/test/id_tokens/key_sources_test.rb +240 -0
  43. data/test/id_tokens/verifier_test.rb +269 -0
  44. metadata +46 -12
@@ -36,12 +36,13 @@ require "googleauth"
36
36
  describe Google::Auth::Credentials, :private do
37
37
  let :default_keyfile_hash do
38
38
  {
39
- "private_key_id" => "testabc1234567890xyz",
40
- "private_key" => "-----BEGIN RSA PRIVATE KEY-----\nMIIBOwIBAAJBAOyi0Hy1l4Ym2m2o71Q0TF4O9E81isZEsX0bb+Bqz1SXEaSxLiXM\nUZE8wu0eEXivXuZg6QVCW/5l+f2+9UPrdNUCAwEAAQJAJkqubA/Chj3RSL92guy3\nktzeodarLyw8gF8pOmpuRGSiEo/OLTeRUMKKD1/kX4f9sxf3qDhB4e7dulXR1co/\nIQIhAPx8kMW4XTTL6lJYd2K5GrH8uBMp8qL5ya3/XHrBgw3dAiEA7+3Iw3ULTn2I\n1J34WlJ2D5fbzMzB4FAHUNEV7Ys3f1kCIQDtUahCMChrl7+H5t9QS+xrn77lRGhs\nB50pjvy95WXpgQIhAI2joW6JzTfz8fAapb+kiJ/h9Vcs1ZN3iyoRlNFb61JZAiA8\nNy5NyNrMVwtB/lfJf1dAK/p/Bwd8LZLtgM6PapRfgw==\n-----END RSA PRIVATE KEY-----\n",
41
- "client_email" => "credz-testabc1234567890xyz@developer.gserviceaccount.com",
42
- "client_id" => "credz-testabc1234567890xyz.apps.googleusercontent.com",
43
- "type" => "service_account",
44
- "project_id" => "a_project_id"
39
+ "private_key_id" => "testabc1234567890xyz",
40
+ "private_key" => "-----BEGIN RSA PRIVATE KEY-----\nMIIBOwIBAAJBAOyi0Hy1l4Ym2m2o71Q0TF4O9E81isZEsX0bb+Bqz1SXEaSxLiXM\nUZE8wu0eEXivXuZg6QVCW/5l+f2+9UPrdNUCAwEAAQJAJkqubA/Chj3RSL92guy3\nktzeodarLyw8gF8pOmpuRGSiEo/OLTeRUMKKD1/kX4f9sxf3qDhB4e7dulXR1co/\nIQIhAPx8kMW4XTTL6lJYd2K5GrH8uBMp8qL5ya3/XHrBgw3dAiEA7+3Iw3ULTn2I\n1J34WlJ2D5fbzMzB4FAHUNEV7Ys3f1kCIQDtUahCMChrl7+H5t9QS+xrn77lRGhs\nB50pjvy95WXpgQIhAI2joW6JzTfz8fAapb+kiJ/h9Vcs1ZN3iyoRlNFb61JZAiA8\nNy5NyNrMVwtB/lfJf1dAK/p/Bwd8LZLtgM6PapRfgw==\n-----END RSA PRIVATE KEY-----\n",
41
+ "client_email" => "credz-testabc1234567890xyz@developer.gserviceaccount.com",
42
+ "client_id" => "credz-testabc1234567890xyz.apps.googleusercontent.com",
43
+ "type" => "service_account",
44
+ "project_id" => "a_project_id",
45
+ "quota_project_id" => "b_project_id"
45
46
  }
46
47
  end
47
48
 
@@ -118,6 +119,7 @@ describe Google::Auth::Credentials, :private do
118
119
  expect(creds).to be_a_kind_of(TestCredentials1)
119
120
  expect(creds.client).to eq(mocked_signet)
120
121
  expect(creds.project_id).to eq(default_keyfile_hash["project_id"])
122
+ expect(creds.quota_project_id).to eq(default_keyfile_hash["quota_project_id"])
121
123
  end
122
124
 
123
125
  it "subclasses can use PATH_ENV_VARS to get keyfile path" do
@@ -153,6 +155,7 @@ describe Google::Auth::Credentials, :private do
153
155
  expect(creds).to be_a_kind_of(TestCredentials2)
154
156
  expect(creds.client).to eq(mocked_signet)
155
157
  expect(creds.project_id).to eq(default_keyfile_hash["project_id"])
158
+ expect(creds.quota_project_id).to eq(default_keyfile_hash["quota_project_id"])
156
159
  end
157
160
 
158
161
  it "subclasses can use JSON_ENV_VARS to get keyfile contents" do
@@ -190,6 +193,7 @@ describe Google::Auth::Credentials, :private do
190
193
  expect(creds).to be_a_kind_of(TestCredentials3)
191
194
  expect(creds.client).to eq(mocked_signet)
192
195
  expect(creds.project_id).to eq(default_keyfile_hash["project_id"])
196
+ expect(creds.quota_project_id).to eq(default_keyfile_hash["quota_project_id"])
193
197
  end
194
198
 
195
199
  it "subclasses can use DEFAULT_PATHS to get keyfile path" do
@@ -225,6 +229,7 @@ describe Google::Auth::Credentials, :private do
225
229
  expect(creds).to be_a_kind_of(TestCredentials4)
226
230
  expect(creds.client).to eq(mocked_signet)
227
231
  expect(creds.project_id).to eq(default_keyfile_hash["project_id"])
232
+ expect(creds.quota_project_id).to eq(default_keyfile_hash["quota_project_id"])
228
233
  end
229
234
 
230
235
  it "subclasses that find no matches default to Google::Auth.get_application_default" do
@@ -266,6 +271,7 @@ describe Google::Auth::Credentials, :private do
266
271
  expect(creds).to be_a_kind_of(TestCredentials5)
267
272
  expect(creds.client).to eq(mocked_signet)
268
273
  expect(creds.project_id).to eq(default_keyfile_hash["project_id"])
274
+ expect(creds.quota_project_id).to eq(default_keyfile_hash["quota_project_id"])
269
275
  end
270
276
  end
271
277
 
@@ -305,6 +311,7 @@ describe Google::Auth::Credentials, :private do
305
311
  expect(creds).to be_a_kind_of(TestCredentials11)
306
312
  expect(creds.client).to eq(mocked_signet)
307
313
  expect(creds.project_id).to eq(default_keyfile_hash["project_id"])
314
+ expect(creds.quota_project_id).to eq(default_keyfile_hash["quota_project_id"])
308
315
  end
309
316
 
310
317
  it "subclasses can use PATH_ENV_VARS to get keyfile path" do
@@ -339,6 +346,7 @@ describe Google::Auth::Credentials, :private do
339
346
  expect(creds).to be_a_kind_of(TestCredentials12)
340
347
  expect(creds.client).to eq(mocked_signet)
341
348
  expect(creds.project_id).to eq(default_keyfile_hash["project_id"])
349
+ expect(creds.quota_project_id).to eq(default_keyfile_hash["quota_project_id"])
342
350
  end
343
351
 
344
352
  it "subclasses can use JSON_ENV_VARS to get keyfile contents" do
@@ -375,6 +383,7 @@ describe Google::Auth::Credentials, :private do
375
383
  expect(creds).to be_a_kind_of(TestCredentials13)
376
384
  expect(creds.client).to eq(mocked_signet)
377
385
  expect(creds.project_id).to eq(default_keyfile_hash["project_id"])
386
+ expect(creds.quota_project_id).to eq(default_keyfile_hash["quota_project_id"])
378
387
  end
379
388
 
380
389
  it "subclasses can use DEFAULT_PATHS to get keyfile path" do
@@ -409,6 +418,7 @@ describe Google::Auth::Credentials, :private do
409
418
  expect(creds).to be_a_kind_of(TestCredentials14)
410
419
  expect(creds.client).to eq(mocked_signet)
411
420
  expect(creds.project_id).to eq(default_keyfile_hash["project_id"])
421
+ expect(creds.quota_project_id).to eq(default_keyfile_hash["quota_project_id"])
412
422
  end
413
423
 
414
424
  it "subclasses that find no matches default to Google::Auth.get_application_default" do
@@ -449,6 +459,7 @@ describe Google::Auth::Credentials, :private do
449
459
  expect(creds).to be_a_kind_of(TestCredentials15)
450
460
  expect(creds.client).to eq(mocked_signet)
451
461
  expect(creds.project_id).to eq(default_keyfile_hash["project_id"])
462
+ expect(creds.quota_project_id).to eq(default_keyfile_hash["quota_project_id"])
452
463
  end
453
464
  end
454
465
 
@@ -112,12 +112,13 @@ describe Google::Auth::ServiceAccountCredentials do
112
112
  let(:client_email) { "app@developer.gserviceaccount.com" }
113
113
  let :cred_json do
114
114
  {
115
- private_key_id: "a_private_key_id",
116
- private_key: @key.to_pem,
117
- client_email: client_email,
118
- client_id: "app.apps.googleusercontent.com",
119
- type: "service_account",
120
- project_id: "a_project_id"
115
+ private_key_id: "a_private_key_id",
116
+ private_key: @key.to_pem,
117
+ client_email: client_email,
118
+ client_id: "app.apps.googleusercontent.com",
119
+ type: "service_account",
120
+ project_id: "a_project_id",
121
+ quota_project_id: "b_project_id"
121
122
  }
122
123
  end
123
124
 
@@ -127,24 +128,28 @@ describe Google::Auth::ServiceAccountCredentials do
127
128
  json_key_io: StringIO.new(cred_json_text),
128
129
  scope: "https://www.googleapis.com/auth/userinfo.profile"
129
130
  )
131
+ @id_client = ServiceAccountCredentials.make_creds(
132
+ json_key_io: StringIO.new(cred_json_text),
133
+ target_audience: "https://pubsub.googleapis.com/"
134
+ )
130
135
  end
131
136
 
132
- def make_auth_stubs opts = {}
133
- access_token = opts[:access_token] || ""
134
- body = MultiJson.dump("access_token" => access_token,
135
- "token_type" => "Bearer",
136
- "expires_in" => 3600)
137
+ def make_auth_stubs opts
138
+ body_fields = { "token_type" => "Bearer", "expires_in" => 3600 }
139
+ body_fields["access_token"] = opts[:access_token] if opts[:access_token]
140
+ body_fields["id_token"] = opts[:id_token] if opts[:id_token]
141
+ body = MultiJson.dump body_fields
137
142
  blk = proc do |request|
138
143
  params = Addressable::URI.form_unencode request.body
139
- _claim, _header = JWT.decode(params.assoc("assertion").last,
140
- @key.public_key, true,
141
- algorithm: "RS256")
144
+ claim, _header = JWT.decode(params.assoc("assertion").last,
145
+ @key.public_key, true,
146
+ algorithm: "RS256")
147
+ !opts[:id_token] || claim["target_audience"] == "https://pubsub.googleapis.com/"
142
148
  end
143
149
  stub_request(:post, "https://www.googleapis.com/oauth2/v4/token")
144
150
  .with(body: hash_including(
145
151
  "grant_type" => "urn:ietf:params:oauth:grant-type:jwt-bearer"
146
- ),
147
- &blk)
152
+ ), &blk)
148
153
  .to_return(body: body,
149
154
  status: 200,
150
155
  headers: { "Content-Type" => "application/json" })
@@ -285,6 +290,7 @@ describe Google::Auth::ServiceAccountCredentials do
285
290
  ENV["APPDATA"] = dir
286
291
  credentials = @clz.from_well_known_path @scope
287
292
  expect(credentials.project_id).to eq(cred_json[:project_id])
293
+ expect(credentials.quota_project_id).to eq(cred_json[:quota_project_id])
288
294
  end
289
295
  end
290
296
 
@@ -476,6 +482,7 @@ describe Google::Auth::ServiceAccountJwtHeaderCredentials do
476
482
  ENV["APPDATA"] = dir
477
483
  credentials = clz.from_well_known_path @scope
478
484
  expect(credentials.project_id).to eq(cred_json[:project_id])
485
+ expect(credentials.quota_project_id).to be_nil
479
486
  end
480
487
  end
481
488
  end
@@ -47,18 +47,26 @@ describe Signet::OAuth2::Client do
47
47
  audience: "https://oauth2.googleapis.com/token",
48
48
  signing_key: @key
49
49
  )
50
+ @id_client = Signet::OAuth2::Client.new(
51
+ token_credential_uri: "https://oauth2.googleapis.com/token",
52
+ target_audience: "https://pubsub.googleapis.com/",
53
+ issuer: "app@example.com",
54
+ audience: "https://oauth2.googleapis.com/token",
55
+ signing_key: @key
56
+ )
50
57
  end
51
58
 
52
59
  def make_auth_stubs opts
53
- access_token = opts[:access_token] || ""
54
- body = MultiJson.dump("access_token" => access_token,
55
- "token_type" => "Bearer",
56
- "expires_in" => 3600)
60
+ body_fields = { "token_type" => "Bearer", "expires_in" => 3600 }
61
+ body_fields["access_token"] = opts[:access_token] if opts[:access_token]
62
+ body_fields["id_token"] = opts[:id_token] if opts[:id_token]
63
+ body = MultiJson.dump body_fields
57
64
  blk = proc do |request|
58
65
  params = Addressable::URI.form_unencode request.body
59
- _claim, _header = JWT.decode(params.assoc("assertion").last,
60
- @key.public_key, true,
61
- algorithm: "RS256")
66
+ claim, _header = JWT.decode(params.assoc("assertion").last,
67
+ @key.public_key, true,
68
+ algorithm: "RS256")
69
+ !opts[:id_token] || claim["target_audience"] == "https://pubsub.googleapis.com/"
62
70
  end
63
71
  with_params = { body: hash_including(
64
72
  "grant_type" => "urn:ietf:params:oauth:grant-type:jwt-bearer"
@@ -80,7 +80,7 @@ describe Google::Auth::UserAuthorizer do
80
80
  expect(URI(uri).query).to_not match(/client_secret/)
81
81
  end
82
82
 
83
- it "should include the callback uri" do
83
+ it "should include the redirect_uri" do
84
84
  expect(URI(uri).query).to match(
85
85
  %r{redirect_uri=https://www.example.com/oauth/callback}
86
86
  )
@@ -91,6 +91,25 @@ describe Google::Auth::UserAuthorizer do
91
91
  end
92
92
  end
93
93
 
94
+ context "when generating authorization URLs and callback_uri is 'postmessage'" do
95
+ let(:callback_uri) { "postmessage" }
96
+ let :authorizer do
97
+ Google::Auth::UserAuthorizer.new(client_id,
98
+ scope,
99
+ token_store,
100
+ callback_uri)
101
+ end
102
+ let :uri do
103
+ authorizer.get_authorization_url login_hint: "user1", state: "mystate"
104
+ end
105
+
106
+ it "should include the redirect_uri 'postmessage'" do
107
+ expect(URI(uri).query).to match(
108
+ %r{redirect_uri=postmessage}
109
+ )
110
+ end
111
+ end
112
+
94
113
  context "when generating authorization URLs with user ID & state" do
95
114
  let :uri do
96
115
  authorizer.get_authorization_url login_hint: "user1", state: "mystate"
@@ -253,6 +272,7 @@ describe Google::Auth::UserAuthorizer do
253
272
  user_id: "user1", code: "code"
254
273
  )
255
274
  expect(credentials.access_token).to eq "1/abc123"
275
+ expect(credentials.redirect_uri.to_s).to eq "https://www.example.com/oauth/callback"
256
276
  end
257
277
 
258
278
  it "should not store credentials when get only requested" do
@@ -64,7 +64,7 @@ describe Google::Auth::UserRefreshCredentials do
64
64
  )
65
65
  end
66
66
 
67
- def make_auth_stubs opts = {}
67
+ def make_auth_stubs opts
68
68
  access_token = opts[:access_token] || ""
69
69
  body = MultiJson.dump("access_token" => access_token,
70
70
  "token_type" => "Bearer",
@@ -0,0 +1,33 @@
1
+ # Copyright 2020 Google LLC
2
+ #
3
+ # Redistribution and use in source and binary forms, with or without
4
+ # modification, are permitted provided that the following conditions are
5
+ # met:
6
+ #
7
+ # * Redistributions of source code must retain the above copyright
8
+ # notice, this list of conditions and the following disclaimer.
9
+ # * Redistributions in binary form must reproduce the above
10
+ # copyright notice, this list of conditions and the following disclaimer
11
+ # in the documentation and/or other materials provided with the
12
+ # distribution.
13
+ # * Neither the name of Google Inc. nor the names of its
14
+ # contributors may be used to endorse or promote products derived from
15
+ # this software without specific prior written permission.
16
+ #
17
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18
+ # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19
+ # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20
+ # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
21
+ # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22
+ # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23
+ # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24
+ # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
25
+ # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26
+ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
+
29
+ require "minitest/autorun"
30
+ require "minitest/focus"
31
+ require "webmock/minitest"
32
+
33
+ require "googleauth"
@@ -0,0 +1,240 @@
1
+ # Copyright 2020 Google LLC
2
+ #
3
+ # Redistribution and use in source and binary forms, with or without
4
+ # modification, are permitted provided that the following conditions are
5
+ # met:
6
+ #
7
+ # * Redistributions of source code must retain the above copyright
8
+ # notice, this list of conditions and the following disclaimer.
9
+ # * Redistributions in binary form must reproduce the above
10
+ # copyright notice, this list of conditions and the following disclaimer
11
+ # in the documentation and/or other materials provided with the
12
+ # distribution.
13
+ # * Neither the name of Google Inc. nor the names of its
14
+ # contributors may be used to endorse or promote products derived from
15
+ # this software without specific prior written permission.
16
+ #
17
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18
+ # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19
+ # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20
+ # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
21
+ # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22
+ # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23
+ # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24
+ # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
25
+ # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26
+ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
+
29
+ require "helper"
30
+
31
+ require "openssl"
32
+
33
+ describe Google::Auth::IDTokens do
34
+ describe "StaticKeySource" do
35
+ let(:key1) { Google::Auth::IDTokens::KeyInfo.new id: "1234", key: :key1, algorithm: "RS256" }
36
+ let(:key2) { Google::Auth::IDTokens::KeyInfo.new id: "5678", key: :key2, algorithm: "ES256" }
37
+ let(:keys) { [key1, key2] }
38
+ let(:source) { Google::Auth::IDTokens::StaticKeySource.new keys }
39
+
40
+ it "returns a static set of keys" do
41
+ assert_equal keys, source.current_keys
42
+ end
43
+
44
+ it "does not change on refresh" do
45
+ assert_equal keys, source.refresh_keys
46
+ end
47
+ end
48
+
49
+ describe "HttpKeySource" do
50
+ let(:certs_uri) { "https://example.com/my-certs" }
51
+ let(:certs_body) { "{}" }
52
+
53
+ it "raises an error when failing to parse json from the site" do
54
+ source = Google::Auth::IDTokens::HttpKeySource.new certs_uri
55
+ stub = stub_request(:get, certs_uri).to_return(body: "whoops")
56
+ error = assert_raises Google::Auth::IDTokens::KeySourceError do
57
+ source.refresh_keys
58
+ end
59
+ assert_equal "Unable to parse JSON", error.message
60
+ assert_requested stub
61
+ end
62
+
63
+ it "downloads data but gets no keys" do
64
+ source = Google::Auth::IDTokens::HttpKeySource.new certs_uri
65
+ stub = stub_request(:get, certs_uri).to_return(body: certs_body)
66
+ keys = source.refresh_keys
67
+ assert_empty keys
68
+ assert_requested stub
69
+ end
70
+ end
71
+
72
+ describe "X509CertHttpKeySource" do
73
+ let(:certs_uri) { "https://example.com/my-certs" }
74
+ let(:key1) { OpenSSL::PKey::RSA.new 2048 }
75
+ let(:key2) { OpenSSL::PKey::RSA.new 2048 }
76
+ let(:cert1) { generate_cert key1 }
77
+ let(:cert2) { generate_cert key2 }
78
+ let(:id1) { "1234" }
79
+ let(:id2) { "5678" }
80
+ let(:certs_body) { JSON.dump({ id1 => cert1.to_pem, id2 => cert2.to_pem }) }
81
+
82
+ after do
83
+ WebMock.reset!
84
+ end
85
+
86
+ def generate_cert key
87
+ cert = OpenSSL::X509::Certificate.new
88
+ cert.subject = cert.issuer = OpenSSL::X509::Name.parse "/C=BE/O=Test/OU=Test/CN=Test"
89
+ cert.not_before = Time.now
90
+ cert.not_after = Time.now + 365 * 24 * 60 * 60
91
+ cert.public_key = key.public_key
92
+ cert.serial = 0x0
93
+ cert.version = 2
94
+ cert.sign key, OpenSSL::Digest::SHA1.new
95
+ cert
96
+ end
97
+
98
+ it "raises an error when failing to reach the site" do
99
+ source = Google::Auth::IDTokens::X509CertHttpKeySource.new certs_uri
100
+ stub = stub_request(:get, certs_uri).to_return(body: "whoops", status: 404)
101
+ error = assert_raises Google::Auth::IDTokens::KeySourceError do
102
+ source.refresh_keys
103
+ end
104
+ assert_equal "Unable to retrieve data from #{certs_uri}", error.message
105
+ assert_requested stub
106
+ end
107
+
108
+ it "raises an error when failing to parse json from the site" do
109
+ source = Google::Auth::IDTokens::X509CertHttpKeySource.new certs_uri
110
+ stub = stub_request(:get, certs_uri).to_return(body: "whoops")
111
+ error = assert_raises Google::Auth::IDTokens::KeySourceError do
112
+ source.refresh_keys
113
+ end
114
+ assert_equal "Unable to parse JSON", error.message
115
+ assert_requested stub
116
+ end
117
+
118
+ it "raises an error when failing to parse x509 from the site" do
119
+ source = Google::Auth::IDTokens::X509CertHttpKeySource.new certs_uri
120
+ stub = stub_request(:get, certs_uri).to_return(body: '{"hi": "whoops"}')
121
+ error = assert_raises Google::Auth::IDTokens::KeySourceError do
122
+ source.refresh_keys
123
+ end
124
+ assert_equal "Unable to parse X509 certificates", error.message
125
+ assert_requested stub
126
+ end
127
+
128
+ it "gets the right certificates" do
129
+ source = Google::Auth::IDTokens::X509CertHttpKeySource.new certs_uri
130
+ stub = stub_request(:get, certs_uri).to_return(body: certs_body)
131
+ keys = source.refresh_keys
132
+ assert_equal id1, keys[0].id
133
+ assert_equal id2, keys[1].id
134
+ assert_equal key1.public_key.to_pem, keys[0].key.to_pem
135
+ assert_equal key2.public_key.to_pem, keys[1].key.to_pem
136
+ assert_equal "RS256", keys[0].algorithm
137
+ assert_equal "RS256", keys[1].algorithm
138
+ assert_requested stub
139
+ end
140
+ end
141
+
142
+ describe "JwkHttpKeySource" do
143
+ let(:jwk_uri) { "https://example.com/my-jwk" }
144
+ let(:id1) { "fb8ca5b7d8d9a5c6c6788071e866c6c40f3fc1f9" }
145
+ let(:id2) { "LYyP2g" }
146
+ let(:jwk1) {
147
+ {
148
+ alg: "RS256",
149
+ e: "AQAB",
150
+ kid: id1,
151
+ kty: "RSA",
152
+ n: "zK8PHf_6V3G5rU-viUOL1HvAYn7q--dxMoUkt7x1rSWX6fimla-lpoYAKhFTLU" \
153
+ "ELkRKy_6UDzfybz0P9eItqS2UxVWYpKYmKTQ08HgUBUde4GtO_B0SkSk8iLtGh" \
154
+ "653UBBjgXmfzdfQEz_DsaWn7BMtuAhY9hpMtJye8LQlwaS8ibQrsC0j0GZM5KX" \
155
+ "RITHwfx06_T1qqC_MOZRA6iJs-J2HNlgeyFuoQVBTY6pRqGXa-qaVsSG3iU-vq" \
156
+ "NIciFquIq-xydwxLqZNksRRer5VAsSHf0eD3g2DX-cf6paSy1aM40svO9EfSvG" \
157
+ "_07MuHafEE44RFvSZZ4ubEN9U7ALSjdw",
158
+ use: "sig"
159
+ }
160
+ }
161
+ let(:jwk2) {
162
+ {
163
+ alg: "ES256",
164
+ crv: "P-256",
165
+ kid: id2,
166
+ kty: "EC",
167
+ use: "sig",
168
+ x: "SlXFFkJ3JxMsXyXNrqzE3ozl_0913PmNbccLLWfeQFU",
169
+ y: "GLSahrZfBErmMUcHP0MGaeVnJdBwquhrhQ8eP05NfCI"
170
+ }
171
+ }
172
+ let(:bad_type_jwk) {
173
+ {
174
+ alg: "RS256",
175
+ kid: "hello",
176
+ kty: "blah",
177
+ use: "sig"
178
+ }
179
+ }
180
+ let(:jwk_body) { JSON.dump({ keys: [jwk1, jwk2] }) }
181
+ let(:bad_type_body) { JSON.dump({ keys: [bad_type_jwk] }) }
182
+
183
+ after do
184
+ WebMock.reset!
185
+ end
186
+
187
+ it "raises an error when failing to reach the site" do
188
+ source = Google::Auth::IDTokens::JwkHttpKeySource.new jwk_uri
189
+ stub = stub_request(:get, jwk_uri).to_return(body: "whoops", status: 404)
190
+ error = assert_raises Google::Auth::IDTokens::KeySourceError do
191
+ source.refresh_keys
192
+ end
193
+ assert_equal "Unable to retrieve data from #{jwk_uri}", error.message
194
+ assert_requested stub
195
+ end
196
+
197
+ it "raises an error when failing to parse json from the site" do
198
+ source = Google::Auth::IDTokens::JwkHttpKeySource.new jwk_uri
199
+ stub = stub_request(:get, jwk_uri).to_return(body: "whoops")
200
+ error = assert_raises Google::Auth::IDTokens::KeySourceError do
201
+ source.refresh_keys
202
+ end
203
+ assert_equal "Unable to parse JSON", error.message
204
+ assert_requested stub
205
+ end
206
+
207
+ it "raises an error when the json structure is malformed" do
208
+ source = Google::Auth::IDTokens::JwkHttpKeySource.new jwk_uri
209
+ stub = stub_request(:get, jwk_uri).to_return(body: '{"hi": "whoops"}')
210
+ error = assert_raises Google::Auth::IDTokens::KeySourceError do
211
+ source.refresh_keys
212
+ end
213
+ assert_equal "No keys found in jwk set", error.message
214
+ assert_requested stub
215
+ end
216
+
217
+ it "raises an error when an unrecognized key type is encountered" do
218
+ source = Google::Auth::IDTokens::JwkHttpKeySource.new jwk_uri
219
+ stub = stub_request(:get, jwk_uri).to_return(body: bad_type_body)
220
+ error = assert_raises Google::Auth::IDTokens::KeySourceError do
221
+ source.refresh_keys
222
+ end
223
+ assert_equal "Cannot use key type blah", error.message
224
+ assert_requested stub
225
+ end
226
+
227
+ it "gets the right keys" do
228
+ source = Google::Auth::IDTokens::JwkHttpKeySource.new jwk_uri
229
+ stub = stub_request(:get, jwk_uri).to_return(body: jwk_body)
230
+ keys = source.refresh_keys
231
+ assert_equal id1, keys[0].id
232
+ assert_equal id2, keys[1].id
233
+ assert_kind_of OpenSSL::PKey::RSA, keys[0].key
234
+ assert_kind_of OpenSSL::PKey::EC, keys[1].key
235
+ assert_equal "RS256", keys[0].algorithm
236
+ assert_equal "ES256", keys[1].algorithm
237
+ assert_requested stub
238
+ end
239
+ end
240
+ end