Presigning and query authentication
Providers authenticate requests in two places. Headers are the default: the signature travels with the request and dies with it. The query string embeds authentication in the URI itself, so whoever holds the URL can perform exactly that request with no credentials of their own — that is what "presigned URLs" are.
// Header signing:
signer.sign(&mut req, None).await?;
// Query authentication, valid for 1 hour (where supported):
signer.sign(&mut req, Some(Duration::from_secs(3600))).await?;
| Situation | Mode |
|---|---|
| Your service makes the request itself | Header signing |
| A browser, another service, or a user uploads/downloads directly | Query authentication |
| The credential itself is already scoped (SAS) | Query by nature |
Produce a presigned URL
use reqsign::aws;
use std::time::Duration;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let signer = aws::default_signer("s3", "us-east-1");
let mut req = http::Request::get("https://s3.amazonaws.com/my-bucket/report.csv")
.body(())?
.into_parts()
.0;
signer
.sign(&mut req, Some(Duration::from_secs(3600)))
.await?;
// Share req.uri over a trusted channel; it contains authentication material.
assert!(req.uri.query().is_some());
Ok(())
}
For AWS this produces X-Amz-Algorithm, X-Amz-Credential,
X-Amz-Expires, X-Amz-Signature, and companions in the query string.
Other providers use their own parameter sets; the shape of the call is
identical.
expires_in is service-specific
Passing Some(duration) does not universally mean "presign" — the
configured service signer and credential type determine how the duration is
interpreted:
- AWS SigV4/SigV4a, Aliyun OSS, Tencent COS, Volcengine TOS, Google Cloud Storage, and Huawei Cloud OBS select query authentication with that validity window.
- Azure Storage SAS credentials authenticate through the query string by nature; shared keys and bearer tokens sign headers.
- Oracle Cloud bounds credential validity but produces no query authentication — header signing is its only mode.
The provider matrix carries the authoritative header/query flags, and the expiration contract states the exact semantics.
Before you ship one
- The method and URI are part of the signature. A URL presigned for
GET /report.csvauthorizes exactly that — notPUT, not another key. - Credential validity must cover the window. The signer refreshes a
cached credential that would expire before
expires_inelapses, and errors if no credential can cover the window — a URL that dies early is a bug, not a surprise. See Credential freshness. - Anyone with the URL is authorized. Treat presigned URLs as secrets with a TTL: transmit over TLS, scope tightly, keep windows short.
- Never re-encode a presigned URI. The signature covers the encoded bytes; a second encoding pass breaks it. See Wire-ready URIs.
- Tighter blast radius: grant a downscoped credential first and presign with that.