googleauth 0.8.1 → 0.13.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/.kokoro/build.bat +9 -1
  3. data/.kokoro/continuous/linux.cfg +12 -2
  4. data/.kokoro/continuous/osx.cfg +5 -0
  5. data/.kokoro/continuous/post.cfg +30 -0
  6. data/.kokoro/continuous/windows.cfg +27 -1
  7. data/.kokoro/presubmit/linux.cfg +11 -1
  8. data/.kokoro/presubmit/osx.cfg +5 -0
  9. data/.kokoro/presubmit/windows.cfg +27 -1
  10. data/.kokoro/release.cfg +42 -1
  11. data/.kokoro/trampoline.bat +10 -0
  12. data/.repo-metadata.json +5 -0
  13. data/.rubocop.yml +10 -2
  14. data/CHANGELOG.md +34 -0
  15. data/Gemfile +8 -3
  16. data/README.md +7 -12
  17. data/Rakefile +48 -5
  18. data/googleauth.gemspec +6 -3
  19. data/integration/helper.rb +31 -0
  20. data/integration/id_tokens/key_source_test.rb +74 -0
  21. data/lib/googleauth.rb +1 -0
  22. data/lib/googleauth/application_default.rb +1 -1
  23. data/lib/googleauth/compute_engine.rb +19 -17
  24. data/lib/googleauth/credentials.rb +318 -63
  25. data/lib/googleauth/credentials_loader.rb +10 -8
  26. data/lib/googleauth/id_tokens.rb +233 -0
  27. data/lib/googleauth/id_tokens/errors.rb +71 -0
  28. data/lib/googleauth/id_tokens/key_sources.rb +394 -0
  29. data/lib/googleauth/id_tokens/verifier.rb +144 -0
  30. data/lib/googleauth/json_key_reader.rb +6 -2
  31. data/lib/googleauth/service_account.rb +16 -7
  32. data/lib/googleauth/signet.rb +8 -5
  33. data/lib/googleauth/user_authorizer.rb +6 -1
  34. data/lib/googleauth/user_refresh.rb +2 -2
  35. data/lib/googleauth/version.rb +1 -1
  36. data/lib/googleauth/web_user_authorizer.rb +13 -8
  37. data/rakelib/devsite_builder.rb +45 -0
  38. data/rakelib/link_checker.rb +64 -0
  39. data/rakelib/repo_metadata.rb +59 -0
  40. data/spec/googleauth/apply_auth_examples.rb +28 -5
  41. data/spec/googleauth/compute_engine_spec.rb +25 -13
  42. data/spec/googleauth/credentials_spec.rb +366 -161
  43. data/spec/googleauth/service_account_spec.rb +23 -16
  44. data/spec/googleauth/signet_spec.rb +46 -7
  45. data/spec/googleauth/user_authorizer_spec.rb +21 -1
  46. data/spec/googleauth/user_refresh_spec.rb +1 -1
  47. data/spec/googleauth/web_user_authorizer_spec.rb +6 -0
  48. data/test/helper.rb +33 -0
  49. data/test/id_tokens/key_sources_test.rb +240 -0
  50. data/test/id_tokens/verifier_test.rb +269 -0
  51. metadata +46 -12
  52. data/.kokoro/windows.sh +0 -4
@@ -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"
@@ -100,4 +108,35 @@ describe Signet::OAuth2::Client do
100
108
  expect(stub).to have_been_requested
101
109
  end
102
110
  end
111
+
112
+ describe "#fetch_access_token!" do
113
+ it "retries when orig_fetch_access_token! raises Signet::RemoteServerError" do
114
+ mocked_responses = [:raise, :raise, "success"]
115
+ allow(@client).to receive(:orig_fetch_access_token!).exactly(3).times do
116
+ response = mocked_responses.shift
117
+ response == :raise ? raise(Signet::RemoteServerError) : response
118
+ end
119
+ expect(@client.fetch_access_token!).to eq("success")
120
+ end
121
+
122
+ it "raises when the max retry count is exceeded" do
123
+ mocked_responses = [:raise, :raise, :raise, :raise, :raise, :raise, "success"]
124
+ allow(@client).to receive(:orig_fetch_access_token!).exactly(6).times do
125
+ response = mocked_responses.shift
126
+ response == :raise ? raise(Signet::RemoteServerError) : response
127
+ end
128
+ expect { @client.fetch_access_token! }.to raise_error Signet::AuthorizationError
129
+ end
130
+
131
+ it "does not retry and raises right away if it encounters a Signet::AuthorizationError" do
132
+ allow(@client).to receive(:orig_fetch_access_token!).at_most(:once)
133
+ .and_raise(Signet::AuthorizationError.new("Some Message"))
134
+ expect { @client.fetch_access_token! }.to raise_error Signet::AuthorizationError
135
+ end
136
+
137
+ it "does not retry and raises right away if it encounters a Signet::ParseError" do
138
+ allow(@client).to receive(:orig_fetch_access_token!).at_most(:once).and_raise(Signet::ParseError)
139
+ expect { @client.fetch_access_token! }.to raise_error Signet::ParseError
140
+ end
141
+ end
103
142
  end
@@ -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",
@@ -63,6 +63,12 @@ describe Google::Auth::WebUserAuthorizer do
63
63
  )
64
64
  end
65
65
 
66
+ it "should allow adding custom state key-value pairs" do
67
+ url = authorizer.get_authorization_url request: request, state: { james: "bond", kind: 1 }
68
+ expect(url).to match(%r{%22james%22:%22bond%22})
69
+ expect(url).to match(%r{%22kind%22:1})
70
+ end
71
+
66
72
  it "should include request forgery token in state" do
67
73
  expect(SecureRandom).to receive(:base64).and_return("aGVsbG8=")
68
74
  url = authorizer.get_authorization_url request: request
@@ -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