omniauth-google-oauth2 1.2.1 → 1.2.3
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/.github/workflows/ci.yml +5 -4
- data/.github/workflows/rubocop.yml +19 -0
- data/.rubocop.yml +10 -3
- data/CHANGELOG.md +180 -1
- data/Gemfile +2 -0
- data/README.md +148 -127
- data/_config.yml +3 -0
- data/examples/Gemfile +1 -0
- data/examples/config.ru +96 -62
- data/examples/omni_auth.rb +7 -15
- data/lib/omniauth/google_oauth2/version.rb +1 -1
- data/lib/omniauth/strategies/google_oauth2.rb +251 -27
- data/omniauth-google-oauth2.gemspec +7 -8
- data/spec/omniauth/strategies/google_oauth2_spec.rb +643 -19
- data/spec/spec_helper.rb +9 -0
- metadata +16 -26
- data/spec/rubocop_spec.rb +0 -9
data/examples/omni_auth.rb
CHANGED
|
@@ -2,36 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
# Google's OAuth2 docs. Make sure you are familiar with all the options
|
|
4
4
|
# before attempting to configure this gem.
|
|
5
|
-
# https://developers.google.com/
|
|
5
|
+
# https://developers.google.com/identity/protocols/oauth2
|
|
6
6
|
|
|
7
7
|
Rails.application.config.middleware.use OmniAuth::Builder do
|
|
8
8
|
# Default usage, this will give you offline access and a refresh token
|
|
9
9
|
# using default scopes 'email' and 'profile'
|
|
10
10
|
#
|
|
11
|
-
provider :google_oauth2, ENV['
|
|
11
|
+
provider :google_oauth2, ENV['GOOGLE_CLIENT_ID'], ENV['GOOGLE_CLIENT_SECRET'], scope: 'email, profile'
|
|
12
12
|
|
|
13
13
|
# Custom redirect_uri
|
|
14
14
|
#
|
|
15
|
-
# provider :google_oauth2, ENV['
|
|
15
|
+
# provider :google_oauth2, ENV['GOOGLE_CLIENT_ID'], ENV['GOOGLE_CLIENT_SECRET'], scope: 'email, profile', redirect_uri: 'https://localhost:3000/redirect'
|
|
16
16
|
|
|
17
17
|
# Manual setup for offline access with a refresh token.
|
|
18
18
|
#
|
|
19
|
-
# provider :google_oauth2, ENV['
|
|
19
|
+
# provider :google_oauth2, ENV['GOOGLE_CLIENT_ID'], ENV['GOOGLE_CLIENT_SECRET'], access_type: 'offline'
|
|
20
20
|
|
|
21
|
-
# Custom scope supporting
|
|
21
|
+
# Custom scope supporting YouTube. If you are customizing scopes, remember
|
|
22
22
|
# to include the default scopes 'email' and 'profile'
|
|
23
23
|
#
|
|
24
|
-
# provider :google_oauth2, ENV['
|
|
24
|
+
# provider :google_oauth2, ENV['GOOGLE_CLIENT_ID'], ENV['GOOGLE_CLIENT_SECRET'], scope: 'https://www.googleapis.com/auth/youtube.readonly, email, profile'
|
|
25
25
|
|
|
26
26
|
# Custom scope for users only using Google for account creation/auth and do not require a refresh token.
|
|
27
27
|
#
|
|
28
|
-
# provider :google_oauth2, ENV['
|
|
29
|
-
|
|
30
|
-
# To include information about people in your circles you must include the 'plus.login' scope.
|
|
31
|
-
#
|
|
32
|
-
# provider :google_oauth2, ENV['GOOGLE_KEY'], ENV['GOOGLE_SECRET'], skip_friends: false, scope: 'email, profile, plus.login'
|
|
33
|
-
|
|
34
|
-
# If you need to acquire whether user picture is a default one or uploaded by user.
|
|
35
|
-
#
|
|
36
|
-
# provider :google_oauth2, ENV['GOOGLE_KEY'], ENV['GOOGLE_SECRET'], skip_image_info: false
|
|
28
|
+
# provider :google_oauth2, ENV['GOOGLE_CLIENT_ID'], ENV['GOOGLE_CLIENT_SECRET'], access_type: 'online', prompt: ''
|
|
37
29
|
end
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
require 'jwt'
|
|
4
4
|
require 'oauth2'
|
|
5
5
|
require 'omniauth/strategies/oauth2'
|
|
6
|
+
require 'openssl'
|
|
7
|
+
require 'stringio'
|
|
6
8
|
require 'uri'
|
|
7
9
|
|
|
8
10
|
module OmniAuth
|
|
@@ -14,12 +16,77 @@ module OmniAuth
|
|
|
14
16
|
BASE_SCOPES = %w[profile email openid].freeze
|
|
15
17
|
DEFAULT_SCOPE = 'email,profile'
|
|
16
18
|
USER_INFO_URL = 'https://www.googleapis.com/oauth2/v3/userinfo'
|
|
17
|
-
|
|
19
|
+
JWKS_URL = 'https://www.googleapis.com/oauth2/v3/certs'
|
|
20
|
+
JWKS_CACHE_TTL = 3600
|
|
21
|
+
JWKS_RETRY_INTERVAL = 60
|
|
22
|
+
LOG_MESSAGE_LIMIT = 200
|
|
23
|
+
REQUIRED_ID_TOKEN_CLAIMS = %w[iss aud exp sub].freeze
|
|
18
24
|
AUTHORIZE_OPTIONS = %i[access_type hd login_hint prompt request_visible_actions scope state redirect_uri include_granted_scopes enable_granular_consent openid_realm device_id device_name]
|
|
19
25
|
|
|
26
|
+
JwksUnavailable = Class.new(StandardError)
|
|
27
|
+
|
|
28
|
+
@jwks_mutex = Mutex.new
|
|
29
|
+
|
|
30
|
+
class << self
|
|
31
|
+
# Google's signing keys rotate, so the key set is shared across requests
|
|
32
|
+
# and refetched on expiry or when a token names a kid we do not hold.
|
|
33
|
+
# The fetch deliberately happens under the lock: concurrent callers wait
|
|
34
|
+
# for one request rather than each issuing their own.
|
|
35
|
+
# The keys belong to Google, not to any one strategy class, so a subclass
|
|
36
|
+
# shares this cache rather than keeping a second one and refetching the
|
|
37
|
+
# same data. Delegating also keeps it from reaching for a mutex it does
|
|
38
|
+
# not own, which is only defined here.
|
|
39
|
+
def cached_jwks(force: false, &fetch)
|
|
40
|
+
return GoogleOauth2.cached_jwks(force: force, &fetch) unless equal?(GoogleOauth2)
|
|
41
|
+
|
|
42
|
+
@jwks_mutex.synchronize do
|
|
43
|
+
now = ::Time.now.to_i
|
|
44
|
+
|
|
45
|
+
# A forced refresh means some token named a kid we do not hold, which
|
|
46
|
+
# the sender chooses freely. Honour it no more often than the retry
|
|
47
|
+
# interval, or it becomes a way to drive unlimited fetches, each one
|
|
48
|
+
# holding this lock while every other login waits.
|
|
49
|
+
force &&= @jwks_forced_at.nil? || now >= @jwks_forced_at + JWKS_RETRY_INTERVAL
|
|
50
|
+
|
|
51
|
+
if force || @jwks.nil? || now >= @jwks_expires_at.to_i
|
|
52
|
+
# Back off even with nothing cached. Otherwise an unreachable
|
|
53
|
+
# endpoint queues every waiting caller behind its own timeout,
|
|
54
|
+
# since the lock serializes them and no expiry has been recorded.
|
|
55
|
+
raise JwksUnavailable, 'within JWKS retry backoff' if @jwks.nil? && now < @jwks_retry_at.to_i
|
|
56
|
+
|
|
57
|
+
@jwks_forced_at = now if force
|
|
58
|
+
|
|
59
|
+
begin
|
|
60
|
+
@jwks = yield
|
|
61
|
+
@jwks_expires_at = ::Time.now.to_i + JWKS_CACHE_TTL
|
|
62
|
+
rescue StandardError
|
|
63
|
+
retry_at = ::Time.now.to_i + JWKS_RETRY_INTERVAL
|
|
64
|
+
@jwks_retry_at = retry_at
|
|
65
|
+
raise if @jwks.nil?
|
|
66
|
+
|
|
67
|
+
# Google's keys outlive this cache by a wide margin, so serve the
|
|
68
|
+
# stale set through a short outage rather than failing every
|
|
69
|
+
# login, and back off instead of refetching on each request.
|
|
70
|
+
@jwks_expires_at = retry_at
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
@jwks
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def reset_jwks_cache!
|
|
78
|
+
return GoogleOauth2.reset_jwks_cache! unless equal?(GoogleOauth2)
|
|
79
|
+
|
|
80
|
+
@jwks_mutex.synchronize do
|
|
81
|
+
@jwks = nil
|
|
82
|
+
@jwks_expires_at = nil
|
|
83
|
+
@jwks_retry_at = nil
|
|
84
|
+
@jwks_forced_at = nil
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
20
89
|
option :name, 'google_oauth2'
|
|
21
|
-
option :skip_friends, true
|
|
22
|
-
option :skip_image_info, true
|
|
23
90
|
option :skip_jwt, false
|
|
24
91
|
option :jwt_leeway, 60
|
|
25
92
|
option :authorize_options, AUTHORIZE_OPTIONS
|
|
@@ -69,21 +136,9 @@ module OmniAuth
|
|
|
69
136
|
|
|
70
137
|
extra do
|
|
71
138
|
hash = {}
|
|
72
|
-
token =
|
|
139
|
+
token = access_token['id_token']
|
|
73
140
|
hash[:id_token] = token
|
|
74
|
-
if !options[:skip_jwt] && !nil_or_empty?(token)
|
|
75
|
-
decoded = ::JWT.decode(token, nil, false).first
|
|
76
|
-
|
|
77
|
-
# We have to manually verify the claims because the third parameter to
|
|
78
|
-
# JWT.decode is false since no verification key is provided.
|
|
79
|
-
::JWT::Claims.verify_payload!(decoded,
|
|
80
|
-
iss: ALLOWED_ISSUERS,
|
|
81
|
-
aud: options.client_id,
|
|
82
|
-
exp: { leeway: options.jwt_leeway },
|
|
83
|
-
nbf: { leeway: options.jwt_leeway })
|
|
84
|
-
|
|
85
|
-
hash[:id_info] = decoded
|
|
86
|
-
end
|
|
141
|
+
hash[:id_info] = id_token_claims(token) if !options[:skip_jwt] && !nil_or_empty?(token)
|
|
87
142
|
hash[:raw_info] = raw_info unless skip_info?
|
|
88
143
|
prune! hash
|
|
89
144
|
end
|
|
@@ -95,6 +150,11 @@ module OmniAuth
|
|
|
95
150
|
def custom_build_access_token
|
|
96
151
|
access_token = get_access_token(request)
|
|
97
152
|
|
|
153
|
+
# Nothing in the request produced a usable credential. Raising here gives
|
|
154
|
+
# the caller an ordinary auth failure; the alternative is omniauth-oauth2
|
|
155
|
+
# calling #expired? on nil and the request dying with a NoMethodError.
|
|
156
|
+
raise CallbackError.new(:invalid_credentials, 'No valid credentials were supplied in the callback request') if access_token.nil?
|
|
157
|
+
|
|
98
158
|
verify_hd(access_token)
|
|
99
159
|
access_token
|
|
100
160
|
end
|
|
@@ -107,6 +167,54 @@ module OmniAuth
|
|
|
107
167
|
obj.is_a?(String) ? obj.empty? : obj.nil?
|
|
108
168
|
end
|
|
109
169
|
|
|
170
|
+
# Failure messages quote the request: an unknown key names the caller's own
|
|
171
|
+
# kid header, and a parse error quotes the body. Left alone, a newline in
|
|
172
|
+
# either forges a log line, so bound the length and flatten control
|
|
173
|
+
# characters to keep one record per event.
|
|
174
|
+
def sanitize_for_log(message)
|
|
175
|
+
message.to_s.gsub(/[[:cntrl:]]/, ' ')[0, LOG_MESSAGE_LIMIT]
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def trusted_client_ids
|
|
179
|
+
[options.client_id, *Array(options.authorized_client_ids)].compact.uniq
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def id_token_claims(token)
|
|
183
|
+
return @id_token_claims if token == @id_token_claims_token && !@id_token_claims.nil?
|
|
184
|
+
|
|
185
|
+
clear_id_token_claims
|
|
186
|
+
|
|
187
|
+
# Safe to decode without a key only because every route into
|
|
188
|
+
# access_token['id_token'] has already established trust: the token
|
|
189
|
+
# endpoint delivers it over TLS from Google, and a caller-supplied one
|
|
190
|
+
# is cached here only after verified_id_token has checked its signature.
|
|
191
|
+
claims = ::JWT.decode(token, nil, false).first
|
|
192
|
+
|
|
193
|
+
# We have to manually verify the claims because the third parameter to
|
|
194
|
+
# JWT.decode is false since no verification key is provided.
|
|
195
|
+
# required is what makes the rest binding: the individual claim checks
|
|
196
|
+
# pass silently when a claim is simply absent, so without this a token
|
|
197
|
+
# missing exp would never expire.
|
|
198
|
+
::JWT::Claims.verify_payload!(claims,
|
|
199
|
+
required: REQUIRED_ID_TOKEN_CLAIMS,
|
|
200
|
+
iss: ALLOWED_ISSUERS,
|
|
201
|
+
aud: trusted_client_ids,
|
|
202
|
+
exp: { leeway: options.jwt_leeway },
|
|
203
|
+
nbf: { leeway: options.jwt_leeway })
|
|
204
|
+
|
|
205
|
+
cache_id_token_claims(token, claims)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def cache_id_token_claims(token, claims)
|
|
209
|
+
@id_token_claims_token = token
|
|
210
|
+
@id_token_claims = claims
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def clear_id_token_claims
|
|
214
|
+
@id_token_claims_token = nil
|
|
215
|
+
@id_token_claims = nil
|
|
216
|
+
end
|
|
217
|
+
|
|
110
218
|
def callback_url
|
|
111
219
|
options[:redirect_uri] || (full_host + callback_path)
|
|
112
220
|
end
|
|
@@ -120,25 +228,138 @@ module OmniAuth
|
|
|
120
228
|
elsif verifier
|
|
121
229
|
client_get_token(verifier, redirect_uri || callback_url)
|
|
122
230
|
elsif access_token && verify_token(access_token)
|
|
123
|
-
::OAuth2::AccessToken.from_hash(client, request.params
|
|
231
|
+
::OAuth2::AccessToken.from_hash(client, direct_token_hash(access_token, request.params['id_token']))
|
|
124
232
|
elsif request.content_type =~ /json/i
|
|
125
233
|
begin
|
|
126
|
-
|
|
127
|
-
|
|
234
|
+
raw_body = request.body.read
|
|
235
|
+
# Rack 3 input streams are not required to be rewindable, so hand
|
|
236
|
+
# downstream middlewares a fresh stream rather than rewinding this one.
|
|
237
|
+
request.env['rack.input'] = StringIO.new(raw_body).tap(&:binmode)
|
|
238
|
+
body = JSON.parse(raw_body)
|
|
239
|
+
# Valid JSON that is not an object carries no credential, and
|
|
240
|
+
# indexing it by string would raise rather than fall through.
|
|
241
|
+
body = nil unless body.is_a?(Hash)
|
|
128
242
|
verifier = body && body['code']
|
|
129
243
|
access_token = body && body['access_token']
|
|
130
244
|
redirect_uri ||= body && body['redirect_uri']
|
|
131
245
|
if verifier
|
|
132
246
|
client_get_token(verifier, redirect_uri || 'postmessage')
|
|
133
247
|
elsif verify_token(access_token)
|
|
134
|
-
::OAuth2::AccessToken.from_hash(client, body
|
|
248
|
+
::OAuth2::AccessToken.from_hash(client, direct_token_hash(access_token, body['id_token']))
|
|
135
249
|
end
|
|
136
250
|
rescue JSON::ParserError => e
|
|
137
|
-
warn "[omniauth google-oauth2] JSON parse error=#{e}"
|
|
251
|
+
warn "[omniauth google-oauth2] JSON parse error=#{sanitize_for_log(e)}"
|
|
138
252
|
end
|
|
139
253
|
end
|
|
140
254
|
end
|
|
141
255
|
|
|
256
|
+
# An id_token supplied by the caller is only as trustworthy as its
|
|
257
|
+
# signature, so it is carried through only once Google has vouched for it.
|
|
258
|
+
# Nothing else from the request is: refresh_token and expiry cannot be
|
|
259
|
+
# verified at all, so they stay dropped.
|
|
260
|
+
def direct_token_hash(access_token, raw_id_token)
|
|
261
|
+
hash = { 'access_token' => access_token }
|
|
262
|
+
id_token = verified_id_token(raw_id_token, access_token)
|
|
263
|
+
hash['id_token'] = id_token if id_token
|
|
264
|
+
hash
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def verified_id_token(raw_id_token, access_token)
|
|
268
|
+
clear_id_token_claims
|
|
269
|
+
return nil if nil_or_empty?(raw_id_token)
|
|
270
|
+
|
|
271
|
+
# Widening this list means revisiting at_hash_matches?, which hardcodes
|
|
272
|
+
# SHA-256 because at_hash is defined over the digest named by the token's
|
|
273
|
+
# own alg header.
|
|
274
|
+
claims = ::JWT.decode(raw_id_token, nil, true,
|
|
275
|
+
algorithms: ['RS256'],
|
|
276
|
+
jwks: ->(opts) { google_jwks(force: opts[:invalidate]) },
|
|
277
|
+
required_claims: REQUIRED_ID_TOKEN_CLAIMS,
|
|
278
|
+
iss: ALLOWED_ISSUERS, verify_iss: true,
|
|
279
|
+
aud: trusted_client_ids, verify_aud: true,
|
|
280
|
+
verify_expiration: true, exp_leeway: options.jwt_leeway,
|
|
281
|
+
verify_not_before: true, nbf_leeway: options.jwt_leeway).first
|
|
282
|
+
return nil unless same_user?(claims, access_token)
|
|
283
|
+
|
|
284
|
+
cache_id_token_claims(raw_id_token, claims)
|
|
285
|
+
raw_id_token
|
|
286
|
+
rescue StandardError => e
|
|
287
|
+
# Fail closed. A token we cannot verify, for any reason including the
|
|
288
|
+
# key set being unreachable, is discarded rather than trusted.
|
|
289
|
+
warn "[omniauth google-oauth2] discarding unverified id_token: #{e.class}: #{sanitize_for_log(e.message)}"
|
|
290
|
+
nil
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# A signature proves Google issued the id_token, not that it describes the
|
|
294
|
+
# same person as the access token it arrived beside. Without that, a
|
|
295
|
+
# genuine id_token for one user could be presented with another user's
|
|
296
|
+
# access token, leaving uid and extra.id_info describing different people.
|
|
297
|
+
def same_user?(claims, access_token)
|
|
298
|
+
return true if at_hash_matches?(claims, access_token)
|
|
299
|
+
|
|
300
|
+
subjects_match?(claims, access_token)
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# at_hash ties an id_token to one specific access token: the left half of
|
|
304
|
+
# the SHA-256 of that token, base64url encoded. SHA-256 because the digest
|
|
305
|
+
# follows the token's alg header, which the decode above pins to RS256.
|
|
306
|
+
# Only a fast path, so absence and mismatch are both just "unproven here",
|
|
307
|
+
# deferring to the subject check rather than deciding anything.
|
|
308
|
+
def at_hash_matches?(claims, access_token)
|
|
309
|
+
expected = claims['at_hash']
|
|
310
|
+
return false if nil_or_empty?(expected)
|
|
311
|
+
|
|
312
|
+
# Encoded with pack rather than Base64, which is a bundled gem as of
|
|
313
|
+
# Ruby 3.4 and would become a dependency of every host application.
|
|
314
|
+
# Compared with ==, not a constant-time helper: both sides derive from
|
|
315
|
+
# the access token the caller just sent, so there is no secret to leak,
|
|
316
|
+
# and OpenSSL.secure_compare does not exist before Ruby 2.7.
|
|
317
|
+
digest = ::OpenSSL::Digest::SHA256.digest(access_token)[0, 16]
|
|
318
|
+
[digest].pack('m0').tr('+/', '-_').delete('=') == expected
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# The claim that actually matters: does the id_token describe the same
|
|
322
|
+
# person the access token belongs to? Comparing subjects tests that
|
|
323
|
+
# directly, where at_hash only tests it by proxy. It also stays correct
|
|
324
|
+
# when a client refreshes its access token but forwards the id_token it
|
|
325
|
+
# was originally issued, which is a legitimate at_hash mismatch.
|
|
326
|
+
def subjects_match?(claims, access_token)
|
|
327
|
+
subject = claims['sub']
|
|
328
|
+
return false if nil_or_empty?(subject)
|
|
329
|
+
return true if subject == userinfo_for(access_token)['sub']
|
|
330
|
+
|
|
331
|
+
warn '[omniauth google-oauth2] discarding id_token: subject does not match the access token'
|
|
332
|
+
false
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
# Memoized into the same ivar raw_info and verify_hd use, so the lookup is
|
|
336
|
+
# shared rather than repeated. skip_info still governs whether any of this
|
|
337
|
+
# reaches the auth hash; it trims output, it does not waive the check.
|
|
338
|
+
def userinfo_for(access_token)
|
|
339
|
+
@raw_info ||= ::OAuth2::AccessToken.from_hash(client, 'access_token' => access_token).get(USER_INFO_URL).parsed
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def google_jwks(force: false)
|
|
343
|
+
# Anchored to this class rather than self.class so that subclasses share
|
|
344
|
+
# the one cache instead of each looking for state they do not own.
|
|
345
|
+
GoogleOauth2.cached_jwks(force: force) do
|
|
346
|
+
# Parsed from the body rather than the response wrapper, which warns
|
|
347
|
+
# about the JWKS "keys" entry colliding with a built-in Hash method.
|
|
348
|
+
::JWT::JWK::Set.new(parse_jwks(client.request(:get, JWKS_URL).body))
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
# Google returns a small, fixed shape. Anything else is treated as an
|
|
353
|
+
# outage rather than coerced, since JWT::JWK::Set will happily build a key
|
|
354
|
+
# set out of surprising input, including an HMAC key from a bare string.
|
|
355
|
+
def parse_jwks(body)
|
|
356
|
+
parsed = JSON.parse(body)
|
|
357
|
+
raise JwksUnavailable, 'JWKS response was not an object' unless parsed.is_a?(Hash)
|
|
358
|
+
raise JwksUnavailable, 'JWKS response had no keys array' unless parsed['keys'].is_a?(Array)
|
|
359
|
+
|
|
360
|
+
parsed
|
|
361
|
+
end
|
|
362
|
+
|
|
142
363
|
def client_get_token(verifier, redirect_uri)
|
|
143
364
|
client.auth_code.get_token(verifier, get_token_options(redirect_uri), get_token_params)
|
|
144
365
|
end
|
|
@@ -149,7 +370,7 @@ module OmniAuth
|
|
|
149
370
|
|
|
150
371
|
def get_scope(params)
|
|
151
372
|
raw_scope = params[:scope] || DEFAULT_SCOPE
|
|
152
|
-
scope_list = raw_scope.split
|
|
373
|
+
scope_list = raw_scope.split.map { |item| item.split(',') }.flatten
|
|
153
374
|
scope_list.map! { |s| s =~ %r{^https?://} || BASE_SCOPES.include?(s) ? s : "#{BASE_SCOPE_URL}#{s}" }
|
|
154
375
|
scope_list.join(' ')
|
|
155
376
|
end
|
|
@@ -212,8 +433,8 @@ module OmniAuth
|
|
|
212
433
|
# strip `sz` parameter (defaults to sz=50) which overrides `image_size` options
|
|
213
434
|
return nil if query_parameters.nil?
|
|
214
435
|
|
|
215
|
-
params =
|
|
216
|
-
stripped_params = params.delete_if { |key| key == 'sz' }
|
|
436
|
+
params = URI.decode_www_form(query_parameters)
|
|
437
|
+
stripped_params = params.delete_if { |key, _value| key == 'sz' }
|
|
217
438
|
|
|
218
439
|
# don't return an empty Hash since that would result
|
|
219
440
|
# in URLs with a trailing ? character: http://image.url?
|
|
@@ -225,8 +446,11 @@ module OmniAuth
|
|
|
225
446
|
def token_info(access_token)
|
|
226
447
|
return nil unless access_token
|
|
227
448
|
|
|
449
|
+
# Keyed on k, not on the access_token this method was first called with:
|
|
450
|
+
# one request can ask about more than one token, and closing over the
|
|
451
|
+
# first would file that answer under every later key.
|
|
228
452
|
@token_info ||= Hash.new do |h, k|
|
|
229
|
-
h[k] = client.request(:post, 'https://www.googleapis.com/oauth2/v3/tokeninfo', body: { access_token:
|
|
453
|
+
h[k] = client.request(:post, 'https://www.googleapis.com/oauth2/v3/tokeninfo', body: { access_token: k }).parsed
|
|
230
454
|
end
|
|
231
455
|
|
|
232
456
|
@token_info[access_token]
|
|
@@ -236,7 +460,7 @@ module OmniAuth
|
|
|
236
460
|
return false unless access_token
|
|
237
461
|
|
|
238
462
|
token_info = token_info(access_token)
|
|
239
|
-
|
|
463
|
+
trusted_client_ids.include?(token_info['aud'])
|
|
240
464
|
end
|
|
241
465
|
|
|
242
466
|
def verify_hd(access_token)
|
|
@@ -9,8 +9,8 @@ Gem::Specification.new do |gem|
|
|
|
9
9
|
gem.name = 'omniauth-google-oauth2'
|
|
10
10
|
gem.version = OmniAuth::GoogleOauth2::VERSION
|
|
11
11
|
gem.license = 'MIT'
|
|
12
|
-
gem.summary = %(A Google OAuth2 strategy for OmniAuth
|
|
13
|
-
gem.description = %(A Google OAuth2 strategy for OmniAuth
|
|
12
|
+
gem.summary = %(A Google OAuth2 strategy for OmniAuth)
|
|
13
|
+
gem.description = %(A Google OAuth2 strategy for OmniAuth. This allows you to login to Google with your ruby app.)
|
|
14
14
|
gem.authors = ['Josh Ellithorpe', 'Yury Korolev']
|
|
15
15
|
gem.email = ['quest@mac.com']
|
|
16
16
|
gem.homepage = 'https://github.com/zquestz/omniauth-google-oauth2'
|
|
@@ -20,12 +20,11 @@ Gem::Specification.new do |gem|
|
|
|
20
20
|
|
|
21
21
|
gem.required_ruby_version = '>= 2.5'
|
|
22
22
|
|
|
23
|
-
gem.
|
|
24
|
-
gem.
|
|
25
|
-
gem.
|
|
26
|
-
gem.
|
|
23
|
+
gem.add_dependency 'jwt', '>= 2.9.2', '< 4'
|
|
24
|
+
gem.add_dependency 'oauth2', '~> 2.0'
|
|
25
|
+
gem.add_dependency 'omniauth', '~> 2.0'
|
|
26
|
+
gem.add_dependency 'omniauth-oauth2', '~> 1.8'
|
|
27
27
|
|
|
28
|
-
gem.add_development_dependency 'rake', '~>
|
|
28
|
+
gem.add_development_dependency 'rake', '~> 13.3'
|
|
29
29
|
gem.add_development_dependency 'rspec', '~> 3.6'
|
|
30
|
-
gem.add_development_dependency 'rubocop', '~> 0.49'
|
|
31
30
|
end
|