googleauth 0.9.0 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.kokoro/continuous/linux.cfg +12 -2
  3. data/.kokoro/continuous/osx.cfg +5 -0
  4. data/.kokoro/continuous/post.cfg +30 -0
  5. data/.kokoro/continuous/windows.cfg +10 -0
  6. data/.kokoro/presubmit/linux.cfg +11 -1
  7. data/.kokoro/presubmit/osx.cfg +5 -0
  8. data/.kokoro/presubmit/windows.cfg +10 -0
  9. data/.kokoro/release.cfg +42 -1
  10. data/.repo-metadata.json +5 -0
  11. data/.rubocop.yml +12 -35
  12. data/CHANGELOG.md +32 -0
  13. data/Gemfile +8 -3
  14. data/README.md +7 -11
  15. data/Rakefile +48 -5
  16. data/googleauth.gemspec +7 -4
  17. data/integration/helper.rb +31 -0
  18. data/integration/id_tokens/key_source_test.rb +74 -0
  19. data/lib/googleauth.rb +1 -0
  20. data/lib/googleauth/application_default.rb +9 -9
  21. data/lib/googleauth/compute_engine.rb +30 -27
  22. data/lib/googleauth/credentials.rb +92 -22
  23. data/lib/googleauth/credentials_loader.rb +14 -15
  24. data/lib/googleauth/id_tokens.rb +233 -0
  25. data/lib/googleauth/id_tokens/errors.rb +71 -0
  26. data/lib/googleauth/id_tokens/key_sources.rb +394 -0
  27. data/lib/googleauth/id_tokens/verifier.rb +144 -0
  28. data/lib/googleauth/json_key_reader.rb +6 -2
  29. data/lib/googleauth/service_account.rb +16 -7
  30. data/lib/googleauth/signet.rb +8 -6
  31. data/lib/googleauth/user_authorizer.rb +8 -3
  32. data/lib/googleauth/user_refresh.rb +1 -1
  33. data/lib/googleauth/version.rb +1 -1
  34. data/lib/googleauth/web_user_authorizer.rb +1 -1
  35. data/rakelib/devsite_builder.rb +45 -0
  36. data/rakelib/link_checker.rb +64 -0
  37. data/rakelib/repo_metadata.rb +59 -0
  38. data/spec/googleauth/apply_auth_examples.rb +28 -5
  39. data/spec/googleauth/compute_engine_spec.rb +37 -13
  40. data/spec/googleauth/credentials_spec.rb +25 -6
  41. data/spec/googleauth/service_account_spec.rb +23 -16
  42. data/spec/googleauth/signet_spec.rb +15 -7
  43. data/spec/googleauth/user_authorizer_spec.rb +21 -1
  44. data/spec/googleauth/user_refresh_spec.rb +1 -1
  45. data/test/helper.rb +33 -0
  46. data/test/id_tokens/key_sources_test.rb +240 -0
  47. data/test/id_tokens/verifier_test.rb +269 -0
  48. metadata +45 -12
@@ -64,13 +64,13 @@ module Google
64
64
  CLOUD_SDK_CLIENT_ID = "764086051850-6qr4p6gpi6hn506pt8ejuq83di341hur.app"\
65
65
  "s.googleusercontent.com".freeze
66
66
 
67
- CLOUD_SDK_CREDENTIALS_WARNING = "Your application has authenticated "\
68
- "using end user credentials from Google Cloud SDK. We recommend that "\
69
- "most server applications use service accounts instead. If your "\
70
- "application continues to use end user credentials from Cloud SDK, "\
71
- 'you might receive a "quota exceeded" or "API not enabled" error. For'\
72
- " more information about service accounts, see "\
73
- "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
74
74
 
75
75
  # make_creds proxies the construction of a credentials instance
76
76
  #
@@ -163,11 +163,13 @@ module Google
163
163
  raise "#{SYSTEM_DEFAULT_ERROR}: #{e}"
164
164
  end
165
165
 
166
+ module_function
167
+
166
168
  # Issues warning if cloud sdk client id is used
167
169
  def warn_if_cloud_sdk_credentials client_id
170
+ return if ENV["GOOGLE_AUTH_SUPPRESS_CREDENTIALS_WARNINGS"]
168
171
  warn CLOUD_SDK_CREDENTIALS_WARNING if client_id == CLOUD_SDK_CLIENT_ID
169
172
  end
170
- module_function :warn_if_cloud_sdk_credentials
171
173
 
172
174
  # Finds project_id from gcloud CLI configuration
173
175
  def load_gcloud_project_id
@@ -179,7 +181,6 @@ module Google
179
181
  rescue StandardError
180
182
  nil
181
183
  end
182
- module_function :load_gcloud_project_id
183
184
 
184
185
  private
185
186
 
@@ -193,15 +194,13 @@ module Google
193
194
  end
194
195
 
195
196
  def service_account_env_vars?
196
- [PRIVATE_KEY_VAR, CLIENT_EMAIL_VAR].all? do |key|
197
- ENV[key] && !ENV[key].empty?
198
- end
197
+ ([PRIVATE_KEY_VAR, CLIENT_EMAIL_VAR] - ENV.keys).empty? &&
198
+ !ENV.to_h.fetch_values(PRIVATE_KEY_VAR, CLIENT_EMAIL_VAR).join(" ").empty?
199
199
  end
200
200
 
201
201
  def authorized_user_env_vars?
202
- [CLIENT_ID_VAR, CLIENT_SECRET_VAR, REFRESH_TOKEN_VAR].all? do |key|
203
- ENV[key] && !ENV[key].empty?
204
- end
202
+ ([CLIENT_ID_VAR, CLIENT_SECRET_VAR, REFRESH_TOKEN_VAR] - ENV.keys).empty? &&
203
+ !ENV.to_h.fetch_values(CLIENT_ID_VAR, CLIENT_SECRET_VAR, REFRESH_TOKEN_VAR).join(" ").empty?
205
204
  end
206
205
  end
207
206
  end
@@ -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