googleauth 0.10.0 → 0.14.0

Sign up to get free protection for your applications and to get access to all the features.
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