googleauth 0.1.0 → 0.16.2
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 +5 -5
- data/.github/CODEOWNERS +7 -0
- data/.github/CONTRIBUTING.md +74 -0
- data/.github/ISSUE_TEMPLATE/bug_report.md +36 -0
- data/.github/ISSUE_TEMPLATE/feature_request.md +21 -0
- data/.github/ISSUE_TEMPLATE/support_request.md +7 -0
- data/.github/renovate.json +6 -0
- data/.github/sync-repo-settings.yaml +18 -0
- data/.github/workflows/ci.yml +55 -0
- data/.github/workflows/release-please.yml +39 -0
- data/.gitignore +3 -0
- data/.kokoro/populate-secrets.sh +76 -0
- data/.kokoro/release.cfg +52 -0
- data/.kokoro/release.sh +18 -0
- data/.kokoro/trampoline_v2.sh +489 -0
- data/.repo-metadata.json +5 -0
- data/.rubocop.yml +17 -0
- data/.toys/.toys.rb +45 -0
- data/.toys/ci.rb +43 -0
- data/.toys/kokoro/.toys.rb +66 -0
- data/.toys/kokoro/publish-docs.rb +67 -0
- data/.toys/kokoro/publish-gem.rb +53 -0
- data/.toys/linkinator.rb +43 -0
- data/.trampolinerc +48 -0
- data/CHANGELOG.md +199 -0
- data/CODE_OF_CONDUCT.md +43 -0
- data/Gemfile +22 -1
- data/{COPYING → LICENSE} +0 -0
- data/README.md +140 -17
- data/googleauth.gemspec +28 -28
- data/integration/helper.rb +31 -0
- data/integration/id_tokens/key_source_test.rb +74 -0
- data/lib/googleauth.rb +7 -37
- data/lib/googleauth/application_default.rb +81 -0
- data/lib/googleauth/client_id.rb +104 -0
- data/lib/googleauth/compute_engine.rb +73 -26
- data/lib/googleauth/credentials.rb +561 -0
- data/lib/googleauth/credentials_loader.rb +207 -0
- data/lib/googleauth/default_credentials.rb +93 -0
- data/lib/googleauth/iam.rb +75 -0
- 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 +396 -0
- data/lib/googleauth/id_tokens/verifier.rb +142 -0
- data/lib/googleauth/json_key_reader.rb +50 -0
- data/lib/googleauth/scope_util.rb +61 -0
- data/lib/googleauth/service_account.rb +177 -67
- data/lib/googleauth/signet.rb +69 -8
- data/lib/googleauth/stores/file_token_store.rb +65 -0
- data/lib/googleauth/stores/redis_token_store.rb +96 -0
- data/lib/googleauth/token_store.rb +69 -0
- data/lib/googleauth/user_authorizer.rb +285 -0
- data/lib/googleauth/user_refresh.rb +129 -0
- data/lib/googleauth/version.rb +1 -1
- data/lib/googleauth/web_user_authorizer.rb +295 -0
- data/spec/googleauth/apply_auth_examples.rb +96 -94
- data/spec/googleauth/client_id_spec.rb +160 -0
- data/spec/googleauth/compute_engine_spec.rb +125 -55
- data/spec/googleauth/credentials_spec.rb +600 -0
- data/spec/googleauth/get_application_default_spec.rb +232 -80
- data/spec/googleauth/iam_spec.rb +80 -0
- data/spec/googleauth/scope_util_spec.rb +77 -0
- data/spec/googleauth/service_account_spec.rb +422 -68
- data/spec/googleauth/signet_spec.rb +101 -25
- data/spec/googleauth/stores/file_token_store_spec.rb +57 -0
- data/spec/googleauth/stores/redis_token_store_spec.rb +50 -0
- data/spec/googleauth/stores/store_examples.rb +58 -0
- data/spec/googleauth/user_authorizer_spec.rb +343 -0
- data/spec/googleauth/user_refresh_spec.rb +359 -0
- data/spec/googleauth/web_user_authorizer_spec.rb +172 -0
- data/spec/spec_helper.rb +51 -10
- 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 +114 -75
- data/.travis.yml +0 -18
- data/CONTRIBUTING.md +0 -32
- data/Rakefile +0 -15
@@ -0,0 +1,129 @@
|
|
1
|
+
# Copyright 2015, Google Inc.
|
2
|
+
# All rights reserved.
|
3
|
+
#
|
4
|
+
# Redistribution and use in source and binary forms, with or without
|
5
|
+
# modification, are permitted provided that the following conditions are
|
6
|
+
# met:
|
7
|
+
#
|
8
|
+
# * Redistributions of source code must retain the above copyright
|
9
|
+
# notice, this list of conditions and the following disclaimer.
|
10
|
+
# * Redistributions in binary form must reproduce the above
|
11
|
+
# copyright notice, this list of conditions and the following disclaimer
|
12
|
+
# in the documentation and/or other materials provided with the
|
13
|
+
# distribution.
|
14
|
+
# * Neither the name of Google Inc. nor the names of its
|
15
|
+
# contributors may be used to endorse or promote products derived from
|
16
|
+
# this software without specific prior written permission.
|
17
|
+
#
|
18
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
19
|
+
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
20
|
+
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
21
|
+
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
22
|
+
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
23
|
+
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
24
|
+
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
25
|
+
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
26
|
+
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
27
|
+
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
28
|
+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
29
|
+
|
30
|
+
require "googleauth/signet"
|
31
|
+
require "googleauth/credentials_loader"
|
32
|
+
require "googleauth/scope_util"
|
33
|
+
require "multi_json"
|
34
|
+
|
35
|
+
module Google
|
36
|
+
# Module Auth provides classes that provide Google-specific authorization
|
37
|
+
# used to access Google APIs.
|
38
|
+
module Auth
|
39
|
+
# Authenticates requests using User Refresh credentials.
|
40
|
+
#
|
41
|
+
# This class allows authorizing requests from user refresh tokens.
|
42
|
+
#
|
43
|
+
# This the end of the result of a 3LO flow. E.g, the end result of
|
44
|
+
# 'gcloud auth login' saves a file with these contents in well known
|
45
|
+
# location
|
46
|
+
#
|
47
|
+
# cf [Application Default Credentials](https://cloud.google.com/docs/authentication/production)
|
48
|
+
class UserRefreshCredentials < Signet::OAuth2::Client
|
49
|
+
TOKEN_CRED_URI = "https://oauth2.googleapis.com/token".freeze
|
50
|
+
AUTHORIZATION_URI = "https://accounts.google.com/o/oauth2/auth".freeze
|
51
|
+
REVOKE_TOKEN_URI = "https://oauth2.googleapis.com/revoke".freeze
|
52
|
+
extend CredentialsLoader
|
53
|
+
attr_reader :project_id
|
54
|
+
|
55
|
+
# Create a UserRefreshCredentials.
|
56
|
+
#
|
57
|
+
# @param json_key_io [IO] an IO from which the JSON key can be read
|
58
|
+
# @param scope [string|array|nil] the scope(s) to access
|
59
|
+
def self.make_creds options = {}
|
60
|
+
json_key_io, scope = options.values_at :json_key_io, :scope
|
61
|
+
user_creds = read_json_key json_key_io if json_key_io
|
62
|
+
user_creds ||= {
|
63
|
+
"client_id" => ENV[CredentialsLoader::CLIENT_ID_VAR],
|
64
|
+
"client_secret" => ENV[CredentialsLoader::CLIENT_SECRET_VAR],
|
65
|
+
"refresh_token" => ENV[CredentialsLoader::REFRESH_TOKEN_VAR],
|
66
|
+
"project_id" => ENV[CredentialsLoader::PROJECT_ID_VAR]
|
67
|
+
}
|
68
|
+
|
69
|
+
new(token_credential_uri: TOKEN_CRED_URI,
|
70
|
+
client_id: user_creds["client_id"],
|
71
|
+
client_secret: user_creds["client_secret"],
|
72
|
+
refresh_token: user_creds["refresh_token"],
|
73
|
+
project_id: user_creds["project_id"],
|
74
|
+
scope: scope)
|
75
|
+
.configure_connection(options)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Reads the client_id, client_secret and refresh_token fields from the
|
79
|
+
# JSON key.
|
80
|
+
def self.read_json_key json_key_io
|
81
|
+
json_key = MultiJson.load json_key_io.read
|
82
|
+
wanted = ["client_id", "client_secret", "refresh_token"]
|
83
|
+
wanted.each do |key|
|
84
|
+
raise "the json is missing the #{key} field" unless json_key.key? key
|
85
|
+
end
|
86
|
+
json_key
|
87
|
+
end
|
88
|
+
|
89
|
+
def initialize options = {}
|
90
|
+
options ||= {}
|
91
|
+
options[:token_credential_uri] ||= TOKEN_CRED_URI
|
92
|
+
options[:authorization_uri] ||= AUTHORIZATION_URI
|
93
|
+
@project_id = options[:project_id]
|
94
|
+
@project_id ||= CredentialsLoader.load_gcloud_project_id
|
95
|
+
super options
|
96
|
+
end
|
97
|
+
|
98
|
+
# Revokes the credential
|
99
|
+
def revoke! options = {}
|
100
|
+
c = options[:connection] || Faraday.default_connection
|
101
|
+
|
102
|
+
retry_with_error do
|
103
|
+
resp = c.post(REVOKE_TOKEN_URI, token: refresh_token || access_token)
|
104
|
+
case resp.status
|
105
|
+
when 200
|
106
|
+
self.access_token = nil
|
107
|
+
self.refresh_token = nil
|
108
|
+
self.expires_at = 0
|
109
|
+
else
|
110
|
+
raise(Signet::AuthorizationError,
|
111
|
+
"Unexpected error code #{resp.status}")
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Verifies that a credential grants the requested scope
|
117
|
+
#
|
118
|
+
# @param [Array<String>, String] required_scope
|
119
|
+
# Scope to verify
|
120
|
+
# @return [Boolean]
|
121
|
+
# True if scope is granted
|
122
|
+
def includes_scope? required_scope
|
123
|
+
missing_scope = Google::Auth::ScopeUtil.normalize(required_scope) -
|
124
|
+
Google::Auth::ScopeUtil.normalize(scope)
|
125
|
+
missing_scope.empty?
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
data/lib/googleauth/version.rb
CHANGED
@@ -0,0 +1,295 @@
|
|
1
|
+
# Copyright 2014, Google Inc.
|
2
|
+
# All rights reserved.
|
3
|
+
#
|
4
|
+
# Redistribution and use in source and binary forms, with or without
|
5
|
+
# modification, are permitted provided that the following conditions are
|
6
|
+
# met:
|
7
|
+
#
|
8
|
+
# * Redistributions of source code must retain the above copyright
|
9
|
+
# notice, this list of conditions and the following disclaimer.
|
10
|
+
# * Redistributions in binary form must reproduce the above
|
11
|
+
# copyright notice, this list of conditions and the following disclaimer
|
12
|
+
# in the documentation and/or other materials provided with the
|
13
|
+
# distribution.
|
14
|
+
# * Neither the name of Google Inc. nor the names of its
|
15
|
+
# contributors may be used to endorse or promote products derived from
|
16
|
+
# this software without specific prior written permission.
|
17
|
+
#
|
18
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
19
|
+
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
20
|
+
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
21
|
+
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
22
|
+
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
23
|
+
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
24
|
+
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
25
|
+
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
26
|
+
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
27
|
+
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
28
|
+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
29
|
+
|
30
|
+
require "multi_json"
|
31
|
+
require "googleauth/signet"
|
32
|
+
require "googleauth/user_authorizer"
|
33
|
+
require "googleauth/user_refresh"
|
34
|
+
require "securerandom"
|
35
|
+
|
36
|
+
module Google
|
37
|
+
module Auth
|
38
|
+
# Varation on {Google::Auth::UserAuthorizer} adapted for Rack based
|
39
|
+
# web applications.
|
40
|
+
#
|
41
|
+
# Example usage:
|
42
|
+
#
|
43
|
+
# get('/') do
|
44
|
+
# user_id = request.session['user_email']
|
45
|
+
# credentials = authorizer.get_credentials(user_id, request)
|
46
|
+
# if credentials.nil?
|
47
|
+
# redirect authorizer.get_authorization_url(user_id: user_id,
|
48
|
+
# request: request)
|
49
|
+
# end
|
50
|
+
# # Credentials are valid, can call APIs
|
51
|
+
# ...
|
52
|
+
# end
|
53
|
+
#
|
54
|
+
# get('/oauth2callback') do
|
55
|
+
# url = Google::Auth::WebUserAuthorizer.handle_auth_callback_deferred(
|
56
|
+
# request)
|
57
|
+
# redirect url
|
58
|
+
# end
|
59
|
+
#
|
60
|
+
# Instead of implementing the callback directly, applications are
|
61
|
+
# encouraged to use {Google::Auth::WebUserAuthorizer::CallbackApp} instead.
|
62
|
+
#
|
63
|
+
# @see CallbackApp
|
64
|
+
# @note Requires sessions are enabled
|
65
|
+
class WebUserAuthorizer < Google::Auth::UserAuthorizer
|
66
|
+
STATE_PARAM = "state".freeze
|
67
|
+
AUTH_CODE_KEY = "code".freeze
|
68
|
+
ERROR_CODE_KEY = "error".freeze
|
69
|
+
SESSION_ID_KEY = "session_id".freeze
|
70
|
+
CALLBACK_STATE_KEY = "g-auth-callback".freeze
|
71
|
+
CURRENT_URI_KEY = "current_uri".freeze
|
72
|
+
XSRF_KEY = "g-xsrf-token".freeze
|
73
|
+
SCOPE_KEY = "scope".freeze
|
74
|
+
|
75
|
+
NIL_REQUEST_ERROR = "Request is required.".freeze
|
76
|
+
NIL_SESSION_ERROR = "Sessions must be enabled".freeze
|
77
|
+
MISSING_AUTH_CODE_ERROR = "Missing authorization code in request".freeze
|
78
|
+
AUTHORIZATION_ERROR = "Authorization error: %s".freeze
|
79
|
+
INVALID_STATE_TOKEN_ERROR =
|
80
|
+
"State token does not match expected value".freeze
|
81
|
+
|
82
|
+
class << self
|
83
|
+
attr_accessor :default
|
84
|
+
end
|
85
|
+
|
86
|
+
# Handle the result of the oauth callback. This version defers the
|
87
|
+
# exchange of the code by temporarily stashing the results in the user's
|
88
|
+
# session. This allows apps to use the generic
|
89
|
+
# {Google::Auth::WebUserAuthorizer::CallbackApp} handler for the callback
|
90
|
+
# without any additional customization.
|
91
|
+
#
|
92
|
+
# Apps that wish to handle the callback directly should use
|
93
|
+
# {#handle_auth_callback} instead.
|
94
|
+
#
|
95
|
+
# @param [Rack::Request] request
|
96
|
+
# Current request
|
97
|
+
def self.handle_auth_callback_deferred request
|
98
|
+
callback_state, redirect_uri = extract_callback_state request
|
99
|
+
request.session[CALLBACK_STATE_KEY] = MultiJson.dump callback_state
|
100
|
+
redirect_uri
|
101
|
+
end
|
102
|
+
|
103
|
+
# Initialize the authorizer
|
104
|
+
#
|
105
|
+
# @param [Google::Auth::ClientID] client_id
|
106
|
+
# Configured ID & secret for this application
|
107
|
+
# @param [String, Array<String>] scope
|
108
|
+
# Authorization scope to request
|
109
|
+
# @param [Google::Auth::Stores::TokenStore] token_store
|
110
|
+
# Backing storage for persisting user credentials
|
111
|
+
# @param [String] callback_uri
|
112
|
+
# URL (either absolute or relative) of the auth callback. Defaults
|
113
|
+
# to '/oauth2callback'
|
114
|
+
def initialize client_id, scope, token_store, callback_uri = nil
|
115
|
+
super client_id, scope, token_store, callback_uri
|
116
|
+
end
|
117
|
+
|
118
|
+
# Handle the result of the oauth callback. Exchanges the authorization
|
119
|
+
# code from the request and persists to storage.
|
120
|
+
#
|
121
|
+
# @param [String] user_id
|
122
|
+
# Unique ID of the user for loading/storing credentials.
|
123
|
+
# @param [Rack::Request] request
|
124
|
+
# Current request
|
125
|
+
# @return (Google::Auth::UserRefreshCredentials, String)
|
126
|
+
# credentials & next URL to redirect to
|
127
|
+
def handle_auth_callback user_id, request
|
128
|
+
callback_state, redirect_uri = WebUserAuthorizer.extract_callback_state(
|
129
|
+
request
|
130
|
+
)
|
131
|
+
WebUserAuthorizer.validate_callback_state callback_state, request
|
132
|
+
credentials = get_and_store_credentials_from_code(
|
133
|
+
user_id: user_id,
|
134
|
+
code: callback_state[AUTH_CODE_KEY],
|
135
|
+
scope: callback_state[SCOPE_KEY],
|
136
|
+
base_url: request.url
|
137
|
+
)
|
138
|
+
[credentials, redirect_uri]
|
139
|
+
end
|
140
|
+
|
141
|
+
# Build the URL for requesting authorization.
|
142
|
+
#
|
143
|
+
# @param [String] login_hint
|
144
|
+
# Login hint if need to authorize a specific account. Should be a
|
145
|
+
# user's email address or unique profile ID.
|
146
|
+
# @param [Rack::Request] request
|
147
|
+
# Current request
|
148
|
+
# @param [String] redirect_to
|
149
|
+
# Optional URL to proceed to after authorization complete. Defaults to
|
150
|
+
# the current URL.
|
151
|
+
# @param [String, Array<String>] scope
|
152
|
+
# Authorization scope to request. Overrides the instance scopes if
|
153
|
+
# not nil.
|
154
|
+
# @param [Hash] state
|
155
|
+
# Optional key-values to be returned to the oauth callback.
|
156
|
+
# @return [String]
|
157
|
+
# Authorization url
|
158
|
+
def get_authorization_url options = {}
|
159
|
+
options = options.dup
|
160
|
+
request = options[:request]
|
161
|
+
raise NIL_REQUEST_ERROR if request.nil?
|
162
|
+
raise NIL_SESSION_ERROR if request.session.nil?
|
163
|
+
|
164
|
+
state = options[:state] || {}
|
165
|
+
|
166
|
+
redirect_to = options[:redirect_to] || request.url
|
167
|
+
request.session[XSRF_KEY] = SecureRandom.base64
|
168
|
+
options[:state] = MultiJson.dump(state.merge(
|
169
|
+
SESSION_ID_KEY => request.session[XSRF_KEY],
|
170
|
+
CURRENT_URI_KEY => redirect_to
|
171
|
+
))
|
172
|
+
options[:base_url] = request.url
|
173
|
+
super options
|
174
|
+
end
|
175
|
+
|
176
|
+
# Fetch stored credentials for the user from the given request session.
|
177
|
+
#
|
178
|
+
# @param [String] user_id
|
179
|
+
# Unique ID of the user for loading/storing credentials.
|
180
|
+
# @param [Rack::Request] 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.
|
183
|
+
# @param [Array<String>, String] scope
|
184
|
+
# If specified, only returns credentials that have all the \
|
185
|
+
# requested scopes
|
186
|
+
# @return [Google::Auth::UserRefreshCredentials]
|
187
|
+
# Stored credentials, nil if none present
|
188
|
+
# @raise [Signet::AuthorizationError]
|
189
|
+
# May raise an error if an authorization code is present in the session
|
190
|
+
# and exchange of the code fails
|
191
|
+
def get_credentials user_id, request = nil, scope = nil
|
192
|
+
if request&.session&.key? CALLBACK_STATE_KEY
|
193
|
+
# Note - in theory, no need to check required scope as this is
|
194
|
+
# expected to be called immediately after a return from authorization
|
195
|
+
state_json = request.session.delete CALLBACK_STATE_KEY
|
196
|
+
callback_state = MultiJson.load state_json
|
197
|
+
WebUserAuthorizer.validate_callback_state callback_state, request
|
198
|
+
get_and_store_credentials_from_code(
|
199
|
+
user_id: user_id,
|
200
|
+
code: callback_state[AUTH_CODE_KEY],
|
201
|
+
scope: callback_state[SCOPE_KEY],
|
202
|
+
base_url: request.url
|
203
|
+
)
|
204
|
+
else
|
205
|
+
super user_id, scope
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
def self.extract_callback_state request
|
210
|
+
state = MultiJson.load(request[STATE_PARAM] || "{}")
|
211
|
+
redirect_uri = state[CURRENT_URI_KEY]
|
212
|
+
callback_state = {
|
213
|
+
AUTH_CODE_KEY => request[AUTH_CODE_KEY],
|
214
|
+
ERROR_CODE_KEY => request[ERROR_CODE_KEY],
|
215
|
+
SESSION_ID_KEY => state[SESSION_ID_KEY],
|
216
|
+
SCOPE_KEY => request[SCOPE_KEY]
|
217
|
+
}
|
218
|
+
[callback_state, redirect_uri]
|
219
|
+
end
|
220
|
+
|
221
|
+
# Verifies the results of an authorization callback
|
222
|
+
#
|
223
|
+
# @param [Hash] state
|
224
|
+
# Callback state
|
225
|
+
# @option state [String] AUTH_CODE_KEY
|
226
|
+
# The authorization code
|
227
|
+
# @option state [String] ERROR_CODE_KEY
|
228
|
+
# Error message if failed
|
229
|
+
# @param [Rack::Request] request
|
230
|
+
# Current request
|
231
|
+
def self.validate_callback_state state, request
|
232
|
+
raise Signet::AuthorizationError, MISSING_AUTH_CODE_ERROR if state[AUTH_CODE_KEY].nil?
|
233
|
+
if state[ERROR_CODE_KEY]
|
234
|
+
raise Signet::AuthorizationError,
|
235
|
+
format(AUTHORIZATION_ERROR, state[ERROR_CODE_KEY])
|
236
|
+
elsif request.session[XSRF_KEY] != state[SESSION_ID_KEY]
|
237
|
+
raise Signet::AuthorizationError, INVALID_STATE_TOKEN_ERROR
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
# Small Rack app which acts as the default callback handler for the app.
|
242
|
+
#
|
243
|
+
# To configure in Rails, add to routes.rb:
|
244
|
+
#
|
245
|
+
# match '/oauth2callback',
|
246
|
+
# to: Google::Auth::WebUserAuthorizer::CallbackApp,
|
247
|
+
# via: :all
|
248
|
+
#
|
249
|
+
# With Rackup, add to config.ru:
|
250
|
+
#
|
251
|
+
# map '/oauth2callback' do
|
252
|
+
# run Google::Auth::WebUserAuthorizer::CallbackApp
|
253
|
+
# end
|
254
|
+
#
|
255
|
+
# Or in a classic Sinatra app:
|
256
|
+
#
|
257
|
+
# get('/oauth2callback') do
|
258
|
+
# Google::Auth::WebUserAuthorizer::CallbackApp.call(env)
|
259
|
+
# end
|
260
|
+
#
|
261
|
+
# @see Google::Auth::WebUserAuthorizer
|
262
|
+
class CallbackApp
|
263
|
+
LOCATION_HEADER = "Location".freeze
|
264
|
+
REDIR_STATUS = 302
|
265
|
+
ERROR_STATUS = 500
|
266
|
+
|
267
|
+
# Handle a rack request. Simply stores the results the authorization
|
268
|
+
# in the session temporarily and redirects back to to the previously
|
269
|
+
# saved redirect URL. Credentials can be later retrieved by calling.
|
270
|
+
# {Google::Auth::Web::WebUserAuthorizer#get_credentials}
|
271
|
+
#
|
272
|
+
# See {Google::Auth::Web::WebUserAuthorizer#get_authorization_uri}
|
273
|
+
# for how to initiate authorization requests.
|
274
|
+
#
|
275
|
+
# @param [Hash] env
|
276
|
+
# Rack environment
|
277
|
+
# @return [Array]
|
278
|
+
# HTTP response
|
279
|
+
def self.call env
|
280
|
+
request = Rack::Request.new env
|
281
|
+
return_url = WebUserAuthorizer.handle_auth_callback_deferred request
|
282
|
+
if return_url
|
283
|
+
[REDIR_STATUS, { LOCATION_HEADER => return_url }, []]
|
284
|
+
else
|
285
|
+
[ERROR_STATUS, {}, ["No return URL is present in the request."]]
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
def call env
|
290
|
+
self.class.call env
|
291
|
+
end
|
292
|
+
end
|
293
|
+
end
|
294
|
+
end
|
295
|
+
end
|
@@ -27,141 +27,143 @@
|
|
27
27
|
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
28
28
|
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
29
29
|
|
30
|
-
spec_dir = File.expand_path
|
31
|
-
$LOAD_PATH.unshift
|
30
|
+
spec_dir = File.expand_path File.join(File.dirname(__FILE__))
|
31
|
+
$LOAD_PATH.unshift spec_dir
|
32
32
|
$LOAD_PATH.uniq!
|
33
33
|
|
34
|
-
require
|
35
|
-
require
|
34
|
+
require "faraday"
|
35
|
+
require "spec_helper"
|
36
36
|
|
37
|
-
|
38
|
-
|
39
|
-
{ 'Content-Type' => 'application/json; charset=utf-8' },
|
40
|
-
MultiJson.dump(payload)]
|
41
|
-
end
|
42
|
-
|
43
|
-
def build_access_token_json(token)
|
44
|
-
build_json_response('access_token' => token,
|
45
|
-
'token_type' => 'Bearer',
|
46
|
-
'expires_in' => 3600)
|
47
|
-
end
|
37
|
+
shared_examples "apply/apply! are OK" do
|
38
|
+
let(:auth_key) { :authorization }
|
48
39
|
|
49
|
-
WANTED_AUTH_KEY = :Authorization
|
50
|
-
|
51
|
-
shared_examples 'apply/apply! are OK' do
|
52
40
|
# tests that use these examples need to define
|
53
41
|
#
|
54
42
|
# @client which should be an auth client
|
55
43
|
#
|
56
44
|
# @make_auth_stubs, which should stub out the expected http behaviour of the
|
57
45
|
# auth client
|
58
|
-
describe
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
46
|
+
describe "#fetch_access_token" do
|
47
|
+
let(:token) { "1/abcdef1234567890" }
|
48
|
+
let :access_stub do
|
49
|
+
make_auth_stubs access_token: token
|
50
|
+
end
|
51
|
+
let :id_stub do
|
52
|
+
make_auth_stubs id_token: token
|
53
|
+
end
|
65
54
|
|
66
|
-
|
55
|
+
it "should set access_token to the fetched value" do
|
56
|
+
access_stub
|
57
|
+
@client.fetch_access_token!
|
67
58
|
expect(@client.access_token).to eq(token)
|
68
|
-
|
59
|
+
expect(access_stub).to have_been_requested
|
60
|
+
end
|
61
|
+
|
62
|
+
it "should set id_token to the fetched value" do
|
63
|
+
skip unless @id_client
|
64
|
+
id_stub
|
65
|
+
@id_client.fetch_access_token!
|
66
|
+
expect(@id_client.id_token).to eq(token)
|
67
|
+
expect(id_stub).to have_been_requested
|
68
|
+
end
|
69
|
+
|
70
|
+
it "should notify refresh listeners after updating" do
|
71
|
+
access_stub
|
72
|
+
expect do |b|
|
73
|
+
@client.on_refresh(&b)
|
74
|
+
@client.fetch_access_token!
|
75
|
+
end.to yield_with_args(have_attributes(
|
76
|
+
access_token: "1/abcdef1234567890"
|
77
|
+
))
|
78
|
+
expect(access_stub).to have_been_requested
|
69
79
|
end
|
70
80
|
end
|
71
81
|
|
72
|
-
describe
|
73
|
-
it
|
74
|
-
token =
|
75
|
-
|
76
|
-
c = Faraday.new do |b|
|
77
|
-
b.adapter(:test, stubs)
|
78
|
-
end
|
82
|
+
describe "#apply!" do
|
83
|
+
it "should update the target hash with fetched access token" do
|
84
|
+
token = "1/abcdef1234567890"
|
85
|
+
stub = make_auth_stubs access_token: token
|
79
86
|
|
80
|
-
md = { foo:
|
81
|
-
@client.apply!
|
82
|
-
want = { :foo =>
|
87
|
+
md = { foo: "bar" }
|
88
|
+
@client.apply! md
|
89
|
+
want = { :foo => "bar", auth_key => "Bearer #{token}" }
|
83
90
|
expect(md).to eq(want)
|
84
|
-
|
91
|
+
expect(stub).to have_been_requested
|
85
92
|
end
|
86
|
-
end
|
87
93
|
|
88
|
-
|
89
|
-
|
90
|
-
token =
|
91
|
-
|
92
|
-
c = Faraday.new do |b|
|
93
|
-
b.adapter(:test, stubs)
|
94
|
-
end
|
94
|
+
it "should update the target hash with fetched ID token" do
|
95
|
+
skip unless @id_client
|
96
|
+
token = "1/abcdef1234567890"
|
97
|
+
stub = make_auth_stubs id_token: token
|
95
98
|
|
96
|
-
md = { foo:
|
99
|
+
md = { foo: "bar" }
|
100
|
+
@id_client.apply! md
|
101
|
+
want = { :foo => "bar", auth_key => "Bearer #{token}" }
|
102
|
+
expect(md).to eq(want)
|
103
|
+
expect(stub).to have_been_requested
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
describe "updater_proc" do
|
108
|
+
it "should provide a proc that updates a hash with the access token" do
|
109
|
+
token = "1/abcdef1234567890"
|
110
|
+
stub = make_auth_stubs access_token: token
|
111
|
+
md = { foo: "bar" }
|
97
112
|
the_proc = @client.updater_proc
|
98
|
-
got = the_proc.call
|
99
|
-
want = { :foo =>
|
113
|
+
got = the_proc.call md
|
114
|
+
want = { :foo => "bar", auth_key => "Bearer #{token}" }
|
100
115
|
expect(got).to eq(want)
|
101
|
-
|
116
|
+
expect(stub).to have_been_requested
|
102
117
|
end
|
103
118
|
end
|
104
119
|
|
105
|
-
describe
|
106
|
-
it
|
107
|
-
token =
|
108
|
-
|
109
|
-
c = Faraday.new do |b|
|
110
|
-
b.adapter(:test, stubs)
|
111
|
-
end
|
120
|
+
describe "#apply" do
|
121
|
+
it "should not update the original hash with the access token" do
|
122
|
+
token = "1/abcdef1234567890"
|
123
|
+
stub = make_auth_stubs access_token: token
|
112
124
|
|
113
|
-
md = { foo:
|
114
|
-
@client.apply
|
115
|
-
want = { foo:
|
125
|
+
md = { foo: "bar" }
|
126
|
+
@client.apply md
|
127
|
+
want = { foo: "bar" }
|
116
128
|
expect(md).to eq(want)
|
117
|
-
|
129
|
+
expect(stub).to have_been_requested
|
118
130
|
end
|
119
131
|
|
120
|
-
it
|
121
|
-
token =
|
122
|
-
|
123
|
-
c = Faraday.new do |b|
|
124
|
-
b.adapter(:test, stubs)
|
125
|
-
end
|
132
|
+
it "should add the token to the returned hash" do
|
133
|
+
token = "1/abcdef1234567890"
|
134
|
+
stub = make_auth_stubs access_token: token
|
126
135
|
|
127
|
-
md = { foo:
|
128
|
-
got = @client.apply
|
129
|
-
want = { :foo =>
|
136
|
+
md = { foo: "bar" }
|
137
|
+
got = @client.apply md
|
138
|
+
want = { :foo => "bar", auth_key => "Bearer #{token}" }
|
130
139
|
expect(got).to eq(want)
|
131
|
-
|
140
|
+
expect(stub).to have_been_requested
|
132
141
|
end
|
133
142
|
|
134
|
-
it
|
135
|
-
token =
|
136
|
-
|
137
|
-
c = Faraday.new do |b|
|
138
|
-
b.adapter(:test, stubs)
|
139
|
-
end
|
143
|
+
it "should not fetch a new token if the current is not expired" do
|
144
|
+
token = "1/abcdef1234567890"
|
145
|
+
stub = make_auth_stubs access_token: token
|
140
146
|
|
141
147
|
n = 5 # arbitrary
|
142
148
|
n.times do |_t|
|
143
|
-
md = { foo:
|
144
|
-
got = @client.apply
|
145
|
-
want = { :foo =>
|
149
|
+
md = { foo: "bar" }
|
150
|
+
got = @client.apply md
|
151
|
+
want = { :foo => "bar", auth_key => "Bearer #{token}" }
|
146
152
|
expect(got).to eq(want)
|
147
153
|
end
|
148
|
-
|
154
|
+
expect(stub).to have_been_requested
|
149
155
|
end
|
150
156
|
|
151
|
-
it
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
[
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
md = { foo: 'bar' }
|
161
|
-
got = @client.apply(md, connection: c)
|
162
|
-
want = { :foo => 'bar', WANTED_AUTH_KEY => "Bearer #{t}" }
|
157
|
+
it "should fetch a new token if the current one is expired" do
|
158
|
+
token1 = "1/abcdef1234567890"
|
159
|
+
token2 = "2/abcdef1234567891"
|
160
|
+
|
161
|
+
[token1, token2].each do |t|
|
162
|
+
make_auth_stubs access_token: t
|
163
|
+
md = { foo: "bar" }
|
164
|
+
got = @client.apply md
|
165
|
+
want = { :foo => "bar", auth_key => "Bearer #{t}" }
|
163
166
|
expect(got).to eq(want)
|
164
|
-
stubs.verify_stubbed_calls
|
165
167
|
@client.expires_at -= 3601 # default is to expire in 1hr
|
166
168
|
end
|
167
169
|
end
|