aws-sdk-core 3.232.0 → 3.239.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +65 -0
  3. data/VERSION +1 -1
  4. data/lib/aws-sdk-core/assume_role_credentials.rb +8 -8
  5. data/lib/aws-sdk-core/assume_role_web_identity_credentials.rb +2 -2
  6. data/lib/aws-sdk-core/credential_provider_chain.rb +71 -22
  7. data/lib/aws-sdk-core/ecs_credentials.rb +13 -13
  8. data/lib/aws-sdk-core/errors.rb +3 -0
  9. data/lib/aws-sdk-core/instance_profile_credentials.rb +7 -7
  10. data/lib/aws-sdk-core/login_credentials.rb +229 -0
  11. data/lib/aws-sdk-core/plugins/checksum_algorithm.rb +28 -14
  12. data/lib/aws-sdk-core/plugins/user_agent.rb +3 -1
  13. data/lib/aws-sdk-core/refreshing_credentials.rb +8 -11
  14. data/lib/aws-sdk-core/shared_config.rb +20 -0
  15. data/lib/aws-sdk-core/sso_credentials.rb +1 -1
  16. data/lib/aws-sdk-core.rb +4 -0
  17. data/lib/aws-sdk-signin/client.rb +604 -0
  18. data/lib/aws-sdk-signin/client_api.rb +119 -0
  19. data/lib/aws-sdk-signin/customizations.rb +1 -0
  20. data/lib/aws-sdk-signin/endpoint_parameters.rb +69 -0
  21. data/lib/aws-sdk-signin/endpoint_provider.rb +59 -0
  22. data/lib/aws-sdk-signin/endpoints.rb +20 -0
  23. data/lib/aws-sdk-signin/errors.rb +122 -0
  24. data/lib/aws-sdk-signin/plugins/endpoints.rb +77 -0
  25. data/lib/aws-sdk-signin/resource.rb +26 -0
  26. data/lib/aws-sdk-signin/types.rb +299 -0
  27. data/lib/aws-sdk-signin.rb +63 -0
  28. data/lib/aws-sdk-sso/client.rb +1 -1
  29. data/lib/aws-sdk-sso/endpoint_parameters.rb +4 -4
  30. data/lib/aws-sdk-sso.rb +1 -1
  31. data/lib/aws-sdk-ssooidc/client.rb +20 -7
  32. data/lib/aws-sdk-ssooidc/client_api.rb +5 -0
  33. data/lib/aws-sdk-ssooidc/endpoint_parameters.rb +4 -4
  34. data/lib/aws-sdk-ssooidc/errors.rb +10 -0
  35. data/lib/aws-sdk-ssooidc/types.rb +27 -15
  36. data/lib/aws-sdk-ssooidc.rb +1 -1
  37. data/lib/aws-sdk-sts/client.rb +136 -12
  38. data/lib/aws-sdk-sts/client_api.rb +72 -0
  39. data/lib/aws-sdk-sts/customizations.rb +0 -1
  40. data/lib/aws-sdk-sts/endpoint_parameters.rb +5 -5
  41. data/lib/aws-sdk-sts/errors.rb +64 -0
  42. data/lib/aws-sdk-sts/types.rb +175 -6
  43. data/lib/aws-sdk-sts.rb +1 -1
  44. data/lib/seahorse/client/h2/handler.rb +6 -1
  45. metadata +13 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ecdbb0ca615cde813f52b03861d36db189562c896e01499ed8dcd80768085300
4
- data.tar.gz: 5ab8e9d52d9f7522afa27ff7c5c6e5e51f27f966ddba8bea62edb153307120c9
3
+ metadata.gz: 9f1e1429692b82c74af4f73ca4302ef10daa7cb7819a46a5571efe119d5219d3
4
+ data.tar.gz: cd44f64eaba5dac2b0cc8057c292098b4512db12b34250397bbaa723a142aa72
5
5
  SHA512:
6
- metadata.gz: b23f5bc51a113cfb8b6c0f7306d42c97872d2eff0c5fa0b395b9e714045c8a3446e91313d6d17317419520229ea809097e3abb794f2f448bd698ec982841032f
7
- data.tar.gz: a02785a5b072cb04d27203fb6e990cfa01b10c59f8abdb221b0181b4d07d56a04d0bcc67f308a10fbeba0f3facdadabb77918815e10dbcd8629df25a6d41cc42
6
+ metadata.gz: b183ab5519ff8d1df36c32dff3abe1aab2759ba4979a8ed9c1fef8d9d5feac08489c632e2313248ad4cbef327b66a9b226e7b7d0a930ff01cf1ba194f47fdaea
7
+ data.tar.gz: 6d64c46bd620408e61d9aac06772001816bf3bf8e674c63e3da8dfb19f167b89169fd333626650930ac7b63b0d797569ccbaffc1a936945c86b219f65cf22923
data/CHANGELOG.md CHANGED
@@ -1,6 +1,71 @@
1
1
  Unreleased Changes
2
2
  ------------------
3
3
 
4
+ 3.239.2 (2025-11-25)
5
+ ------------------
6
+
7
+ * Issue - Fix `login_credentials` in credentials chain when config is enabled.
8
+
9
+ 3.239.1 (2025-11-21)
10
+ ------------------
11
+
12
+ * Issue - Fixed HTTP/2 connection issues when using custom ports.
13
+
14
+ 3.239.0 (2025-11-20)
15
+ ------------------
16
+
17
+ * Feature - Updated Aws::Signin::Client with the latest API changes.
18
+
19
+ * Issue - Fix region configuration for LoginCredential's Signin client.
20
+
21
+ 3.238.0 (2025-11-19)
22
+ ------------------
23
+
24
+ * Feature - Updated Aws::Signin::Client with the latest API changes.
25
+
26
+ * Feature - Updated Aws::STS::Client with the latest API changes.
27
+
28
+ * Feature - IAM now supports outbound identity federation via the STS GetWebIdentityToken API, enabling AWS workloads to securely authenticate with external services using short-lived JSON Web Tokens.
29
+
30
+ * Feature - Add `LoginCredentials` which retrieves credentials from AWS Sign-In. Support `aws-sdk-signin` alias gem.
31
+
32
+ 3.237.0 (2025-11-10)
33
+ ------------------
34
+
35
+ * Feature - Updated Aws::STS::Client with the latest API changes.
36
+
37
+ * Feature - Added GetDelegatedAccessToken API, which is not available for general use at this time.
38
+
39
+ 3.236.0 (2025-10-30)
40
+ ------------------
41
+
42
+ * Feature - Updated Aws::STS::Client with the latest API changes.
43
+
44
+ * Feature - Updated Aws::SSO::Client with the latest API changes.
45
+
46
+ * Feature - Update endpoint ruleset parameters casing
47
+
48
+ 3.235.0 (2025-10-24)
49
+ ------------------
50
+
51
+ * Feature - Updated Aws::SSOOIDC::Client with the latest API changes.
52
+
53
+ * Feature - Update endpoint ruleset parameters casing
54
+
55
+ 3.234.0 (2025-10-21)
56
+ ------------------
57
+
58
+ * Issue - Fix `request_checksum_calculation` `when_required` mode to only calculate checksums when explicitly provided by user.
59
+
60
+ * Feature - Add `CREDENTIALS_CODE` metric for `static_profile_` prefixed methods in default credential chain.
61
+
62
+ 3.233.0 (2025-09-23)
63
+ ------------------
64
+
65
+ * Feature - Updated Aws::SSOOIDC::Client with the latest API changes.
66
+
67
+ * Feature - This release includes exception definition and documentation updates.
68
+
4
69
  3.232.0 (2025-08-28)
5
70
  ------------------
6
71
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 3.232.0
1
+ 3.239.2
@@ -7,7 +7,7 @@ module Aws
7
7
  # {Aws::STS::Client#assume_role}.
8
8
  #
9
9
  # role_credentials = Aws::AssumeRoleCredentials.new(
10
- # client: Aws::STS::Client.new(...),
10
+ # client: Aws::STS::Client.new(sts_options),
11
11
  # role_arn: "linked::account::arn",
12
12
  # role_session_name: "session-name"
13
13
  # )
@@ -28,15 +28,15 @@ module Aws
28
28
  # @option options [Integer] :duration_seconds
29
29
  # @option options [String] :external_id
30
30
  # @option options [STS::Client] :client
31
- # @option options [Callable] before_refresh Proc called before
31
+ # @option options [Proc] :before_refresh A Proc called before
32
32
  # credentials are refreshed. Useful for updating tokens.
33
- # `before_refresh` is called when AWS credentials are
34
- # required and need to be refreshed. Tokens can be refreshed using
35
- # the following example:
33
+ # `:before_refresh` is called when AWS credentials are
34
+ # required and need to be refreshed. See the example in this doc.
36
35
  #
37
- # before_refresh = Proc.new do |assume_role_credentials| do
38
- # assume_role_credentials.assume_role_params['token_code'] = update_token
39
- # end
36
+ # @example Tokens can be refreshed using a Proc.
37
+ # before_refresh = Proc.new do |assume_role_credentials|
38
+ # assume_role_credentials.assume_role_params['token_code'] = update_token
39
+ # end
40
40
  #
41
41
  def initialize(options = {})
42
42
  client_opts = {}
@@ -9,11 +9,11 @@ module Aws
9
9
  # {Aws::STS::Client#assume_role_with_web_identity}.
10
10
  #
11
11
  # role_credentials = Aws::AssumeRoleWebIdentityCredentials.new(
12
- # client: Aws::STS::Client.new(...),
12
+ # client: Aws::STS::Client.new(sts_options),
13
13
  # role_arn: "linked::account::arn",
14
14
  # web_identity_token_file: "/path/to/token/file",
15
15
  # role_session_name: "session-name"
16
- # ...
16
+ # # ...
17
17
  # )
18
18
  # ec2 = Aws::EC2::Client.new(credentials: role_credentials)
19
19
  #
@@ -11,7 +11,7 @@ module Aws
11
11
  def resolve
12
12
  providers.each do |method_name, options|
13
13
  provider = send(method_name, options.merge(config: @config))
14
- return provider if provider && provider.set?
14
+ return provider if provider&.set?
15
15
  end
16
16
  nil
17
17
  end
@@ -25,12 +25,14 @@ module Aws
25
25
  [:static_profile_sso_credentials, {}],
26
26
  [:static_profile_assume_role_credentials, {}],
27
27
  [:static_profile_credentials, {}],
28
+ [:static_profile_login_credentials, {}],
28
29
  [:static_profile_process_credentials, {}],
29
30
  [:env_credentials, {}],
30
31
  [:assume_role_web_identity_credentials, {}],
31
32
  [:sso_credentials, {}],
32
33
  [:assume_role_credentials, {}],
33
34
  [:shared_credentials, {}],
35
+ [:login_credentials, {}],
34
36
  [:process_credentials, {}],
35
37
  [:instance_profile_credentials, {
36
38
  retries: @config ? @config.instance_profile_credentials_retries : 0,
@@ -54,47 +56,80 @@ module Aws
54
56
  end
55
57
 
56
58
  def static_profile_assume_role_web_identity_credentials(options)
57
- if Aws.shared_config.config_enabled? && options[:config] && options[:config].profile
58
- Aws.shared_config.assume_role_web_identity_credentials_from_config(
59
+ return unless Aws.shared_config.config_enabled? && options[:config]&.profile
60
+
61
+ with_metrics('CREDENTIALS_CODE') do
62
+ creds = Aws.shared_config.assume_role_web_identity_credentials_from_config(
59
63
  profile: options[:config].profile,
60
64
  region: options[:config].region
61
65
  )
66
+ return unless creds
67
+
68
+ creds.metrics << 'CREDENTIALS_CODE'
69
+ creds
62
70
  end
63
71
  end
64
72
 
65
73
  def static_profile_sso_credentials(options)
66
- if Aws.shared_config.config_enabled? && options[:config] && options[:config].profile
67
- Aws.shared_config.sso_credentials_from_config(
74
+ return unless Aws.shared_config.config_enabled? && options[:config]&.profile
75
+
76
+ with_metrics('CREDENTIALS_CODE') do
77
+ creds = Aws.shared_config.sso_credentials_from_config(
68
78
  profile: options[:config].profile
69
79
  )
80
+ return unless creds
81
+
82
+ creds.metrics << 'CREDENTIALS_CODE'
83
+ creds
70
84
  end
71
85
  end
72
86
 
73
87
  def static_profile_assume_role_credentials(options)
74
- if Aws.shared_config.config_enabled? && options[:config] && options[:config].profile
75
- assume_role_with_profile(options, options[:config].profile)
88
+ return unless Aws.shared_config.config_enabled? && options[:config]&.profile
89
+
90
+ with_metrics('CREDENTIALS_CODE') do
91
+ creds = assume_role_with_profile(options, options[:config].profile)
92
+ return unless creds
93
+
94
+ creds.metrics << 'CREDENTIALS_CODE'
95
+ creds
76
96
  end
77
97
  end
78
98
 
79
99
  def static_profile_credentials(options)
80
- if options[:config] && options[:config].profile
81
- creds = SharedCredentials.new(profile_name: options[:config].profile)
82
- creds.metrics = ['CREDENTIALS_PROFILE']
83
- creds
84
- end
100
+ return unless options[:config]&.profile
101
+
102
+ creds = SharedCredentials.new(profile_name: options[:config].profile)
103
+ creds.metrics << 'CREDENTIALS_PROFILE'
104
+ creds
85
105
  rescue Errors::NoSuchProfileError
86
106
  nil
87
107
  end
88
108
 
89
- def static_profile_process_credentials(options)
90
- if Aws.shared_config.config_enabled? && options[:config] && options[:config].profile
91
- process_provider = Aws.shared_config.credential_process(profile: options[:config].profile)
92
- if process_provider
93
- creds = ProcessCredentials.new([process_provider])
94
- creds.metrics << 'CREDENTIALS_PROFILE_PROCESS'
95
- creds
96
- end
109
+ def static_profile_login_credentials(options)
110
+ return unless Aws.shared_config.config_enabled? && options[:config]&.profile
111
+
112
+ with_metrics('CREDENTIALS_CODE') do
113
+ creds = Aws.shared_config.login_credentials_from_config(
114
+ profile: options[:config].profile,
115
+ region: options[:config].region
116
+ )
117
+ return unless creds
118
+
119
+ creds.metrics << 'CREDENTIALS_CODE'
120
+ creds
97
121
  end
122
+ end
123
+
124
+ def static_profile_process_credentials(options)
125
+ return unless Aws.shared_config.config_enabled? && options[:config]&.profile
126
+
127
+ process_provider = Aws.shared_config.credential_process(profile: options[:config].profile)
128
+ return unless process_provider
129
+
130
+ creds = ProcessCredentials.new([process_provider])
131
+ creds.metrics.concat(%w[CREDENTIALS_PROFILE_PROCESS CREDENTIALS_CODE])
132
+ creds
98
133
  rescue Errors::NoSuchProfileError
99
134
  nil
100
135
  end
@@ -122,7 +157,7 @@ module Aws
122
157
  end
123
158
 
124
159
  def determine_profile_name(options)
125
- (options[:config] && options[:config].profile) || ENV['AWS_PROFILE'] || ENV['AWS_DEFAULT_PROFILE'] || 'default'
160
+ (options[:config]&.profile) || ENV['AWS_PROFILE'] || ENV['AWS_DEFAULT_PROFILE'] || 'default'
126
161
  end
127
162
 
128
163
  def shared_credentials(options)
@@ -134,6 +169,16 @@ module Aws
134
169
  nil
135
170
  end
136
171
 
172
+ def login_credentials(options)
173
+ return unless Aws.shared_config.config_enabled?
174
+
175
+ profile_name = determine_profile_name(options)
176
+ region = options[:config].region if options[:config]
177
+ Aws.shared_config.login_credentials_from_config(profile: profile_name, region: region)
178
+ rescue Errors::NoSuchProfileError
179
+ nil
180
+ end
181
+
137
182
  def process_credentials(options)
138
183
  profile_name = determine_profile_name(options)
139
184
  if Aws.shared_config.config_enabled?
@@ -201,10 +246,14 @@ module Aws
201
246
  profile: profile_name,
202
247
  chain_config: @config
203
248
  }
204
- if options[:config] && options[:config].region
249
+ if options[:config]&.region
205
250
  assume_opts[:region] = options[:config].region
206
251
  end
207
252
  Aws.shared_config.assume_role_credentials_from_config(assume_opts)
208
253
  end
254
+
255
+ def with_metrics(metrics, &block)
256
+ Aws::Plugins::UserAgent.metric(*metrics, &block)
257
+ end
209
258
  end
210
259
  end
@@ -42,26 +42,26 @@ module Aws
42
42
  # @option options [Integer] :retries (5) Number of times to retry
43
43
  # when retrieving credentials.
44
44
  # @option options [String] :ip_address ('169.254.170.2') This value is
45
- # ignored if `endpoint` is set and `credential_path` is not set.
46
- # @option options [Integer] :port (80) This value is ignored if `endpoint`
47
- # is set and `credential_path` is not set.
45
+ # ignored if `:endpoint` is set and `:credential_path` is not set.
46
+ # @option options [Integer] :port (80) This value is ignored if `:endpoint`
47
+ # is set and `:credential_path` is not set.
48
48
  # @option options [String] :credential_path By default, the value of the
49
- # AWS_CONTAINER_CREDENTIALS_RELATIVE_URI environment variable.
49
+ # `AWS_CONTAINER_CREDENTIALS_RELATIVE_URI` environment variable.
50
50
  # @option options [String] :endpoint The container credential endpoint.
51
- # By default, this is the value of the AWS_CONTAINER_CREDENTIALS_FULL_URI
52
- # environment variable. This value is ignored if `credential_path` or
53
- # ENV['AWS_CONTAINER_CREDENTIALS_RELATIVE_URI'] is set.
51
+ # By default, this is the value of the `AWS_CONTAINER_CREDENTIALS_FULL_URI`
52
+ # environment variable. This value is ignored if `:credential_path` or
53
+ # `ENV['AWS_CONTAINER_CREDENTIALS_RELATIVE_URI']` is set.
54
54
  # @option options [Float] :http_open_timeout (5)
55
55
  # @option options [Float] :http_read_timeout (5)
56
- # @option options [Numeric, Proc] :delay By default, failures are retried
56
+ # @option options [IO] :http_debug_output (nil) HTTP wire
57
+ # traces are sent to this object. You can specify something
58
+ # like `$stdout`.
59
+ # @option options [Numeric, Proc] :backoff By default, failures are retried
57
60
  # with exponential back-off, i.e. `sleep(1.2 ** num_failures)`. You can
58
61
  # pass a number of seconds to sleep between failed attempts, or
59
62
  # a Proc that accepts the number of failures.
60
- # @option options [IO] :http_debug_output (nil) HTTP wire
61
- # traces are sent to this object. You can specify something
62
- # like $stdout.
63
- # @option options [Callable] before_refresh Proc called before
64
- # credentials are refreshed. `before_refresh` is called
63
+ # @option options [Proc] :before_refresh A Proc called before
64
+ # credentials are refreshed. `:before_refresh` is called
65
65
  # with an instance of this object when
66
66
  # AWS credentials are required and need to be refreshed.
67
67
  def initialize(options = {})
@@ -213,6 +213,9 @@ module Aws
213
213
  # Raised when SSO Token is invalid
214
214
  class InvalidSSOToken < RuntimeError; end
215
215
 
216
+ # Raised when Login Token is invalid
217
+ class InvalidLoginToken < RuntimeError; end
218
+
216
219
  # Raised when a client is unable to sign a request because
217
220
  # the bearer token is not configured or available
218
221
  class MissingBearerTokenError < RuntimeError
@@ -57,6 +57,9 @@ module Aws
57
57
 
58
58
  # @param [Hash] options
59
59
  # @option options [Integer] :retries (1) Number of times to retry when retrieving credentials.
60
+ # @option options [Numeric, Proc] :backoff By default, failures are retried with exponential back-off, i.e.
61
+ # `lambda { |num_failures| sleep(1.2 ** num_failures) }`. You can pass a number of seconds to sleep
62
+ # between failed attempts, or a Proc that accepts the number of failures.
60
63
  # @option options [String] :endpoint ('http://169.254.169.254') The IMDS endpoint. This option has precedence
61
64
  # over the `:endpoint_mode`.
62
65
  # @option options [String] :endpoint_mode ('IPv4') The endpoint mode for the instance metadata service. This is
@@ -67,14 +70,11 @@ module Aws
67
70
  # @option options [Integer] :port (80)
68
71
  # @option options [Float] :http_open_timeout (1)
69
72
  # @option options [Float] :http_read_timeout (1)
70
- # @option options [Numeric, Proc] :delay By default, failures are retried with exponential back-off, i.e.
71
- # `sleep(1.2 ** num_failures)`. You can pass a number of seconds to sleep between failed attempts, or a Proc
72
- # that accepts the number of failures.
73
73
  # @option options [IO] :http_debug_output (nil) HTTP wire traces are sent to this object.
74
74
  # You can specify something like `$stdout`.
75
- # @option options [Integer] :token_ttl Time-to-Live in seconds for EC2 Metadata Token used for fetching
76
- # Metadata Profile Credentials, defaults to 21600 seconds.
77
- # @option options [Callable] :before_refresh Proc called before credentials are refreshed. `before_refresh`
75
+ # @option options [Integer] :token_ttl (21600) Time-to-Live in seconds for EC2 Metadata Token used for fetching
76
+ # Metadata Profile Credentials.
77
+ # @option options [Proc] :before_refresh A Proc called before credentials are refreshed. `:before_refresh`
78
78
  # is called with an instance of this object when AWS credentials are required and need to be refreshed.
79
79
  def initialize(options = {})
80
80
  @backoff = resolve_backoff(options[:backoff])
@@ -95,7 +95,7 @@ module Aws
95
95
  super
96
96
  end
97
97
 
98
- # @return [Boolean0
98
+ # @return [Boolean]
99
99
  attr_reader :disable_imds_v1
100
100
 
101
101
  # @return [Integer]
@@ -0,0 +1,229 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aws
4
+ # An auto-refreshing credential provider that retrieves credentials from
5
+ # a cached login token. This class does NOT implement the AWS Sign-In
6
+ # login flow - tokens must be generated separately by running `aws login`
7
+ # from the AWS CLI/AWS Tools for PowerShell with the correct profile.
8
+ # The {LoginCredentials} will auto-refresh the AWS credentials from AWS Sign-In.
9
+ #
10
+ # # You must first run aws login --profile your-login-profile
11
+ # login_credentials = Aws::LoginCredentials.new(login_session: 'my_login_session')
12
+ # ec2 = Aws::EC2::Client.new(credentials: login_credentials)
13
+ #
14
+ # If you omit the `:client` option, a new {Aws::Signin::Client} object will
15
+ # be constructed with additional options that were provided.
16
+ class LoginCredentials
17
+ include CredentialProvider
18
+ include RefreshingCredentials
19
+
20
+ # @option options [required, String] :login_session An opaque string
21
+ # used to determine the cache file location. This value can be found
22
+ # in the AWS config file which is set by the AWS CLI/AWS Tools for
23
+ # PowerShell automatically.
24
+ #
25
+ # @option options [Signin::Client] :client Optional `Signin::Client`.
26
+ # If not provided, a client will be constructed.
27
+ def initialize(options = {})
28
+ raise ArgumentError, 'Missing login_session' unless options[:login_session]
29
+
30
+ @login_session = options.delete(:login_session)
31
+ @client = options[:client]
32
+ unless @client
33
+ client_opts = options.reject { |key, _| CLIENT_EXCLUDE_OPTIONS.include?(key) }
34
+ @client = Signin::Client.new(client_opts.merge(credentials: nil))
35
+ end
36
+ @metrics = ['CREDENTIALS_LOGIN']
37
+ @async_refresh = true
38
+ super
39
+ end
40
+
41
+ # @return [Signin::Client]
42
+ attr_reader :client
43
+
44
+ private
45
+
46
+ def refresh
47
+ # First reload the token from disk to ensure it hasn't been refreshed externally
48
+ token_json = read_cached_token
49
+ update_creds(token_json['accessToken'])
50
+ return if @credentials && @expiration && !near_expiration?(sync_expiration_length)
51
+
52
+ # Using OpenSSL 3.6.0 may result in errors like "certificate verify failed (unable to get certificate CRL)."
53
+ # A recommended workaround is to use OpenSSL version < 3.6.0 or requiring the openssl gem with a version of at
54
+ # least 3.2.2. GitHub issue: https://github.com/openssl/openssl/issues/28752.
55
+ if OpenSSL::OPENSSL_LIBRARY_VERSION.include?('3.6.') &&
56
+ (!Gem.loaded_specs['openssl'] || Gem.loaded_specs['openssl'].version < Gem::Version.new('3.2.2'))
57
+ warn 'WARNING: OpenSSL 3.6.x may cause certificate verify errors - use OpenSSL < 3.6.0 or openssl gem >= 3.2.2'
58
+ end
59
+
60
+ # Attempt to refresh the token
61
+ attempt_refresh(token_json)
62
+
63
+ # Raise if token is hard expired
64
+ return unless !@expiration || @expiration < Time.now
65
+
66
+ raise Errors::InvalidLoginToken,
67
+ 'Login token is invalid and failed to refresh. Please reauthenticate.'
68
+ end
69
+
70
+ def read_cached_token
71
+ cached_token = JSON.load_file(login_cache_file)
72
+ validate_cached_token(cached_token)
73
+ cached_token
74
+ rescue Errno::ENOENT, Aws::Json::ParseError
75
+ raise Errors::InvalidLoginToken,
76
+ "Failed to load a Login token for login session #{@login_session}. Please reauthenticate."
77
+ end
78
+
79
+ def login_cache_file
80
+ directory = ENV['AWS_LOGIN_CACHE_DIRECTORY'] || File.join(Dir.home, '.aws', 'login', 'cache')
81
+ login_session_sha = OpenSSL::Digest::SHA256.hexdigest(@login_session.strip.encode('utf-8'))
82
+ File.join(directory, "#{login_session_sha}.json")
83
+ end
84
+
85
+ def validate_cached_token(cached_token)
86
+ required_cached_token_fields = %w[accessToken clientId refreshToken dpopKey]
87
+ missing_fields = required_cached_token_fields.reject { |field| cached_token[field] }
88
+ unless missing_fields.empty?
89
+ raise ArgumentError, "Cached login token is missing required field(s): #{missing_fields}. " \
90
+ 'Please reauthenticate.'
91
+ end
92
+
93
+ access_token = cached_token['accessToken']
94
+ required_access_token_fields = %w[accessKeyId secretAccessKey sessionToken accountId expiresAt]
95
+ missing_fields = required_access_token_fields.reject { |field| access_token[field] }
96
+
97
+ return if missing_fields.empty?
98
+
99
+ raise ArgumentError, "Access token in cached login token is missing required field(s): #{missing_fields}. " \
100
+ 'Please reauthenticate.'
101
+ end
102
+
103
+ def update_creds(access_token)
104
+ @credentials = Credentials.new(
105
+ access_token['accessKeyId'],
106
+ access_token['secretAccessKey'],
107
+ access_token['sessionToken'],
108
+ account_id: access_token['accountId']
109
+ )
110
+ @expiration = Time.parse(access_token['expiresAt'])
111
+ end
112
+
113
+ def attempt_refresh(token_json)
114
+ resp = make_request(token_json)
115
+ parse_resp(resp.token_output, token_json)
116
+ update_creds(token_json['accessToken'])
117
+ update_token_cache(token_json)
118
+ rescue Signin::Errors::AccessDeniedException => e
119
+ case e.error
120
+ when 'TOKEN_EXPIRED'
121
+ warn 'Your session has expired. Please reauthenticate.'
122
+ when 'USER_CREDENTIALS_CHANGED'
123
+ warn 'Unable to refresh credentials because of a change in your password. ' \
124
+ 'Please reauthenticate with your new password.'
125
+ when 'INSUFFICIENT_PERMISSIONS'
126
+ warn 'Unable to refresh credentials due to insufficient permissions. ' \
127
+ 'You may be missing permission for the `CreateOAuth2Token` action.'
128
+ end
129
+ rescue StandardError => e
130
+ warn("Failed to refresh Login token for LoginCredentials: #{e.message}")
131
+ end
132
+
133
+ def make_request(token_json)
134
+ options = {
135
+ token_input: {
136
+ client_id: token_json['clientId'],
137
+ grant_type: 'refresh_token',
138
+ refresh_token: token_json['refreshToken']
139
+ }
140
+ }
141
+ req = @client.build_request(:create_o_auth_2_token, options)
142
+ endpoint_params = Aws::Signin::EndpointParameters.create(req.context.config)
143
+ endpoint = req.context.config.endpoint_provider.resolve_endpoint(endpoint_params)
144
+ endpoint = URI.join(endpoint.url, @client.config.api.operation(:create_o_auth_2_token).http_request_uri).to_s
145
+ req.context.http_request.headers['DPoP'] = dpop_proof(token_json['dpopKey'], endpoint)
146
+ req.send_request
147
+ end
148
+
149
+ def dpop_proof(dpop_key, endpoint)
150
+ # Load private key from cached token file
151
+ private_key = OpenSSL::PKey.read(dpop_key)
152
+ public_key = private_key.public_key.to_octet_string(:uncompressed)
153
+
154
+ # Construct header and payload
155
+ header = build_header(public_key[1, 32], public_key[33, 32])
156
+ payload = build_payload(endpoint)
157
+
158
+ # Base64URL encode header and payload, sign message using private key, and create header
159
+ message = build_message(header, payload)
160
+ signature = private_key.sign(OpenSSL::Digest.new('SHA256'), message)
161
+ jws_signature = der_to_jws(signature)
162
+ "#{message}.#{Base64.urlsafe_encode64(jws_signature, padding: false)}"
163
+ end
164
+
165
+ def build_header(x_bytes, y_bytes)
166
+ {
167
+ 'alg' => 'ES256', # signing algorithm
168
+ 'jwk' => {
169
+ 'crv' => 'P-256', # curve name
170
+ 'kty' => 'EC', # key type
171
+ 'x' => Base64.urlsafe_encode64(x_bytes, padding: false), # public x coordinate
172
+ 'y' => Base64.urlsafe_encode64(y_bytes, padding: false) # public y coordinate
173
+ },
174
+ 'typ' => 'dpop+jwt' # hardcoded
175
+ }
176
+ end
177
+
178
+ def build_payload(htu)
179
+ {
180
+ 'jti' => SecureRandom.uuid, # unique identifier (UUID4)
181
+ 'htm' => @client.config.api.operation(:create_o_auth_2_token).http_method, # POST
182
+ 'htu' => htu, # endpoint of the CreateOAuth2Token operation, with path
183
+ 'iat' => Time.now.utc.to_i # UTC timestamp, specified number of seconds from 1970-01-01T00:00:00Z UTC
184
+ }
185
+ end
186
+
187
+ def build_message(header, payload)
188
+ encoded_header = Base64.urlsafe_encode64(JSON.dump(header), padding: false)
189
+ encoded_payload = Base64.urlsafe_encode64(JSON.dump(payload), padding: false)
190
+ "#{encoded_header}.#{encoded_payload}"
191
+ end
192
+
193
+ # Converts DER-encoded ASN.1 signature to JWS
194
+ def der_to_jws(der_signature)
195
+ asn1 = OpenSSL::ASN1.decode(der_signature)
196
+ r = asn1.value[0].value
197
+ s = asn1.value[1].value
198
+
199
+ r_hex = r.to_s(16).rjust(64, '0')
200
+ s_hex = s.to_s(16).rjust(64, '0')
201
+
202
+ [r_hex + s_hex].pack('H*')
203
+ end
204
+
205
+ def parse_resp(resp, token_json)
206
+ access_token = token_json['accessToken']
207
+ access_token.merge!(
208
+ 'accessKeyId' => resp.access_token.access_key_id,
209
+ 'secretAccessKey' => resp.access_token.secret_access_key,
210
+ 'sessionToken' => resp.access_token.session_token,
211
+ 'expiresAt' => (Time.now.utc + resp.expires_in).to_datetime.rfc3339
212
+ )
213
+ token_json['refreshToken'] = resp.refresh_token
214
+ end
215
+
216
+ def update_token_cache(token_json)
217
+ cached_token = token_json.dup
218
+ # File.write is not atomic so use temp file and move
219
+ temp_file = Tempfile.new('temp_file')
220
+ begin
221
+ temp_file.write(Json.dump(cached_token))
222
+ temp_file.close
223
+ FileUtils.mv(temp_file.path, login_cache_file)
224
+ ensure
225
+ temp_file.unlink if File.exist?(temp_file.path) # Ensure temp file is cleaned up
226
+ end
227
+ end
228
+ end
229
+ end