googleauth 0.4.2 → 0.5.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.
@@ -42,7 +42,7 @@ module Signet
42
42
  def apply!(a_hash, opts = {})
43
43
  # fetch the access token there is currently not one, or if the client
44
44
  # has expired
45
- fetch_access_token!(opts) if access_token.nil? || expired?
45
+ fetch_access_token!(opts) if access_token.nil? || expires_within?(60)
46
46
  a_hash[AUTH_METADATA_KEY] = "Bearer #{access_token}"
47
47
  end
48
48
 
@@ -58,6 +58,25 @@ module Signet
58
58
  def updater_proc
59
59
  lambda(&method(:apply))
60
60
  end
61
+
62
+ def on_refresh(&block)
63
+ @refresh_listeners ||= []
64
+ @refresh_listeners << block
65
+ end
66
+
67
+ alias_method :orig_fetch_access_token!, :fetch_access_token!
68
+ def fetch_access_token!(options = {})
69
+ info = orig_fetch_access_token!(options)
70
+ notify_refresh_listeners
71
+ info
72
+ end
73
+
74
+ def notify_refresh_listeners
75
+ listeners = @refresh_listeners || []
76
+ listeners.each do |block|
77
+ block.call(self)
78
+ end
79
+ end
61
80
  end
62
81
  end
63
82
  end
@@ -0,0 +1,64 @@
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 'yaml/store'
31
+ require 'googleauth/token_store'
32
+
33
+ module Google
34
+ module Auth
35
+ module Stores
36
+ # Implementation of user token storage backed by a local YAML file
37
+ class FileTokenStore < Google::Auth::TokenStore
38
+ # Create a new store with the supplied file.
39
+ #
40
+ # @param [String, File] file
41
+ # Path to storage file
42
+ def initialize(options = {})
43
+ path = options[:file]
44
+ @store = YAML::Store.new(path)
45
+ end
46
+
47
+ # (see Google::Auth::Stores::TokenStore#load)
48
+ def load(id)
49
+ @store.transaction { @store[id] }
50
+ end
51
+
52
+ # (see Google::Auth::Stores::TokenStore#store)
53
+ def store(id, token)
54
+ @store.transaction { @store[id] = token }
55
+ end
56
+
57
+ # (see Google::Auth::Stores::TokenStore#delete)
58
+ def delete(id)
59
+ @store.transaction { @store.delete(id) }
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,95 @@
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 'redis'
31
+ require 'googleauth/token_store'
32
+
33
+ module Google
34
+ module Auth
35
+ module Stores
36
+ # Implementation of user token storage backed by Redis. Tokens
37
+ # are stored as JSON using the supplied key, prefixed with
38
+ # `g-user-token:`
39
+ class RedisTokenStore < Google::Auth::TokenStore
40
+ DEFAULT_KEY_PREFIX = 'g-user-token:'
41
+
42
+ # Create a new store with the supplied redis client.
43
+ #
44
+ # @param [::Redis, String] redis
45
+ # Initialized redis client to connect to.
46
+ # @param [String] prefix
47
+ # Prefix for keys in redis. Defaults to 'g-user-token:'
48
+ # @note If no redis instance is provided, a new one is created and
49
+ # the options passed through. You may include any other keys accepted
50
+ # by `Redis.new`
51
+ def initialize(options = {})
52
+ redis = options.delete(:redis)
53
+ prefix = options.delete(:prefix)
54
+ case redis
55
+ when Redis
56
+ @redis = redis
57
+ else
58
+ @redis = Redis.new(options)
59
+ end
60
+ @prefix = prefix || DEFAULT_KEY_PREFIX
61
+ end
62
+
63
+ # (see Google::Auth::Stores::TokenStore#load)
64
+ def load(id)
65
+ key = key_for(id)
66
+ @redis.get(key)
67
+ end
68
+
69
+ # (see Google::Auth::Stores::TokenStore#store)
70
+ def store(id, token)
71
+ key = key_for(id)
72
+ @redis.set(key, token)
73
+ end
74
+
75
+ # (see Google::Auth::Stores::TokenStore#delete)
76
+ def delete(id)
77
+ key = key_for(id)
78
+ @redis.del(key)
79
+ end
80
+
81
+ private
82
+
83
+ # Generate a redis key from a token ID
84
+ #
85
+ # @param [String] id
86
+ # ID of the token
87
+ # @return [String]
88
+ # Redis key
89
+ def key_for(id)
90
+ @prefix + id
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,69 @@
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
+ module Google
31
+ module Auth
32
+ # Interface definition for token stores. It is not required that
33
+ # implementations inherit from this class. It is provided for documentation
34
+ # purposes to illustrate the API contract.
35
+ class TokenStore
36
+ class << self
37
+ attr_accessor :default
38
+ end
39
+
40
+ # Load the token data from storage for the given ID.
41
+ #
42
+ # @param [String] id
43
+ # ID of token data to load.
44
+ # @return [String]
45
+ # The loaded token data.
46
+ def load(_id)
47
+ fail 'Not implemented'
48
+ end
49
+
50
+ # Put the token data into storage for the given ID.
51
+ #
52
+ # @param [String] id
53
+ # ID of token data to store.
54
+ # @param [String] token
55
+ # The token data to store.
56
+ def store(_id, _token)
57
+ fail 'Not implemented'
58
+ end
59
+
60
+ # Remove the token data from storage for the given ID.
61
+ #
62
+ # @param [String] id
63
+ # ID of the token data to delete
64
+ def delete(_id)
65
+ fail 'Not implemented'
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,273 @@
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 'uri'
31
+ require 'multi_json'
32
+ require 'googleauth/signet'
33
+ require 'googleauth/user_refresh'
34
+
35
+ module Google
36
+ module Auth
37
+ # Handles an interactive 3-Legged-OAuth2 (3LO) user consent authorization.
38
+ #
39
+ # Example usage for a simple command line app:
40
+ #
41
+ # credentials = authorizer.get_credentials(user_id)
42
+ # if credentials.nil?
43
+ # url = authorizer.get_authorization_url(
44
+ # base_url: OOB_URI)
45
+ # puts "Open the following URL in the browser and enter the " +
46
+ # "resulting code after authorization"
47
+ # puts url
48
+ # code = gets
49
+ # credentials = authorizer.get_and_store_credentials_from_code(
50
+ # user_id: user_id, code: code, base_url: OOB_URI)
51
+ # end
52
+ # # Credentials ready to use, call APIs
53
+ # ...
54
+ class UserAuthorizer
55
+ MISMATCHED_CLIENT_ID_ERROR =
56
+ 'Token client ID of %s does not match configured client id %s'
57
+ NIL_CLIENT_ID_ERROR = 'Client id can not be nil.'
58
+ NIL_SCOPE_ERROR = 'Scope can not be nil.'
59
+ NIL_USER_ID_ERROR = 'User ID can not be nil.'
60
+ NIL_TOKEN_STORE_ERROR = 'Can not call method if token store is nil'
61
+ MISSING_ABSOLUTE_URL_ERROR =
62
+ 'Absolute base url required for relative callback url "%s"'
63
+
64
+ # Initialize the authorizer
65
+ #
66
+ # @param [Google::Auth::ClientID] client_id
67
+ # Configured ID & secret for this application
68
+ # @param [String, Array<String>] scope
69
+ # Authorization scope to request
70
+ # @param [Google::Auth::Stores::TokenStore] token_store
71
+ # Backing storage for persisting user credentials
72
+ # @param [String] callback_uri
73
+ # URL (either absolute or relative) of the auth callback.
74
+ # Defaults to '/oauth2callback'
75
+ def initialize(client_id, scope, token_store, callback_uri = nil)
76
+ fail NIL_CLIENT_ID_ERROR if client_id.nil?
77
+ fail NIL_SCOPE_ERROR if scope.nil?
78
+
79
+ @client_id = client_id
80
+ @scope = Array(scope)
81
+ @token_store = token_store
82
+ @callback_uri = callback_uri || '/oauth2callback'
83
+ end
84
+
85
+ # Build the URL for requesting authorization.
86
+ #
87
+ # @param [String] login_hint
88
+ # Login hint if need to authorize a specific account. Should be a
89
+ # user's email address or unique profile ID.
90
+ # @param [String] state
91
+ # Opaque state value to be returned to the oauth callback.
92
+ # @param [String] base_url
93
+ # Absolute URL to resolve the configured callback uri against. Required
94
+ # if the configured callback uri is a relative.
95
+ # @param [String, Array<String>] scope
96
+ # Authorization scope to request. Overrides the instance scopes if not
97
+ # nil.
98
+ # @return [String]
99
+ # Authorization url
100
+ def get_authorization_url(options = {})
101
+ scope = options[:scope] || @scope
102
+ credentials = UserRefreshCredentials.new(
103
+ client_id: @client_id.id,
104
+ client_secret: @client_id.secret,
105
+ scope: scope)
106
+ redirect_uri = redirect_uri_for(options[:base_url])
107
+ url = credentials.authorization_uri(access_type: 'offline',
108
+ redirect_uri: redirect_uri,
109
+ approval_prompt: 'force',
110
+ state: options[:state],
111
+ include_granted_scopes: true,
112
+ login_hint: options[:login_hint])
113
+ url.to_s
114
+ end
115
+
116
+ # Fetch stored credentials for the user.
117
+ #
118
+ # @param [String] user_id
119
+ # Unique ID of the user for loading/storing credentials.
120
+ # @param [Array<String>, String] scope
121
+ # If specified, only returns credentials that have all
122
+ # the requested scopes
123
+ # @return [Google::Auth::UserRefreshCredentials]
124
+ # Stored credentials, nil if none present
125
+ def get_credentials(user_id, scope = nil)
126
+ fail NIL_USER_ID_ERROR if user_id.nil?
127
+ fail NIL_TOKEN_STORE_ERROR if @token_store.nil?
128
+
129
+ scope ||= @scope
130
+ saved_token = @token_store.load(user_id)
131
+ return nil if saved_token.nil?
132
+ data = MultiJson.load(saved_token)
133
+
134
+ if data.fetch('client_id', @client_id.id) != @client_id.id
135
+ fail sprintf(MISMATCHED_CLIENT_ID_ERROR,
136
+ data['client_id'], @client_id.id)
137
+ end
138
+
139
+ credentials = UserRefreshCredentials.new(
140
+ client_id: @client_id.id,
141
+ client_secret: @client_id.secret,
142
+ scope: data['scope'] || @scope,
143
+ access_token: data['access_token'],
144
+ refresh_token: data['refresh_token'],
145
+ expires_at: data.fetch('expiration_time_millis', 0) / 1000)
146
+ if credentials.includes_scope?(scope)
147
+ monitor_credentials(user_id, credentials)
148
+ return credentials
149
+ end
150
+ nil
151
+ end
152
+
153
+ # Exchanges an authorization code returned in the oauth callback
154
+ #
155
+ # @param [String] user_id
156
+ # Unique ID of the user for loading/storing credentials.
157
+ # @param [String] code
158
+ # The authorization code from the OAuth callback
159
+ # @param [String, Array<String>] scope
160
+ # Authorization scope requested. Overrides the instance
161
+ # scopes if not nil.
162
+ # @param [String] base_url
163
+ # Absolute URL to resolve the configured callback uri against.
164
+ # Required if the configured
165
+ # callback uri is a relative.
166
+ # @return [Google::Auth::UserRefreshCredentials]
167
+ # Credentials if exchange is successful
168
+ def get_credentials_from_code(options = {})
169
+ user_id = options[:user_id]
170
+ code = options[:code]
171
+ scope = options[:scope] || @scope
172
+ base_url = options[:base_url]
173
+ credentials = UserRefreshCredentials.new(
174
+ client_id: @client_id.id,
175
+ client_secret: @client_id.secret,
176
+ redirect_uri: redirect_uri_for(base_url),
177
+ scope: scope)
178
+ credentials.code = code
179
+ credentials.fetch_access_token!({})
180
+ monitor_credentials(user_id, credentials)
181
+ end
182
+
183
+ # Exchanges an authorization code returned in the oauth callback.
184
+ # Additionally, stores the resulting credentials in the token store if
185
+ # the exchange is successful.
186
+ #
187
+ # @param [String] user_id
188
+ # Unique ID of the user for loading/storing credentials.
189
+ # @param [String] code
190
+ # The authorization code from the OAuth callback
191
+ # @param [String, Array<String>] scope
192
+ # Authorization scope requested. Overrides the instance
193
+ # scopes if not nil.
194
+ # @param [String] base_url
195
+ # Absolute URL to resolve the configured callback uri against.
196
+ # Required if the configured
197
+ # callback uri is a relative.
198
+ # @return [Google::Auth::UserRefreshCredentials]
199
+ # Credentials if exchange is successful
200
+ def get_and_store_credentials_from_code(options = {})
201
+ credentials = get_credentials_from_code(options)
202
+ monitor_credentials(options[:user_id], credentials)
203
+ store_credentials(options[:user_id], credentials)
204
+ end
205
+
206
+ # Revokes a user's credentials. This both revokes the actual
207
+ # grant as well as removes the token from the token store.
208
+ #
209
+ # @param [String] user_id
210
+ # Unique ID of the user for loading/storing credentials.
211
+ def revoke_authorization(user_id)
212
+ credentials = get_credentials(user_id)
213
+ if credentials
214
+ begin
215
+ @token_store.delete(user_id)
216
+ ensure
217
+ credentials.revoke!
218
+ end
219
+ end
220
+ nil
221
+ end
222
+
223
+ # Store credentials for a user. Generally not required to be
224
+ # called directly, but may be used to migrate tokens from one
225
+ # store to another.
226
+ #
227
+ # @param [String] user_id
228
+ # Unique ID of the user for loading/storing credentials.
229
+ # @param [Google::Auth::UserRefreshCredentials] credentials
230
+ # Credentials to store.
231
+ def store_credentials(user_id, credentials)
232
+ json = MultiJson.dump(
233
+ client_id: credentials.client_id,
234
+ access_token: credentials.access_token,
235
+ refresh_token: credentials.refresh_token,
236
+ scope: credentials.scope,
237
+ expiration_time_millis: (credentials.expires_at.to_i) * 1000)
238
+ @token_store.store(user_id, json)
239
+ credentials
240
+ end
241
+
242
+ private
243
+
244
+ # Begin watching a credential for refreshes so the access token can be
245
+ # saved.
246
+ #
247
+ # @param [String] user_id
248
+ # Unique ID of the user for loading/storing credentials.
249
+ # @param [Google::Auth::UserRefreshCredentials] credentials
250
+ # Credentials to store.
251
+ def monitor_credentials(user_id, credentials)
252
+ credentials.on_refresh do |cred|
253
+ store_credentials(user_id, cred)
254
+ end
255
+ credentials
256
+ end
257
+
258
+ # Resolve the redirect uri against a base.
259
+ #
260
+ # @param [String] base_url
261
+ # Absolute URL to resolve the callback against if necessary.
262
+ # @return [String]
263
+ # Redirect URI
264
+ def redirect_uri_for(base_url)
265
+ return @callback_uri unless URI(@callback_uri).scheme.nil?
266
+ fail sprintf(
267
+ MISSING_ABSOLUTE_URL_ERROR,
268
+ @callback_uri) if base_url.nil? || URI(base_url).scheme.nil?
269
+ URI.join(base_url, @callback_uri).to_s
270
+ end
271
+ end
272
+ end
273
+ end