googleauth 0.1.0 → 0.16.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/.github/CODEOWNERS +7 -0
- data/.github/CONTRIBUTING.md +74 -0
- data/.github/ISSUE_TEMPLATE/bug_report.md +36 -0
- data/.github/ISSUE_TEMPLATE/feature_request.md +21 -0
- data/.github/ISSUE_TEMPLATE/support_request.md +7 -0
- data/.github/renovate.json +6 -0
- data/.github/sync-repo-settings.yaml +18 -0
- data/.github/workflows/ci.yml +55 -0
- data/.github/workflows/release-please.yml +39 -0
- data/.gitignore +3 -0
- data/.kokoro/populate-secrets.sh +76 -0
- data/.kokoro/release.cfg +52 -0
- data/.kokoro/release.sh +18 -0
- data/.kokoro/trampoline_v2.sh +489 -0
- data/.repo-metadata.json +5 -0
- data/.rubocop.yml +17 -0
- data/.toys/.toys.rb +45 -0
- data/.toys/ci.rb +43 -0
- data/.toys/kokoro/.toys.rb +66 -0
- data/.toys/kokoro/publish-docs.rb +67 -0
- data/.toys/kokoro/publish-gem.rb +53 -0
- data/.toys/linkinator.rb +43 -0
- data/.trampolinerc +48 -0
- data/CHANGELOG.md +199 -0
- data/CODE_OF_CONDUCT.md +43 -0
- data/Gemfile +22 -1
- data/{COPYING → LICENSE} +0 -0
- data/README.md +140 -17
- data/googleauth.gemspec +28 -28
- data/integration/helper.rb +31 -0
- data/integration/id_tokens/key_source_test.rb +74 -0
- data/lib/googleauth.rb +7 -37
- data/lib/googleauth/application_default.rb +81 -0
- data/lib/googleauth/client_id.rb +104 -0
- data/lib/googleauth/compute_engine.rb +73 -26
- data/lib/googleauth/credentials.rb +561 -0
- data/lib/googleauth/credentials_loader.rb +207 -0
- data/lib/googleauth/default_credentials.rb +93 -0
- data/lib/googleauth/iam.rb +75 -0
- data/lib/googleauth/id_tokens.rb +233 -0
- data/lib/googleauth/id_tokens/errors.rb +71 -0
- data/lib/googleauth/id_tokens/key_sources.rb +396 -0
- data/lib/googleauth/id_tokens/verifier.rb +142 -0
- data/lib/googleauth/json_key_reader.rb +50 -0
- data/lib/googleauth/scope_util.rb +61 -0
- data/lib/googleauth/service_account.rb +177 -67
- data/lib/googleauth/signet.rb +69 -8
- data/lib/googleauth/stores/file_token_store.rb +65 -0
- data/lib/googleauth/stores/redis_token_store.rb +96 -0
- data/lib/googleauth/token_store.rb +69 -0
- data/lib/googleauth/user_authorizer.rb +285 -0
- data/lib/googleauth/user_refresh.rb +129 -0
- data/lib/googleauth/version.rb +1 -1
- data/lib/googleauth/web_user_authorizer.rb +295 -0
- data/spec/googleauth/apply_auth_examples.rb +96 -94
- data/spec/googleauth/client_id_spec.rb +160 -0
- data/spec/googleauth/compute_engine_spec.rb +125 -55
- data/spec/googleauth/credentials_spec.rb +600 -0
- data/spec/googleauth/get_application_default_spec.rb +232 -80
- data/spec/googleauth/iam_spec.rb +80 -0
- data/spec/googleauth/scope_util_spec.rb +77 -0
- data/spec/googleauth/service_account_spec.rb +422 -68
- data/spec/googleauth/signet_spec.rb +101 -25
- data/spec/googleauth/stores/file_token_store_spec.rb +57 -0
- data/spec/googleauth/stores/redis_token_store_spec.rb +50 -0
- data/spec/googleauth/stores/store_examples.rb +58 -0
- data/spec/googleauth/user_authorizer_spec.rb +343 -0
- data/spec/googleauth/user_refresh_spec.rb +359 -0
- data/spec/googleauth/web_user_authorizer_spec.rb +172 -0
- data/spec/spec_helper.rb +51 -10
- data/test/helper.rb +33 -0
- data/test/id_tokens/key_sources_test.rb +240 -0
- data/test/id_tokens/verifier_test.rb +269 -0
- metadata +114 -75
- data/.travis.yml +0 -18
- data/CONTRIBUTING.md +0 -32
- data/Rakefile +0 -15
data/lib/googleauth/signet.rb
CHANGED
@@ -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
|
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 = :
|
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!
|
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
|
-
|
46
|
-
|
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
|
57
|
+
def apply a_hash, opts = {}
|
51
58
|
a_copy = a_hash.clone
|
52
|
-
apply!
|
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
|
-
|
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
|