googleauth 1.2.0 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +99 -0
- data/README.md +49 -8
- data/lib/googleauth/application_default.rb +5 -9
- data/lib/googleauth/base_client.rb +80 -0
- data/lib/googleauth/client_id.rb +25 -8
- data/lib/googleauth/compute_engine.rb +65 -35
- data/lib/googleauth/credentials.rb +32 -30
- data/lib/googleauth/credentials_loader.rb +6 -14
- data/lib/googleauth/default_credentials.rb +5 -2
- data/lib/googleauth/external_account/aws_credentials.rb +378 -0
- data/lib/googleauth/external_account/base_credentials.rb +159 -0
- data/lib/googleauth/external_account/external_account_utils.rb +103 -0
- data/lib/googleauth/external_account/identity_pool_credentials.rb +118 -0
- data/lib/googleauth/external_account/pluggable_credentials.rb +156 -0
- data/lib/googleauth/external_account.rb +94 -0
- data/lib/googleauth/helpers/connection.rb +35 -0
- data/lib/googleauth/id_tokens/key_sources.rb +9 -10
- data/lib/googleauth/id_tokens.rb +2 -2
- data/lib/googleauth/json_key_reader.rb +2 -1
- data/lib/googleauth/oauth2/sts_client.rb +109 -0
- data/lib/googleauth/scope_util.rb +35 -2
- data/lib/googleauth/service_account.rb +25 -11
- data/lib/googleauth/signet.rb +14 -38
- data/lib/googleauth/user_authorizer.rb +66 -9
- data/lib/googleauth/user_refresh.rb +4 -2
- data/lib/googleauth/version.rb +1 -1
- data/lib/googleauth/web_user_authorizer.rb +19 -8
- metadata +29 -20
|
@@ -130,13 +130,8 @@ module Google
|
|
|
130
130
|
end
|
|
131
131
|
n_bn = OpenSSL::BN.new n_data, 2
|
|
132
132
|
e_bn = OpenSSL::BN.new e_data, 2
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
rsa_key.set_key n_bn, e_bn, nil
|
|
136
|
-
else
|
|
137
|
-
rsa_key.n = n_bn
|
|
138
|
-
rsa_key.e = e_bn
|
|
139
|
-
end
|
|
133
|
+
sequence = [OpenSSL::ASN1::Integer.new(n_bn), OpenSSL::ASN1::Integer.new(e_bn)]
|
|
134
|
+
rsa_key = OpenSSL::PKey::RSA.new OpenSSL::ASN1::Sequence(sequence).to_der
|
|
140
135
|
rsa_key.public_key
|
|
141
136
|
end
|
|
142
137
|
|
|
@@ -161,9 +156,13 @@ module Google
|
|
|
161
156
|
x_hex = x_data.unpack1 "H*"
|
|
162
157
|
y_hex = y_data.unpack1 "H*"
|
|
163
158
|
bn = OpenSSL::BN.new ["04#{x_hex}#{y_hex}"].pack("H*"), 2
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
159
|
+
point = OpenSSL::PKey::EC::Point.new group, bn
|
|
160
|
+
sequence = OpenSSL::ASN1::Sequence([
|
|
161
|
+
OpenSSL::ASN1::Sequence([OpenSSL::ASN1::ObjectId("id-ecPublicKey"),
|
|
162
|
+
OpenSSL::ASN1::ObjectId(curve_name)]),
|
|
163
|
+
OpenSSL::ASN1::BitString(point.to_octet_string(:uncompressed))
|
|
164
|
+
])
|
|
165
|
+
OpenSSL::PKey::EC.new sequence.to_der
|
|
167
166
|
end
|
|
168
167
|
end
|
|
169
168
|
end
|
data/lib/googleauth/id_tokens.rb
CHANGED
|
@@ -153,7 +153,7 @@ module Google
|
|
|
153
153
|
# one of the provided values, or the verification will fail with
|
|
154
154
|
# {Google::Auth::IDToken::AuthorizedPartyMismatchError}. If `nil`
|
|
155
155
|
# (the default), no azp checking is performed.
|
|
156
|
-
# @param
|
|
156
|
+
# @param iss [String,Array<String>,nil] The expected issuer. At least
|
|
157
157
|
# one `iss` field in the token must match at least one of the
|
|
158
158
|
# provided issuers, or the verification will fail with
|
|
159
159
|
# {Google::Auth::IDToken::IssuerMismatchError}. If `nil`, no issuer
|
|
@@ -191,7 +191,7 @@ module Google
|
|
|
191
191
|
# one of the provided values, or the verification will fail with
|
|
192
192
|
# {Google::Auth::IDToken::AuthorizedPartyMismatchError}. If `nil`
|
|
193
193
|
# (the default), no azp checking is performed.
|
|
194
|
-
# @param
|
|
194
|
+
# @param iss [String,Array<String>,nil] The expected issuer. At least
|
|
195
195
|
# one `iss` field in the token must match at least one of the
|
|
196
196
|
# provided issuers, or the verification will fail with
|
|
197
197
|
# {Google::Auth::IDToken::IssuerMismatchError}. If `nil`, no issuer
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Copyright 2023 Google LLC
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
require "googleauth/helpers/connection"
|
|
16
|
+
|
|
17
|
+
module Google
|
|
18
|
+
module Auth
|
|
19
|
+
module OAuth2
|
|
20
|
+
# OAuth 2.0 Token Exchange Spec.
|
|
21
|
+
# This module defines a token exchange utility based on the
|
|
22
|
+
# [OAuth 2.0 Token Exchange](https://tools.ietf.org/html/rfc8693) spec. This will be mainly
|
|
23
|
+
# used to exchange external credentials for GCP access tokens in workload identity pools to
|
|
24
|
+
# access Google APIs.
|
|
25
|
+
# The implementation will support various types of client authentication as allowed in the spec.
|
|
26
|
+
#
|
|
27
|
+
# A deviation on the spec will be for additional Google specific options that cannot be easily
|
|
28
|
+
# mapped to parameters defined in the RFC.
|
|
29
|
+
# The returned dictionary response will be based on the [rfc8693 section 2.2.1]
|
|
30
|
+
# (https://tools.ietf.org/html/rfc8693#section-2.2.1) spec JSON response.
|
|
31
|
+
#
|
|
32
|
+
class STSClient
|
|
33
|
+
include Helpers::Connection
|
|
34
|
+
|
|
35
|
+
URLENCODED_HEADERS = { "Content-Type": "application/x-www-form-urlencoded" }.freeze
|
|
36
|
+
|
|
37
|
+
# Create a new instance of the STSClient.
|
|
38
|
+
#
|
|
39
|
+
# @param [String] token_exchange_endpoint
|
|
40
|
+
# The token exchange endpoint.
|
|
41
|
+
def initialize options = {}
|
|
42
|
+
raise "Token exchange endpoint can not be nil" if options[:token_exchange_endpoint].nil?
|
|
43
|
+
self.default_connection = options[:connection]
|
|
44
|
+
@token_exchange_endpoint = options[:token_exchange_endpoint]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Exchanges the provided token for another type of token based on the
|
|
48
|
+
# rfc8693 spec
|
|
49
|
+
#
|
|
50
|
+
# @param [Faraday instance] connection
|
|
51
|
+
# A callable faraday instance used to make HTTP requests.
|
|
52
|
+
# @param [String] grant_type
|
|
53
|
+
# The OAuth 2.0 token exchange grant type.
|
|
54
|
+
# @param [String] subject_token
|
|
55
|
+
# The OAuth 2.0 token exchange subject token.
|
|
56
|
+
# @param [String] subject_token_type
|
|
57
|
+
# The OAuth 2.0 token exchange subject token type.
|
|
58
|
+
# @param [String] resource
|
|
59
|
+
# The optional OAuth 2.0 token exchange resource field.
|
|
60
|
+
# @param [String] audience
|
|
61
|
+
# The optional OAuth 2.0 token exchange audience field.
|
|
62
|
+
# @param [Array<String>] scopes
|
|
63
|
+
# The optional list of scopes to use.
|
|
64
|
+
# @param [String] requested_token_type
|
|
65
|
+
# The optional OAuth 2.0 token exchange requested token type.
|
|
66
|
+
# @param additional_headers (Hash<String,String>):
|
|
67
|
+
# The optional additional headers to pass to the token exchange endpoint.
|
|
68
|
+
#
|
|
69
|
+
# @return [Hash] A hash containing the token exchange response.
|
|
70
|
+
def exchange_token options = {}
|
|
71
|
+
missing_required_opts = [:grant_type, :subject_token, :subject_token_type] - options.keys
|
|
72
|
+
unless missing_required_opts.empty?
|
|
73
|
+
raise ArgumentError, "Missing required options: #{missing_required_opts.join ', '}"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# TODO: Add the ability to add authentication to the headers
|
|
77
|
+
headers = URLENCODED_HEADERS.dup.merge(options[:additional_headers] || {})
|
|
78
|
+
|
|
79
|
+
request_body = make_request options
|
|
80
|
+
|
|
81
|
+
response = connection.post @token_exchange_endpoint, URI.encode_www_form(request_body), headers
|
|
82
|
+
|
|
83
|
+
if response.status != 200
|
|
84
|
+
raise "Token exchange failed with status #{response.status}"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
MultiJson.load response.body
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
def make_request options = {}
|
|
93
|
+
request_body = {
|
|
94
|
+
grant_type: options[:grant_type],
|
|
95
|
+
audience: options[:audience],
|
|
96
|
+
scope: Array(options[:scopes])&.join(" ") || [],
|
|
97
|
+
requested_token_type: options[:requested_token_type],
|
|
98
|
+
subject_token: options[:subject_token],
|
|
99
|
+
subject_token_type: options[:subject_token_type]
|
|
100
|
+
}
|
|
101
|
+
unless options[:additional_options].nil?
|
|
102
|
+
request_body[:options] = CGI.escape MultiJson.dump(options[:additional_options], symbolize_name: true)
|
|
103
|
+
end
|
|
104
|
+
request_body
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -18,27 +18,60 @@ require "multi_json"
|
|
|
18
18
|
|
|
19
19
|
module Google
|
|
20
20
|
module Auth
|
|
21
|
-
|
|
21
|
+
##
|
|
22
|
+
# Small utility for normalizing scopes into canonical form.
|
|
23
|
+
#
|
|
24
|
+
# The canonical form of scopes is as an array of strings, each in the form
|
|
25
|
+
# of a full URL. This utility converts space-delimited scope strings into
|
|
26
|
+
# this form, and handles a small number of common aliases.
|
|
27
|
+
#
|
|
28
|
+
# This is used by UserRefreshCredentials to verify that a credential grants
|
|
29
|
+
# a requested scope.
|
|
30
|
+
#
|
|
22
31
|
module ScopeUtil
|
|
32
|
+
##
|
|
33
|
+
# Aliases understood by this utility
|
|
34
|
+
#
|
|
23
35
|
ALIASES = {
|
|
24
36
|
"email" => "https://www.googleapis.com/auth/userinfo.email",
|
|
25
37
|
"profile" => "https://www.googleapis.com/auth/userinfo.profile",
|
|
26
38
|
"openid" => "https://www.googleapis.com/auth/plus.me"
|
|
27
39
|
}.freeze
|
|
28
40
|
|
|
41
|
+
##
|
|
42
|
+
# Normalize the input, which may be an array of scopes or a whitespace-
|
|
43
|
+
# delimited scope string. The output is always an array, even if a single
|
|
44
|
+
# scope is input.
|
|
45
|
+
#
|
|
46
|
+
# @param scope [String,Array<String>] Input scope(s)
|
|
47
|
+
# @return [Array<String>] An array of scopes in canonical form.
|
|
48
|
+
#
|
|
29
49
|
def self.normalize scope
|
|
30
50
|
list = as_array scope
|
|
31
51
|
list.map { |item| ALIASES[item] || item }
|
|
32
52
|
end
|
|
33
53
|
|
|
54
|
+
##
|
|
55
|
+
# Ensure the input is an array. If a single string is passed in, splits
|
|
56
|
+
# it via whitespace. Does not interpret aliases.
|
|
57
|
+
#
|
|
58
|
+
# @param scope [String,Array<String>] Input scope(s)
|
|
59
|
+
# @return [Array<String>] Always an array of strings
|
|
60
|
+
# @raise ArgumentError If the input is not a string or array of strings
|
|
61
|
+
#
|
|
34
62
|
def self.as_array scope
|
|
35
63
|
case scope
|
|
36
64
|
when Array
|
|
65
|
+
scope.each do |item|
|
|
66
|
+
unless item.is_a? String
|
|
67
|
+
raise ArgumentError, "Invalid scope value: #{item.inspect}. Must be string or array"
|
|
68
|
+
end
|
|
69
|
+
end
|
|
37
70
|
scope
|
|
38
71
|
when String
|
|
39
72
|
scope.split
|
|
40
73
|
else
|
|
41
|
-
raise "Invalid scope value. Must be string or array"
|
|
74
|
+
raise ArgumentError, "Invalid scope value: #{scope.inspect}. Must be string or array"
|
|
42
75
|
end
|
|
43
76
|
end
|
|
44
77
|
end
|
|
@@ -39,7 +39,11 @@ module Google
|
|
|
39
39
|
attr_reader :quota_project_id
|
|
40
40
|
|
|
41
41
|
def enable_self_signed_jwt?
|
|
42
|
-
|
|
42
|
+
# Use a self-singed JWT if there's no information that can be used to
|
|
43
|
+
# obtain an OAuth token, OR if there are scopes but also an assertion
|
|
44
|
+
# that they are default scopes that shouldn't be used to fetch a token,
|
|
45
|
+
# OR we are not in the default universe and thus OAuth isn't supported.
|
|
46
|
+
target_audience.nil? && (scope.nil? || @enable_self_signed_jwt || universe_domain != "googleapis.com")
|
|
43
47
|
end
|
|
44
48
|
|
|
45
49
|
# Creates a ServiceAccountCredentials.
|
|
@@ -53,12 +57,13 @@ module Google
|
|
|
53
57
|
raise ArgumentError, "Cannot specify both scope and target_audience" if scope && target_audience
|
|
54
58
|
|
|
55
59
|
if json_key_io
|
|
56
|
-
private_key, client_email, project_id, quota_project_id = read_json_key json_key_io
|
|
60
|
+
private_key, client_email, project_id, quota_project_id, universe_domain = read_json_key json_key_io
|
|
57
61
|
else
|
|
58
62
|
private_key = unescape ENV[CredentialsLoader::PRIVATE_KEY_VAR]
|
|
59
63
|
client_email = ENV[CredentialsLoader::CLIENT_EMAIL_VAR]
|
|
60
64
|
project_id = ENV[CredentialsLoader::PROJECT_ID_VAR]
|
|
61
65
|
quota_project_id = nil
|
|
66
|
+
universe_domain = nil
|
|
62
67
|
end
|
|
63
68
|
project_id ||= CredentialsLoader.load_gcloud_project_id
|
|
64
69
|
|
|
@@ -70,7 +75,8 @@ module Google
|
|
|
70
75
|
issuer: client_email,
|
|
71
76
|
signing_key: OpenSSL::PKey::RSA.new(private_key),
|
|
72
77
|
project_id: project_id,
|
|
73
|
-
quota_project_id: quota_project_id
|
|
78
|
+
quota_project_id: quota_project_id,
|
|
79
|
+
universe_domain: universe_domain || "googleapis.com")
|
|
74
80
|
.configure_connection(options)
|
|
75
81
|
end
|
|
76
82
|
|
|
@@ -93,16 +99,18 @@ module Google
|
|
|
93
99
|
# Extends the base class to use a transient
|
|
94
100
|
# ServiceAccountJwtHeaderCredentials for certain cases.
|
|
95
101
|
def apply! a_hash, opts = {}
|
|
96
|
-
|
|
97
|
-
# obtain an OAuth token, OR if there are scopes but also an assertion
|
|
98
|
-
# that they are default scopes that shouldn't be used to fetch a token.
|
|
99
|
-
if target_audience.nil? && (scope.nil? || enable_self_signed_jwt?)
|
|
102
|
+
if enable_self_signed_jwt?
|
|
100
103
|
apply_self_signed_jwt! a_hash
|
|
101
104
|
else
|
|
102
105
|
super
|
|
103
106
|
end
|
|
104
107
|
end
|
|
105
108
|
|
|
109
|
+
# Modifies this logic so it also requires self-signed-jwt to be disabled
|
|
110
|
+
def needs_access_token?
|
|
111
|
+
super && !enable_self_signed_jwt?
|
|
112
|
+
end
|
|
113
|
+
|
|
106
114
|
private
|
|
107
115
|
|
|
108
116
|
def apply_self_signed_jwt! a_hash
|
|
@@ -130,7 +138,7 @@ module Google
|
|
|
130
138
|
# cf [Application Default Credentials](https://cloud.google.com/docs/authentication/production)
|
|
131
139
|
class ServiceAccountJwtHeaderCredentials
|
|
132
140
|
JWT_AUD_URI_KEY = :jwt_aud_uri
|
|
133
|
-
AUTH_METADATA_KEY =
|
|
141
|
+
AUTH_METADATA_KEY = Google::Auth::BaseClient::AUTH_METADATA_KEY
|
|
134
142
|
TOKEN_CRED_URI = "https://www.googleapis.com/oauth2/v4/token".freeze
|
|
135
143
|
SIGNING_ALGORITHM = "RS256".freeze
|
|
136
144
|
EXPIRY = 60
|
|
@@ -138,6 +146,7 @@ module Google
|
|
|
138
146
|
extend JsonKeyReader
|
|
139
147
|
attr_reader :project_id
|
|
140
148
|
attr_reader :quota_project_id
|
|
149
|
+
attr_accessor :universe_domain
|
|
141
150
|
|
|
142
151
|
# Create a ServiceAccountJwtHeaderCredentials.
|
|
143
152
|
#
|
|
@@ -154,14 +163,16 @@ module Google
|
|
|
154
163
|
def initialize options = {}
|
|
155
164
|
json_key_io = options[:json_key_io]
|
|
156
165
|
if json_key_io
|
|
157
|
-
@private_key, @issuer, @project_id, @quota_project_id =
|
|
166
|
+
@private_key, @issuer, @project_id, @quota_project_id, @universe_domain =
|
|
158
167
|
self.class.read_json_key json_key_io
|
|
159
168
|
else
|
|
160
169
|
@private_key = ENV[CredentialsLoader::PRIVATE_KEY_VAR]
|
|
161
170
|
@issuer = ENV[CredentialsLoader::CLIENT_EMAIL_VAR]
|
|
162
171
|
@project_id = ENV[CredentialsLoader::PROJECT_ID_VAR]
|
|
163
172
|
@quota_project_id = nil
|
|
173
|
+
@universe_domain = nil
|
|
164
174
|
end
|
|
175
|
+
@universe_domain ||= "googleapis.com"
|
|
165
176
|
@project_id ||= CredentialsLoader.load_gcloud_project_id
|
|
166
177
|
@signing_key = OpenSSL::PKey::RSA.new @private_key
|
|
167
178
|
@scope = options[:scope]
|
|
@@ -192,8 +203,6 @@ module Google
|
|
|
192
203
|
proc { |a_hash, opts = {}| apply a_hash, opts }
|
|
193
204
|
end
|
|
194
205
|
|
|
195
|
-
protected
|
|
196
|
-
|
|
197
206
|
# Creates a jwt uri token.
|
|
198
207
|
def new_jwt_token jwt_aud_uri = nil, options = {}
|
|
199
208
|
now = Time.new
|
|
@@ -212,6 +221,11 @@ module Google
|
|
|
212
221
|
|
|
213
222
|
JWT.encode assertion, @signing_key, SIGNING_ALGORITHM
|
|
214
223
|
end
|
|
224
|
+
|
|
225
|
+
# Duck-types the corresponding method from BaseClient
|
|
226
|
+
def needs_access_token?
|
|
227
|
+
false
|
|
228
|
+
end
|
|
215
229
|
end
|
|
216
230
|
end
|
|
217
231
|
end
|
data/lib/googleauth/signet.rb
CHANGED
|
@@ -13,16 +13,27 @@
|
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
|
|
15
15
|
require "signet/oauth_2/client"
|
|
16
|
+
require "googleauth/base_client"
|
|
16
17
|
|
|
17
18
|
module Signet
|
|
18
19
|
# OAuth2 supports OAuth2 authentication.
|
|
19
20
|
module OAuth2
|
|
20
|
-
AUTH_METADATA_KEY = :authorization
|
|
21
21
|
# Signet::OAuth2::Client creates an OAuth2 client
|
|
22
22
|
#
|
|
23
23
|
# This reopens Client to add #apply and #apply! methods which update a
|
|
24
24
|
# hash with the fetched authentication token.
|
|
25
25
|
class Client
|
|
26
|
+
include Google::Auth::BaseClient
|
|
27
|
+
|
|
28
|
+
alias update_token_signet_base update_token!
|
|
29
|
+
|
|
30
|
+
def update_token! options = {}
|
|
31
|
+
options = deep_hash_normalize options
|
|
32
|
+
update_token_signet_base options
|
|
33
|
+
self.universe_domain = options[:universe_domain] if options.key? :universe_domain
|
|
34
|
+
self
|
|
35
|
+
end
|
|
36
|
+
|
|
26
37
|
def configure_connection options
|
|
27
38
|
@connection_info =
|
|
28
39
|
options[:connection_builder] || options[:default_connection]
|
|
@@ -34,36 +45,8 @@ module Signet
|
|
|
34
45
|
target_audience ? :id_token : :access_token
|
|
35
46
|
end
|
|
36
47
|
|
|
37
|
-
#
|
|
38
|
-
|
|
39
|
-
send(token_type).nil? || expires_within?(60)
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
# Updates a_hash updated with the authentication token
|
|
43
|
-
def apply! a_hash, opts = {}
|
|
44
|
-
# fetch the access token there is currently not one, or if the client
|
|
45
|
-
# has expired
|
|
46
|
-
fetch_access_token! opts if needs_access_token?
|
|
47
|
-
a_hash[AUTH_METADATA_KEY] = "Bearer #{send token_type}"
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
# Returns a clone of a_hash updated with the authentication token
|
|
51
|
-
def apply a_hash, opts = {}
|
|
52
|
-
a_copy = a_hash.clone
|
|
53
|
-
apply! a_copy, opts
|
|
54
|
-
a_copy
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
# Returns a reference to the #apply method, suitable for passing as
|
|
58
|
-
# a closure
|
|
59
|
-
def updater_proc
|
|
60
|
-
proc { |a_hash, opts = {}| apply a_hash, opts }
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
def on_refresh &block
|
|
64
|
-
@refresh_listeners = [] unless defined? @refresh_listeners
|
|
65
|
-
@refresh_listeners << block
|
|
66
|
-
end
|
|
48
|
+
# Set the universe domain
|
|
49
|
+
attr_accessor :universe_domain
|
|
67
50
|
|
|
68
51
|
alias orig_fetch_access_token! fetch_access_token!
|
|
69
52
|
def fetch_access_token! options = {}
|
|
@@ -78,13 +61,6 @@ module Signet
|
|
|
78
61
|
info
|
|
79
62
|
end
|
|
80
63
|
|
|
81
|
-
def notify_refresh_listeners
|
|
82
|
-
listeners = defined?(@refresh_listeners) ? @refresh_listeners : []
|
|
83
|
-
listeners.each do |block|
|
|
84
|
-
block.call self
|
|
85
|
-
end
|
|
86
|
-
end
|
|
87
|
-
|
|
88
64
|
def build_default_connection
|
|
89
65
|
if !defined?(@connection_info)
|
|
90
66
|
nil
|
|
@@ -16,6 +16,7 @@ require "uri"
|
|
|
16
16
|
require "multi_json"
|
|
17
17
|
require "googleauth/signet"
|
|
18
18
|
require "googleauth/user_refresh"
|
|
19
|
+
require "securerandom"
|
|
19
20
|
|
|
20
21
|
module Google
|
|
21
22
|
module Auth
|
|
@@ -54,17 +55,26 @@ module Google
|
|
|
54
55
|
# Authorization scope to request
|
|
55
56
|
# @param [Google::Auth::Stores::TokenStore] token_store
|
|
56
57
|
# Backing storage for persisting user credentials
|
|
57
|
-
# @param [String]
|
|
58
|
+
# @param [String] legacy_callback_uri
|
|
58
59
|
# URL (either absolute or relative) of the auth callback.
|
|
59
|
-
# Defaults to '/oauth2callback'
|
|
60
|
-
|
|
60
|
+
# Defaults to '/oauth2callback'.
|
|
61
|
+
# @deprecated This field is deprecated. Instead, use the keyword
|
|
62
|
+
# argument callback_uri.
|
|
63
|
+
# @param [String] code_verifier
|
|
64
|
+
# Random string of 43-128 chars used to verify the key exchange using
|
|
65
|
+
# PKCE.
|
|
66
|
+
def initialize client_id, scope, token_store,
|
|
67
|
+
legacy_callback_uri = nil,
|
|
68
|
+
callback_uri: nil,
|
|
69
|
+
code_verifier: nil
|
|
61
70
|
raise NIL_CLIENT_ID_ERROR if client_id.nil?
|
|
62
71
|
raise NIL_SCOPE_ERROR if scope.nil?
|
|
63
72
|
|
|
64
73
|
@client_id = client_id
|
|
65
74
|
@scope = Array(scope)
|
|
66
75
|
@token_store = token_store
|
|
67
|
-
@callback_uri = callback_uri || "/oauth2callback"
|
|
76
|
+
@callback_uri = legacy_callback_uri || callback_uri || "/oauth2callback"
|
|
77
|
+
@code_verifier = code_verifier
|
|
68
78
|
end
|
|
69
79
|
|
|
70
80
|
# Build the URL for requesting authorization.
|
|
@@ -80,14 +90,29 @@ module Google
|
|
|
80
90
|
# @param [String, Array<String>] scope
|
|
81
91
|
# Authorization scope to request. Overrides the instance scopes if not
|
|
82
92
|
# nil.
|
|
93
|
+
# @param [Hash] additional_parameters
|
|
94
|
+
# Additional query parameters to be added to the authorization URL.
|
|
83
95
|
# @return [String]
|
|
84
96
|
# Authorization url
|
|
85
97
|
def get_authorization_url options = {}
|
|
86
98
|
scope = options[:scope] || @scope
|
|
99
|
+
|
|
100
|
+
options[:additional_parameters] ||= {}
|
|
101
|
+
|
|
102
|
+
if @code_verifier
|
|
103
|
+
options[:additional_parameters].merge!(
|
|
104
|
+
{
|
|
105
|
+
code_challenge: generate_code_challenge(@code_verifier),
|
|
106
|
+
code_challenge_method: code_challenge_method
|
|
107
|
+
}
|
|
108
|
+
)
|
|
109
|
+
end
|
|
110
|
+
|
|
87
111
|
credentials = UserRefreshCredentials.new(
|
|
88
112
|
client_id: @client_id.id,
|
|
89
113
|
client_secret: @client_id.secret,
|
|
90
|
-
scope: scope
|
|
114
|
+
scope: scope,
|
|
115
|
+
additional_parameters: options[:additional_parameters]
|
|
91
116
|
)
|
|
92
117
|
redirect_uri = redirect_uri_for options[:base_url]
|
|
93
118
|
url = credentials.authorization_uri(access_type: "offline",
|
|
@@ -144,6 +169,9 @@ module Google
|
|
|
144
169
|
# Absolute URL to resolve the configured callback uri against.
|
|
145
170
|
# Required if the configured
|
|
146
171
|
# callback uri is a relative.
|
|
172
|
+
# @param [Hash] additional_parameters
|
|
173
|
+
# Additional parameters to be added to the post body of token
|
|
174
|
+
# endpoint request.
|
|
147
175
|
# @return [Google::Auth::UserRefreshCredentials]
|
|
148
176
|
# Credentials if exchange is successful
|
|
149
177
|
def get_credentials_from_code options = {}
|
|
@@ -151,11 +179,14 @@ module Google
|
|
|
151
179
|
code = options[:code]
|
|
152
180
|
scope = options[:scope] || @scope
|
|
153
181
|
base_url = options[:base_url]
|
|
182
|
+
options[:additional_parameters] ||= {}
|
|
183
|
+
options[:additional_parameters].merge!({ code_verifier: @code_verifier })
|
|
154
184
|
credentials = UserRefreshCredentials.new(
|
|
155
|
-
client_id:
|
|
156
|
-
client_secret:
|
|
157
|
-
redirect_uri:
|
|
158
|
-
scope:
|
|
185
|
+
client_id: @client_id.id,
|
|
186
|
+
client_secret: @client_id.secret,
|
|
187
|
+
redirect_uri: redirect_uri_for(base_url),
|
|
188
|
+
scope: scope,
|
|
189
|
+
additional_parameters: options[:additional_parameters]
|
|
159
190
|
)
|
|
160
191
|
credentials.code = code
|
|
161
192
|
credentials.fetch_access_token!({})
|
|
@@ -221,6 +252,23 @@ module Google
|
|
|
221
252
|
credentials
|
|
222
253
|
end
|
|
223
254
|
|
|
255
|
+
# The code verifier for PKCE for OAuth 2.0. When set, the
|
|
256
|
+
# authorization URI will contain the Code Challenge and Code
|
|
257
|
+
# Challenge Method querystring parameters, and the token URI will
|
|
258
|
+
# contain the Code Verifier parameter.
|
|
259
|
+
#
|
|
260
|
+
# @param [String|nil] new_code_erifier
|
|
261
|
+
def code_verifier= new_code_verifier
|
|
262
|
+
@code_verifier = new_code_verifier
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# Generate the code verifier needed to be sent while fetching
|
|
266
|
+
# authorization URL.
|
|
267
|
+
def self.generate_code_verifier
|
|
268
|
+
random_number = rand 32..96
|
|
269
|
+
SecureRandom.alphanumeric random_number
|
|
270
|
+
end
|
|
271
|
+
|
|
224
272
|
private
|
|
225
273
|
|
|
226
274
|
# @private Fetch stored token with given user_id
|
|
@@ -265,6 +313,15 @@ module Google
|
|
|
265
313
|
def uri_is_postmessage? uri
|
|
266
314
|
uri.to_s.casecmp("postmessage").zero?
|
|
267
315
|
end
|
|
316
|
+
|
|
317
|
+
def generate_code_challenge code_verifier
|
|
318
|
+
digest = Digest::SHA256.digest code_verifier
|
|
319
|
+
Base64.urlsafe_encode64 digest, padding: false
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def code_challenge_method
|
|
323
|
+
"S256"
|
|
324
|
+
end
|
|
268
325
|
end
|
|
269
326
|
end
|
|
270
327
|
end
|
|
@@ -50,7 +50,8 @@ module Google
|
|
|
50
50
|
"client_secret" => ENV[CredentialsLoader::CLIENT_SECRET_VAR],
|
|
51
51
|
"refresh_token" => ENV[CredentialsLoader::REFRESH_TOKEN_VAR],
|
|
52
52
|
"project_id" => ENV[CredentialsLoader::PROJECT_ID_VAR],
|
|
53
|
-
"quota_project_id" => nil
|
|
53
|
+
"quota_project_id" => nil,
|
|
54
|
+
"universe_domain" => nil
|
|
54
55
|
}
|
|
55
56
|
new(token_credential_uri: TOKEN_CRED_URI,
|
|
56
57
|
client_id: user_creds["client_id"],
|
|
@@ -58,7 +59,8 @@ module Google
|
|
|
58
59
|
refresh_token: user_creds["refresh_token"],
|
|
59
60
|
project_id: user_creds["project_id"],
|
|
60
61
|
quota_project_id: user_creds["quota_project_id"],
|
|
61
|
-
scope: scope
|
|
62
|
+
scope: scope,
|
|
63
|
+
universe_domain: user_creds["universe_domain"] || "googleapis.com")
|
|
62
64
|
.configure_connection(options)
|
|
63
65
|
end
|
|
64
66
|
|
data/lib/googleauth/version.rb
CHANGED
|
@@ -93,11 +93,22 @@ module Google
|
|
|
93
93
|
# Authorization scope to request
|
|
94
94
|
# @param [Google::Auth::Stores::TokenStore] token_store
|
|
95
95
|
# Backing storage for persisting user credentials
|
|
96
|
-
# @param [String]
|
|
96
|
+
# @param [String] legacy_callback_uri
|
|
97
97
|
# URL (either absolute or relative) of the auth callback. Defaults
|
|
98
|
-
# to '/oauth2callback'
|
|
99
|
-
|
|
100
|
-
|
|
98
|
+
# to '/oauth2callback'.
|
|
99
|
+
# @deprecated This field is deprecated. Instead, use the keyword
|
|
100
|
+
# argument callback_uri.
|
|
101
|
+
# @param [String] code_verifier
|
|
102
|
+
# Random string of 43-128 chars used to verify the key exchange using
|
|
103
|
+
# PKCE.
|
|
104
|
+
def initialize client_id, scope, token_store,
|
|
105
|
+
legacy_callback_uri = nil,
|
|
106
|
+
callback_uri: nil,
|
|
107
|
+
code_verifier: nil
|
|
108
|
+
super client_id, scope, token_store,
|
|
109
|
+
legacy_callback_uri,
|
|
110
|
+
code_verifier: code_verifier,
|
|
111
|
+
callback_uri: callback_uri
|
|
101
112
|
end
|
|
102
113
|
|
|
103
114
|
# Handle the result of the oauth callback. Exchanges the authorization
|
|
@@ -192,13 +203,13 @@ module Google
|
|
|
192
203
|
end
|
|
193
204
|
|
|
194
205
|
def self.extract_callback_state request
|
|
195
|
-
state = MultiJson.load(request[STATE_PARAM] || "{}")
|
|
206
|
+
state = MultiJson.load(request.params[STATE_PARAM] || "{}")
|
|
196
207
|
redirect_uri = state[CURRENT_URI_KEY]
|
|
197
208
|
callback_state = {
|
|
198
|
-
AUTH_CODE_KEY => request[AUTH_CODE_KEY],
|
|
199
|
-
ERROR_CODE_KEY => request[ERROR_CODE_KEY],
|
|
209
|
+
AUTH_CODE_KEY => request.params[AUTH_CODE_KEY],
|
|
210
|
+
ERROR_CODE_KEY => request.params[ERROR_CODE_KEY],
|
|
200
211
|
SESSION_ID_KEY => state[SESSION_ID_KEY],
|
|
201
|
-
SCOPE_KEY => request[SCOPE_KEY]
|
|
212
|
+
SCOPE_KEY => request.params[SCOPE_KEY]
|
|
202
213
|
}
|
|
203
214
|
[callback_state, redirect_uri]
|
|
204
215
|
end
|