Signed URL Generator & Signed Redirect¶
Lilya provides a built-in system for generating and verifying signed URLs with expiration, as well as a SignedRedirect
response class that issues redirects to signed URLs.
from lilya.contrib.security.signed_urls import SignedURLGenerator, SignedRedirect
Overview¶
The Signed URL system in Lilya allows developers to generate tamper-proof, time-limited URLs for secure access to resources such as:
- File downloads
- Private media links
- Authenticated redirect flows
- One-time verification links
- Password reset or magic login URLs
Every signed URL contains a cryptographic HMAC signature and an expiration timestamp, ensuring that:
- The URL cannot be modified without detection.
- The URL automatically expires after the given time window.
This is essential for protecting endpoints that must be temporarily accessible, e.g., links sent to external clients or users who shouldn't have permanent access.
How It Works¶
At the core lies the SignedURLGenerator
:
from lilya.contrib.security.signed_urls import SignedURLGenerator
signer = SignedURLGenerator(secret_key="supersecret")
signed = signer.sign("https://example.com/resource", expires_in=300)
print(signed)
# -> https://example.com/resource?expires=1739311820&sig=naK3F_q3s4LzZQnBiRAaXq...
signer.verify(signed) # True (before expiry)
The signature is derived from:
HMAC(secret_key, f"{path}?expires={timestamp}&<other_params>")
This ensures:
- Integrity — Any modification to the path or query invalidates the signature.
- Expiration — If the timestamp is older than
now()
, the URL fails verification. - Algorithm flexibility — You can use any hash algorithm supported by
hashlib
(sha256
,sha512
, etc.).
Class Reference¶
SignedURLGenerator
¶
Initialization¶
SignedURLGenerator(secret_key: str, algorithm: str = "sha256")
Parameter | Type | Description |
---|---|---|
secret_key |
str |
Secret used to sign URLs. Must be kept private and consistent across signing and verifying services. |
algorithm |
str |
Hash algorithm to use for the HMAC. Default is "sha256" . |
Signing URLs¶
sign(url: str, expires_in: int = 3600) -> str
Parameter | Type | Description |
---|---|---|
url |
str |
The URL to be signed. |
expires_in |
int |
Time (in seconds) before the URL expires. Default: 1 hour. |
Returns a fully signed URL with ?expires=<ts>&sig=<signature>
appended.
Example:
signer = SignedURLGenerator("key123")
signed = signer.sign("https://myapp.com/downloads/video.mp4", expires_in=600)
print(signed)
# -> https://myapp.com/downloads/video.mp4?expires=1739312400&sig=abcd1234xyz
Verifying URLs¶
verify(signed_url: str) -> bool
Parameter | Type | Description |
---|---|---|
signed_url |
str |
A previously signed URL. |
Returns True
if:
- The signature matches, and
- The current time <
expires
Otherwise returns False
.
SignedRedirect
¶
A subclass of RedirectResponse
that automatically signs its target URL using a SignedURLGenerator
.
Usage¶
from lilya.apps import Lilya
from lilya.routing import Path
from lilya.contrib.security.signed_urls import SignedRedirect, SignedURLGenerator
signer = SignedURLGenerator("supersecret")
async def go():
return SignedRedirect("https://example.com/secure", signer, expires_in=300)
app = Lilya(routes=[Path("/redirect", go)])
This sends a redirect response with a signed Location
header:
Location: https://example.com/secure?expires=1739311920&sig=abc...
Dependency Injection Integration¶
Signed URL signing can be injected through Lilya's dependency system, ideal when your app shares a global signing key or uses multiple scopes.
Example 1 — Global scope (one signer across app)¶
from lilya.apps import Lilya
from lilya.dependencies import Provide, Provides
from lilya.contrib.security.signed_urls import SignedURLGenerator, SignedRedirect
from lilya.routing import Path
signer = SignedURLGenerator(secret_key="global-key")
async def redirect_endpoint(signer=Provides()):
return SignedRedirect("https://example.com/profile", signer, expires_in=120)
app = Lilya(
dependencies={"signer": Provide(lambda: signer, scope="GLOBAL")},
routes=[Path("/redir", redirect_endpoint)],
)
✅ The same signer instance is reused for every request, great for global app secrets.
Example 2 — App scope (isolated per sub-app)¶
from lilya.apps import Lilya
from lilya.routing import Path
from lilya.contrib.security.signed_urls import SignedURLGenerator
from lilya.dependencies import Provide, Provides
created = []
def factory():
signer = SignedURLGenerator(secret_key="per-app-key")
created.append(signer)
return signer
async def endpoint(signer=Provides()):
return {"signed": signer.sign("https://example.com/app", expires_in=30)}
sub1 = Lilya(dependencies={"signer": Provide(factory, scope="APP")}, routes=[Path("/a", endpoint)])
sub2 = Lilya(dependencies={"signer": Provide(factory, scope="APP")}, routes=[Path("/b", endpoint)])
- ✅ Each app mount gets its own instance.
- ✅ Global state isolation, useful for multi-tenant or per-customer setups.
Example 3 — Mixed scopes¶
from lilya.apps import Lilya
from lilya.routing import Path
from lilya.contrib.security.signed_urls import SignedURLGenerator
from lilya.dependencies import Provide, Provides
from lilya.enums import Scope
def global_factory():
return SignedURLGenerator("global-secret")
def request_factory():
return SignedURLGenerator("request-secret")
async def endpoint(global_signer=Provides(), req_signer=Provides()):
u1 = global_signer.sign("https://example.com/a", expires_in=30)
u2 = req_signer.sign("https://example.com/b", expires_in=30)
return {"valid1": global_signer.verify(u1), "valid2": req_signer.verify(u2)}
app = Lilya(
dependencies={
"global_signer": Provide(global_factory, scope=Scope.GLOBAL),
"req_signer": Provide(request_factory, scope=Scope.REQUEST),
},
routes=[Path("/multi", endpoint)],
)
- ✅
GLOBAL
— one instance reused - ✅
REQUEST
— new signer each request - ✅ Best pattern when mixing user-specific and global secrets.
When to Use Signed URLs¶
Use Case | Description |
---|---|
Temporary Downloads | Grant time-limited access to S3, GCS, or CDN resources. |
Private Redirects | Safely redirect users to external destinations after verifying a token or login. |
Magic Login Links | Send one-time authentication URLs via email. |
User Verification Links | Sign verification links to prevent tampering. |
Expiring Invitations | Create share links that expire after a time window. |
What Signed URLs Are Not¶
- They do not encrypt the URL, they only ensure integrity and validity.
- They should not be used as authentication tokens; instead, use them to gate access for already authenticated or limited scope actions.
Example: Secure File Download Endpoint¶
from lilya.apps import Lilya
from lilya.dependencies import Provide, Provides
from lilya.contrib.security.signed_urls import SignedURLGenerator
from lilya.responses import FileResponse
from lilya.routing import Path
signer = SignedURLGenerator("media-secret")
async def generate_link(signer=Provides()):
signed = signer.sign("https://cdn.example.com/private/video.mp4", expires_in=300)
return {"download_url": signed}
async def serve_file(request):
url = str(request.url)
if not signer.verify(url):
return {"error": "Link expired or invalid"}, 403
return FileResponse("/data/private/video.mp4")
app = Lilya(
dependencies={"signer": Provide(lambda: signer, scope="GLOBAL")},
routes=[
Path("/link", generate_link),
Path("/media/video.mp4", serve_file),
],
)
Key Takeaways¶
Concept | Explanation |
---|---|
Integrity | HMAC ensures URLs cannot be tampered with. |
Expiry | expires timestamp automatically invalidates old links. |
Dependency-Friendly | Works seamlessly with Lilya's DI system. |
Flexible Scoping | Supports REQUEST , APP , and GLOBAL scopes. |
Framework Agnostic | SignedURLGenerator can be used standalone without Lilya. |
Summary¶
The Signed URL and Redirect system is one of Lilya's most practical security primitives — small but powerful. It balances simplicity and robustness, ensuring your links remain secure, temporary, and verifiable, both in internal redirects and in user-facing workflows.