Skip to content

Commit 85fe907

Browse files
authored
Minimum of expiration time and expires_in for presigned urls (#2872)
1 parent 08f1e3d commit 85fe907

File tree

10 files changed

+97
-29
lines changed

10 files changed

+97
-29
lines changed

build_tools/services.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ class ServiceEnumerator
1010
MANIFEST_PATH = File.expand_path('../../services.json', __FILE__)
1111

1212
# Minimum `aws-sdk-core` version for new gem builds
13-
MINIMUM_CORE_VERSION = "3.174.0"
13+
MINIMUM_CORE_VERSION = "3.176.0"
1414

1515
EVENTSTREAM_PLUGIN = "Aws::Plugins::EventStreamConfiguration"
1616

gems/aws-sdk-core/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
Unreleased Changes
22
------------------
33

4+
* Feature - Add :expiration accessor to `CredentialProvider` and do not refresh credentials when checking expiration.
5+
46
3.175.0 (2023-06-15)
57
------------------
68

gems/aws-sdk-core/lib/aws-sdk-core/credential_provider.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ module CredentialProvider
66
# @return [Credentials]
77
attr_reader :credentials
88

9+
# @param [Time]
10+
attr_reader :expiration
11+
912
# @return [Boolean]
1013
def set?
1114
!!credentials && credentials.set?

gems/aws-sdk-core/lib/aws-sdk-core/refreshing_credentials.rb

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,6 @@ def credentials
3636
@credentials
3737
end
3838

39-
# @return [Time,nil]
40-
def expiration
41-
refresh_if_near_expiration!
42-
@expiration
43-
end
44-
4539
# Refresh credentials.
4640
# @return [void]
4741
def refresh!

gems/aws-sdk-s3/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
Unreleased Changes
22
------------------
33

4+
* Feature - Select minimum expiration time for presigned urls between the expiration time option and the credential expiration time.
5+
46
1.126.0 (2023-06-16)
57
------------------
68

gems/aws-sdk-s3/lib/aws-sdk-s3/presigner.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ def initialize(options = {})
4949
# before the presigned URL expires. Defaults to 15 minutes. As signature
5050
# version 4 has a maximum expiry time of one week for presigned URLs,
5151
# attempts to set this value to greater than one week (604800) will
52-
# raise an exception.
52+
# raise an exception. The min value of this option and the credentials
53+
# expiration time is used in the presigned URL.
5354
#
5455
# @option params [Time] :time (Time.now) The starting time for when the
5556
# presigned url becomes active.
@@ -96,7 +97,8 @@ def presigned_url(method, params = {})
9697
# before the presigned URL expires. Defaults to 15 minutes. As signature
9798
# version 4 has a maximum expiry time of one week for presigned URLs,
9899
# attempts to set this value to greater than one week (604800) will
99-
# raise an exception.
100+
# raise an exception. The min value of this option and the credentials
101+
# expiration time is used in the presigned URL.
100102
#
101103
# @option params [Time] :time (Time.now) The starting time for when the
102104
# presigned url becomes active.

gems/aws-sdk-s3/spec/presigner_spec.rb

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,40 @@ module S3
77
describe Presigner do
88
let(:client) { Aws::S3::Client.new(**client_opts) }
99

10+
let(:credentials) do
11+
Credentials.new(
12+
'ACCESS_KEY_ID',
13+
'SECRET_ACCESS_KEY'
14+
)
15+
end
1016
let(:client_opts) do
1117
{
1218
region: 'us-east-1',
13-
credentials: Credentials.new(
14-
'ACCESS_KEY_ID',
15-
'SECRET_ACCESS_KEY'
16-
),
19+
credentials: credentials,
1720
stub_responses: true
1821
}
1922
end
2023

2124
subject { Presigner.new(client: client) }
2225

23-
before { allow(Time).to receive(:now).and_return(Time.utc(2021, 8, 27)) }
26+
let(:time) { Time.utc(2021, 8, 27) }
27+
before { allow(Time).to receive(:now).and_return(time) }
28+
29+
let(:expiration_time) { time + 180 }
30+
let(:credentials_provider_class) do
31+
Class.new do
32+
include CredentialProvider
33+
34+
def initialize(expiration_time)
35+
@credentials = Credentials.new(
36+
'akid',
37+
'secret',
38+
'session'
39+
)
40+
@expiration = expiration_time
41+
end
42+
end
43+
end
2444

2545
describe '#initialize' do
2646
it 'accepts an injected S3 client' do
@@ -170,6 +190,22 @@ module S3
170190
)
171191
expect(url).to match(/x-amz-acl=public-read/)
172192
end
193+
194+
context 'credential expiration' do
195+
let(:credentials) do
196+
credentials_provider_class.new(expiration_time)
197+
end
198+
199+
it 'picks the minimum time between expires_in and credential expiration' do
200+
url = subject.presigned_url(
201+
:get_object,
202+
bucket: 'aws-sdk',
203+
key: 'foo',
204+
expires_in: 3600
205+
)
206+
expect(url).to match(/X-Amz-Expires=180/)
207+
end
208+
end
173209
end
174210

175211
describe '#presigned_request' do
@@ -306,6 +342,22 @@ module S3
306342
expect(url).to match(/X-Amz-SignedHeaders=host%3Bx-amz-acl/)
307343
expect(headers).to eq('x-amz-acl' => 'public-read')
308344
end
345+
346+
context 'credential expiration' do
347+
let(:credentials) do
348+
credentials_provider_class.new(expiration_time)
349+
end
350+
351+
it 'picks the minimum time between expires_in and credential expiration' do
352+
url, = subject.presigned_request(
353+
:get_object,
354+
bucket: 'aws-sdk',
355+
key: 'foo',
356+
expires_in: 3600
357+
)
358+
expect(url).to match(/X-Amz-Expires=180/)
359+
end
360+
end
309361
end
310362

311363
context 'outpost access point ARNs' do

gems/aws-sigv4/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
Unreleased Changes
22
------------------
33

4+
* Feature - Select the minimum expiration time for presigned urls between the expiration time option and the credential expiration time.
5+
46
1.5.2 (2022-09-30)
57
------------------
68

gems/aws-sigv4/lib/aws-sigv4/signer.rb

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ def sign_request(request)
235235

236236
return crt_sign_request(request) if Signer.use_crt?
237237

238-
creds = fetch_credentials
238+
creds, _ = fetch_credentials
239239

240240
http_method = extract_http_method(request)
241241
url = extract_url(request)
@@ -314,7 +314,7 @@ def sign_request(request)
314314
# hex-encoded string using #unpack
315315
def sign_event(prior_signature, payload, encoder)
316316
# Note: CRT does not currently provide event stream signing, so we always use the ruby implementation.
317-
creds = fetch_credentials
317+
creds, _ = fetch_credentials
318318
time = Time.now
319319
headers = {}
320320

@@ -403,7 +403,7 @@ def presign_url(options)
403403

404404
return crt_presign_url(options) if Signer.use_crt?
405405

406-
creds = fetch_credentials
406+
creds, expiration = fetch_credentials
407407

408408
http_method = extract_http_method(options)
409409
url = extract_url(options)
@@ -423,7 +423,7 @@ def presign_url(options)
423423
params['X-Amz-Algorithm'] = 'AWS4-HMAC-SHA256'
424424
params['X-Amz-Credential'] = credential(creds, date)
425425
params['X-Amz-Date'] = datetime
426-
params['X-Amz-Expires'] = extract_expires_in(options)
426+
params['X-Amz-Expires'] = presigned_url_expiration(options, expiration).to_s
427427
params['X-Amz-Security-Token'] = creds.session_token if creds.session_token
428428
params['X-Amz-SignedHeaders'] = signed_headers(headers)
429429

@@ -526,7 +526,6 @@ def event_signature(secret_access_key, date, string_to_sign)
526526
hmac(k_credentials, string_to_sign)
527527
end
528528

529-
530529
def path(url)
531530
path = url.path
532531
path = '/' if path == ''
@@ -682,8 +681,8 @@ def downcase_headers(headers)
682681

683682
def extract_expires_in(options)
684683
case options[:expires_in]
685-
when nil then 900.to_s
686-
when Integer then options[:expires_in].to_s
684+
when nil then 900
685+
when Integer then options[:expires_in]
687686
else
688687
msg = "expected :expires_in to be a number of seconds"
689688
raise ArgumentError, msg
@@ -698,11 +697,14 @@ def uri_escape_path(string)
698697
self.class.uri_escape_path(string)
699698
end
700699

701-
702700
def fetch_credentials
703701
credentials = @credentials_provider.credentials
704702
if credentials_set?(credentials)
705-
credentials
703+
expiration = nil
704+
if @credentials_provider.respond_to?(:expiration)
705+
expiration = @credentials_provider.expiration
706+
end
707+
[credentials, expiration]
706708
else
707709
raise Errors::MissingCredentialsError,
708710
'unable to sign request without credentials set'
@@ -720,21 +722,30 @@ def credentials_set?(credentials)
720722
!credentials.secret_access_key.empty?
721723
end
722724

725+
def presigned_url_expiration(options, expiration)
726+
expires_in = extract_expires_in(options)
727+
return expires_in unless expiration
728+
729+
expiration_seconds = (expiration - Time.now).to_i
730+
[expires_in, expiration_seconds].min
731+
end
732+
723733
### CRT Code
724734

725735
# the credentials used by CRT must be a
726736
# CRT StaticCredentialsProvider object
727737
def crt_fetch_credentials
728-
creds = fetch_credentials
729-
Aws::Crt::Auth::StaticCredentialsProvider.new(
738+
creds, expiration = fetch_credentials
739+
crt_creds = Aws::Crt::Auth::StaticCredentialsProvider.new(
730740
creds.access_key_id,
731741
creds.secret_access_key,
732742
creds.session_token
733743
)
744+
[crt_creds, expiration]
734745
end
735746

736747
def crt_sign_request(request)
737-
creds = crt_fetch_credentials
748+
creds, _ = crt_fetch_credentials
738749
http_method = extract_http_method(request)
739750
url = extract_url(request)
740751
headers = downcase_headers(request[:headers])
@@ -793,7 +804,7 @@ def crt_sign_request(request)
793804
end
794805

795806
def crt_presign_url(options)
796-
creds = crt_fetch_credentials
807+
creds, expiration = crt_fetch_credentials
797808

798809
http_method = extract_http_method(options)
799810
url = extract_url(options)
@@ -821,7 +832,7 @@ def crt_presign_url(options)
821832
use_double_uri_encode: @uri_escape_path,
822833
should_normalize_uri_path: @normalize_path,
823834
omit_session_token: @omit_session_token,
824-
expiration_in_seconds: options.fetch(:expires_in, 900)
835+
expiration_in_seconds: presigned_url_expiration(options, expiration)
825836
)
826837
http_request = Aws::Crt::Http::Message.new(
827838
http_method, url.to_s, headers

services.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -921,7 +921,7 @@
921921
"models": "s3/2006-03-01",
922922
"dependencies": {
923923
"aws-sdk-kms": "~> 1",
924-
"aws-sigv4": "~> 1.4"
924+
"aws-sigv4": "~> 1.6"
925925
},
926926
"addPlugins": [
927927
"Aws::S3::Plugins::Accelerate",

0 commit comments

Comments
 (0)