A private video works perfectly when you press play.
Then you drag the timeline to 12:43.
The player spins for a few seconds and fails.
This is a common problem when building authenticated video delivery. The application correctly protects the original file, but the implementation assumes that playing a video is equivalent to downloading one file in one HTTP request.
Browsers do not necessarily work that way.
Video players often request small byte ranges from different parts of the file. Seeking, buffering, reconnecting, and switching playback position can all generate additional HTTP requests.
If authentication, signed URLs, caching, and range handling are not designed together, private video playback becomes unreliable surprisingly quickly.
Why video players use range requests
Suppose a browser wants only bytes 5,000,000 through 7,000,000 of a video.
It may send:
GET /media/video.mp4
Range: bytes=5000000-7000000
A range-aware server should respond with 206 Partial Content rather than 200 OK:
HTTP/1.1 206 Partial Content
Accept-Ranges: bytes
Content-Range: bytes 5000000-7000000/94837522
Content-Length: 2000001
Content-Type: video/mp4
The browser can then fetch only the portion it needs.
That makes seeking fast and avoids downloading a 500 MB video just because somebody wants to watch the final 30 seconds.
For large media applications, preserving this behavior is essential.
Do not stream private files through the application server
A tempting architecture looks like this:
Browser
↓
Application server
↓
Object storage
The application authenticates the user, reads the video from storage, and sends the bytes back.
It works.
It also turns the application server into an expensive media proxy.
A better architecture is usually:
Browser
↓
Application API
↓
Authorization check
↓
Short-lived media authorization
↓
CDN
↓
Object storage
Your application decides who may access the video.
Your CDN and object storage handle moving the bytes.
Those are different responsibilities and separating them makes scaling much easier.
Sign access to a specific resource
One simple approach is to create a short-lived token containing the media path and expiry.
For example:
import crypto from "node:crypto";
const SECRET = process.env.MEDIA_SIGNING_SECRET;
function signMediaPath(path, expiresAt) {
const payload = `${path}:${expiresAt}`;
return crypto
.createHmac("sha256", SECRET)
.update(payload)
.digest("hex");
}
You could generate a playback URL such as:
const path = "/private/videos/8f32b1.mp4";
const expires = Math.floor(Date.now() / 1000) + 3600;
const signature = signMediaPath(path, expires);
const url =
`${path}?expires=${expires}&signature=${signature}`;
At the edge or media endpoint, validate both values:
function verifyMediaRequest(path, expires, signature) {
if (Number(expires) < Math.floor(Date.now() / 1000)) {
return false;
}
const expected = signMediaPath(path, expires);
const a = Buffer.from(expected);
const b = Buffer.from(signature);
if (a.length !== b.length) {
return false;
}
return crypto.timingSafeEqual(a, b);
}
The important detail is that the signature covers the resource path.
A token created for:
/private/videos/a.mp4
must not also authorize:
/private/videos/b.mp4
Otherwise a user may be able to modify the path while reusing a valid token.
Expiry gets tricky during playback
Very short-lived signed URLs sound more secure.
For images, a five-minute expiration might be perfectly reasonable.
For a 90-minute video, it may cause problems.
The browser can begin playback while the token is valid and make another range request 20 minutes later. If authorization is checked again and the token expired after five minutes, seeking or buffering may suddenly fail.
There are several ways to handle this.
One is simply making playback authorization long enough to cover a reasonable viewing session.
Another is using a renewable playback session. Your application issues a media credential, the frontend refreshes it before expiry, and later range requests use the refreshed authorization.
Some CDN systems also support signed cookies or equivalent session-based authorization. That can work particularly well because the media URL itself does not need to change every time credentials are refreshed.
The right implementation depends on your infrastructure.
The important point is this:
Token lifetime should be designed around browser playback behavior, not just around the initial page request.
Preserve the Range header
Another easy mistake happens when a reverse proxy or CDN sits in front of storage.
The browser sends:
Range: bytes=20000000-
but an intermediate service removes the Range header.
The origin then returns the entire video.
Playback may still appear to work, making the bug harder to notice, but seeking becomes slower and bandwidth usage increases dramatically.
Check that your delivery chain correctly forwards and caches:
Range
If-Range
Content-Range
Accept-Ranges
Then test actual seeking in browser developer tools rather than checking only whether the video starts.
Keep playback and download permissions separate
A user being allowed to watch a video does not necessarily mean they should receive a convenient permanent download URL.
Model those permissions separately.
For example:
{
mediaId: "video_123",
canView: true,
canDownload: false
}
Playback authorization can produce access suitable for browser streaming.
Explicit downloads can go through another authorization path.
This is especially useful in systems where gallery owners can independently enable viewing and downloading.
Do not rely on hiding a download button as access control. If the browser can retrieve a resource, users can inspect network traffic. Security has to exist at the delivery layer.
Be careful with CDN caching
Signed URLs also interact with caching.
If every request includes a unique signature:
video.mp4?signature=abc
video.mp4?signature=def
video.mp4?signature=ghi
and the CDN includes the complete query string in its cache key, the same video may be cached repeatedly.
That destroys much of the benefit of using a CDN.
Depending on the CDN, you may be able to validate authorization parameters while excluding them from the underlying object cache key.
The goal is:
Authorization: per request
Cached media object: shared
not:
Authorization: per request
Cached media object: duplicated per token
This becomes particularly important with large video files.
Test the failure cases
Before considering private video delivery finished, test more than pressing play.
Try:
- seeking before the video has buffered
- seeking near the end of a large file
- refreshing after the token expires
- opening an expired media URL directly
- modifying the media path while keeping the signature
- disabling download permission while retaining playback permission
- loading several videos concurrently
- testing through the production CDN rather than only localhost
Media delivery tends to expose assumptions that normal API requests never reveal.
The underlying architecture is fairly simple once the responsibilities are separated: the application handles authorization, the CDN handles delivery, object storage holds the original media, and HTTP range requests remain intact throughout the path.
Getting those boundaries right produces private video playback that is both secure and fast without turning the application server into a video streaming service.
AI DISCLOSURE
This article was created with AI assistance and reviewed for technical accuracy before publication.
Top comments (0)