googleauth 0.8.1 → 0.16.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.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/.github/CODEOWNERS +7 -0
  3. data/.github/workflows/release.yml +39 -0
  4. data/.kokoro/build.bat +9 -1
  5. data/.kokoro/continuous/linux.cfg +12 -2
  6. data/.kokoro/continuous/osx.cfg +5 -0
  7. data/.kokoro/continuous/post.cfg +30 -0
  8. data/.kokoro/continuous/windows.cfg +27 -1
  9. data/.kokoro/presubmit/linux.cfg +11 -1
  10. data/.kokoro/presubmit/osx.cfg +5 -0
  11. data/.kokoro/presubmit/windows.cfg +27 -1
  12. data/.kokoro/release.cfg +42 -1
  13. data/.kokoro/trampoline.bat +10 -0
  14. data/.repo-metadata.json +5 -0
  15. data/.rubocop.yml +8 -2
  16. data/CHANGELOG.md +94 -20
  17. data/Gemfile +7 -7
  18. data/{COPYING → LICENSE} +0 -0
  19. data/README.md +12 -15
  20. data/Rakefile +48 -5
  21. data/googleauth.gemspec +7 -3
  22. data/integration/helper.rb +31 -0
  23. data/integration/id_tokens/key_source_test.rb +74 -0
  24. data/lib/googleauth.rb +1 -0
  25. data/lib/googleauth/application_default.rb +2 -2
  26. data/lib/googleauth/compute_engine.rb +45 -20
  27. data/lib/googleauth/credentials.rb +445 -71
  28. data/lib/googleauth/credentials_loader.rb +11 -9
  29. data/lib/googleauth/iam.rb +1 -1
  30. data/lib/googleauth/id_tokens.rb +233 -0
  31. data/lib/googleauth/id_tokens/errors.rb +71 -0
  32. data/lib/googleauth/id_tokens/key_sources.rb +396 -0
  33. data/lib/googleauth/id_tokens/verifier.rb +142 -0
  34. data/lib/googleauth/json_key_reader.rb +6 -2
  35. data/lib/googleauth/scope_util.rb +1 -1
  36. data/lib/googleauth/service_account.rb +42 -23
  37. data/lib/googleauth/signet.rb +9 -6
  38. data/lib/googleauth/stores/file_token_store.rb +1 -0
  39. data/lib/googleauth/stores/redis_token_store.rb +1 -0
  40. data/lib/googleauth/user_authorizer.rb +6 -1
  41. data/lib/googleauth/user_refresh.rb +2 -2
  42. data/lib/googleauth/version.rb +1 -1
  43. data/lib/googleauth/web_user_authorizer.rb +16 -14
  44. data/rakelib/devsite_builder.rb +45 -0
  45. data/rakelib/link_checker.rb +64 -0
  46. data/rakelib/repo_metadata.rb +59 -0
  47. data/spec/googleauth/apply_auth_examples.rb +28 -5
  48. data/spec/googleauth/compute_engine_spec.rb +69 -13
  49. data/spec/googleauth/credentials_spec.rb +492 -165
  50. data/spec/googleauth/service_account_spec.rb +31 -16
  51. data/spec/googleauth/signet_spec.rb +46 -7
  52. data/spec/googleauth/user_authorizer_spec.rb +21 -1
  53. data/spec/googleauth/user_refresh_spec.rb +1 -1
  54. data/spec/googleauth/web_user_authorizer_spec.rb +6 -0
  55. data/test/helper.rb +33 -0
  56. data/test/id_tokens/key_sources_test.rb +240 -0
  57. data/test/id_tokens/verifier_test.rb +269 -0
  58. metadata +49 -13
  59. data/.kokoro/windows.sh +0 -4
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2020 Google LLC
4
+ #
5
+ # Redistribution and use in source and binary forms, with or without
6
+ # modification, are permitted provided that the following conditions are
7
+ # met:
8
+ #
9
+ # * Redistributions of source code must retain the above copyright
10
+ # notice, this list of conditions and the following disclaimer.
11
+ # * Redistributions in binary form must reproduce the above
12
+ # copyright notice, this list of conditions and the following disclaimer
13
+ # in the documentation and/or other materials provided with the
14
+ # distribution.
15
+ # * Neither the name of Google Inc. nor the names of its
16
+ # contributors may be used to endorse or promote products derived from
17
+ # this software without specific prior written permission.
18
+ #
19
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20
+ # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21
+ # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22
+ # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23
+ # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24
+ # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25
+ # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26
+ # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27
+ # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28
+ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
+
31
+ require "jwt"
32
+
33
+ module Google
34
+ module Auth
35
+ module IDTokens
36
+ ##
37
+ # An object that can verify ID tokens.
38
+ #
39
+ # A verifier maintains a set of default settings, including the key
40
+ # source and fields to verify. However, individual verification calls can
41
+ # override any of these settings.
42
+ #
43
+ class Verifier
44
+ ##
45
+ # Create a verifier.
46
+ #
47
+ # @param key_source [key source] The default key source to use. All
48
+ # verification calls must have a key source, so if no default key
49
+ # source is provided here, then calls to {#verify} _must_ provide
50
+ # a key source.
51
+ # @param aud [String,nil] The default audience (`aud`) check, or `nil`
52
+ # for no check.
53
+ # @param azp [String,nil] The default authorized party (`azp`) check,
54
+ # or `nil` for no check.
55
+ # @param iss [String,nil] The default issuer (`iss`) check, or `nil`
56
+ # for no check.
57
+ #
58
+ def initialize key_source: nil,
59
+ aud: nil,
60
+ azp: nil,
61
+ iss: nil
62
+ @key_source = key_source
63
+ @aud = aud
64
+ @azp = azp
65
+ @iss = iss
66
+ end
67
+
68
+ ##
69
+ # Verify the given token.
70
+ #
71
+ # @param token [String] the ID token to verify.
72
+ # @param key_source [key source] If given, override the key source.
73
+ # @param aud [String,nil] If given, override the `aud` check.
74
+ # @param azp [String,nil] If given, override the `azp` check.
75
+ # @param iss [String,nil] If given, override the `iss` check.
76
+ #
77
+ # @return [Hash] the decoded payload, if verification succeeded.
78
+ # @raise [KeySourceError] if the key source failed to obtain public keys
79
+ # @raise [VerificationError] if the token verification failed.
80
+ # Additional data may be available in the error subclass and message.
81
+ #
82
+ def verify token,
83
+ key_source: :default,
84
+ aud: :default,
85
+ azp: :default,
86
+ iss: :default
87
+ key_source = @key_source if key_source == :default
88
+ aud = @aud if aud == :default
89
+ azp = @azp if azp == :default
90
+ iss = @iss if iss == :default
91
+
92
+ raise KeySourceError, "No key sources" unless key_source
93
+ keys = key_source.current_keys
94
+ payload = decode_token token, keys, aud, azp, iss
95
+ unless payload
96
+ keys = key_source.refresh_keys
97
+ payload = decode_token token, keys, aud, azp, iss
98
+ end
99
+ raise SignatureError, "Token not verified as issued by Google" unless payload
100
+ payload
101
+ end
102
+
103
+ private
104
+
105
+ def decode_token token, keys, aud, azp, iss
106
+ payload = nil
107
+ keys.find do |key|
108
+ options = { algorithms: key.algorithm }
109
+ decoded_token = JWT.decode token, key.key, true, options
110
+ payload = decoded_token.first
111
+ rescue JWT::ExpiredSignature
112
+ raise ExpiredTokenError, "Token signature is expired"
113
+ rescue JWT::DecodeError
114
+ nil # Try the next key
115
+ end
116
+
117
+ normalize_and_verify_payload payload, aud, azp, iss
118
+ end
119
+
120
+ def normalize_and_verify_payload payload, aud, azp, iss
121
+ return nil unless payload
122
+
123
+ # Map the legacy "cid" claim to the canonical "azp"
124
+ payload["azp"] ||= payload["cid"] if payload.key? "cid"
125
+
126
+ # Payload content validation
127
+ if aud && (Array(aud) & Array(payload["aud"])).empty?
128
+ raise AudienceMismatchError, "Token aud mismatch: #{payload['aud']}"
129
+ end
130
+ if azp && (Array(azp) & Array(payload["azp"])).empty?
131
+ raise AuthorizedPartyMismatchError, "Token azp mismatch: #{payload['azp']}"
132
+ end
133
+ if iss && (Array(iss) & Array(payload["iss"])).empty?
134
+ raise IssuerMismatchError, "Token iss mismatch: #{payload['iss']}"
135
+ end
136
+
137
+ payload
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
@@ -38,8 +38,12 @@ module Google
38
38
  json_key = MultiJson.load json_key_io.read
39
39
  raise "missing client_email" unless json_key.key? "client_email"
40
40
  raise "missing private_key" unless json_key.key? "private_key"
41
- project_id = json_key["project_id"]
42
- [json_key["private_key"], json_key["client_email"], project_id]
41
+ [
42
+ json_key["private_key"],
43
+ json_key["client_email"],
44
+ json_key["project_id"],
45
+ json_key["quota_project_id"]
46
+ ]
43
47
  end
44
48
  end
45
49
  end
@@ -51,7 +51,7 @@ module Google
51
51
  when Array
52
52
  scope
53
53
  when String
54
- scope.split " "
54
+ scope.split
55
55
  else
56
56
  raise "Invalid scope value. Must be string or array"
57
57
  end
@@ -45,34 +45,47 @@ module Google
45
45
  # from credentials from a json key file downloaded from the developer
46
46
  # console (via 'Generate new Json Key').
47
47
  #
48
- # cf [Application Default Credentials](http://goo.gl/mkAHpZ)
48
+ # cf [Application Default Credentials](https://cloud.google.com/docs/authentication/production)
49
49
  class ServiceAccountCredentials < Signet::OAuth2::Client
50
50
  TOKEN_CRED_URI = "https://www.googleapis.com/oauth2/v4/token".freeze
51
51
  extend CredentialsLoader
52
52
  extend JsonKeyReader
53
53
  attr_reader :project_id
54
+ attr_reader :quota_project_id
55
+
56
+ def enable_self_signed_jwt?
57
+ @enable_self_signed_jwt
58
+ end
54
59
 
55
60
  # Creates a ServiceAccountCredentials.
56
61
  #
57
62
  # @param json_key_io [IO] an IO from which the JSON key can be read
58
63
  # @param scope [string|array|nil] the scope(s) to access
59
64
  def self.make_creds options = {}
60
- json_key_io, scope = options.values_at :json_key_io, :scope
65
+ json_key_io, scope, enable_self_signed_jwt, target_audience, audience, token_credential_uri =
66
+ options.values_at :json_key_io, :scope, :enable_self_signed_jwt, :target_audience,
67
+ :audience, :token_credential_uri
68
+ raise ArgumentError, "Cannot specify both scope and target_audience" if scope && target_audience
69
+
61
70
  if json_key_io
62
- private_key, client_email, project_id = read_json_key json_key_io
71
+ private_key, client_email, project_id, quota_project_id = read_json_key json_key_io
63
72
  else
64
73
  private_key = unescape ENV[CredentialsLoader::PRIVATE_KEY_VAR]
65
74
  client_email = ENV[CredentialsLoader::CLIENT_EMAIL_VAR]
66
75
  project_id = ENV[CredentialsLoader::PROJECT_ID_VAR]
76
+ quota_project_id = nil
67
77
  end
68
78
  project_id ||= CredentialsLoader.load_gcloud_project_id
69
79
 
70
- new(token_credential_uri: TOKEN_CRED_URI,
71
- audience: TOKEN_CRED_URI,
72
- scope: scope,
73
- issuer: client_email,
74
- signing_key: OpenSSL::PKey::RSA.new(private_key),
75
- project_id: project_id)
80
+ new(token_credential_uri: token_credential_uri || TOKEN_CRED_URI,
81
+ audience: audience || TOKEN_CRED_URI,
82
+ scope: scope,
83
+ enable_self_signed_jwt: enable_self_signed_jwt,
84
+ target_audience: target_audience,
85
+ issuer: client_email,
86
+ signing_key: OpenSSL::PKey::RSA.new(private_key),
87
+ project_id: project_id,
88
+ quota_project_id: quota_project_id)
76
89
  .configure_connection(options)
77
90
  end
78
91
 
@@ -87,30 +100,34 @@ module Google
87
100
 
88
101
  def initialize options = {}
89
102
  @project_id = options[:project_id]
103
+ @quota_project_id = options[:quota_project_id]
104
+ @enable_self_signed_jwt = options[:enable_self_signed_jwt] ? true : false
90
105
  super options
91
106
  end
92
107
 
93
- # Extends the base class.
94
- #
95
- # If scope(s) is not set, it creates a transient
96
- # ServiceAccountJwtHeaderCredentials instance and uses that to
97
- # authenticate instead.
108
+ # Extends the base class to use a transient
109
+ # ServiceAccountJwtHeaderCredentials for certain cases.
98
110
  def apply! a_hash, opts = {}
99
- # Use the base implementation if scopes are set
100
- unless scope.nil?
111
+ # Use a self-singed JWT if there's no information that can be used to
112
+ # obtain an OAuth token, OR if there are scopes but also an assertion
113
+ # that they are default scopes that shouldn't be used to fetch a token.
114
+ if target_audience.nil? && (scope.nil? || enable_self_signed_jwt?)
115
+ apply_self_signed_jwt! a_hash
116
+ else
101
117
  super
102
- return
103
118
  end
119
+ end
120
+
121
+ private
104
122
 
123
+ def apply_self_signed_jwt! a_hash
105
124
  # Use the ServiceAccountJwtHeaderCredentials using the same cred values
106
- # if no scopes are set.
107
125
  cred_json = {
108
126
  private_key: @signing_key.to_s,
109
127
  client_email: @issuer
110
128
  }
111
- alt_clz = ServiceAccountJwtHeaderCredentials
112
129
  key_io = StringIO.new MultiJson.dump(cred_json)
113
- alt = alt_clz.make_creds json_key_io: key_io
130
+ alt = ServiceAccountJwtHeaderCredentials.make_creds json_key_io: key_io
114
131
  alt.apply! a_hash
115
132
  end
116
133
  end
@@ -123,7 +140,7 @@ module Google
123
140
  # console (via 'Generate new Json Key'). It is not part of any OAuth2
124
141
  # flow, rather it creates a JWT and sends that as a credential.
125
142
  #
126
- # cf [Application Default Credentials](http://goo.gl/mkAHpZ)
143
+ # cf [Application Default Credentials](https://cloud.google.com/docs/authentication/production)
127
144
  class ServiceAccountJwtHeaderCredentials
128
145
  JWT_AUD_URI_KEY = :jwt_aud_uri
129
146
  AUTH_METADATA_KEY = Signet::OAuth2::AUTH_METADATA_KEY
@@ -133,6 +150,7 @@ module Google
133
150
  extend CredentialsLoader
134
151
  extend JsonKeyReader
135
152
  attr_reader :project_id
153
+ attr_reader :quota_project_id
136
154
 
137
155
  # make_creds proxies the construction of a credentials instance
138
156
  #
@@ -151,12 +169,13 @@ module Google
151
169
  def initialize options = {}
152
170
  json_key_io = options[:json_key_io]
153
171
  if json_key_io
154
- @private_key, @issuer, @project_id =
172
+ @private_key, @issuer, @project_id, @quota_project_id =
155
173
  self.class.read_json_key json_key_io
156
174
  else
157
175
  @private_key = ENV[CredentialsLoader::PRIVATE_KEY_VAR]
158
176
  @issuer = ENV[CredentialsLoader::CLIENT_EMAIL_VAR]
159
177
  @project_id = ENV[CredentialsLoader::PROJECT_ID_VAR]
178
+ @quota_project_id = nil
160
179
  end
161
180
  @project_id ||= CredentialsLoader.load_gcloud_project_id
162
181
  @signing_key = OpenSSL::PKey::RSA.new @private_key
@@ -184,7 +203,7 @@ module Google
184
203
  # Returns a reference to the #apply method, suitable for passing as
185
204
  # a closure
186
205
  def updater_proc
187
- lambda(&method(:apply))
206
+ proc { |a_hash, opts = {}| apply a_hash, opts }
188
207
  end
189
208
 
190
209
  protected
@@ -48,8 +48,9 @@ module Signet
48
48
  def apply! a_hash, opts = {}
49
49
  # fetch the access token there is currently not one, or if the client
50
50
  # has expired
51
- fetch_access_token! opts if access_token.nil? || expires_within?(60)
52
- a_hash[AUTH_METADATA_KEY] = "Bearer #{access_token}"
51
+ token_type = target_audience ? :id_token : :access_token
52
+ fetch_access_token! opts if send(token_type).nil? || expires_within?(60)
53
+ a_hash[AUTH_METADATA_KEY] = "Bearer #{send token_type}"
53
54
  end
54
55
 
55
56
  # Returns a clone of a_hash updated with the authentication token
@@ -62,11 +63,11 @@ module Signet
62
63
  # Returns a reference to the #apply method, suitable for passing as
63
64
  # a closure
64
65
  def updater_proc
65
- lambda(&method(:apply))
66
+ proc { |a_hash, opts = {}| apply a_hash, opts }
66
67
  end
67
68
 
68
69
  def on_refresh &block
69
- @refresh_listeners ||= []
70
+ @refresh_listeners = [] unless defined? @refresh_listeners
70
71
  @refresh_listeners << block
71
72
  end
72
73
 
@@ -76,13 +77,15 @@ module Signet
76
77
  connection = build_default_connection
77
78
  options = options.merge connection: connection if connection
78
79
  end
79
- info = orig_fetch_access_token! options
80
+ info = retry_with_error do
81
+ orig_fetch_access_token! options
82
+ end
80
83
  notify_refresh_listeners
81
84
  info
82
85
  end
83
86
 
84
87
  def notify_refresh_listeners
85
- listeners = @refresh_listeners || []
88
+ listeners = defined?(@refresh_listeners) ? @refresh_listeners : []
86
89
  listeners.each do |block|
87
90
  block.call self
88
91
  end
@@ -40,6 +40,7 @@ module Google
40
40
  # @param [String, File] file
41
41
  # Path to storage file
42
42
  def initialize options = {}
43
+ super()
43
44
  path = options[:file]
44
45
  @store = YAML::Store.new path
45
46
  end
@@ -49,6 +49,7 @@ module Google
49
49
  # the options passed through. You may include any other keys accepted
50
50
  # by `Redis.new`
51
51
  def initialize options = {}
52
+ super()
52
53
  redis = options.delete :redis
53
54
  prefix = options.delete :prefix
54
55
  @redis = case redis
@@ -271,10 +271,15 @@ module Google
271
271
  # @return [String]
272
272
  # Redirect URI
273
273
  def redirect_uri_for base_url
274
- return @callback_uri unless URI(@callback_uri).scheme.nil?
274
+ return @callback_uri if uri_is_postmessage?(@callback_uri) || !URI(@callback_uri).scheme.nil?
275
275
  raise format(MISSING_ABSOLUTE_URL_ERROR, @callback_uri) if base_url.nil? || URI(base_url).scheme.nil?
276
276
  URI.join(base_url, @callback_uri).to_s
277
277
  end
278
+
279
+ # Check if URI is Google's postmessage flow (not a valid redirect_uri by spec, but allowed)
280
+ def uri_is_postmessage? uri
281
+ uri.to_s.casecmp("postmessage").zero?
282
+ end
278
283
  end
279
284
  end
280
285
  end
@@ -44,7 +44,7 @@ module Google
44
44
  # 'gcloud auth login' saves a file with these contents in well known
45
45
  # location
46
46
  #
47
- # cf [Application Default Credentials](http://goo.gl/mkAHpZ)
47
+ # cf [Application Default Credentials](https://cloud.google.com/docs/authentication/production)
48
48
  class UserRefreshCredentials < Signet::OAuth2::Client
49
49
  TOKEN_CRED_URI = "https://oauth2.googleapis.com/token".freeze
50
50
  AUTHORIZATION_URI = "https://accounts.google.com/o/oauth2/auth".freeze
@@ -79,7 +79,7 @@ module Google
79
79
  # JSON key.
80
80
  def self.read_json_key json_key_io
81
81
  json_key = MultiJson.load json_key_io.read
82
- wanted = %w[client_id client_secret refresh_token]
82
+ wanted = ["client_id", "client_secret", "refresh_token"]
83
83
  wanted.each do |key|
84
84
  raise "the json is missing the #{key} field" unless json_key.key? key
85
85
  end
@@ -31,6 +31,6 @@ module Google
31
31
  # Module Auth provides classes that provide Google-specific authorization
32
32
  # used to access Google APIs.
33
33
  module Auth
34
- VERSION = "0.8.1".freeze
34
+ VERSION = "0.16.0".freeze
35
35
  end
36
36
  end
@@ -58,12 +58,9 @@ module Google
58
58
  # end
59
59
  #
60
60
  # Instead of implementing the callback directly, applications are
61
- # encouraged to use {Google::Auth::Web::AuthCallbackApp} instead.
61
+ # encouraged to use {Google::Auth::WebUserAuthorizer::CallbackApp} instead.
62
62
  #
63
- # For rails apps, see {Google::Auth::ControllerHelpers}
64
- #
65
- # @see {Google::Auth::AuthCallbackApp}
66
- # @see {Google::Auth::ControllerHelpers}
63
+ # @see CallbackApp
67
64
  # @note Requires sessions are enabled
68
65
  class WebUserAuthorizer < Google::Auth::UserAuthorizer
69
66
  STATE_PARAM = "state".freeze
@@ -154,6 +151,8 @@ module Google
154
151
  # @param [String, Array<String>] scope
155
152
  # Authorization scope to request. Overrides the instance scopes if
156
153
  # not nil.
154
+ # @param [Hash] state
155
+ # Optional key-values to be returned to the oauth callback.
157
156
  # @return [String]
158
157
  # Authorization url
159
158
  def get_authorization_url options = {}
@@ -162,22 +161,25 @@ module Google
162
161
  raise NIL_REQUEST_ERROR if request.nil?
163
162
  raise NIL_SESSION_ERROR if request.session.nil?
164
163
 
164
+ state = options[:state] || {}
165
+
165
166
  redirect_to = options[:redirect_to] || request.url
166
167
  request.session[XSRF_KEY] = SecureRandom.base64
167
- options[:state] = MultiJson.dump(
168
- SESSION_ID_KEY => request.session[XSRF_KEY],
169
- CURRENT_URI_KEY => redirect_to
170
- )
168
+ options[:state] = MultiJson.dump(state.merge(
169
+ SESSION_ID_KEY => request.session[XSRF_KEY],
170
+ CURRENT_URI_KEY => redirect_to
171
+ ))
171
172
  options[:base_url] = request.url
172
173
  super options
173
174
  end
174
175
 
175
- # Fetch stored credentials for the user.
176
+ # Fetch stored credentials for the user from the given request session.
176
177
  #
177
178
  # @param [String] user_id
178
179
  # Unique ID of the user for loading/storing credentials.
179
180
  # @param [Rack::Request] request
180
- # Current request
181
+ # Current request. Optional. If omitted, this will attempt to fall back
182
+ # on the base class behavior of reading from the token store.
181
183
  # @param [Array<String>, String] scope
182
184
  # If specified, only returns credentials that have all the \
183
185
  # requested scopes
@@ -186,8 +188,8 @@ module Google
186
188
  # @raise [Signet::AuthorizationError]
187
189
  # May raise an error if an authorization code is present in the session
188
190
  # and exchange of the code fails
189
- def get_credentials user_id, request, scope = nil
190
- if request.session.key? CALLBACK_STATE_KEY
191
+ def get_credentials user_id, request = nil, scope = nil
192
+ if request&.session&.key? CALLBACK_STATE_KEY
191
193
  # Note - in theory, no need to check required scope as this is
192
194
  # expected to be called immediately after a return from authorization
193
195
  state_json = request.session.delete CALLBACK_STATE_KEY
@@ -256,7 +258,7 @@ module Google
256
258
  # Google::Auth::WebUserAuthorizer::CallbackApp.call(env)
257
259
  # end
258
260
  #
259
- # @see {Google::Auth::WebUserAuthorizer}
261
+ # @see Google::Auth::WebUserAuthorizer
260
262
  class CallbackApp
261
263
  LOCATION_HEADER = "Location".freeze
262
264
  REDIR_STATUS = 302