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.
Files changed (78) 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/renovate.json +6 -0
  8. data/.github/sync-repo-settings.yaml +18 -0
  9. data/.github/workflows/ci.yml +55 -0
  10. data/.github/workflows/release-please.yml +39 -0
  11. data/.gitignore +3 -0
  12. data/.kokoro/populate-secrets.sh +76 -0
  13. data/.kokoro/release.cfg +52 -0
  14. data/.kokoro/release.sh +18 -0
  15. data/.kokoro/trampoline_v2.sh +489 -0
  16. data/.repo-metadata.json +5 -0
  17. data/.rubocop.yml +17 -0
  18. data/.toys/.toys.rb +45 -0
  19. data/.toys/ci.rb +43 -0
  20. data/.toys/kokoro/.toys.rb +66 -0
  21. data/.toys/kokoro/publish-docs.rb +67 -0
  22. data/.toys/kokoro/publish-gem.rb +53 -0
  23. data/.toys/linkinator.rb +43 -0
  24. data/.trampolinerc +48 -0
  25. data/CHANGELOG.md +199 -0
  26. data/CODE_OF_CONDUCT.md +43 -0
  27. data/Gemfile +22 -1
  28. data/{COPYING → LICENSE} +0 -0
  29. data/README.md +140 -17
  30. data/googleauth.gemspec +28 -28
  31. data/integration/helper.rb +31 -0
  32. data/integration/id_tokens/key_source_test.rb +74 -0
  33. data/lib/googleauth.rb +7 -37
  34. data/lib/googleauth/application_default.rb +81 -0
  35. data/lib/googleauth/client_id.rb +104 -0
  36. data/lib/googleauth/compute_engine.rb +73 -26
  37. data/lib/googleauth/credentials.rb +561 -0
  38. data/lib/googleauth/credentials_loader.rb +207 -0
  39. data/lib/googleauth/default_credentials.rb +93 -0
  40. data/lib/googleauth/iam.rb +75 -0
  41. data/lib/googleauth/id_tokens.rb +233 -0
  42. data/lib/googleauth/id_tokens/errors.rb +71 -0
  43. data/lib/googleauth/id_tokens/key_sources.rb +396 -0
  44. data/lib/googleauth/id_tokens/verifier.rb +142 -0
  45. data/lib/googleauth/json_key_reader.rb +50 -0
  46. data/lib/googleauth/scope_util.rb +61 -0
  47. data/lib/googleauth/service_account.rb +177 -67
  48. data/lib/googleauth/signet.rb +69 -8
  49. data/lib/googleauth/stores/file_token_store.rb +65 -0
  50. data/lib/googleauth/stores/redis_token_store.rb +96 -0
  51. data/lib/googleauth/token_store.rb +69 -0
  52. data/lib/googleauth/user_authorizer.rb +285 -0
  53. data/lib/googleauth/user_refresh.rb +129 -0
  54. data/lib/googleauth/version.rb +1 -1
  55. data/lib/googleauth/web_user_authorizer.rb +295 -0
  56. data/spec/googleauth/apply_auth_examples.rb +96 -94
  57. data/spec/googleauth/client_id_spec.rb +160 -0
  58. data/spec/googleauth/compute_engine_spec.rb +125 -55
  59. data/spec/googleauth/credentials_spec.rb +600 -0
  60. data/spec/googleauth/get_application_default_spec.rb +232 -80
  61. data/spec/googleauth/iam_spec.rb +80 -0
  62. data/spec/googleauth/scope_util_spec.rb +77 -0
  63. data/spec/googleauth/service_account_spec.rb +422 -68
  64. data/spec/googleauth/signet_spec.rb +101 -25
  65. data/spec/googleauth/stores/file_token_store_spec.rb +57 -0
  66. data/spec/googleauth/stores/redis_token_store_spec.rb +50 -0
  67. data/spec/googleauth/stores/store_examples.rb +58 -0
  68. data/spec/googleauth/user_authorizer_spec.rb +343 -0
  69. data/spec/googleauth/user_refresh_spec.rb +359 -0
  70. data/spec/googleauth/web_user_authorizer_spec.rb +172 -0
  71. data/spec/spec_helper.rb +51 -10
  72. data/test/helper.rb +33 -0
  73. data/test/id_tokens/key_sources_test.rb +240 -0
  74. data/test/id_tokens/verifier_test.rb +269 -0
  75. metadata +114 -75
  76. data/.travis.yml +0 -18
  77. data/CONTRIBUTING.md +0 -32
  78. 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