googleauth 0.17.1 → 1.7.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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +112 -62
  3. data/README.md +49 -14
  4. data/lib/googleauth/application_default.rb +11 -26
  5. data/lib/googleauth/base_client.rb +80 -0
  6. data/lib/googleauth/client_id.rb +10 -25
  7. data/lib/googleauth/compute_engine.rb +10 -25
  8. data/lib/googleauth/credentials.rb +12 -27
  9. data/lib/googleauth/credentials_loader.rb +27 -43
  10. data/lib/googleauth/default_credentials.rb +15 -25
  11. data/lib/googleauth/external_account/aws_credentials.rb +378 -0
  12. data/lib/googleauth/external_account/base_credentials.rb +158 -0
  13. data/lib/googleauth/external_account/external_account_utils.rb +103 -0
  14. data/lib/googleauth/external_account/identity_pool_credentials.rb +118 -0
  15. data/lib/googleauth/external_account/pluggable_credentials.rb +156 -0
  16. data/lib/googleauth/external_account.rb +93 -0
  17. data/lib/googleauth/helpers/connection.rb +35 -0
  18. data/lib/googleauth/iam.rb +10 -25
  19. data/lib/googleauth/id_tokens/errors.rb +9 -23
  20. data/lib/googleauth/id_tokens/key_sources.rb +19 -33
  21. data/lib/googleauth/id_tokens/verifier.rb +9 -23
  22. data/lib/googleauth/id_tokens.rb +11 -25
  23. data/lib/googleauth/json_key_reader.rb +10 -25
  24. data/lib/googleauth/oauth2/sts_client.rb +109 -0
  25. data/lib/googleauth/scope_util.rb +10 -25
  26. data/lib/googleauth/service_account.rb +11 -28
  27. data/lib/googleauth/signet.rb +16 -58
  28. data/lib/googleauth/stores/file_token_store.rb +10 -25
  29. data/lib/googleauth/stores/redis_token_store.rb +10 -25
  30. data/lib/googleauth/token_store.rb +10 -25
  31. data/lib/googleauth/user_authorizer.rb +10 -25
  32. data/lib/googleauth/user_refresh.rb +15 -27
  33. data/lib/googleauth/version.rb +11 -26
  34. data/lib/googleauth/web_user_authorizer.rb +10 -25
  35. data/lib/googleauth.rb +10 -25
  36. metadata +26 -11
@@ -1,31 +1,16 @@
1
- # Copyright 2017, Google Inc.
2
- # All rights reserved.
1
+ # Copyright 2017 Google, Inc.
3
2
  #
4
- # Redistribution and use in source and binary forms, with or without
5
- # modification, are permitted provided that the following conditions are
6
- # met:
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
7
6
  #
8
- # * Redistributions of source code must retain the above copyright
9
- # notice, this list of conditions and the following disclaimer.
10
- # * Redistributions in binary form must reproduce the above
11
- # copyright notice, this list of conditions and the following disclaimer
12
- # in the documentation and/or other materials provided with the
13
- # distribution.
14
- # * Neither the name of Google Inc. nor the names of its
15
- # contributors may be used to endorse or promote products derived from
16
- # this software without specific prior written permission.
7
+ # http://www.apache.org/licenses/LICENSE-2.0
17
8
  #
18
- # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19
- # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20
- # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21
- # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22
- # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23
- # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24
- # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25
- # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26
- # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27
- # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
- # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
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.
29
14
 
30
15
  require "forwardable"
31
16
  require "json"
@@ -370,7 +355,7 @@ module Google
370
355
  @project_id = options["project_id"] || options["project"]
371
356
  @quota_project_id = options["quota_project_id"]
372
357
  case keyfile
373
- when Signet::OAuth2::Client
358
+ when Google::Auth::BaseClient
374
359
  update_from_signet keyfile
375
360
  when Hash
376
361
  update_from_hash keyfile, options
@@ -379,7 +364,7 @@ module Google
379
364
  end
380
365
  CredentialsLoader.warn_if_cloud_sdk_credentials @client.client_id
381
366
  @project_id ||= CredentialsLoader.load_gcloud_project_id
382
- @client.fetch_access_token!
367
+ @client.fetch_access_token! if @client.needs_access_token?
383
368
  @env_vars = nil
384
369
  @paths = nil
385
370
  @scope = nil
@@ -1,33 +1,17 @@
1
- # Copyright 2015, Google Inc.
2
- # All rights reserved.
1
+ # Copyright 2015 Google, Inc.
3
2
  #
4
- # Redistribution and use in source and binary forms, with or without
5
- # modification, are permitted provided that the following conditions are
6
- # met:
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
7
6
  #
8
- # * Redistributions of source code must retain the above copyright
9
- # notice, this list of conditions and the following disclaimer.
10
- # * Redistributions in binary form must reproduce the above
11
- # copyright notice, this list of conditions and the following disclaimer
12
- # in the documentation and/or other materials provided with the
13
- # distribution.
14
- # * Neither the name of Google Inc. nor the names of its
15
- # contributors may be used to endorse or promote products derived from
16
- # this software without specific prior written permission.
7
+ # http://www.apache.org/licenses/LICENSE-2.0
17
8
  #
18
- # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19
- # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20
- # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21
- # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22
- # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23
- # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24
- # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25
- # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26
- # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27
- # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
- # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
-
30
- require "memoist"
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
+
31
15
  require "os"
32
16
  require "rbconfig"
33
17
 
@@ -38,7 +22,6 @@ module Google
38
22
  # CredentialsLoader contains the behaviour used to locate and find default
39
23
  # credentials files on the file system.
40
24
  module CredentialsLoader
41
- extend Memoist
42
25
  ENV_VAR = "GOOGLE_APPLICATION_CREDENTIALS".freeze
43
26
  PRIVATE_KEY_VAR = "GOOGLE_PRIVATE_KEY".freeze
44
27
  CLIENT_EMAIL_VAR = "GOOGLE_CLIENT_EMAIL".freeze
@@ -47,29 +30,30 @@ module Google
47
30
  REFRESH_TOKEN_VAR = "GOOGLE_REFRESH_TOKEN".freeze
48
31
  ACCOUNT_TYPE_VAR = "GOOGLE_ACCOUNT_TYPE".freeze
49
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
50
38
  GCLOUD_POSIX_COMMAND = "gcloud".freeze
51
39
  GCLOUD_WINDOWS_COMMAND = "gcloud.cmd".freeze
52
- GCLOUD_CONFIG_COMMAND =
53
- "config config-helper --format json --verbosity none".freeze
40
+ GCLOUD_CONFIG_COMMAND = "config config-helper --format json --verbosity none".freeze
54
41
 
55
42
  CREDENTIALS_FILE_NAME = "application_default_credentials.json".freeze
56
- NOT_FOUND_ERROR =
57
- "Unable to read the credential file specified by #{ENV_VAR}".freeze
43
+ NOT_FOUND_ERROR = "Unable to read the credential file specified by #{ENV_VAR}".freeze
58
44
  WELL_KNOWN_PATH = "gcloud/#{CREDENTIALS_FILE_NAME}".freeze
59
45
  WELL_KNOWN_ERROR = "Unable to read the default credential file".freeze
60
46
 
61
- SYSTEM_DEFAULT_ERROR =
62
- "Unable to read the system default credential file".freeze
47
+ SYSTEM_DEFAULT_ERROR = "Unable to read the system default credential file".freeze
63
48
 
64
- CLOUD_SDK_CLIENT_ID = "764086051850-6qr4p6gpi6hn506pt8ejuq83di341hur.app"\
65
- "s.googleusercontent.com".freeze
49
+ CLOUD_SDK_CLIENT_ID = "764086051850-6qr4p6gpi6hn506pt8ejuq83di341hur.app" \
50
+ "s.googleusercontent.com".freeze
66
51
 
67
- CLOUD_SDK_CREDENTIALS_WARNING = "Your application has authenticated using end user "\
68
- "credentials from Google Cloud SDK. We recommend that most server applications use "\
69
- "service accounts instead. If your application continues to use end user credentials "\
70
- 'from Cloud SDK, you might receive a "quota exceeded" or "API not enabled" error. For '\
71
- "more information about service accounts, see "\
72
- "https://cloud.google.com/docs/authentication/. To suppress this message, set the "\
52
+ CLOUD_SDK_CREDENTIALS_WARNING =
53
+ "Your application has authenticated using end user credentials from Google Cloud SDK. We recommend that most " \
54
+ "server applications use service accounts instead. If your application continues to use end user credentials " \
55
+ 'from Cloud SDK, you might receive a "quota exceeded" or "API not enabled" error. For more information about ' \
56
+ "service accounts, see https://cloud.google.com/docs/authentication/. To suppress this message, set the " \
73
57
  "GOOGLE_AUTH_SUPPRESS_CREDENTIALS_WARNINGS environment variable.".freeze
74
58
 
75
59
  # make_creds proxies the construction of a credentials instance
@@ -175,7 +159,7 @@ module Google
175
159
  def load_gcloud_project_id
176
160
  gcloud = GCLOUD_WINDOWS_COMMAND if OS.windows?
177
161
  gcloud = GCLOUD_POSIX_COMMAND unless OS.windows?
178
- gcloud_json = IO.popen("#{gcloud} #{GCLOUD_CONFIG_COMMAND}", &:read)
162
+ gcloud_json = IO.popen("#{gcloud} #{GCLOUD_CONFIG_COMMAND}", in: :close, err: :close, &:read)
179
163
  config = MultiJson.load gcloud_json
180
164
  config["configuration"]["properties"]["core"]["project"]
181
165
  rescue StandardError
@@ -1,31 +1,16 @@
1
- # Copyright 2015, Google Inc.
2
- # All rights reserved.
1
+ # Copyright 2015 Google, Inc.
3
2
  #
4
- # Redistribution and use in source and binary forms, with or without
5
- # modification, are permitted provided that the following conditions are
6
- # met:
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
7
6
  #
8
- # * Redistributions of source code must retain the above copyright
9
- # notice, this list of conditions and the following disclaimer.
10
- # * Redistributions in binary form must reproduce the above
11
- # copyright notice, this list of conditions and the following disclaimer
12
- # in the documentation and/or other materials provided with the
13
- # distribution.
14
- # * Neither the name of Google Inc. nor the names of its
15
- # contributors may be used to endorse or promote products derived from
16
- # this software without specific prior written permission.
7
+ # http://www.apache.org/licenses/LICENSE-2.0
17
8
  #
18
- # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19
- # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20
- # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21
- # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22
- # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23
- # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24
- # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25
- # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26
- # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27
- # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
- # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
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.
29
14
 
30
15
  require "multi_json"
31
16
  require "stringio"
@@ -33,6 +18,7 @@ require "stringio"
33
18
  require "googleauth/credentials_loader"
34
19
  require "googleauth/service_account"
35
20
  require "googleauth/user_refresh"
21
+ require "googleauth/external_account"
36
22
 
37
23
  module Google
38
24
  # Module Auth provides classes that provide Google-specific authorization
@@ -68,6 +54,8 @@ module Google
68
54
  ServiceAccountCredentials
69
55
  when "authorized_user"
70
56
  UserRefreshCredentials
57
+ when "external_account"
58
+ ExternalAccount::Credentials
71
59
  else
72
60
  raise "credentials type '#{type}' is not supported"
73
61
  end
@@ -84,6 +72,8 @@ module Google
84
72
  [json_key, ServiceAccountCredentials]
85
73
  when "authorized_user"
86
74
  [json_key, UserRefreshCredentials]
75
+ when "external_account"
76
+ [json_key, ExternalAccount::Credentials]
87
77
  else
88
78
  raise "credentials type '#{type}' is not supported"
89
79
  end
@@ -0,0 +1,378 @@
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
+ require "googleauth/external_account/external_account_utils"
18
+
19
+ module Google
20
+ # Module Auth provides classes that provide Google-specific authorization used to access Google APIs.
21
+ module Auth
22
+ # Authenticates requests using External Account credentials, such as those provided by the AWS provider.
23
+ module ExternalAccount
24
+ # This module handles the retrieval of credentials from Google Cloud by utilizing the AWS EC2 metadata service and
25
+ # then exchanging the credentials for a short-lived Google Cloud access token.
26
+ class AwsCredentials
27
+ # Constant for imdsv2 session token expiration in seconds
28
+ IMDSV2_TOKEN_EXPIRATION_IN_SECONDS = 300
29
+
30
+ include Google::Auth::ExternalAccount::BaseCredentials
31
+ include Google::Auth::ExternalAccount::ExternalAccountUtils
32
+ extend CredentialsLoader
33
+
34
+ # Will always be nil, but method still gets used.
35
+ attr_reader :client_id
36
+
37
+ def initialize options = {}
38
+ base_setup options
39
+
40
+ @audience = options[:audience]
41
+ @credential_source = options[:credential_source] || {}
42
+ @environment_id = @credential_source[:environment_id]
43
+ @region_url = @credential_source[:region_url]
44
+ @credential_verification_url = @credential_source[:url]
45
+ @regional_cred_verification_url = @credential_source[:regional_cred_verification_url]
46
+ @imdsv2_session_token_url = @credential_source[:imdsv2_session_token_url]
47
+
48
+ # These will be lazily loaded when needed, or will raise an error if not provided
49
+ @region = nil
50
+ @request_signer = nil
51
+ @imdsv2_session_token = nil
52
+ @imdsv2_session_token_expiry = nil
53
+ end
54
+
55
+ # Retrieves the subject token using the credential_source object.
56
+ # The subject token is a serialized [AWS GetCallerIdentity signed request](
57
+ # https://cloud.google.com/iam/docs/access-resources-aws#exchange-token).
58
+ #
59
+ # The logic is summarized as:
60
+ #
61
+ # Retrieve the AWS region from the AWS_REGION or AWS_DEFAULT_REGION environment variable or from the AWS
62
+ # metadata server availability-zone if not found in the environment variable.
63
+ #
64
+ # Check AWS credentials in environment variables. If not found, retrieve from the AWS metadata server
65
+ # security-credentials endpoint.
66
+ #
67
+ # When retrieving AWS credentials from the metadata server security-credentials endpoint, the AWS role needs to
68
+ # be determined by # calling the security-credentials endpoint without any argument.
69
+ # Then the credentials can be retrieved via: security-credentials/role_name
70
+ #
71
+ # Generate the signed request to AWS STS GetCallerIdentity action.
72
+ #
73
+ # Inject x-goog-cloud-target-resource into header and serialize the signed request.
74
+ # This will be the subject-token to pass to GCP STS.
75
+ #
76
+ # @return [string] The retrieved subject token.
77
+ #
78
+ def retrieve_subject_token!
79
+ if @request_signer.nil?
80
+ @region = region
81
+ @request_signer = AwsRequestSigner.new @region
82
+ end
83
+
84
+ request = {
85
+ method: "POST",
86
+ url: @regional_cred_verification_url.sub("{region}", @region)
87
+ }
88
+
89
+ request_options = @request_signer.generate_signed_request fetch_security_credentials, request
90
+
91
+ request_headers = request_options[:headers]
92
+ request_headers["x-goog-cloud-target-resource"] = @audience
93
+
94
+ aws_signed_request = {
95
+ headers: [],
96
+ method: request_options[:method],
97
+ url: request_options[:url]
98
+ }
99
+
100
+ aws_signed_request[:headers] = request_headers.keys.sort.map do |key|
101
+ { key: key, value: request_headers[key] }
102
+ end
103
+
104
+ uri_escape aws_signed_request.to_json
105
+ end
106
+
107
+ private
108
+
109
+ def imdsv2_session_token
110
+ return @imdsv2_session_token unless imdsv2_session_token_invalid?
111
+ raise "IMDSV2 token url must be provided" if @imdsv2_session_token_url.nil?
112
+ begin
113
+ response = connection.put @imdsv2_session_token_url do |req|
114
+ req.headers["x-aws-ec2-metadata-token-ttl-seconds"] = IMDSV2_TOKEN_EXPIRATION_IN_SECONDS.to_s
115
+ end
116
+ rescue Faraday::Error => e
117
+ raise "Fetching AWS IMDSV2 token error: #{e}"
118
+ end
119
+ raise Faraday::Error unless response.success?
120
+ @imdsv2_session_token = response.body
121
+ @imdsv2_session_token_expiry = Time.now + IMDSV2_TOKEN_EXPIRATION_IN_SECONDS
122
+ @imdsv2_session_token
123
+ end
124
+
125
+ def imdsv2_session_token_invalid?
126
+ return true if @imdsv2_session_token.nil?
127
+ @imdsv2_session_token_expiry.nil? || @imdsv2_session_token_expiry < Time.now
128
+ end
129
+
130
+ def get_aws_resource url, name, data: nil, headers: {}
131
+ begin
132
+ headers["x-aws-ec2-metadata-token"] = imdsv2_session_token
133
+ response = if data
134
+ headers["Content-Type"] = "application/json"
135
+ connection.post url, data, headers
136
+ else
137
+ connection.get url, nil, headers
138
+ end
139
+
140
+ raise Faraday::Error unless response.success?
141
+ response
142
+ rescue Faraday::Error
143
+ raise "Failed to retrieve AWS #{name}."
144
+ end
145
+ end
146
+
147
+ def uri_escape string
148
+ if string.nil?
149
+ nil
150
+ else
151
+ CGI.escape(string.encode("UTF-8")).gsub("+", "%20").gsub("%7E", "~")
152
+ end
153
+ end
154
+
155
+ # Retrieves the AWS security credentials required for signing AWS requests from either the AWS security
156
+ # credentials environment variables or from the AWS metadata server.
157
+ def fetch_security_credentials
158
+ env_aws_access_key_id = ENV[CredentialsLoader::AWS_ACCESS_KEY_ID_VAR]
159
+ env_aws_secret_access_key = ENV[CredentialsLoader::AWS_SECRET_ACCESS_KEY_VAR]
160
+ # This is normally not available for permanent credentials.
161
+ env_aws_session_token = ENV[CredentialsLoader::AWS_SESSION_TOKEN_VAR]
162
+
163
+ if env_aws_access_key_id && env_aws_secret_access_key
164
+ return {
165
+ access_key_id: env_aws_access_key_id,
166
+ secret_access_key: env_aws_secret_access_key,
167
+ session_token: env_aws_session_token
168
+ }
169
+ end
170
+
171
+ role_name = fetch_metadata_role_name
172
+ credentials = fetch_metadata_security_credentials role_name
173
+
174
+ {
175
+ access_key_id: credentials["AccessKeyId"],
176
+ secret_access_key: credentials["SecretAccessKey"],
177
+ session_token: credentials["Token"]
178
+ }
179
+ end
180
+
181
+ # Retrieves the AWS role currently attached to the current AWS workload by querying the AWS metadata server.
182
+ # This is needed for the AWS metadata server security credentials endpoint in order to retrieve the AWS security
183
+ # credentials needed to sign requests to AWS APIs.
184
+ def fetch_metadata_role_name
185
+ unless @credential_verification_url
186
+ raise "Unable to determine the AWS metadata server security credentials endpoint"
187
+ end
188
+
189
+ get_aws_resource(@credential_verification_url, "IAM Role").body
190
+ end
191
+
192
+ # Retrieves the AWS security credentials required for signing AWS requests from the AWS metadata server.
193
+ def fetch_metadata_security_credentials role_name
194
+ response = get_aws_resource "#{@credential_verification_url}/#{role_name}", "credentials"
195
+ MultiJson.load response.body
196
+ end
197
+
198
+ def region
199
+ @region = ENV[CredentialsLoader::AWS_REGION_VAR] || ENV[CredentialsLoader::AWS_DEFAULT_REGION_VAR]
200
+
201
+ unless @region
202
+ raise "region_url or region must be set for external account credentials" unless @region_url
203
+
204
+ @region ||= get_aws_resource(@region_url, "region").body[0..-2]
205
+ end
206
+
207
+ @region
208
+ end
209
+ end
210
+
211
+ # Implements an AWS request signer based on the AWS Signature Version 4 signing process.
212
+ # https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html
213
+ class AwsRequestSigner
214
+ # Instantiates an AWS request signer used to compute authenticated signed requests to AWS APIs based on the AWS
215
+ # Signature Version 4 signing process.
216
+ #
217
+ # @param [string] region_name
218
+ # The AWS region to use.
219
+ def initialize region_name
220
+ @region_name = region_name
221
+ end
222
+
223
+ # Generates the signed request for the provided HTTP request for calling
224
+ # an AWS API. This follows the steps described at:
225
+ # https://docs.aws.amazon.com/general/latest/gr/sigv4_signing.html
226
+ #
227
+ # @param [Hash[string, string]] aws_security_credentials
228
+ # A dictionary containing the AWS security credentials.
229
+ # @param [string] url
230
+ # The AWS service URL containing the canonical URI and query string.
231
+ # @param [string] method
232
+ # The HTTP method used to call this API.
233
+ #
234
+ # @return [hash{string => string}]
235
+ # The AWS signed request dictionary object.
236
+ #
237
+ def generate_signed_request aws_credentials, original_request
238
+ uri = Addressable::URI.parse original_request[:url]
239
+ raise "Invalid AWS service URL" unless uri.hostname && uri.scheme == "https"
240
+ service_name = uri.host.split(".").first
241
+
242
+ datetime = Time.now.utc.strftime "%Y%m%dT%H%M%SZ"
243
+ date = datetime[0, 8]
244
+
245
+ headers = aws_headers aws_credentials, original_request, datetime
246
+
247
+ request_payload = original_request[:data] || ""
248
+ content_sha256 = sha256_hexdigest request_payload
249
+
250
+ canonical_req = canonical_request original_request[:method], uri, headers, content_sha256
251
+ sts = string_to_sign datetime, canonical_req, service_name
252
+
253
+ # Authorization header requires everything else to be properly setup in order to be properly
254
+ # calculated.
255
+ headers["Authorization"] = build_authorization_header headers, sts, aws_credentials, service_name, date
256
+
257
+ {
258
+ url: uri.to_s,
259
+ headers: headers,
260
+ method: original_request[:method],
261
+ data: (request_payload unless request_payload.empty?)
262
+ }.compact
263
+ end
264
+
265
+ private
266
+
267
+ def aws_headers aws_credentials, original_request, datetime
268
+ uri = Addressable::URI.parse original_request[:url]
269
+ temp_headers = original_request[:headers] || {}
270
+ headers = {}
271
+ temp_headers.each_key { |k| headers[k.to_s] = temp_headers[k] }
272
+ headers["host"] = uri.host
273
+ headers["x-amz-date"] = datetime
274
+ headers["x-amz-security-token"] = aws_credentials[:session_token] if aws_credentials[:session_token]
275
+ headers
276
+ end
277
+
278
+ def build_authorization_header headers, sts, aws_credentials, service_name, date
279
+ [
280
+ "AWS4-HMAC-SHA256",
281
+ "Credential=#{credential aws_credentials[:access_key_id], date, service_name},",
282
+ "SignedHeaders=#{headers.keys.sort.join ';'},",
283
+ "Signature=#{signature aws_credentials[:secret_access_key], date, sts, service_name}"
284
+ ].join(" ")
285
+ end
286
+
287
+ def signature secret_access_key, date, string_to_sign, service
288
+ k_date = hmac "AWS4#{secret_access_key}", date
289
+ k_region = hmac k_date, @region_name
290
+ k_service = hmac k_region, service
291
+ k_credentials = hmac k_service, "aws4_request"
292
+
293
+ hexhmac k_credentials, string_to_sign
294
+ end
295
+
296
+ def hmac key, value
297
+ OpenSSL::HMAC.digest OpenSSL::Digest.new("sha256"), key, value
298
+ end
299
+
300
+ def hexhmac key, value
301
+ OpenSSL::HMAC.hexdigest OpenSSL::Digest.new("sha256"), key, value
302
+ end
303
+
304
+ def credential access_key_id, date, service
305
+ "#{access_key_id}/#{credential_scope date, service}"
306
+ end
307
+
308
+ def credential_scope date, service
309
+ [
310
+ date,
311
+ @region_name,
312
+ service,
313
+ "aws4_request"
314
+ ].join("/")
315
+ end
316
+
317
+ def string_to_sign datetime, canonical_request, service
318
+ [
319
+ "AWS4-HMAC-SHA256",
320
+ datetime,
321
+ credential_scope(datetime[0, 8], service),
322
+ sha256_hexdigest(canonical_request)
323
+ ].join("\n")
324
+ end
325
+
326
+ def host uri
327
+ # Handles known and unknown URI schemes; default_port nil when unknown.
328
+ if uri.default_port == uri.port
329
+ uri.host
330
+ else
331
+ "#{uri.host}:#{uri.port}"
332
+ end
333
+ end
334
+
335
+ def canonical_request http_method, uri, headers, content_sha256
336
+ headers = headers.sort_by(&:first) # transforms to a sorted array of [key, value]
337
+
338
+ [
339
+ http_method,
340
+ uri.path.empty? ? "/" : uri.path,
341
+ build_canonical_querystring(uri.query || ""),
342
+ headers.map { |k, v| "#{k}:#{v}\n" }.join, # Canonical headers
343
+ headers.map(&:first).join(";"), # Signed headers
344
+ content_sha256
345
+ ].join("\n")
346
+ end
347
+
348
+ def sha256_hexdigest string
349
+ OpenSSL::Digest::SHA256.hexdigest string
350
+ end
351
+
352
+ # Generates the canonical query string given a raw query string.
353
+ # Logic is based on
354
+ # https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
355
+ # Code is from the AWS SDK for Ruby
356
+ # https://github.com/aws/aws-sdk-ruby/blob/0ac3d0a393ed216290bfb5f0383380376f6fb1f1/gems/aws-sigv4/lib/aws-sigv4/signer.rb#L532
357
+ def build_canonical_querystring query
358
+ params = query.split "&"
359
+ params = params.map { |p| p.include?("=") ? p : "#{p}=" }
360
+
361
+ params.each.with_index.sort do |(a, a_offset), (b, b_offset)|
362
+ a_name, a_value = a.split "="
363
+ b_name, b_value = b.split "="
364
+ if a_name == b_name
365
+ if a_value == b_value
366
+ a_offset <=> b_offset
367
+ else
368
+ a_value <=> b_value
369
+ end
370
+ else
371
+ a_name <=> b_name
372
+ end
373
+ end.map(&:first).join("&")
374
+ end
375
+ end
376
+ end
377
+ end
378
+ end