Building Secure Video Streaming with Signed URLs

Building Secure Video Streaming with Signed URLs

Summary:

To protect video content on an OTT platform, I implemented secure video streaming using signed URLs with Symfony and Bunny CDN. This approach ensures only authorised users can access the videos, preventing unauthorised sharing and piracy. The solution enhances content security and helps maintain revenue integrity.

Video streaming platforms, like an OTT video platform I’ve recently worked on, need to ensure that content is accessible only to authorised users. Without proper security, videos can be easily accessed or shared, leading to revenue loss or piracy. One effective way to secure video streaming is by using signed URLs, a method I implemented on this project using Symfony and Bunny CDN. In this blog, I’ll walk you through how I built this solution and share some best practices I learned along the way.

Why Signed URLs for Video Streaming?

Signed URLs are temporary, secure links that grant access to a resource like a video file for a limited time. They include a token hashed value that the CDN validates before serving the content. This ensures that only users with a valid URL can access the video, and the URL expires after a set period, reducing the risk of unauthorised sharing.

On this OTT video platform, we use Bunny CDN to deliver videos. The videos are stored in the CDN, and their URLS are saved in our database. Without security, anyone with the raw CDN URL could access the videos. Signed URLs add a layer of protection by requiring a valid token and expiration time, which we generate dynamically for each user request.

Implementing Signed URLs in Symfony

Let’s explore how I implemented this solution, focusing on the key steps and logic.

Generating Signed URLs

The main task is to generate a signed URL for each video request. In our Symfony app, I handle this in a controller. Here’s the process:

  • Retrieve the Video URL: First, I fetch the video’s CDN URL from the database. This URL points to CDN, like https://main12X.b-cdn.net/ScriptServer5/MOVIESbest4/video.mp4.

Generate the Signed URL: I use a function called generateSignedUrl to create the signed URL by adding a token and expiration time.

Here’s the generic code snippet from the controller :

$videoUrl = $this->getVideoUrl($id); // Fetch the CDN URL from the database

$securityKey = $this->getParameter('video_security_key'); // Secret key from the CDN provider

$signedUrl = $this->generateSignedUrl(

$videoUrl,

$securityKey,

300, // 5-minute expiration

$request->getClientIp(), // User's IP for validation

$request->headers->get('Referer') // Referer for additional security

);

function generateSignedUrl($videoUrl, $securityKey, $expirationTime, $userIp, $referer) 
{
  // Calculate the expiration timestamp (current time + expiration time in seconds)
  $expires = time() + $expirationTime;
  // Extract the path from the video URL (e.g., /path/to/video.mp4)
  $urlParts = parse_url($videoUrl);
  $path = $urlParts[‘path’];
  // Combine the security key, URL path, expiration, IP, and referer to create a hash string
  $hashableString = $securityKey . $path . $expires . $userIp . $referer;
  $token = hash(‘sha256’, $hashableString, true); //generate token using sha256 hash
  $token = base64_encode($token);
  $token = strtr($token, ‘+/’, ‘-_’); // Replace + and / with – and _ for URL safety
  $token = str_replace(‘=’, ”, $token); // Remove padding characters
  return $videoUrl . “?token=$token&expires=$expires”;//append token to url
}

This generates a URL like https://main12X.b-cdn.net/ScriptServer5/MOVIESbest4/video.mp4?token=abc123&expires=1713930000. The token ensures the URL is secure, and the 5-minute expiration limits its usability.

Using the Signed URL in the Video Player

Once the signed URL is generated, I pass it to the frontend through a template. The video player uses this URL directly to stream the video from CDN. Here’s the relevant part of the template:

<video id="my-video" class="video-js" controls preload="auto">

    {% if signedUrl %}

        <source src="{{ signedUrl }}" type="video/mp4">

    {% else %}

        <p>Error: Video URL not available.</p>

    {% endif %}

</video>

The video player makes requests directly to CDN using the signed URL. CDN validates the token and expiration time before serving the video.

Keeping the Session Alive with Heartbeats

To ensure the user’s session remains active while streaming, I implemented a heartbeat mechanism. Every 5 seconds, the frontend sends a request to a session validation endpoint in the controller. The endpoint checks if the session is valid and logs the activity:

public function validateSession($videoId, Request $request)

{

    if (!$this->session->getId()) {

        $this->get('logger')->error("Session validation failed for videoId: $videoId, no active session");

        return new Response('Unauthorized', 403);

    }

    $this->session->set('last_heartbeat_' . $videoId, time());

    return new Response('OK');

}

In the frontend, I use JavaScript to call this endpoint periodically:

function sendHeartbeat(videoId, signedUrl) {

    fetch(`/video/${videoId}/heartbeat?signedUrl=${encodeURIComponent(signedUrl)}`, {

        method: 'POST',

        credentials: 'include'

    })

        .then(response => response.text())

        .then(data => console.log('Heartbeat success:', data))

        .catch(error => console.error('Heartbeat error:', error));

}

setInterval(() => sendHeartbeat(videoId, signedUrl), 5000);

Best Practices for Signed URLs

Here are some tips I learned while working on this:

  • Use Short Expiration Times: I set the expiration to 5 minutes (300 seconds). This minimizes the risk of URL sharing but still allows enough time for the video player to buffer and stream.
  • Validate IP and Referer: Including the user’s IP and referer in the signed URL, which adds an extra layer of security.
  • Test with the CDN’s Authentication Enabled: Make sure your CDN’s token authentication is enabled in the dashboard, or the signed URLs won’t be validated.
  • Log and Monitor: Use logging to debug issues and monitor usage.

Conclusion

Implementing signed URLs for secure video streaming on this OTT video platform was a rewarding experience. It allowed us to protect our content while ensuring a seamless streaming experience for users. By generating signed URLs in Symfony, integrating them with CDN, and handling session validation with heartbeats, we built a robust solution that balances security and usability.

If you’re working on a similar project, I hope this guide helps!

Anand Dattani

Author

Anand Dattani is a Senior Developer at BrainStream Technolabs. He is an experienced developer specializing in PHP and modern MVC frameworks, with expertise in backend architecture and building scalable web solutions.

Related Blog

Symfony

How Symfony Can Add Values in Your Music Streaming Web App?

You’ve been given a unique opportunity to fit into a booming music streaming app industry that probably no one else will, so if you’ve been thinking about developing a music web app, it’s time to kickstart. Almost, approximately 77% of...

Symfony

How Good is Symfony Framework for Your Startup Project?

Likewise, choosing the right development technology is very important if you want to start a startup. It is the very first crucial step you need to take while heading over to the way to success and growth. A good development company can help you...

newslatter_bg_image
newslatter_image

Keep up-to-date with our newsletter.

Sign up for our newsletter to receive weekly updates and news directly to your inbox.