googleauth 1.3.0 → 1.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|