googleauth 0.12.0 → 0.13.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,394 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2020 Google LLC
4
+ #
5
+ # Redistribution and use in source and binary forms, with or without
6
+ # modification, are permitted provided that the following conditions are
7
+ # met:
8
+ #
9
+ # * Redistributions of source code must retain the above copyright
10
+ # notice, this list of conditions and the following disclaimer.
11
+ # * Redistributions in binary form must reproduce the above
12
+ # copyright notice, this list of conditions and the following disclaimer
13
+ # in the documentation and/or other materials provided with the
14
+ # distribution.
15
+ # * Neither the name of Google Inc. nor the names of its
16
+ # contributors may be used to endorse or promote products derived from
17
+ # this software without specific prior written permission.
18
+ #
19
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20
+ # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21
+ # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22
+ # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23
+ # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24
+ # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25
+ # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26
+ # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27
+ # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28
+ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
+
31
+ require "base64"
32
+ require "json"
33
+ require "monitor"
34
+ require "net/http"
35
+ require "openssl"
36
+
37
+ require "jwt"
38
+
39
+ module Google
40
+ module Auth
41
+ module IDTokens
42
+ ##
43
+ # A public key used for verifying ID tokens.
44
+ #
45
+ # This includes the public key data, ID, and the algorithm used for
46
+ # signature verification. RSA and Elliptical Curve (EC) keys are
47
+ # supported.
48
+ #
49
+ class KeyInfo
50
+ ##
51
+ # Create a public key info structure.
52
+ #
53
+ # @param id [String] The key ID.
54
+ # @param key [OpenSSL::PKey::RSA,OpenSSL::PKey::EC] The key itself.
55
+ # @param algorithm [String] The algorithm (normally `RS256` or `ES256`)
56
+ #
57
+ def initialize id: nil, key: nil, algorithm: nil
58
+ @id = id
59
+ @key = key
60
+ @algorithm = algorithm
61
+ end
62
+
63
+ ##
64
+ # The key ID.
65
+ # @return [String]
66
+ #
67
+ attr_reader :id
68
+
69
+ ##
70
+ # The key itself.
71
+ # @return [OpenSSL::PKey::RSA,OpenSSL::PKey::EC]
72
+ #
73
+ attr_reader :key
74
+
75
+ ##
76
+ # The signature algorithm. (normally `RS256` or `ES256`)
77
+ # @return [String]
78
+ #
79
+ attr_reader :algorithm
80
+
81
+ class << self
82
+ ##
83
+ # Create a KeyInfo from a single JWK, which may be given as either a
84
+ # hash or an unparsed JSON string.
85
+ #
86
+ # @param jwk [Hash,String] The JWK specification.
87
+ # @return [KeyInfo]
88
+ # @raise [KeySourceError] If the key could not be extracted from the
89
+ # JWK.
90
+ #
91
+ def from_jwk jwk
92
+ jwk = symbolize_keys ensure_json_parsed jwk
93
+ key = case jwk[:kty]
94
+ when "RSA"
95
+ extract_rsa_key jwk
96
+ when "EC"
97
+ extract_ec_key jwk
98
+ when nil
99
+ raise KeySourceError, "Key type not found"
100
+ else
101
+ raise KeySourceError, "Cannot use key type #{jwk[:kty]}"
102
+ end
103
+ new id: jwk[:kid], key: key, algorithm: jwk[:alg]
104
+ end
105
+
106
+ ##
107
+ # Create an array of KeyInfo from a JWK Set, which may be given as
108
+ # either a hash or an unparsed JSON string.
109
+ #
110
+ # @param jwk [Hash,String] The JWK Set specification.
111
+ # @return [Array<KeyInfo>]
112
+ # @raise [KeySourceError] If a key could not be extracted from the
113
+ # JWK Set.
114
+ #
115
+ def from_jwk_set jwk_set
116
+ jwk_set = symbolize_keys ensure_json_parsed jwk_set
117
+ jwks = jwk_set[:keys]
118
+ raise KeySourceError, "No keys found in jwk set" unless jwks
119
+ jwks.map { |jwk| from_jwk jwk }
120
+ end
121
+
122
+ private
123
+
124
+ def ensure_json_parsed input
125
+ return input unless input.is_a? String
126
+ JSON.parse input
127
+ rescue JSON::ParserError
128
+ raise KeySourceError, "Unable to parse JSON"
129
+ end
130
+
131
+ def symbolize_keys hash
132
+ result = {}
133
+ hash.each { |key, val| result[key.to_sym] = val }
134
+ result
135
+ end
136
+
137
+ def extract_rsa_key jwk
138
+ begin
139
+ n_data = Base64.urlsafe_decode64 jwk[:n]
140
+ e_data = Base64.urlsafe_decode64 jwk[:e]
141
+ rescue ArgumentError
142
+ raise KeySourceError, "Badly formatted key data"
143
+ end
144
+ n_bn = OpenSSL::BN.new n_data, 2
145
+ e_bn = OpenSSL::BN.new e_data, 2
146
+ rsa_key = OpenSSL::PKey::RSA.new
147
+ if rsa_key.respond_to? :set_key
148
+ rsa_key.set_key n_bn, e_bn, nil
149
+ else
150
+ rsa_key.n = n_bn
151
+ rsa_key.e = e_bn
152
+ end
153
+ rsa_key.public_key
154
+ end
155
+
156
+ # @private
157
+ CURVE_NAME_MAP = {
158
+ "P-256" => "prime256v1",
159
+ "P-384" => "secp384r1",
160
+ "P-521" => "secp521r1",
161
+ "secp256k1" => "secp256k1"
162
+ }.freeze
163
+
164
+ def extract_ec_key jwk
165
+ begin
166
+ x_data = Base64.urlsafe_decode64 jwk[:x]
167
+ y_data = Base64.urlsafe_decode64 jwk[:y]
168
+ rescue ArgumentError
169
+ raise KeySourceError, "Badly formatted key data"
170
+ end
171
+ curve_name = CURVE_NAME_MAP[jwk[:crv]]
172
+ raise KeySourceError, "Unsupported EC curve #{jwk[:crv]}" unless curve_name
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
175
+ key = OpenSSL::PKey::EC.new curve_name
176
+ key.public_key = OpenSSL::PKey::EC::Point.new group, bn
177
+ key
178
+ end
179
+ end
180
+ end
181
+
182
+ ##
183
+ # A key source that contains a static set of keys.
184
+ #
185
+ class StaticKeySource
186
+ ##
187
+ # Create a static key source with the given keys.
188
+ #
189
+ # @param keys [Array<KeyInfo>] The keys
190
+ #
191
+ def initialize keys
192
+ @current_keys = Array(keys)
193
+ end
194
+
195
+ ##
196
+ # Return the current keys. Does not perform any refresh.
197
+ #
198
+ # @return [Array<KeyInfo>]
199
+ #
200
+ attr_reader :current_keys
201
+ alias refresh_keys current_keys
202
+
203
+ class << self
204
+ ##
205
+ # Create a static key source containing a single key parsed from a
206
+ # single JWK, which may be given as either a hash or an unparsed
207
+ # JSON string.
208
+ #
209
+ # @param jwk [Hash,String] The JWK specification.
210
+ # @return [StaticKeySource]
211
+ #
212
+ def from_jwk jwk
213
+ new KeyInfo.from_jwk jwk
214
+ end
215
+
216
+ ##
217
+ # Create a static key source containing multiple keys parsed from a
218
+ # JWK Set, which may be given as either a hash or an unparsed JSON
219
+ # string.
220
+ #
221
+ # @param jwk_set [Hash,String] The JWK Set specification.
222
+ # @return [StaticKeySource]
223
+ #
224
+ def from_jwk_set jwk_set
225
+ new KeyInfo.from_jwk_set jwk_set
226
+ end
227
+ end
228
+ end
229
+
230
+ ##
231
+ # A base key source that downloads keys from a URI. Subclasses should
232
+ # override {HttpKeySource#interpret_json} to parse the response.
233
+ #
234
+ class HttpKeySource
235
+ ##
236
+ # The default interval between retries in seconds (3600s = 1hr).
237
+ #
238
+ # @return [Integer]
239
+ #
240
+ DEFAULT_RETRY_INTERVAL = 3600
241
+
242
+ ##
243
+ # Create an HTTP key source.
244
+ #
245
+ # @param uri [String,URI] The URI from which to download keys.
246
+ # @param retry_interval [Integer,nil] Override the retry interval in
247
+ # seconds. This is the minimum time between retries of failed key
248
+ # downloads.
249
+ #
250
+ def initialize uri, retry_interval: nil
251
+ @uri = URI uri
252
+ @retry_interval = retry_interval || DEFAULT_RETRY_INTERVAL
253
+ @allow_refresh_at = Time.now
254
+ @current_keys = []
255
+ @monitor = Monitor.new
256
+ end
257
+
258
+ ##
259
+ # The URI from which to download keys.
260
+ # @return [Array<KeyInfo>]
261
+ #
262
+ attr_reader :uri
263
+
264
+ ##
265
+ # Return the current keys, without attempting to re-download.
266
+ #
267
+ # @return [Array<KeyInfo>]
268
+ #
269
+ attr_reader :current_keys
270
+
271
+ ##
272
+ # Attempt to re-download keys (if the retry interval has expired) and
273
+ # return the new keys.
274
+ #
275
+ # @return [Array<KeyInfo>]
276
+ # @raise [KeySourceError] if key retrieval failed.
277
+ #
278
+ def refresh_keys
279
+ @monitor.synchronize do
280
+ return @current_keys if Time.now < @allow_refresh_at
281
+ @allow_refresh_at = Time.now + @retry_interval
282
+
283
+ response = Net::HTTP.get_response uri
284
+ raise KeySourceError, "Unable to retrieve data from #{uri}" unless response.is_a? Net::HTTPSuccess
285
+
286
+ data = begin
287
+ JSON.parse response.body
288
+ rescue JSON::ParserError
289
+ raise KeySourceError, "Unable to parse JSON"
290
+ end
291
+
292
+ @current_keys = Array(interpret_json(data))
293
+ end
294
+ end
295
+
296
+ protected
297
+
298
+ def interpret_json _data
299
+ nil
300
+ end
301
+ end
302
+
303
+ ##
304
+ # A key source that downloads X509 certificates.
305
+ # Used by the legacy OAuth V1 public certs endpoint.
306
+ #
307
+ class X509CertHttpKeySource < HttpKeySource
308
+ ##
309
+ # Create a key source that downloads X509 certificates.
310
+ #
311
+ # @param uri [String,URI] The URI from which to download keys.
312
+ # @param algorithm [String] The algorithm to use for signature
313
+ # verification. Defaults to "`RS256`".
314
+ # @param retry_interval [Integer,nil] Override the retry interval in
315
+ # seconds. This is the minimum time between retries of failed key
316
+ # downloads.
317
+ #
318
+ def initialize uri, algorithm: "RS256", retry_interval: nil
319
+ super uri, retry_interval: retry_interval
320
+ @algorithm = algorithm
321
+ end
322
+
323
+ protected
324
+
325
+ def interpret_json data
326
+ data.map do |id, cert_str|
327
+ key = OpenSSL::X509::Certificate.new(cert_str).public_key
328
+ KeyInfo.new id: id, key: key, algorithm: @algorithm
329
+ end
330
+ rescue OpenSSL::X509::CertificateError
331
+ raise KeySourceError, "Unable to parse X509 certificates"
332
+ end
333
+ end
334
+
335
+ ##
336
+ # A key source that downloads a JWK set.
337
+ #
338
+ class JwkHttpKeySource < HttpKeySource
339
+ ##
340
+ # Create a key source that downloads a JWT Set.
341
+ #
342
+ # @param uri [String,URI] The URI from which to download keys.
343
+ # @param retry_interval [Integer,nil] Override the retry interval in
344
+ # seconds. This is the minimum time between retries of failed key
345
+ # downloads.
346
+ #
347
+ def initialize uri, retry_interval: nil
348
+ super uri, retry_interval: retry_interval
349
+ end
350
+
351
+ protected
352
+
353
+ def interpret_json data
354
+ KeyInfo.from_jwk_set data
355
+ end
356
+ end
357
+
358
+ ##
359
+ # A key source that aggregates other key sources. This means it will
360
+ # aggregate the keys provided by its constituent sources. Additionally,
361
+ # when asked to refresh, it will refresh all its constituent sources.
362
+ #
363
+ class AggregateKeySource
364
+ ##
365
+ # Create a key source that aggregates other key sources.
366
+ #
367
+ # @param sources [Array<key source>] The key sources to aggregate.
368
+ #
369
+ def initialize sources
370
+ @sources = Array(sources)
371
+ end
372
+
373
+ ##
374
+ # Return the current keys, without attempting to refresh.
375
+ #
376
+ # @return [Array<KeyInfo>]
377
+ #
378
+ def current_keys
379
+ @sources.flat_map(&:current_keys)
380
+ end
381
+
382
+ ##
383
+ # Attempt to refresh keys and return the new keys.
384
+ #
385
+ # @return [Array<KeyInfo>]
386
+ # @raise [KeySourceError] if key retrieval failed.
387
+ #
388
+ def refresh_keys
389
+ @sources.flat_map(&:refresh_keys)
390
+ end
391
+ end
392
+ end
393
+ end
394
+ end
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2020 Google LLC
4
+ #
5
+ # Redistribution and use in source and binary forms, with or without
6
+ # modification, are permitted provided that the following conditions are
7
+ # met:
8
+ #
9
+ # * Redistributions of source code must retain the above copyright
10
+ # notice, this list of conditions and the following disclaimer.
11
+ # * Redistributions in binary form must reproduce the above
12
+ # copyright notice, this list of conditions and the following disclaimer
13
+ # in the documentation and/or other materials provided with the
14
+ # distribution.
15
+ # * Neither the name of Google Inc. nor the names of its
16
+ # contributors may be used to endorse or promote products derived from
17
+ # this software without specific prior written permission.
18
+ #
19
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20
+ # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21
+ # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22
+ # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23
+ # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24
+ # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25
+ # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26
+ # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27
+ # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28
+ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
+
31
+ require "jwt"
32
+
33
+ module Google
34
+ module Auth
35
+ module IDTokens
36
+ ##
37
+ # An object that can verify ID tokens.
38
+ #
39
+ # A verifier maintains a set of default settings, including the key
40
+ # source and fields to verify. However, individual verification calls can
41
+ # override any of these settings.
42
+ #
43
+ class Verifier
44
+ ##
45
+ # Create a verifier.
46
+ #
47
+ # @param key_source [key source] The default key source to use. All
48
+ # verification calls must have a key source, so if no default key
49
+ # source is provided here, then calls to {#verify} _must_ provide
50
+ # a key source.
51
+ # @param aud [String,nil] The default audience (`aud`) check, or `nil`
52
+ # for no check.
53
+ # @param azp [String,nil] The default authorized party (`azp`) check,
54
+ # or `nil` for no check.
55
+ # @param iss [String,nil] The default issuer (`iss`) check, or `nil`
56
+ # for no check.
57
+ #
58
+ def initialize key_source: nil,
59
+ aud: nil,
60
+ azp: nil,
61
+ iss: nil
62
+ @key_source = key_source
63
+ @aud = aud
64
+ @azp = azp
65
+ @iss = iss
66
+ end
67
+
68
+ ##
69
+ # Verify the given token.
70
+ #
71
+ # @param token [String] the ID token to verify.
72
+ # @param key_source [key source] If given, override the key source.
73
+ # @param aud [String,nil] If given, override the `aud` check.
74
+ # @param azp [String,nil] If given, override the `azp` check.
75
+ # @param iss [String,nil] If given, override the `iss` check.
76
+ #
77
+ # @return [Hash] the decoded payload, if verification succeeded.
78
+ # @raise [KeySourceError] if the key source failed to obtain public keys
79
+ # @raise [VerificationError] if the token verification failed.
80
+ # Additional data may be available in the error subclass and message.
81
+ #
82
+ def verify token,
83
+ key_source: :default,
84
+ aud: :default,
85
+ azp: :default,
86
+ iss: :default
87
+ key_source = @key_source if key_source == :default
88
+ aud = @aud if aud == :default
89
+ azp = @azp if azp == :default
90
+ iss = @iss if iss == :default
91
+
92
+ raise KeySourceError, "No key sources" unless key_source
93
+ keys = key_source.current_keys
94
+ payload = decode_token token, keys, aud, azp, iss
95
+ unless payload
96
+ keys = key_source.refresh_keys
97
+ payload = decode_token token, keys, aud, azp, iss
98
+ end
99
+ raise SignatureError, "Token not verified as issued by Google" unless payload
100
+ payload
101
+ end
102
+
103
+ private
104
+
105
+ def decode_token token, keys, aud, azp, iss
106
+ payload = nil
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
117
+ end
118
+
119
+ normalize_and_verify_payload payload, aud, azp, iss
120
+ end
121
+
122
+ def normalize_and_verify_payload payload, aud, azp, iss
123
+ return nil unless payload
124
+
125
+ # Map the legacy "cid" claim to the canonical "azp"
126
+ payload["azp"] ||= payload["cid"] if payload.key? "cid"
127
+
128
+ # Payload content validation
129
+ if aud && (Array(aud) & Array(payload["aud"])).empty?
130
+ raise AudienceMismatchError, "Token aud mismatch: #{payload['aud']}"
131
+ end
132
+ if azp && (Array(azp) & Array(payload["azp"])).empty?
133
+ raise AuthorizedPartyMismatchError, "Token azp mismatch: #{payload['azp']}"
134
+ end
135
+ if iss && (Array(iss) & Array(payload["iss"])).empty?
136
+ raise IssuerMismatchError, "Token iss mismatch: #{payload['iss']}"
137
+ end
138
+
139
+ payload
140
+ end
141
+ end
142
+ end
143
+ end
144
+ end