Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(Static Stability) use provided expires_in in presigned url when credentials are expired #2933

Merged
merged 2 commits into from
Oct 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions gems/aws-sigv4/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
Unreleased Changes
------------------

* Issue - (Static Stability) use provided `expires_in` in presigned url when credentials are expired.

1.6.0 (2023-06-28)
------------------

Expand Down
19 changes: 13 additions & 6 deletions gems/aws-sigv4/lib/aws-sigv4/signer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -423,7 +423,7 @@ def presign_url(options)
params['X-Amz-Algorithm'] = 'AWS4-HMAC-SHA256'
params['X-Amz-Credential'] = credential(creds, date)
params['X-Amz-Date'] = datetime
params['X-Amz-Expires'] = presigned_url_expiration(options, expiration).to_s
params['X-Amz-Expires'] = presigned_url_expiration(options, expiration, Time.strptime(datetime, "%Y%m%dT%H%M%S%Z")).to_s
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can see passing in datetime, but why does it need to be formatted? It's used for arithmetic.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thats was a bit confusing and I believe actually broken previously. The datetime above is use for the x-amz-date header and must be the formatted string. But it can be sourced from a few places, either the passed in header or options[:time] or fallback to Time.now (but in all of those cases it must be formatted as a string). I'm now passing in that datetime so that the expire_at vs expiration is computed consistently when either the header or options[:time] is provided.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. I think you can just pass datetime to Time.parse? You can possibly do that parsing inside the private method when calculating expiration seconds, and just pass datetime into the method here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was trying to avoid branching in the private method on the class of datetime. in CRT it is a Time. In non-CRT it is a string. The header needs to be the string. And you can provide the header as an input and we will use that which complicates this code a bit.

I could use Time.parse but wanted to be explicit about the format.

params['X-Amz-Security-Token'] = creds.session_token if creds.session_token
params['X-Amz-SignedHeaders'] = signed_headers(headers)

Expand Down Expand Up @@ -722,12 +722,19 @@ def credentials_set?(credentials)
!credentials.secret_access_key.empty?
end

def presigned_url_expiration(options, expiration)
def presigned_url_expiration(options, expiration, datetime)
expires_in = extract_expires_in(options)
return expires_in unless expiration

expiration_seconds = (expiration - Time.now).to_i
[expires_in, expiration_seconds].min
expiration_seconds = (expiration - datetime).to_i
# In the static stability case, credentials may expire in the past
# but still be valid. For those cases, use the user configured
# expires_in and ingore expiration.
if expiration_seconds <= 0
expires_in
else
[expires_in, expiration_seconds].min
end
end

### CRT Code
Expand Down Expand Up @@ -811,7 +818,7 @@ def crt_presign_url(options)
headers = downcase_headers(options[:headers])
headers['host'] ||= host(url)

datetime = headers.delete('x-amz-date')
datetime = Time.strptime(headers.delete('x-amz-date'), "%Y%m%dT%H%M%S%Z") if headers['x-amz-date']
datetime ||= (options[:time] || Time.now)

content_sha256 = headers.delete('x-amz-content-sha256')
Expand All @@ -832,7 +839,7 @@ def crt_presign_url(options)
use_double_uri_encode: @uri_escape_path,
should_normalize_uri_path: @normalize_path,
omit_session_token: @omit_session_token,
expiration_in_seconds: presigned_url_expiration(options, expiration)
expiration_in_seconds: presigned_url_expiration(options, expiration, datetime)
)
http_request = Aws::Crt::Http::Message.new(
http_method, url.to_s, headers
Expand Down
70 changes: 70 additions & 0 deletions gems/aws-sigv4/spec/signer_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,76 @@ module Sigv4
end

end

describe '#presign_url' do
let(:now) { Time.now }

let(:credentials) do
{
access_key_id: 'akid',
secret_access_key: 'secret',
session_token: nil,
expiration: expiration
}
end

let(:signer_options) do
{
service: 'SERVICE',
region: 'REGION',
credentials_provider: double(credentials: double(credentials), expiration: expiration),
}
end

let(:presign_options) do
{
http_method: 'GET',
url: 'https://example.com',
expires_in: expires_in,
time: now
}
end

let(:expires_in) { 60 }

let(:signer) { Signer.new(signer_options) }

context 'expiration is nil' do
let(:expiration) { nil }

it 'creates a presigned url with the provided expires_at' do
url = signer.presign_url(presign_options)
expect(url.to_s).to include('X-Amz-Expires=60')
end
end

context 'expiration is after expires_at' do
let(:expiration) { now + 3600 }

it 'creates a presigned url with the provided expires_at' do
url = signer.presign_url(presign_options)
expect(url.to_s).to include('X-Amz-Expires=60')
end
end

context 'expiration is before expires_at' do
let(:expiration) { now + 10 }

it 'creates a presigned url that expires at the credential expiration time' do
url = signer.presign_url(presign_options)
expect(url.to_s).to include('X-Amz-Expires=10')
end
end

context 'expired credentials (static stability)' do
let(:expiration) { now - 10 }

it 'creates a presigned url with the provided expires_at' do
url = signer.presign_url(presign_options)
expect(url.to_s).to include('X-Amz-Expires=60')
end
end
end
end
end
end