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

  1. 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.
  2. 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:

  1. Check if currentTime > exp. If true, reject immediately (403).
  2. Re-compute the expected signature using the incoming HTTP_METHOD, REQUEST_URI, and exp.
  3. 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:

  1. Protected Resources: 5 endpoints representing protected actions across different HTTP methods.
  2. 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!