When building modern web applications or APIs, you often need to grant users temporary access to a specific resource. Common scenarios include:
- Generating a single-use or time-bound download link for a private file.
- Sending an e-mail link for account verification or password reset.
- Creating temporary webhooks or one-off API action links.
A common reaction is to reach for traditional authorization methods like JWT (JSON Web Tokens) or Database-Backed Session Tokens. However, when applied to temporary resource access, these traditional approaches come with trade-offs.
In this post, we’ll explore why Signed URLs (using HMAC-SHA256) are often the superior pattern for temporary access, compare them against alternative approaches, and walk through a complete Spring Boot demo project called temporary-url-demo.
The Comparison: 3 Approaches to Temporary Access
| Feature | Database Tokens | JWT in Headers | Signed URLs (HMAC-SHA256) |
|---|---|---|---|
| Stateful / Database Load | High (Requires DB write & read) | Stateless | Stateless (Zero DB I/O) |
| URL-Friendly | Yes (e.g. ?token=xyz) |
No (Headers required; awkward in URL query) | Yes (Parameters in URL) |
| Tamper-Proof | Yes | Yes | Yes (Cryptographic Signature) |
| Method & Path Binding | Manual tracking needed | Requires custom claims | Built-in (Signed Payload) |
| Cleanup Maintenance | Requires cron jobs to purge expired tokens | None | None (Automatic via TTL) |
Why Traditional Approaches Fall Short
- Database-Backed Tokens: Every time you generate or validate a temporary link, you hit the database. Over time, you need background cleanup jobs to purge expired tokens.
- JWT via Headers: JWTs are excellent for stateless authentication, but they rely on request headers (
Authorization: Bearer <token>). Standard HTML links (<a href="...">), image tags, or e-mail links cannot send custom HTTP headers natively when clicked by a user in a browser.
How Signed URLs Work (The HMAC-SHA256 Pattern)
A Signed URL locks down a specific HTTP method, request URI, and expiration timestamp into a single cryptographic hash using a secret key stored on the server.
Data Canonicalization & Signing
To issue a signed URL, the server constructs a deterministic payload string:
HTTP_METHOD + "\n" + REQUEST_URI + "\n" + EXPIRES_AT_TIMESTAMP
This string is signed using HmacSHA256 with a server-side secretKey and encoded using URL-Safe Base64 (without padding):
// Example Canonical Payload: POST /protected/123123123 1787582041957
Validation & Verification
When a request arrives at the server with query parameters exp and sig:
- Check if
currentTime > exp. If true, reject immediately (403). - Re-compute the expected signature using the incoming
HTTP_METHOD,REQUEST_URI, andexp. - Perform a constant-time equality check between the expected signature and
sig.
If anyone alters the URL (e.g., changing /protected/123123123 to /protected/999999999 or modifying exp), the signature will no longer match, making the request instantly invalid and tamper-proof.
⚠️ Since Signed URLs are purely stateless, a valid link can be reused multiple times until it reaches its expiration timestamp (exp). If your use case strictly requires a one-time access link (e.g., a single-use password reset or single-download link), you will need to complement this pattern with a stateful check—such as tracking a unique jti/nonce in a fast cache like Redis to invalidate the link upon its first consumption.
Hands-On Demo: temporary-url-demo in Spring Boot
To demonstrate this pattern in practice, I built a Spring Boot application called temporary-url-demo.
The application consists of two main parts:
- Protected Resources: 5 endpoints representing protected actions across different HTTP methods.
- Temporary URL Generator: An endpoint that accepts target resource details and returns a signed, time-bound URL.
Simulated Protected Endpoints
To test method binding, we cover 5 distinct HTTP methods under /protected/{id}:
@GetMapping("/protected/{id}")@PostMapping("/protected/{id}")@PutMapping("/protected/{id}")@PatchMapping("/protected/{id}")@DeleteMapping("/protected/{id}")
Every incoming request under /protected/** is intercepted before reaching the controllers.
Generating a Temporary URL
To create a signed link, we issue a POST request to /temporary-url with the desired target HTTP method, resource ID, and time-to-live (expiresIn in seconds).
Request:
curl --request POST \
--url http://localhost:8080/temporary-url \
--header 'Content-Type: application/json' \
--data '{
"method": "POST",
"id": "123123123",
"expiresIn": 60
}'
Response:
{
"url": "http://localhost:8080/protected/123123123?sig=_Vix2IsIUA-F0erIr056IdwPKSKe04Yo_DwCxC4irts&exp=1787582041957"
}
Conclusion
Signed URLs offer an elegant, stateless, and secure solution for temporary access control without adding database overhead or requiring authorization headers. By binding HTTP methods, URIs, and expiration times directly into an HMAC-SHA256 signature, you achieve tamper-proof authorization out of the box.
The full demo code is available in the temporary-url-demo repository. Feel free to explore the code, adapt it to your Spring Boot applications!