googleauth 0.1.0 → 0.16.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (76) hide show
  1. checksums.yaml +5 -5
  2. data/.github/CODEOWNERS +7 -0
  3. data/.github/CONTRIBUTING.md +74 -0
  4. data/.github/ISSUE_TEMPLATE/bug_report.md +36 -0
  5. data/.github/ISSUE_TEMPLATE/feature_request.md +21 -0
  6. data/.github/ISSUE_TEMPLATE/support_request.md +7 -0
  7. data/.github/workflows/ci.yml +55 -0
  8. data/.github/workflows/release-please.yml +39 -0
  9. data/.gitignore +3 -0
  10. data/.kokoro/populate-secrets.sh +76 -0
  11. data/.kokoro/release.cfg +52 -0
  12. data/.kokoro/release.sh +18 -0
  13. data/.kokoro/trampoline_v2.sh +489 -0
  14. data/.repo-metadata.json +5 -0
  15. data/.rubocop.yml +17 -0
  16. data/.toys/.toys.rb +45 -0
  17. data/.toys/ci.rb +43 -0
  18. data/.toys/kokoro/.toys.rb +66 -0
  19. data/.toys/kokoro/publish-docs.rb +67 -0
  20. data/.toys/kokoro/publish-gem.rb +53 -0
  21. data/.toys/linkinator.rb +43 -0
  22. data/.trampolinerc +48 -0
  23. data/CHANGELOG.md +192 -0
  24. data/CODE_OF_CONDUCT.md +43 -0
  25. data/Gemfile +22 -1
  26. data/{COPYING → LICENSE} +0 -0
  27. data/README.md +140 -17
  28. data/googleauth.gemspec +28 -28
  29. data/integration/helper.rb +31 -0
  30. data/integration/id_tokens/key_source_test.rb +74 -0
  31. data/lib/googleauth.rb +7 -37
  32. data/lib/googleauth/application_default.rb +81 -0
  33. data/lib/googleauth/client_id.rb +104 -0
  34. data/lib/googleauth/compute_engine.rb +73 -26
  35. data/lib/googleauth/credentials.rb +561 -0
  36. data/lib/googleauth/credentials_loader.rb +207 -0
  37. data/lib/googleauth/default_credentials.rb +93 -0
  38. data/lib/googleauth/iam.rb +75 -0
  39. data/lib/googleauth/id_tokens.rb +233 -0
  40. data/lib/googleauth/id_tokens/errors.rb +71 -0
  41. data/lib/googleauth/id_tokens/key_sources.rb +396 -0
  42. data/lib/googleauth/id_tokens/verifier.rb +142 -0
  43. data/lib/googleauth/json_key_reader.rb +50 -0
  44. data/lib/googleauth/scope_util.rb +61 -0
  45. data/lib/googleauth/service_account.rb +175 -67
  46. data/lib/googleauth/signet.rb +69 -8
  47. data/lib/googleauth/stores/file_token_store.rb +65 -0
  48. data/lib/googleauth/stores/redis_token_store.rb +96 -0
  49. data/lib/googleauth/token_store.rb +69 -0
  50. data/lib/googleauth/user_authorizer.rb +285 -0
  51. data/lib/googleauth/user_refresh.rb +129 -0
  52. data/lib/googleauth/version.rb +1 -1
  53. data/lib/googleauth/web_user_authorizer.rb +295 -0
  54. data/spec/googleauth/apply_auth_examples.rb +96 -94
  55. data/spec/googleauth/client_id_spec.rb +160 -0
  56. data/spec/googleauth/compute_engine_spec.rb +125 -55
  57. data/spec/googleauth/credentials_spec.rb +600 -0
  58. data/spec/googleauth/get_application_default_spec.rb +232 -80
  59. data/spec/googleauth/iam_spec.rb +80 -0
  60. data/spec/googleauth/scope_util_spec.rb +77 -0
  61. data/spec/googleauth/service_account_spec.rb +422 -68
  62. data/spec/googleauth/signet_spec.rb +101 -25
  63. data/spec/googleauth/stores/file_token_store_spec.rb +57 -0
  64. data/spec/googleauth/stores/redis_token_store_spec.rb +50 -0
  65. data/spec/googleauth/stores/store_examples.rb +58 -0
  66. data/spec/googleauth/user_authorizer_spec.rb +343 -0
  67. data/spec/googleauth/user_refresh_spec.rb +359 -0
  68. data/spec/googleauth/web_user_authorizer_spec.rb +172 -0
  69. data/spec/spec_helper.rb +51 -10
  70. data/test/helper.rb +33 -0
  71. data/test/id_tokens/key_sources_test.rb +240 -0
  72. data/test/id_tokens/verifier_test.rb +269 -0
  73. metadata +112 -75
  74. data/.travis.yml +0 -18
  75. data/CONTRIBUTING.md +0 -32
  76. data/Rakefile +0 -15
@@ -27,36 +27,97 @@
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
- require 'signet/oauth_2/client'
30
+ require "signet/oauth_2/client"
31
31
 
32
32
  module Signet
33
33
  # OAuth2 supports OAuth2 authentication.
34
34
  module OAuth2
35
- AUTH_METADATA_KEY = :Authorization
35
+ AUTH_METADATA_KEY = :authorization
36
36
  # Signet::OAuth2::Client creates an OAuth2 client
37
37
  #
38
38
  # This reopens Client to add #apply and #apply! methods which update a
39
39
  # hash with the fetched authentication token.
40
40
  class Client
41
+ def configure_connection options
42
+ @connection_info =
43
+ options[:connection_builder] || options[:default_connection]
44
+ self
45
+ end
46
+
41
47
  # Updates a_hash updated with the authentication token
42
- def apply!(a_hash, opts = {})
48
+ def apply! a_hash, opts = {}
43
49
  # fetch the access token there is currently not one, or if the client
44
50
  # has expired
45
- fetch_access_token!(opts) if access_token.nil? || expired?
46
- 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}"
47
54
  end
48
55
 
49
56
  # Returns a clone of a_hash updated with the authentication token
50
- def apply(a_hash, opts = {})
57
+ def apply a_hash, opts = {}
51
58
  a_copy = a_hash.clone
52
- apply!(a_copy, opts)
59
+ apply! a_copy, opts
53
60
  a_copy
54
61
  end
55
62
 
56
63
  # Returns a reference to the #apply method, suitable for passing as
57
64
  # a closure
58
65
  def updater_proc
59
- lambda(&method(:apply))
66
+ proc { |a_hash, opts = {}| apply a_hash, opts }
67
+ end
68
+
69
+ def on_refresh &block
70
+ @refresh_listeners = [] unless defined? @refresh_listeners
71
+ @refresh_listeners << block
72
+ end
73
+
74
+ alias orig_fetch_access_token! fetch_access_token!
75
+ def fetch_access_token! options = {}
76
+ unless options[:connection]
77
+ connection = build_default_connection
78
+ options = options.merge connection: connection if connection
79
+ end
80
+ info = retry_with_error do
81
+ orig_fetch_access_token! options
82
+ end
83
+ notify_refresh_listeners
84
+ info
85
+ end
86
+
87
+ def notify_refresh_listeners
88
+ listeners = defined?(@refresh_listeners) ? @refresh_listeners : []
89
+ listeners.each do |block|
90
+ block.call self
91
+ end
92
+ end
93
+
94
+ def build_default_connection
95
+ if !defined?(@connection_info)
96
+ nil
97
+ elsif @connection_info.respond_to? :call
98
+ @connection_info.call
99
+ else
100
+ @connection_info
101
+ end
102
+ end
103
+
104
+ def retry_with_error max_retry_count = 5
105
+ retry_count = 0
106
+
107
+ begin
108
+ yield
109
+ rescue StandardError => e
110
+ raise e if e.is_a?(Signet::AuthorizationError) || e.is_a?(Signet::ParseError)
111
+
112
+ if retry_count < max_retry_count
113
+ retry_count += 1
114
+ sleep retry_count * 0.3
115
+ retry
116
+ else
117
+ msg = "Unexpected error: #{e.inspect}"
118
+ raise Signet::AuthorizationError, msg
119
+ end
120
+ end
60
121
  end
61
122
  end
62
123
  end
@@ -0,0 +1,65 @@
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
+ super()
44
+ path = options[:file]
45
+ @store = YAML::Store.new path
46
+ end
47
+
48
+ # (see Google::Auth::Stores::TokenStore#load)
49
+ def load id
50
+ @store.transaction { @store[id] }
51
+ end
52
+
53
+ # (see Google::Auth::Stores::TokenStore#store)
54
+ def store id, token
55
+ @store.transaction { @store[id] = token }
56
+ end
57
+
58
+ # (see Google::Auth::Stores::TokenStore#delete)
59
+ def delete id
60
+ @store.transaction { @store.delete id }
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,96 @@
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:".freeze
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
+ super()
53
+ redis = options.delete :redis
54
+ prefix = options.delete :prefix
55
+ @redis = case redis
56
+ when Redis
57
+ redis
58
+ else
59
+ Redis.new options
60
+ end
61
+ @prefix = prefix || DEFAULT_KEY_PREFIX
62
+ end
63
+
64
+ # (see Google::Auth::Stores::TokenStore#load)
65
+ def load id
66
+ key = key_for id
67
+ @redis.get key
68
+ end
69
+
70
+ # (see Google::Auth::Stores::TokenStore#store)
71
+ def store id, token
72
+ key = key_for id
73
+ @redis.set key, token
74
+ end
75
+
76
+ # (see Google::Auth::Stores::TokenStore#delete)
77
+ def delete id
78
+ key = key_for id
79
+ @redis.del key
80
+ end
81
+
82
+ private
83
+
84
+ # Generate a redis key from a token ID
85
+ #
86
+ # @param [String] id
87
+ # ID of the token
88
+ # @return [String]
89
+ # Redis key
90
+ def key_for id
91
+ @prefix + id
92
+ end
93
+ end
94
+ end
95
+ end
96
+ 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
+ raise "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
+ raise "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
+ raise "Not implemented"
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,285 @@
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".freeze
57
+ NIL_CLIENT_ID_ERROR = "Client id can not be nil.".freeze
58
+ NIL_SCOPE_ERROR = "Scope can not be nil.".freeze
59
+ NIL_USER_ID_ERROR = "User ID can not be nil.".freeze
60
+ NIL_TOKEN_STORE_ERROR = "Can not call method if token store is nil".freeze
61
+ MISSING_ABSOLUTE_URL_ERROR =
62
+ 'Absolute base url required for relative callback url "%s"'.freeze
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
+ raise NIL_CLIENT_ID_ERROR if client_id.nil?
77
+ raise 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
+ )
107
+ redirect_uri = redirect_uri_for options[:base_url]
108
+ url = credentials.authorization_uri(access_type: "offline",
109
+ redirect_uri: redirect_uri,
110
+ approval_prompt: "force",
111
+ state: options[:state],
112
+ include_granted_scopes: true,
113
+ login_hint: options[:login_hint])
114
+ url.to_s
115
+ end
116
+
117
+ # Fetch stored credentials for the user.
118
+ #
119
+ # @param [String] user_id
120
+ # Unique ID of the user for loading/storing credentials.
121
+ # @param [Array<String>, String] scope
122
+ # If specified, only returns credentials that have all
123
+ # the requested scopes
124
+ # @return [Google::Auth::UserRefreshCredentials]
125
+ # Stored credentials, nil if none present
126
+ def get_credentials user_id, scope = nil
127
+ saved_token = stored_token user_id
128
+ return nil if saved_token.nil?
129
+ data = MultiJson.load saved_token
130
+
131
+ if data.fetch("client_id", @client_id.id) != @client_id.id
132
+ raise format(MISMATCHED_CLIENT_ID_ERROR,
133
+ data["client_id"], @client_id.id)
134
+ end
135
+
136
+ credentials = UserRefreshCredentials.new(
137
+ client_id: @client_id.id,
138
+ client_secret: @client_id.secret,
139
+ scope: data["scope"] || @scope,
140
+ access_token: data["access_token"],
141
+ refresh_token: data["refresh_token"],
142
+ expires_at: data.fetch("expiration_time_millis", 0) / 1000
143
+ )
144
+ scope ||= @scope
145
+ return monitor_credentials user_id, credentials if credentials.includes_scope? scope
146
+ nil
147
+ end
148
+
149
+ # Exchanges an authorization code returned in the oauth callback
150
+ #
151
+ # @param [String] user_id
152
+ # Unique ID of the user for loading/storing credentials.
153
+ # @param [String] code
154
+ # The authorization code from the OAuth callback
155
+ # @param [String, Array<String>] scope
156
+ # Authorization scope requested. Overrides the instance
157
+ # scopes if not nil.
158
+ # @param [String] base_url
159
+ # Absolute URL to resolve the configured callback uri against.
160
+ # Required if the configured
161
+ # callback uri is a relative.
162
+ # @return [Google::Auth::UserRefreshCredentials]
163
+ # Credentials if exchange is successful
164
+ def get_credentials_from_code options = {}
165
+ user_id = options[:user_id]
166
+ code = options[:code]
167
+ scope = options[:scope] || @scope
168
+ base_url = options[:base_url]
169
+ credentials = UserRefreshCredentials.new(
170
+ client_id: @client_id.id,
171
+ client_secret: @client_id.secret,
172
+ redirect_uri: redirect_uri_for(base_url),
173
+ scope: scope
174
+ )
175
+ credentials.code = code
176
+ credentials.fetch_access_token!({})
177
+ monitor_credentials user_id, credentials
178
+ end
179
+
180
+ # Exchanges an authorization code returned in the oauth callback.
181
+ # Additionally, stores the resulting credentials in the token store if
182
+ # the exchange is successful.
183
+ #
184
+ # @param [String] user_id
185
+ # Unique ID of the user for loading/storing credentials.
186
+ # @param [String] code
187
+ # The authorization code from the OAuth callback
188
+ # @param [String, Array<String>] scope
189
+ # Authorization scope requested. Overrides the instance
190
+ # scopes if not nil.
191
+ # @param [String] base_url
192
+ # Absolute URL to resolve the configured callback uri against.
193
+ # Required if the configured
194
+ # callback uri is a relative.
195
+ # @return [Google::Auth::UserRefreshCredentials]
196
+ # Credentials if exchange is successful
197
+ def get_and_store_credentials_from_code options = {}
198
+ credentials = get_credentials_from_code options
199
+ store_credentials options[:user_id], credentials
200
+ end
201
+
202
+ # Revokes a user's credentials. This both revokes the actual
203
+ # grant as well as removes the token from the token store.
204
+ #
205
+ # @param [String] user_id
206
+ # Unique ID of the user for loading/storing credentials.
207
+ def revoke_authorization user_id
208
+ credentials = get_credentials user_id
209
+ if credentials
210
+ begin
211
+ @token_store.delete user_id
212
+ ensure
213
+ credentials.revoke!
214
+ end
215
+ end
216
+ nil
217
+ end
218
+
219
+ # Store credentials for a user. Generally not required to be
220
+ # called directly, but may be used to migrate tokens from one
221
+ # store to another.
222
+ #
223
+ # @param [String] user_id
224
+ # Unique ID of the user for loading/storing credentials.
225
+ # @param [Google::Auth::UserRefreshCredentials] credentials
226
+ # Credentials to store.
227
+ def store_credentials user_id, credentials
228
+ json = MultiJson.dump(
229
+ client_id: credentials.client_id,
230
+ access_token: credentials.access_token,
231
+ refresh_token: credentials.refresh_token,
232
+ scope: credentials.scope,
233
+ expiration_time_millis: credentials.expires_at.to_i * 1000
234
+ )
235
+ @token_store.store user_id, json
236
+ credentials
237
+ end
238
+
239
+ private
240
+
241
+ # @private Fetch stored token with given user_id
242
+ #
243
+ # @param [String] user_id
244
+ # Unique ID of the user for loading/storing credentials.
245
+ # @return [String] The saved token from @token_store
246
+ def stored_token user_id
247
+ raise NIL_USER_ID_ERROR if user_id.nil?
248
+ raise NIL_TOKEN_STORE_ERROR if @token_store.nil?
249
+
250
+ @token_store.load user_id
251
+ end
252
+
253
+ # Begin watching a credential for refreshes so the access token can be
254
+ # saved.
255
+ #
256
+ # @param [String] user_id
257
+ # Unique ID of the user for loading/storing credentials.
258
+ # @param [Google::Auth::UserRefreshCredentials] credentials
259
+ # Credentials to store.
260
+ def monitor_credentials user_id, credentials
261
+ credentials.on_refresh do |cred|
262
+ store_credentials user_id, cred
263
+ end
264
+ credentials
265
+ end
266
+
267
+ # Resolve the redirect uri against a base.
268
+ #
269
+ # @param [String] base_url
270
+ # Absolute URL to resolve the callback against if necessary.
271
+ # @return [String]
272
+ # Redirect URI
273
+ def redirect_uri_for base_url
274
+ return @callback_uri if uri_is_postmessage?(@callback_uri) || !URI(@callback_uri).scheme.nil?
275
+ raise format(MISSING_ABSOLUTE_URL_ERROR, @callback_uri) if base_url.nil? || URI(base_url).scheme.nil?
276
+ URI.join(base_url, @callback_uri).to_s
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
283
+ end
284
+ end
285
+ end