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
@@ -49,7 +49,8 @@ module Google
49
49
  PROJECT_ID_VAR = "GOOGLE_PROJECT_ID".freeze
50
50
  GCLOUD_POSIX_COMMAND = "gcloud".freeze
51
51
  GCLOUD_WINDOWS_COMMAND = "gcloud.cmd".freeze
52
- GCLOUD_CONFIG_COMMAND = "config config-helper --format json".freeze
52
+ GCLOUD_CONFIG_COMMAND =
53
+ "config config-helper --format json --verbosity none".freeze
53
54
 
54
55
  CREDENTIALS_FILE_NAME = "application_default_credentials.json".freeze
55
56
  NOT_FOUND_ERROR =
@@ -63,13 +64,13 @@ module Google
63
64
  CLOUD_SDK_CLIENT_ID = "764086051850-6qr4p6gpi6hn506pt8ejuq83di341hur.app"\
64
65
  "s.googleusercontent.com".freeze
65
66
 
66
- CLOUD_SDK_CREDENTIALS_WARNING = "Your application has authenticated "\
67
- "using end user credentials from Google Cloud SDK. We recommend that "\
68
- "most server applications use service accounts instead. If your "\
69
- "application continues to use end user credentials from Cloud SDK, "\
70
- 'you might receive a "quota exceeded" or "API not enabled" error. For'\
71
- " more information about service accounts, see "\
72
- "https://cloud.google.com/docs/authentication/.".freeze
67
+ CLOUD_SDK_CREDENTIALS_WARNING = "Your application has authenticated using end user "\
68
+ "credentials from Google Cloud SDK. We recommend that most server applications use "\
69
+ "service accounts instead. If your application continues to use end user credentials "\
70
+ 'from Cloud SDK, you might receive a "quota exceeded" or "API not enabled" error. For '\
71
+ "more information about service accounts, see "\
72
+ "https://cloud.google.com/docs/authentication/. To suppress this message, set the "\
73
+ "GOOGLE_AUTH_SUPPRESS_CREDENTIALS_WARNINGS environment variable.".freeze
73
74
 
74
75
  # make_creds proxies the construction of a credentials instance
75
76
  #
@@ -166,6 +167,7 @@ module Google
166
167
 
167
168
  # Issues warning if cloud sdk client id is used
168
169
  def warn_if_cloud_sdk_credentials client_id
170
+ return if ENV["GOOGLE_AUTH_SUPPRESS_CREDENTIALS_WARNINGS"]
169
171
  warn CLOUD_SDK_CREDENTIALS_WARNING if client_id == CLOUD_SDK_CLIENT_ID
170
172
  end
171
173
 
@@ -0,0 +1,233 @@
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 "googleauth/id_tokens/errors"
32
+ require "googleauth/id_tokens/key_sources"
33
+ require "googleauth/id_tokens/verifier"
34
+
35
+ module Google
36
+ module Auth
37
+ ##
38
+ # ## Verifying Google ID tokens
39
+ #
40
+ # This module verifies ID tokens issued by Google. This can be used to
41
+ # authenticate signed-in users using OpenID Connect. See
42
+ # https://developers.google.com/identity/sign-in/web/backend-auth for more
43
+ # information.
44
+ #
45
+ # ### Basic usage
46
+ #
47
+ # To verify an ID token issued by Google accounts:
48
+ #
49
+ # payload = Google::Auth::IDTokens.verify_oidc the_token,
50
+ # aud: "my-app-client-id"
51
+ #
52
+ # If verification succeeds, you will receive the token's payload as a hash.
53
+ # If verification fails, an exception (normally a subclass of
54
+ # {Google::Auth::IDTokens::VerificationError}) will be raised.
55
+ #
56
+ # To verify an ID token issued by the Google identity-aware proxy (IAP):
57
+ #
58
+ # payload = Google::Auth::IDTokens.verify_iap the_token,
59
+ # aud: "my-app-client-id"
60
+ #
61
+ # These methods will automatically download and cache the Google public
62
+ # keys necessary to verify these tokens. They will also automatically
63
+ # verify the issuer (`iss`) field for their respective types of ID tokens.
64
+ #
65
+ # ### Advanced usage
66
+ #
67
+ # If you want to provide your own public keys, either by pointing at a
68
+ # custom URI or by providing the key data directly, use the Verifier class
69
+ # and pass in a key source.
70
+ #
71
+ # To point to a custom URI that returns a JWK set:
72
+ #
73
+ # source = Google::Auth::IDTokens::JwkHttpKeySource.new "https://example.com/jwk"
74
+ # verifier = Google::Auth::IDTokens::Verifier.new key_source: source
75
+ # payload = verifier.verify the_token, aud: "my-app-client-id"
76
+ #
77
+ # To provide key data directly:
78
+ #
79
+ # jwk_data = {
80
+ # keys: [
81
+ # {
82
+ # alg: "ES256",
83
+ # crv: "P-256",
84
+ # kid: "LYyP2g",
85
+ # kty: "EC",
86
+ # use: "sig",
87
+ # x: "SlXFFkJ3JxMsXyXNrqzE3ozl_0913PmNbccLLWfeQFU",
88
+ # y: "GLSahrZfBErmMUcHP0MGaeVnJdBwquhrhQ8eP05NfCI"
89
+ # }
90
+ # ]
91
+ # }
92
+ # source = Google::Auth::IDTokens::StaticKeySource.from_jwk_set jwk_data
93
+ # verifier = Google::Auth::IDTokens::Verifier key_source: source
94
+ # payload = verifier.verify the_token, aud: "my-app-client-id"
95
+ #
96
+ module IDTokens
97
+ ##
98
+ # A list of issuers expected for Google OIDC-issued tokens.
99
+ #
100
+ # @return [Array<String>]
101
+ #
102
+ OIDC_ISSUERS = ["accounts.google.com", "https://accounts.google.com"].freeze
103
+
104
+ ##
105
+ # A list of issuers expected for Google IAP-issued tokens.
106
+ #
107
+ # @return [Array<String>]
108
+ #
109
+ IAP_ISSUERS = ["https://cloud.google.com/iap"].freeze
110
+
111
+ ##
112
+ # The URL for Google OAuth2 V3 public certs
113
+ #
114
+ # @return [String]
115
+ #
116
+ OAUTH2_V3_CERTS_URL = "https://www.googleapis.com/oauth2/v3/certs"
117
+
118
+ ##
119
+ # The URL for Google IAP public keys
120
+ #
121
+ # @return [String]
122
+ #
123
+ IAP_JWK_URL = "https://www.gstatic.com/iap/verify/public_key-jwk"
124
+
125
+ class << self
126
+ ##
127
+ # The key source providing public keys that can be used to verify
128
+ # ID tokens issued by Google OIDC.
129
+ #
130
+ # @return [Google::Auth::IDTokens::JwkHttpKeySource]
131
+ #
132
+ def oidc_key_source
133
+ @oidc_key_source ||= JwkHttpKeySource.new OAUTH2_V3_CERTS_URL
134
+ end
135
+
136
+ ##
137
+ # The key source providing public keys that can be used to verify
138
+ # ID tokens issued by Google IAP.
139
+ #
140
+ # @return [Google::Auth::IDTokens::JwkHttpKeySource]
141
+ #
142
+ def iap_key_source
143
+ @iap_key_source ||= JwkHttpKeySource.new IAP_JWK_URL
144
+ end
145
+
146
+ ##
147
+ # Reset all convenience key sources. Used for testing.
148
+ # @private
149
+ #
150
+ def forget_sources!
151
+ @oidc_key_source = @iap_key_source = nil
152
+ self
153
+ end
154
+
155
+ ##
156
+ # A convenience method that verifies a token allegedly issued by Google
157
+ # OIDC.
158
+ #
159
+ # @param token [String] The ID token to verify
160
+ # @param aud [String,Array<String>,nil] The expected audience. At least
161
+ # one `aud` field in the token must match at least one of the
162
+ # provided audiences, or the verification will fail with
163
+ # {Google::Auth::IDToken::AudienceMismatchError}. If `nil` (the
164
+ # default), no audience checking is performed.
165
+ # @param azp [String,Array<String>,nil] The expected authorized party
166
+ # (azp). At least one `azp` field in the token must match at least
167
+ # one of the provided values, or the verification will fail with
168
+ # {Google::Auth::IDToken::AuthorizedPartyMismatchError}. If `nil`
169
+ # (the default), no azp checking is performed.
170
+ # @param aud [String,Array<String>,nil] The expected audience. At least
171
+ # one `iss` field in the token must match at least one of the
172
+ # provided issuers, or the verification will fail with
173
+ # {Google::Auth::IDToken::IssuerMismatchError}. If `nil`, no issuer
174
+ # checking is performed. Default is to check against {OIDC_ISSUERS}.
175
+ #
176
+ # @return [Hash] The decoded token payload.
177
+ # @raise [KeySourceError] if the key source failed to obtain public keys
178
+ # @raise [VerificationError] if the token verification failed.
179
+ # Additional data may be available in the error subclass and message.
180
+ #
181
+ def verify_oidc token,
182
+ aud: nil,
183
+ azp: nil,
184
+ iss: OIDC_ISSUERS
185
+
186
+ verifier = Verifier.new key_source: oidc_key_source,
187
+ aud: aud,
188
+ azp: azp,
189
+ iss: iss
190
+ verifier.verify token
191
+ end
192
+
193
+ ##
194
+ # A convenience method that verifies a token allegedly issued by Google
195
+ # IAP.
196
+ #
197
+ # @param token [String] The ID token to verify
198
+ # @param aud [String,Array<String>,nil] The expected audience. At least
199
+ # one `aud` field in the token must match at least one of the
200
+ # provided audiences, or the verification will fail with
201
+ # {Google::Auth::IDToken::AudienceMismatchError}. If `nil` (the
202
+ # default), no audience checking is performed.
203
+ # @param azp [String,Array<String>,nil] The expected authorized party
204
+ # (azp). At least one `azp` field in the token must match at least
205
+ # one of the provided values, or the verification will fail with
206
+ # {Google::Auth::IDToken::AuthorizedPartyMismatchError}. If `nil`
207
+ # (the default), no azp checking is performed.
208
+ # @param aud [String,Array<String>,nil] The expected audience. At least
209
+ # one `iss` field in the token must match at least one of the
210
+ # provided issuers, or the verification will fail with
211
+ # {Google::Auth::IDToken::IssuerMismatchError}. If `nil`, no issuer
212
+ # checking is performed. Default is to check against {IAP_ISSUERS}.
213
+ #
214
+ # @return [Hash] The decoded token payload.
215
+ # @raise [KeySourceError] if the key source failed to obtain public keys
216
+ # @raise [VerificationError] if the token verification failed.
217
+ # Additional data may be available in the error subclass and message.
218
+ #
219
+ def verify_iap token,
220
+ aud: nil,
221
+ azp: nil,
222
+ iss: IAP_ISSUERS
223
+
224
+ verifier = Verifier.new key_source: iap_key_source,
225
+ aud: aud,
226
+ azp: azp,
227
+ iss: iss
228
+ verifier.verify token
229
+ end
230
+ end
231
+ end
232
+ end
233
+ end
@@ -0,0 +1,71 @@
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
+
32
+ module Google
33
+ module Auth
34
+ module IDTokens
35
+ ##
36
+ # Failed to obtain keys from the key source.
37
+ #
38
+ class KeySourceError < StandardError; end
39
+
40
+ ##
41
+ # Failed to verify a token.
42
+ #
43
+ class VerificationError < StandardError; end
44
+
45
+ ##
46
+ # Failed to verify a token because it is expired.
47
+ #
48
+ class ExpiredTokenError < VerificationError; end
49
+
50
+ ##
51
+ # Failed to verify a token because its signature did not match.
52
+ #
53
+ class SignatureError < VerificationError; end
54
+
55
+ ##
56
+ # Failed to verify a token because its issuer did not match.
57
+ #
58
+ class IssuerMismatchError < VerificationError; end
59
+
60
+ ##
61
+ # Failed to verify a token because its audience did not match.
62
+ #
63
+ class AudienceMismatchError < VerificationError; end
64
+
65
+ ##
66
+ # Failed to verify a token because its authorized party did not match.
67
+ #
68
+ class AuthorizedPartyMismatchError < VerificationError; end
69
+ end
70
+ end
71
+ end
@@ -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