googleauth 0.8.1 → 0.13.0
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.
- checksums.yaml +4 -4
- data/.kokoro/build.bat +9 -1
- data/.kokoro/continuous/linux.cfg +12 -2
- data/.kokoro/continuous/osx.cfg +5 -0
- data/.kokoro/continuous/post.cfg +30 -0
- data/.kokoro/continuous/windows.cfg +27 -1
- data/.kokoro/presubmit/linux.cfg +11 -1
- data/.kokoro/presubmit/osx.cfg +5 -0
- data/.kokoro/presubmit/windows.cfg +27 -1
- data/.kokoro/release.cfg +42 -1
- data/.kokoro/trampoline.bat +10 -0
- data/.repo-metadata.json +5 -0
- data/.rubocop.yml +10 -2
- data/CHANGELOG.md +34 -0
- data/Gemfile +8 -3
- data/README.md +7 -12
- data/Rakefile +48 -5
- data/googleauth.gemspec +6 -3
- data/integration/helper.rb +31 -0
- data/integration/id_tokens/key_source_test.rb +74 -0
- data/lib/googleauth.rb +1 -0
- data/lib/googleauth/application_default.rb +1 -1
- data/lib/googleauth/compute_engine.rb +19 -17
- data/lib/googleauth/credentials.rb +318 -63
- data/lib/googleauth/credentials_loader.rb +10 -8
- data/lib/googleauth/id_tokens.rb +233 -0
- data/lib/googleauth/id_tokens/errors.rb +71 -0
- data/lib/googleauth/id_tokens/key_sources.rb +394 -0
- data/lib/googleauth/id_tokens/verifier.rb +144 -0
- data/lib/googleauth/json_key_reader.rb +6 -2
- data/lib/googleauth/service_account.rb +16 -7
- data/lib/googleauth/signet.rb +8 -5
- data/lib/googleauth/user_authorizer.rb +6 -1
- data/lib/googleauth/user_refresh.rb +2 -2
- data/lib/googleauth/version.rb +1 -1
- data/lib/googleauth/web_user_authorizer.rb +13 -8
- data/rakelib/devsite_builder.rb +45 -0
- data/rakelib/link_checker.rb +64 -0
- data/rakelib/repo_metadata.rb +59 -0
- data/spec/googleauth/apply_auth_examples.rb +28 -5
- data/spec/googleauth/compute_engine_spec.rb +25 -13
- data/spec/googleauth/credentials_spec.rb +366 -161
- data/spec/googleauth/service_account_spec.rb +23 -16
- data/spec/googleauth/signet_spec.rb +46 -7
- data/spec/googleauth/user_authorizer_spec.rb +21 -1
- data/spec/googleauth/user_refresh_spec.rb +1 -1
- data/spec/googleauth/web_user_authorizer_spec.rb +6 -0
- data/test/helper.rb +33 -0
- data/test/id_tokens/key_sources_test.rb +240 -0
- data/test/id_tokens/verifier_test.rb +269 -0
- metadata +46 -12
- 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 =
|
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
|
-
"
|
68
|
-
"
|
69
|
-
|
70
|
-
|
71
|
-
"
|
72
|
-
"
|
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
|