googleauth 1.3.0 → 1.5.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 +12 -0
- data/lib/googleauth/base_client.rb +80 -0
- data/lib/googleauth/credentials.rb +1 -1
- data/lib/googleauth/credentials_loader.rb +5 -0
- data/lib/googleauth/default_credentials.rb +5 -0
- data/lib/googleauth/external_account/aws_credentials.rb +373 -0
- data/lib/googleauth/external_account/base_credentials.rb +200 -0
- data/lib/googleauth/external_account.rb +111 -0
- data/lib/googleauth/helpers/connection.rb +35 -0
- data/lib/googleauth/oauth2/sts_client.rb +99 -0
- data/lib/googleauth/service_account.rb +1 -3
- data/lib/googleauth/signet.rb +3 -39
- data/lib/googleauth/version.rb +1 -1
- metadata +12 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 42581efbf67b1cafdcdcd18cd227d22b5d456d695f3b5cfaa089f4121c17bce2
|
4
|
+
data.tar.gz: 968618cfa8048d5c246c83acc769b19c3f258958e21810481464d1b297d651bc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 392dc977400f0229fd416cdcd2d5ed60fa0a8592926c622fed3dea2ba6ba1e083169d978c7a48d4e895014909352196fca56ac8c21df1f0b3694e55439cfedb2
|
7
|
+
data.tar.gz: 1a33a41171c9963196f833fa83702d61af6e335b677a229bf0f93316e7f0272f9b211f00fc7e180c4f07e1e7dd5ffbe29b484f5cd72578a49096046c6d017fc4
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,17 @@
|
|
1
1
|
# Release History
|
2
2
|
|
3
|
+
### 1.5.0 (2023-03-21)
|
4
|
+
|
5
|
+
#### Features
|
6
|
+
|
7
|
+
* Add support for AWS Workload Identity Federation ([#418](https://github.com/googleapis/google-auth-library-ruby/issues/418))
|
8
|
+
|
9
|
+
### 1.4.0 (2022-12-14)
|
10
|
+
|
11
|
+
#### Features
|
12
|
+
|
13
|
+
* make new_jwt_token public in order to fetch raw token directly ([#405](https://github.com/googleapis/google-auth-library-ruby/issues/405))
|
14
|
+
|
3
15
|
### 1.3.0 (2022-10-18)
|
4
16
|
|
5
17
|
#### Features
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# Copyright 2023 Google, Inc.
|
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
|
+
module Google
|
16
|
+
# Module Auth provides classes that provide Google-specific authorization
|
17
|
+
# used to access Google APIs.
|
18
|
+
module Auth
|
19
|
+
# BaseClient is a class used to contain common methods that are required by any
|
20
|
+
# Credentials Client, including AwsCredentials, ServiceAccountCredentials,
|
21
|
+
# and UserRefreshCredentials. This is a superclass of Signet::OAuth2::Client
|
22
|
+
# and has been created to create a generic interface for all credentials clients
|
23
|
+
# to use, including ones which do not inherit from Signet::OAuth2::Client.
|
24
|
+
module BaseClient
|
25
|
+
AUTH_METADATA_KEY = :authorization
|
26
|
+
|
27
|
+
# Updates a_hash updated with the authentication token
|
28
|
+
def apply! a_hash, opts = {}
|
29
|
+
# fetch the access token there is currently not one, or if the client
|
30
|
+
# has expired
|
31
|
+
fetch_access_token! opts if needs_access_token?
|
32
|
+
a_hash[AUTH_METADATA_KEY] = "Bearer #{send token_type}"
|
33
|
+
end
|
34
|
+
|
35
|
+
# Returns a clone of a_hash updated with the authentication token
|
36
|
+
def apply a_hash, opts = {}
|
37
|
+
a_copy = a_hash.clone
|
38
|
+
apply! a_copy, opts
|
39
|
+
a_copy
|
40
|
+
end
|
41
|
+
|
42
|
+
# Whether the id_token or access_token is missing or about to expire.
|
43
|
+
def needs_access_token?
|
44
|
+
send(token_type).nil? || expires_within?(60)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Returns a reference to the #apply method, suitable for passing as
|
48
|
+
# a closure
|
49
|
+
def updater_proc
|
50
|
+
proc { |a_hash, opts = {}| apply a_hash, opts }
|
51
|
+
end
|
52
|
+
|
53
|
+
def on_refresh &block
|
54
|
+
@refresh_listeners = [] unless defined? @refresh_listeners
|
55
|
+
@refresh_listeners << block
|
56
|
+
end
|
57
|
+
|
58
|
+
def notify_refresh_listeners
|
59
|
+
listeners = defined?(@refresh_listeners) ? @refresh_listeners : []
|
60
|
+
listeners.each do |block|
|
61
|
+
block.call self
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def expires_within?
|
66
|
+
raise NotImplementedError
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def token_type
|
72
|
+
raise NotImplementedError
|
73
|
+
end
|
74
|
+
|
75
|
+
def fetch_access_token!
|
76
|
+
raise NotImplementedError
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -355,7 +355,7 @@ module Google
|
|
355
355
|
@project_id = options["project_id"] || options["project"]
|
356
356
|
@quota_project_id = options["quota_project_id"]
|
357
357
|
case keyfile
|
358
|
-
when
|
358
|
+
when Google::Auth::BaseClient
|
359
359
|
update_from_signet keyfile
|
360
360
|
when Hash
|
361
361
|
update_from_hash keyfile, options
|
@@ -30,6 +30,11 @@ module Google
|
|
30
30
|
REFRESH_TOKEN_VAR = "GOOGLE_REFRESH_TOKEN".freeze
|
31
31
|
ACCOUNT_TYPE_VAR = "GOOGLE_ACCOUNT_TYPE".freeze
|
32
32
|
PROJECT_ID_VAR = "GOOGLE_PROJECT_ID".freeze
|
33
|
+
AWS_REGION_VAR = "AWS_REGION".freeze
|
34
|
+
AWS_DEFAULT_REGION_VAR = "AWS_DEFAULT_REGION".freeze
|
35
|
+
AWS_ACCESS_KEY_ID_VAR = "AWS_ACCESS_KEY_ID".freeze
|
36
|
+
AWS_SECRET_ACCESS_KEY_VAR = "AWS_SECRET_ACCESS_KEY".freeze
|
37
|
+
AWS_SESSION_TOKEN_VAR = "AWS_SESSION_TOKEN".freeze
|
33
38
|
GCLOUD_POSIX_COMMAND = "gcloud".freeze
|
34
39
|
GCLOUD_WINDOWS_COMMAND = "gcloud.cmd".freeze
|
35
40
|
GCLOUD_CONFIG_COMMAND = "config config-helper --format json --verbosity none".freeze
|
@@ -18,6 +18,7 @@ require "stringio"
|
|
18
18
|
require "googleauth/credentials_loader"
|
19
19
|
require "googleauth/service_account"
|
20
20
|
require "googleauth/user_refresh"
|
21
|
+
require "googleauth/external_account"
|
21
22
|
|
22
23
|
module Google
|
23
24
|
# Module Auth provides classes that provide Google-specific authorization
|
@@ -53,6 +54,8 @@ module Google
|
|
53
54
|
ServiceAccountCredentials
|
54
55
|
when "authorized_user"
|
55
56
|
UserRefreshCredentials
|
57
|
+
when "external_account"
|
58
|
+
ExternalAccount::Credentials
|
56
59
|
else
|
57
60
|
raise "credentials type '#{type}' is not supported"
|
58
61
|
end
|
@@ -69,6 +72,8 @@ module Google
|
|
69
72
|
[json_key, ServiceAccountCredentials]
|
70
73
|
when "authorized_user"
|
71
74
|
[json_key, UserRefreshCredentials]
|
75
|
+
when "external_account"
|
76
|
+
[json_key, ExternalAccount::Credentials]
|
72
77
|
else
|
73
78
|
raise "credentials type '#{type}' is not supported"
|
74
79
|
end
|
@@ -0,0 +1,373 @@
|
|
1
|
+
# Copyright 2023 Google, Inc.
|
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 "time"
|
16
|
+
require "googleauth/external_account/base_credentials"
|
17
|
+
|
18
|
+
module Google
|
19
|
+
# Module Auth provides classes that provide Google-specific authorization
|
20
|
+
# used to access Google APIs.
|
21
|
+
module Auth
|
22
|
+
# Authenticates requests using External Account credentials, such
|
23
|
+
# as those provided by the AWS provider.
|
24
|
+
module ExternalAccount
|
25
|
+
# This module handles the retrieval of credentials from Google Cloud
|
26
|
+
# by utilizing the AWS EC2 metadata service and then exchanging the
|
27
|
+
# credentials for a short-lived Google Cloud access token.
|
28
|
+
class AwsCredentials
|
29
|
+
include Google::Auth::ExternalAccount::BaseCredentials
|
30
|
+
extend CredentialsLoader
|
31
|
+
|
32
|
+
# Will always be nil, but method still gets used.
|
33
|
+
attr_reader :client_id
|
34
|
+
|
35
|
+
def initialize options = {}
|
36
|
+
base_setup options
|
37
|
+
|
38
|
+
@audience = options[:audience]
|
39
|
+
@credential_source = options[:credential_source] || {}
|
40
|
+
@environment_id = @credential_source["environment_id"]
|
41
|
+
@region_url = validate_metadata_server @credential_source["region_url"], "region_url"
|
42
|
+
@credential_verification_url = validate_metadata_server @credential_source["url"], "url"
|
43
|
+
@regional_cred_verification_url = @credential_source["regional_cred_verification_url"]
|
44
|
+
@imdsv2_session_token_url = validate_metadata_server @credential_source["imdsv2_session_token_url"],
|
45
|
+
"imdsv2_session_token_url"
|
46
|
+
|
47
|
+
# These will be lazily loaded when needed, or will raise an error if not provided
|
48
|
+
@region = nil
|
49
|
+
@request_signer = nil
|
50
|
+
end
|
51
|
+
|
52
|
+
# Retrieves the subject token using the credential_source object.
|
53
|
+
# The subject token is a serialized [AWS GetCallerIdentity signed request](
|
54
|
+
# https://cloud.google.com/iam/docs/access-resources-aws#exchange-token).
|
55
|
+
#
|
56
|
+
# The logic is summarized as:
|
57
|
+
#
|
58
|
+
# Retrieve the AWS region from the AWS_REGION or AWS_DEFAULT_REGION
|
59
|
+
# environment variable or from the AWS metadata server availability-zone
|
60
|
+
# if not found in the environment variable.
|
61
|
+
#
|
62
|
+
# Check AWS credentials in environment variables. If not found, retrieve
|
63
|
+
# from the AWS metadata server security-credentials endpoint.
|
64
|
+
#
|
65
|
+
# When retrieving AWS credentials from the metadata server
|
66
|
+
# security-credentials endpoint, the AWS role needs to be determined by
|
67
|
+
# calling the security-credentials endpoint without any argument. Then the
|
68
|
+
# credentials can be retrieved via: security-credentials/role_name
|
69
|
+
#
|
70
|
+
# Generate the signed request to AWS STS GetCallerIdentity action.
|
71
|
+
#
|
72
|
+
# Inject x-goog-cloud-target-resource into header and serialize the
|
73
|
+
# signed request. This will be the subject-token to pass to GCP STS.
|
74
|
+
#
|
75
|
+
# @return [string] The retrieved subject token.
|
76
|
+
#
|
77
|
+
def retrieve_subject_token!
|
78
|
+
if @request_signer.nil?
|
79
|
+
@region = region
|
80
|
+
@request_signer = AwsRequestSigner.new @region
|
81
|
+
end
|
82
|
+
|
83
|
+
request = {
|
84
|
+
method: "POST",
|
85
|
+
url: @regional_cred_verification_url.sub("{region}", @region)
|
86
|
+
}
|
87
|
+
|
88
|
+
request_options = @request_signer.generate_signed_request fetch_security_credentials, request
|
89
|
+
|
90
|
+
request_headers = request_options[:headers]
|
91
|
+
request_headers["x-goog-cloud-target-resource"] = @audience
|
92
|
+
|
93
|
+
aws_signed_request = {
|
94
|
+
headers: [],
|
95
|
+
method: request_options[:method],
|
96
|
+
url: request_options[:url]
|
97
|
+
}
|
98
|
+
|
99
|
+
aws_signed_request[:headers] = request_headers.keys.sort.map do |key|
|
100
|
+
{ key: key, value: request_headers[key] }
|
101
|
+
end
|
102
|
+
|
103
|
+
uri_escape aws_signed_request.to_json
|
104
|
+
end
|
105
|
+
|
106
|
+
private
|
107
|
+
|
108
|
+
def validate_metadata_server url, name
|
109
|
+
return nil if url.nil?
|
110
|
+
host = URI(url).host
|
111
|
+
raise "Invalid host #{host} for #{name}." unless ["169.254.169.254", "[fd00:ec2::254]"].include? host
|
112
|
+
url
|
113
|
+
end
|
114
|
+
|
115
|
+
def get_aws_resource url, name, data: nil, headers: {}
|
116
|
+
begin
|
117
|
+
unless [nil, url].include? @imdsv2_session_token_url
|
118
|
+
headers["x-aws-ec2-metadata-token"] = get_aws_resource(
|
119
|
+
@imdsv2_session_token_url,
|
120
|
+
"Session Token",
|
121
|
+
headers: { "x-aws-ec2-metadata-token-ttl-seconds": "300" }
|
122
|
+
).body
|
123
|
+
end
|
124
|
+
|
125
|
+
response = if data
|
126
|
+
headers["Content-Type"] = "application/json"
|
127
|
+
connection.post url, data, headers
|
128
|
+
else
|
129
|
+
connection.get url, nil, headers
|
130
|
+
end
|
131
|
+
|
132
|
+
raise Faraday::Error unless response.success?
|
133
|
+
response
|
134
|
+
rescue Faraday::Error
|
135
|
+
raise "Failed to retrieve AWS #{name}."
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def uri_escape string
|
140
|
+
if string.nil?
|
141
|
+
nil
|
142
|
+
else
|
143
|
+
CGI.escape(string.encode("UTF-8")).gsub("+", "%20").gsub("%7E", "~")
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
# Retrieves the AWS security credentials required for signing AWS
|
148
|
+
# requests from either the AWS security credentials environment variables
|
149
|
+
# or from the AWS metadata server.
|
150
|
+
def fetch_security_credentials
|
151
|
+
env_aws_access_key_id = ENV[CredentialsLoader::AWS_ACCESS_KEY_ID_VAR]
|
152
|
+
env_aws_secret_access_key = ENV[CredentialsLoader::AWS_SECRET_ACCESS_KEY_VAR]
|
153
|
+
# This is normally not available for permanent credentials.
|
154
|
+
env_aws_session_token = ENV[CredentialsLoader::AWS_SESSION_TOKEN_VAR]
|
155
|
+
|
156
|
+
if env_aws_access_key_id && env_aws_secret_access_key
|
157
|
+
return {
|
158
|
+
access_key_id: env_aws_access_key_id,
|
159
|
+
secret_access_key: env_aws_secret_access_key,
|
160
|
+
session_token: env_aws_session_token
|
161
|
+
}
|
162
|
+
end
|
163
|
+
|
164
|
+
role_name = fetch_metadata_role_name
|
165
|
+
credentials = fetch_metadata_security_credentials role_name
|
166
|
+
|
167
|
+
{
|
168
|
+
access_key_id: credentials["AccessKeyId"],
|
169
|
+
secret_access_key: credentials["SecretAccessKey"],
|
170
|
+
session_token: credentials["Token"]
|
171
|
+
}
|
172
|
+
end
|
173
|
+
|
174
|
+
# Retrieves the AWS role currently attached to the current AWS
|
175
|
+
# workload by querying the AWS metadata server. This is needed for the
|
176
|
+
# AWS metadata server security credentials endpoint in order to retrieve
|
177
|
+
# the AWS security credentials needed to sign requests to AWS APIs.
|
178
|
+
def fetch_metadata_role_name
|
179
|
+
unless @credential_verification_url
|
180
|
+
raise "Unable to determine the AWS metadata server security credentials endpoint"
|
181
|
+
end
|
182
|
+
|
183
|
+
get_aws_resource(@credential_verification_url, "IAM Role").body
|
184
|
+
end
|
185
|
+
|
186
|
+
# Retrieves the AWS security credentials required for signing AWS
|
187
|
+
# requests from the AWS metadata server.
|
188
|
+
def fetch_metadata_security_credentials role_name
|
189
|
+
response = get_aws_resource "#{@credential_verification_url}/#{role_name}", "credentials"
|
190
|
+
MultiJson.load response.body
|
191
|
+
end
|
192
|
+
|
193
|
+
def region
|
194
|
+
@region = ENV[CredentialsLoader::AWS_REGION_VAR] || ENV[CredentialsLoader::AWS_DEFAULT_REGION_VAR]
|
195
|
+
|
196
|
+
unless @region
|
197
|
+
raise "region_url or region must be set for external account credentials" unless @region_url
|
198
|
+
|
199
|
+
@region ||= get_aws_resource(@region_url, "region").body[0..-2]
|
200
|
+
end
|
201
|
+
|
202
|
+
@region
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
# Implements an AWS request signer based on the AWS Signature Version 4 signing process.
|
207
|
+
# https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html
|
208
|
+
class AwsRequestSigner
|
209
|
+
# Instantiates an AWS request signer used to compute authenticated signed
|
210
|
+
# requests to AWS APIs based on the AWS Signature Version 4 signing process.
|
211
|
+
#
|
212
|
+
# @param [string] region_name
|
213
|
+
# The AWS region to use.
|
214
|
+
def initialize region_name
|
215
|
+
@region_name = region_name
|
216
|
+
end
|
217
|
+
|
218
|
+
# Generates the signed request for the provided HTTP request for calling
|
219
|
+
# an AWS API. This follows the steps described at:
|
220
|
+
# https://docs.aws.amazon.com/general/latest/gr/sigv4_signing.html
|
221
|
+
#
|
222
|
+
# @param [Hash[string, string]] aws_security_credentials
|
223
|
+
# A dictionary containing the AWS security credentials.
|
224
|
+
# @param [string] url
|
225
|
+
# The AWS service URL containing the canonical URI and query string.
|
226
|
+
# @param [string] method
|
227
|
+
# The HTTP method used to call this API.
|
228
|
+
#
|
229
|
+
# @return [hash{string => string}]
|
230
|
+
# The AWS signed request dictionary object.
|
231
|
+
#
|
232
|
+
def generate_signed_request aws_credentials, original_request
|
233
|
+
uri = Addressable::URI.parse original_request[:url]
|
234
|
+
raise "Invalid AWS service URL" unless uri.hostname && uri.scheme == "https"
|
235
|
+
service_name = uri.host.split(".").first
|
236
|
+
|
237
|
+
datetime = Time.now.utc.strftime "%Y%m%dT%H%M%SZ"
|
238
|
+
date = datetime[0, 8]
|
239
|
+
|
240
|
+
headers = aws_headers aws_credentials, original_request, datetime
|
241
|
+
|
242
|
+
request_payload = original_request[:data] || ""
|
243
|
+
content_sha256 = sha256_hexdigest request_payload
|
244
|
+
|
245
|
+
canonical_req = canonical_request original_request[:method], uri, headers, content_sha256
|
246
|
+
sts = string_to_sign datetime, canonical_req, service_name
|
247
|
+
|
248
|
+
# Authorization header requires everything else to be properly setup in order to be properly
|
249
|
+
# calculated.
|
250
|
+
headers["Authorization"] = build_authorization_header headers, sts, aws_credentials, service_name, date
|
251
|
+
|
252
|
+
{
|
253
|
+
url: uri.to_s,
|
254
|
+
headers: headers,
|
255
|
+
method: original_request[:method],
|
256
|
+
data: (request_payload unless request_payload.empty?)
|
257
|
+
}.compact
|
258
|
+
end
|
259
|
+
|
260
|
+
private
|
261
|
+
|
262
|
+
def aws_headers aws_credentials, original_request, datetime
|
263
|
+
uri = Addressable::URI.parse original_request[:url]
|
264
|
+
temp_headers = original_request[:headers] || {}
|
265
|
+
headers = {}
|
266
|
+
temp_headers.each_key { |k| headers[k.to_s] = temp_headers[k] }
|
267
|
+
headers["host"] = uri.host
|
268
|
+
headers["x-amz-date"] = datetime
|
269
|
+
headers["x-amz-security-token"] = aws_credentials[:session_token] if aws_credentials[:session_token]
|
270
|
+
headers
|
271
|
+
end
|
272
|
+
|
273
|
+
def build_authorization_header headers, sts, aws_credentials, service_name, date
|
274
|
+
[
|
275
|
+
"AWS4-HMAC-SHA256",
|
276
|
+
"Credential=#{credential aws_credentials[:access_key_id], date, service_name},",
|
277
|
+
"SignedHeaders=#{headers.keys.sort.join ';'},",
|
278
|
+
"Signature=#{signature aws_credentials[:secret_access_key], date, sts, service_name}"
|
279
|
+
].join(" ")
|
280
|
+
end
|
281
|
+
|
282
|
+
def signature secret_access_key, date, string_to_sign, service
|
283
|
+
k_date = hmac "AWS4#{secret_access_key}", date
|
284
|
+
k_region = hmac k_date, @region_name
|
285
|
+
k_service = hmac k_region, service
|
286
|
+
k_credentials = hmac k_service, "aws4_request"
|
287
|
+
|
288
|
+
hexhmac k_credentials, string_to_sign
|
289
|
+
end
|
290
|
+
|
291
|
+
def hmac key, value
|
292
|
+
OpenSSL::HMAC.digest OpenSSL::Digest.new("sha256"), key, value
|
293
|
+
end
|
294
|
+
|
295
|
+
def hexhmac key, value
|
296
|
+
OpenSSL::HMAC.hexdigest OpenSSL::Digest.new("sha256"), key, value
|
297
|
+
end
|
298
|
+
|
299
|
+
def credential access_key_id, date, service
|
300
|
+
"#{access_key_id}/#{credential_scope date, service}"
|
301
|
+
end
|
302
|
+
|
303
|
+
def credential_scope date, service
|
304
|
+
[
|
305
|
+
date,
|
306
|
+
@region_name,
|
307
|
+
service,
|
308
|
+
"aws4_request"
|
309
|
+
].join("/")
|
310
|
+
end
|
311
|
+
|
312
|
+
def string_to_sign datetime, canonical_request, service
|
313
|
+
[
|
314
|
+
"AWS4-HMAC-SHA256",
|
315
|
+
datetime,
|
316
|
+
credential_scope(datetime[0, 8], service),
|
317
|
+
sha256_hexdigest(canonical_request)
|
318
|
+
].join("\n")
|
319
|
+
end
|
320
|
+
|
321
|
+
def host uri
|
322
|
+
# Handles known and unknown URI schemes; default_port nil when unknown.
|
323
|
+
if uri.default_port == uri.port
|
324
|
+
uri.host
|
325
|
+
else
|
326
|
+
"#{uri.host}:#{uri.port}"
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
330
|
+
def canonical_request http_method, uri, headers, content_sha256
|
331
|
+
headers = headers.sort_by(&:first) # transforms to a sorted array of [key, value]
|
332
|
+
|
333
|
+
[
|
334
|
+
http_method,
|
335
|
+
uri.path.empty? ? "/" : uri.path,
|
336
|
+
build_canonical_querystring(uri.query || ""),
|
337
|
+
headers.map { |k, v| "#{k}:#{v}\n" }.join, # Canonical headers
|
338
|
+
headers.map(&:first).join(";"), # Signed headers
|
339
|
+
content_sha256
|
340
|
+
].join("\n")
|
341
|
+
end
|
342
|
+
|
343
|
+
def sha256_hexdigest string
|
344
|
+
OpenSSL::Digest::SHA256.hexdigest string
|
345
|
+
end
|
346
|
+
|
347
|
+
# Generates the canonical query string given a raw query string.
|
348
|
+
# Logic is based on
|
349
|
+
# https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
|
350
|
+
# Code is from the AWS SDK for Ruby
|
351
|
+
# https://github.com/aws/aws-sdk-ruby/blob/0ac3d0a393ed216290bfb5f0383380376f6fb1f1/gems/aws-sigv4/lib/aws-sigv4/signer.rb#L532
|
352
|
+
def build_canonical_querystring query
|
353
|
+
params = query.split "&"
|
354
|
+
params = params.map { |p| p.include?("=") ? p : "#{p}=" }
|
355
|
+
|
356
|
+
params.each.with_index.sort do |(a, a_offset), (b, b_offset)|
|
357
|
+
a_name, a_value = a.split "="
|
358
|
+
b_name, b_value = b.split "="
|
359
|
+
if a_name == b_name
|
360
|
+
if a_value == b_value
|
361
|
+
a_offset <=> b_offset
|
362
|
+
else
|
363
|
+
a_value <=> b_value
|
364
|
+
end
|
365
|
+
else
|
366
|
+
a_name <=> b_name
|
367
|
+
end
|
368
|
+
end.map(&:first).join("&")
|
369
|
+
end
|
370
|
+
end
|
371
|
+
end
|
372
|
+
end
|
373
|
+
end
|
@@ -0,0 +1,200 @@
|
|
1
|
+
# Copyright 2023 Google, Inc.
|
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.require "time"
|
14
|
+
|
15
|
+
require "googleauth/base_client"
|
16
|
+
require "googleauth/helpers/connection"
|
17
|
+
require "googleauth/oauth2/sts_client"
|
18
|
+
|
19
|
+
module Google
|
20
|
+
# Module Auth provides classes that provide Google-specific authorization
|
21
|
+
# used to access Google APIs.
|
22
|
+
module Auth
|
23
|
+
# Authenticates requests using External Account credentials, such
|
24
|
+
# as those provided by the AWS provider.
|
25
|
+
module ExternalAccount
|
26
|
+
# Authenticates requests using External Account credentials, such
|
27
|
+
# as those provided by the AWS provider.
|
28
|
+
module BaseCredentials
|
29
|
+
# Contains all methods needed for all external account credentials.
|
30
|
+
# Other credentials should call `base_setup` during initialization
|
31
|
+
# And should define the :retrieve_subject_token method
|
32
|
+
|
33
|
+
# External account JSON type identifier.
|
34
|
+
EXTERNAL_ACCOUNT_JSON_TYPE = "external_account".freeze
|
35
|
+
# The token exchange grant_type used for exchanging credentials.
|
36
|
+
STS_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:token-exchange".freeze
|
37
|
+
# The token exchange requested_token_type. This is always an access_token.
|
38
|
+
STS_REQUESTED_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token".freeze
|
39
|
+
# Cloud resource manager URL used to retrieve project information.
|
40
|
+
CLOUD_RESOURCE_MANAGER = "https://cloudresourcemanager.googleapis.com/v1/projects/".freeze
|
41
|
+
# Default IAM_SCOPE
|
42
|
+
IAM_SCOPE = ["https://www.googleapis.com/auth/iam".freeze].freeze
|
43
|
+
|
44
|
+
include Google::Auth::BaseClient
|
45
|
+
include Helpers::Connection
|
46
|
+
|
47
|
+
attr_reader :expires_at
|
48
|
+
attr_accessor :access_token
|
49
|
+
|
50
|
+
def expires_within? seconds
|
51
|
+
# This method is needed for BaseClient
|
52
|
+
@expires_at && @expires_at - Time.now.utc < seconds
|
53
|
+
end
|
54
|
+
|
55
|
+
def expires_at= new_expires_at
|
56
|
+
@expires_at = normalize_timestamp new_expires_at
|
57
|
+
end
|
58
|
+
|
59
|
+
def fetch_access_token! _options = {}
|
60
|
+
# This method is needed for BaseClient
|
61
|
+
response = exchange_token
|
62
|
+
|
63
|
+
if @service_account_impersonation_url
|
64
|
+
impersonated_response = get_impersonated_access_token response["access_token"]
|
65
|
+
self.expires_at = impersonated_response["expireTime"]
|
66
|
+
self.access_token = impersonated_response["accessToken"]
|
67
|
+
else
|
68
|
+
# Extract the expiration time in seconds from the response and calculate the actual expiration time
|
69
|
+
# and then save that to the expiry variable.
|
70
|
+
self.expires_at = Time.now.utc + response["expires_in"].to_i
|
71
|
+
self.access_token = response["access_token"]
|
72
|
+
end
|
73
|
+
|
74
|
+
notify_refresh_listeners
|
75
|
+
end
|
76
|
+
|
77
|
+
##
|
78
|
+
# Retrieves the project ID corresponding to the workload identity or workforce pool.
|
79
|
+
# For workforce pool credentials, it returns the project ID corresponding to the workforce_pool_user_project.
|
80
|
+
# When not determinable, None is returned.
|
81
|
+
#
|
82
|
+
# The resource may not have permission (resourcemanager.projects.get) to
|
83
|
+
# call this API or the required scopes may not be selected:
|
84
|
+
# https://cloud.google.com/resource-manager/reference/rest/v1/projects/get#authorization-scopes
|
85
|
+
#
|
86
|
+
# @return [string,nil]
|
87
|
+
# The project ID corresponding to the workload identity pool or workforce pool if determinable.
|
88
|
+
#
|
89
|
+
def project_id
|
90
|
+
return @project_id unless @project_id.nil?
|
91
|
+
project_number = self.project_number || @workforce_pool_user_project
|
92
|
+
|
93
|
+
# if we missing either project number or scope, we won't retrieve project_id
|
94
|
+
return nil if project_number.nil? || @scope.nil?
|
95
|
+
|
96
|
+
url = "#{CLOUD_RESOURCE_MANAGER}#{project_number}"
|
97
|
+
|
98
|
+
response = connection.get url do |req|
|
99
|
+
req.headers["Authorization"] = "Bearer #{@access_token}"
|
100
|
+
req.headers["Content-Type"] = "application/json"
|
101
|
+
end
|
102
|
+
|
103
|
+
if response.status == 200
|
104
|
+
response_data = MultiJson.load response.body
|
105
|
+
@project_id = response_data[:projectId]
|
106
|
+
end
|
107
|
+
|
108
|
+
@project_id
|
109
|
+
end
|
110
|
+
|
111
|
+
##
|
112
|
+
# Retrieve the project number corresponding to workload identity pool
|
113
|
+
# STS audience pattern:
|
114
|
+
# `//iam.googleapis.com/projects/$PROJECT_NUMBER/locations/...`
|
115
|
+
#
|
116
|
+
# @return [string, nil]
|
117
|
+
#
|
118
|
+
def project_number
|
119
|
+
segments = @audience.split "/"
|
120
|
+
idx = segments.index "projects"
|
121
|
+
return nil if idx.nil? || idx + 1 == segments.size
|
122
|
+
segments[idx + 1]
|
123
|
+
end
|
124
|
+
|
125
|
+
private
|
126
|
+
|
127
|
+
def token_type
|
128
|
+
# This method is needed for BaseClient
|
129
|
+
:access_token
|
130
|
+
end
|
131
|
+
|
132
|
+
def base_setup options
|
133
|
+
self.default_connection = options[:connection]
|
134
|
+
|
135
|
+
@audience = options[:audience]
|
136
|
+
@scope = options[:scope] || IAM_SCOPE
|
137
|
+
@subject_token_type = options[:subject_token_type]
|
138
|
+
@token_url = options[:token_url]
|
139
|
+
@service_account_impersonation_url = options[:service_account_impersonation_url]
|
140
|
+
@service_account_impersonation_options = options[:service_account_impersonation_options] || {}
|
141
|
+
@client_id = options[:client_id]
|
142
|
+
@client_secret = options[:client_secret]
|
143
|
+
@quota_project_id = options[:quota_project_id]
|
144
|
+
@project_id = nil
|
145
|
+
@workforce_pool_user_project = [:workforce_pool_user_project]
|
146
|
+
|
147
|
+
@expires_at = nil
|
148
|
+
@access_token = nil
|
149
|
+
|
150
|
+
@sts_client = Google::Auth::OAuth2::STSClient.new(
|
151
|
+
token_exchange_endpoint: @token_url,
|
152
|
+
connection: default_connection
|
153
|
+
)
|
154
|
+
end
|
155
|
+
|
156
|
+
def normalize_timestamp time
|
157
|
+
case time
|
158
|
+
when NilClass
|
159
|
+
nil
|
160
|
+
when Time
|
161
|
+
time
|
162
|
+
when String
|
163
|
+
Time.parse time
|
164
|
+
else
|
165
|
+
raise "Invalid time value #{time}"
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def exchange_token
|
170
|
+
@sts_client.exchange_token(
|
171
|
+
audience: @audience,
|
172
|
+
grant_type: STS_GRANT_TYPE,
|
173
|
+
subject_token: retrieve_subject_token!,
|
174
|
+
subject_token_type: @subject_token_type,
|
175
|
+
scopes: @service_account_impersonation_url ? IAM_SCOPE : @scope,
|
176
|
+
requested_token_type: STS_REQUESTED_TOKEN_TYPE
|
177
|
+
)
|
178
|
+
end
|
179
|
+
|
180
|
+
def get_impersonated_access_token token, _options = {}
|
181
|
+
response = connection.post @service_account_impersonation_url do |req|
|
182
|
+
req.headers["Authorization"] = "Bearer #{token}"
|
183
|
+
req.headers["Content-Type"] = "application/json"
|
184
|
+
req.body = MultiJson.dump({ scope: @scope })
|
185
|
+
end
|
186
|
+
|
187
|
+
if response.status != 200
|
188
|
+
raise "Service account impersonation failed with status #{response.status}"
|
189
|
+
end
|
190
|
+
|
191
|
+
MultiJson.load response.body
|
192
|
+
end
|
193
|
+
|
194
|
+
def retrieve_subject_token!
|
195
|
+
raise NotImplementedError
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
# Copyright 2022 Google, Inc.
|
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 "time"
|
16
|
+
require "uri"
|
17
|
+
require "googleauth/credentials_loader"
|
18
|
+
require "googleauth/external_account/aws_credentials"
|
19
|
+
|
20
|
+
module Google
|
21
|
+
# Module Auth provides classes that provide Google-specific authorization
|
22
|
+
# used to access Google APIs.
|
23
|
+
module Auth
|
24
|
+
# Authenticates requests using External Account credentials, such
|
25
|
+
# as those provided by the AWS provider.
|
26
|
+
module ExternalAccount
|
27
|
+
# Provides an entrypoint for all Exernal Account credential classes.
|
28
|
+
class Credentials
|
29
|
+
# The subject token type used for AWS external_account credentials.
|
30
|
+
AWS_SUBJECT_TOKEN_TYPE = "urn:ietf:params:aws:token-type:aws4_request".freeze
|
31
|
+
AWS_SUBJECT_TOKEN_INVALID = "aws is the only currently supported external account type".freeze
|
32
|
+
|
33
|
+
TOKEN_URL_PATTERNS = [
|
34
|
+
/^[^.\s\/\\]+\.sts(?:\.mtls)?\.googleapis\.com$/,
|
35
|
+
/^sts(?:\.mtls)?\.googleapis\.com$/,
|
36
|
+
/^sts\.[^.\s\/\\]+(?:\.mtls)?\.googleapis\.com$/,
|
37
|
+
/^[^.\s\/\\]+-sts(?:\.mtls)?\.googleapis\.com$/,
|
38
|
+
/^sts-[^.\s\/\\]+\.p(?:\.mtls)?\.googleapis\.com$/
|
39
|
+
].freeze
|
40
|
+
|
41
|
+
SERVICE_ACCOUNT_IMPERSONATION_URL_PATTERNS = [
|
42
|
+
/^[^.\s\/\\]+\.iamcredentials\.googleapis\.com$/.freeze,
|
43
|
+
/^iamcredentials\.googleapis\.com$/.freeze,
|
44
|
+
/^iamcredentials\.[^.\s\/\\]+\.googleapis\.com$/.freeze,
|
45
|
+
/^[^.\s\/\\]+-iamcredentials\.googleapis\.com$/.freeze,
|
46
|
+
/^iamcredentials-[^.\s\/\\]+\.p\.googleapis\.com$/.freeze
|
47
|
+
].freeze
|
48
|
+
|
49
|
+
# Create a ExternalAccount::Credentials
|
50
|
+
#
|
51
|
+
# @param json_key_io [IO] an IO from which the JSON key can be read
|
52
|
+
# @param scope [String,Array,nil] the scope(s) to access
|
53
|
+
def self.make_creds options = {}
|
54
|
+
json_key_io, scope = options.values_at :json_key_io, :scope
|
55
|
+
|
56
|
+
raise "A json file is required for external account credentials." unless json_key_io
|
57
|
+
user_creds = read_json_key json_key_io
|
58
|
+
|
59
|
+
raise "The provided token URL is invalid." unless is_token_url_valid? user_creds["token_url"]
|
60
|
+
unless is_service_account_impersonation_url_valid? user_creds["service_account_impersonation_url"]
|
61
|
+
raise "The provided service account impersonation url is invalid."
|
62
|
+
end
|
63
|
+
|
64
|
+
# TODO: check for other External Account Credential types. Currently only AWS is supported.
|
65
|
+
raise AWS_SUBJECT_TOKEN_INVALID unless user_creds["subject_token_type"] == AWS_SUBJECT_TOKEN_TYPE
|
66
|
+
|
67
|
+
Google::Auth::ExternalAccount::AwsCredentials.new(
|
68
|
+
audience: user_creds["audience"],
|
69
|
+
scope: scope,
|
70
|
+
subject_token_type: user_creds["subject_token_type"],
|
71
|
+
token_url: user_creds["token_url"],
|
72
|
+
credential_source: user_creds["credential_source"],
|
73
|
+
service_account_impersonation_url: user_creds["service_account_impersonation_url"]
|
74
|
+
)
|
75
|
+
end
|
76
|
+
|
77
|
+
# Reads the required fields from the JSON.
|
78
|
+
def self.read_json_key json_key_io
|
79
|
+
json_key = MultiJson.load json_key_io.read
|
80
|
+
wanted = [
|
81
|
+
"audience", "subject_token_type", "token_url", "credential_source"
|
82
|
+
]
|
83
|
+
wanted.each do |key|
|
84
|
+
raise "the json is missing the #{key} field" unless json_key.key? key
|
85
|
+
end
|
86
|
+
json_key
|
87
|
+
end
|
88
|
+
|
89
|
+
def self.is_valid_url? url, valid_hostnames
|
90
|
+
begin
|
91
|
+
uri = URI(url)
|
92
|
+
rescue URI::InvalidURIError, ArgumentError
|
93
|
+
return false
|
94
|
+
end
|
95
|
+
|
96
|
+
return false unless uri.scheme == "https"
|
97
|
+
|
98
|
+
valid_hostnames.any? { |hostname| hostname =~ uri.host }
|
99
|
+
end
|
100
|
+
|
101
|
+
def self.is_token_url_valid? url
|
102
|
+
is_valid_url? url, TOKEN_URL_PATTERNS
|
103
|
+
end
|
104
|
+
|
105
|
+
def self.is_service_account_impersonation_url_valid? url
|
106
|
+
!url or is_valid_url? url, SERVICE_ACCOUNT_IMPERSONATION_URL_PATTERNS
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# Copyright 2023 Google, Inc.
|
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 "faraday"
|
16
|
+
|
17
|
+
module Google
|
18
|
+
# Module Auth provides classes that provide Google-specific authorization
|
19
|
+
# used to access Google APIs.
|
20
|
+
module Auth
|
21
|
+
# Helpers provides utility methods for Google::Auth.
|
22
|
+
module Helpers
|
23
|
+
# Connection provides a Faraday connection for use with Google::Auth.
|
24
|
+
module Connection
|
25
|
+
module_function
|
26
|
+
|
27
|
+
attr_accessor :default_connection
|
28
|
+
|
29
|
+
def connection
|
30
|
+
@default_connection || Faraday.default_connection
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,99 @@
|
|
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 = {
|
80
|
+
grant_type: options[:grant_type],
|
81
|
+
audience: options[:audience],
|
82
|
+
scope: Array(options[:scopes])&.join(" ") || [],
|
83
|
+
requested_token_type: options[:requested_token_type],
|
84
|
+
subject_token: options[:subject_token],
|
85
|
+
subject_token_type: options[:subject_token_type]
|
86
|
+
}
|
87
|
+
|
88
|
+
response = connection.post @token_exchange_endpoint, URI.encode_www_form(request_body), headers
|
89
|
+
|
90
|
+
if response.status != 200
|
91
|
+
raise "Token exchange failed with status #{response.status}"
|
92
|
+
end
|
93
|
+
|
94
|
+
MultiJson.load response.body
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -130,7 +130,7 @@ module Google
|
|
130
130
|
# cf [Application Default Credentials](https://cloud.google.com/docs/authentication/production)
|
131
131
|
class ServiceAccountJwtHeaderCredentials
|
132
132
|
JWT_AUD_URI_KEY = :jwt_aud_uri
|
133
|
-
AUTH_METADATA_KEY =
|
133
|
+
AUTH_METADATA_KEY = Google::Auth::BaseClient::AUTH_METADATA_KEY
|
134
134
|
TOKEN_CRED_URI = "https://www.googleapis.com/oauth2/v4/token".freeze
|
135
135
|
SIGNING_ALGORITHM = "RS256".freeze
|
136
136
|
EXPIRY = 60
|
@@ -192,8 +192,6 @@ module Google
|
|
192
192
|
proc { |a_hash, opts = {}| apply a_hash, opts }
|
193
193
|
end
|
194
194
|
|
195
|
-
protected
|
196
|
-
|
197
195
|
# Creates a jwt uri token.
|
198
196
|
def new_jwt_token jwt_aud_uri = nil, options = {}
|
199
197
|
now = Time.new
|
data/lib/googleauth/signet.rb
CHANGED
@@ -13,16 +13,18 @@
|
|
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
|
+
|
26
28
|
def configure_connection options
|
27
29
|
@connection_info =
|
28
30
|
options[:connection_builder] || options[:default_connection]
|
@@ -34,37 +36,6 @@ module Signet
|
|
34
36
|
target_audience ? :id_token : :access_token
|
35
37
|
end
|
36
38
|
|
37
|
-
# Whether the id_token or access_token is missing or about to expire.
|
38
|
-
def needs_access_token?
|
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
|
67
|
-
|
68
39
|
alias orig_fetch_access_token! fetch_access_token!
|
69
40
|
def fetch_access_token! options = {}
|
70
41
|
unless options[:connection]
|
@@ -78,13 +49,6 @@ module Signet
|
|
78
49
|
info
|
79
50
|
end
|
80
51
|
|
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
52
|
def build_default_connection
|
89
53
|
if !defined?(@connection_info)
|
90
54
|
nil
|
data/lib/googleauth/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: googleauth
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tim Emiola
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2023-03-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: faraday
|
@@ -134,17 +134,23 @@ files:
|
|
134
134
|
- SECURITY.md
|
135
135
|
- lib/googleauth.rb
|
136
136
|
- lib/googleauth/application_default.rb
|
137
|
+
- lib/googleauth/base_client.rb
|
137
138
|
- lib/googleauth/client_id.rb
|
138
139
|
- lib/googleauth/compute_engine.rb
|
139
140
|
- lib/googleauth/credentials.rb
|
140
141
|
- lib/googleauth/credentials_loader.rb
|
141
142
|
- lib/googleauth/default_credentials.rb
|
143
|
+
- lib/googleauth/external_account.rb
|
144
|
+
- lib/googleauth/external_account/aws_credentials.rb
|
145
|
+
- lib/googleauth/external_account/base_credentials.rb
|
146
|
+
- lib/googleauth/helpers/connection.rb
|
142
147
|
- lib/googleauth/iam.rb
|
143
148
|
- lib/googleauth/id_tokens.rb
|
144
149
|
- lib/googleauth/id_tokens/errors.rb
|
145
150
|
- lib/googleauth/id_tokens/key_sources.rb
|
146
151
|
- lib/googleauth/id_tokens/verifier.rb
|
147
152
|
- lib/googleauth/json_key_reader.rb
|
153
|
+
- lib/googleauth/oauth2/sts_client.rb
|
148
154
|
- lib/googleauth/scope_util.rb
|
149
155
|
- lib/googleauth/service_account.rb
|
150
156
|
- lib/googleauth/signet.rb
|
@@ -162,7 +168,7 @@ metadata:
|
|
162
168
|
changelog_uri: https://github.com/googleapis/google-auth-library-ruby/blob/main/CHANGELOG.md
|
163
169
|
source_code_uri: https://github.com/googleapis/google-auth-library-ruby
|
164
170
|
bug_tracker_uri: https://github.com/googleapis/google-auth-library-ruby/issues
|
165
|
-
post_install_message:
|
171
|
+
post_install_message:
|
166
172
|
rdoc_options: []
|
167
173
|
require_paths:
|
168
174
|
- lib
|
@@ -177,8 +183,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
177
183
|
- !ruby/object:Gem::Version
|
178
184
|
version: '0'
|
179
185
|
requirements: []
|
180
|
-
rubygems_version: 3.3.
|
181
|
-
signing_key:
|
186
|
+
rubygems_version: 3.3.26
|
187
|
+
signing_key:
|
182
188
|
specification_version: 4
|
183
189
|
summary: Google Auth Library for Ruby
|
184
190
|
test_files: []
|