aws-sdk-core 3.237.0 → 3.238.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 95d07e9b7ee04171f8d93e7d0616d89c8d7e738372b772d6f66a67fac0710c41
4
- data.tar.gz: 00b8b6b00d2e61dee0054b198e73f76f27d2af1e282e01374126522e67991d98
3
+ metadata.gz: 9b5f0c19ee115b6aeb64a492c9f5d44dd35c12bfd090d8f66a5b207b48ec056e
4
+ data.tar.gz: 5f2ac56bae1ff72f54aebdc714f73f2c0550b7f2a189d2e54e872832b562c07f
5
5
  SHA512:
6
- metadata.gz: b77063dba0c37595b4096dd4a022a5446dc3fcb850bf24fff5546c025ce0dfb9fa96570e3259591708836a40c591df299879ebfb7b8648a4cbd736101fad30be
7
- data.tar.gz: 05b16dade46a24d7c53d4dc4fa48226f18deaa2ebb410b9bf42a63305cdac965b888bc5b11d283d1ff6c8c5b73ee107135bc8b6dd861386eaea44a02148341cb
6
+ metadata.gz: 34e19cf1ed28ea6f8989e2a8c010773548932aa44e470c2190d71837c012adb049e4f9e2fcf3096e8344563f233ccc293a5e16a72203b0a6cbe381b49c12cbc2
7
+ data.tar.gz: 7b3b5b81484bb41872b4949163af33e63d820b5d8902f9fe8a78bc35dd2f4b7f49571780411f75425bbb4b741321ff262894251675cbb13df27aa90370295d47
data/CHANGELOG.md CHANGED
@@ -1,6 +1,17 @@
1
1
  Unreleased Changes
2
2
  ------------------
3
3
 
4
+ 3.238.0 (2025-11-19)
5
+ ------------------
6
+
7
+ * Feature - Updated Aws::Signin::Client with the latest API changes.
8
+
9
+ * Feature - Updated Aws::STS::Client with the latest API changes.
10
+
11
+ * 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.
12
+
13
+ * Feature - Add `LoginCredentials` which retrieves credentials from AWS Sign-In. Support `aws-sdk-signin` alias gem.
14
+
4
15
  3.237.0 (2025-11-10)
5
16
  ------------------
6
17
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 3.237.0
1
+ 3.238.0
@@ -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,
@@ -104,6 +106,18 @@ module Aws
104
106
  nil
105
107
  end
106
108
 
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(profile: options[:config].profile)
114
+ return unless creds
115
+
116
+ creds.metrics << 'CREDENTIALS_CODE'
117
+ creds
118
+ end
119
+ end
120
+
107
121
  def static_profile_process_credentials(options)
108
122
  return unless Aws.shared_config.config_enabled? && options[:config]&.profile
109
123
 
@@ -152,6 +166,15 @@ module Aws
152
166
  nil
153
167
  end
154
168
 
169
+ def login_credentials(options)
170
+ return unless Aws.shared_config.config_enabled?
171
+
172
+ profile_name = determine_profile_name(options)
173
+ Aws.shared_config.login_credentials_from_config(profile: profile_name)
174
+ rescue Errors::NoSuchProfileError
175
+ nil
176
+ end
177
+
155
178
  def process_credentials(options)
156
179
  profile_name = determine_profile_name(options)
157
180
  if Aws.shared_config.config_enabled?
@@ -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
@@ -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
@@ -55,7 +55,9 @@ module Aws
55
55
  "CREDENTIALS_IMDS" : "0",
56
56
  "SSO_LOGIN_DEVICE" : "1",
57
57
  "SSO_LOGIN_AUTH" : "2",
58
- "BEARER_SERVICE_ENV_VARS": "3"
58
+ "BEARER_SERVICE_ENV_VARS": "3",
59
+ "CREDENTIALS_PROFILE_LOGIN": "AC",
60
+ "CREDENTIALS_LOGIN": "AD"
59
61
  }
60
62
  METRICS
61
63
 
@@ -171,6 +171,16 @@ module Aws
171
171
  token
172
172
  end
173
173
 
174
+ # Attempts to load from shared config or shared credentials file.
175
+ # Will always attempt first to load from the shared credentials
176
+ # file, if present.
177
+ def login_credentials_from_config(opts = {})
178
+ p = opts[:profile] || @profile_name
179
+ credentials = login_credentials_from_profile(@parsed_credentials, p)
180
+ credentials ||= login_credentials_from_profile(@parsed_config, p) if @parsed_config
181
+ credentials
182
+ end
183
+
174
184
  # Source a custom configured endpoint from the shared configuration file
175
185
  #
176
186
  # @param [Hash] opts
@@ -469,6 +479,14 @@ module Aws
469
479
  end
470
480
  end
471
481
 
482
+ def login_credentials_from_profile(cfg, profile)
483
+ return unless @parsed_config && (prof_config = cfg[profile]) && prof_config['login_session']
484
+
485
+ creds = LoginCredentials.new(login_session: prof_config['login_session'])
486
+ creds.metrics << 'CREDENTIALS_PROFILE_LOGIN'
487
+ creds
488
+ end
489
+
472
490
  def credentials_from_profile(prof_config)
473
491
  creds = Credentials.new(
474
492
  prof_config['aws_access_key_id'],
data/lib/aws-sdk-core.rb CHANGED
@@ -25,6 +25,7 @@ module Aws
25
25
  autoload :SharedCredentials, 'aws-sdk-core/shared_credentials'
26
26
  autoload :ProcessCredentials, 'aws-sdk-core/process_credentials'
27
27
  autoload :SSOCredentials, 'aws-sdk-core/sso_credentials'
28
+ autoload :LoginCredentials, 'aws-sdk-core/login_credentials'
28
29
 
29
30
 
30
31
  # tokens and token providers
@@ -175,3 +176,6 @@ require_relative 'aws-sdk-sts'
175
176
  # aws-sdk-sso is included to support Aws::SSOCredentials
176
177
  require_relative 'aws-sdk-sso'
177
178
  require_relative 'aws-sdk-ssooidc'
179
+
180
+ # aws-sdk-signin is included to support Aws::SignInCredentials
181
+ require_relative 'aws-sdk-signin'