googleauth 0.4.2 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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