How to Secure API-Generated Video Embed Links in Laravel and Prevent Sharing
Learn how to generate expiring, single-use video embed links in Laravel APIs. Prevent users from sharing protected video content with token-based access control.
A common challenge in Laravel applications is generating secure embed links for paid video content. The workflow seems straightforward: a user pays, hits a watch page, your API generates an embed URL from a hosting provider like UIshare or Vimeo, and returns it to the client. But there is a problem: if that embed URL is a static link, the user can share it with anyone, and your paid content protection is gone.
This guide walks through a Laravel implementation that generates expiring, single-use, device-bound embed links so your protected video content stays protected.
The Problem with Static Embed Links
Most video hosting platforms generate a standard embed URL that looks something like this:
https://player.vimeo.com/video/123456789
https://uisha.re/embed/abc123def These URLs are static and permanent. Once your Laravel backend returns one to the client, the user can copy it, share it, or embed it anywhere. The only thing preventing sharing is obscurity, and that is not a security strategy.
The Solution: Token-Based Access Control
Instead of returning the embed URL directly, you create a proxy endpoint in Laravel that validates access before serving the embed. Here is the architecture:
- User purchases access and lands on the watch page
- Your backend verifies their entitlement
- Generate a signed, expiring token tied to the user and the specific video
- Return the token (not the embed URL) to the frontend
- The frontend uses the token to request the actual embed URL via a secure endpoint
- Your backend validates the token and returns a short-lived proxy URL
Implementation
Step 1: Create the Token Generation
Laravel's built-in URL signing is perfect for this. You do not need any external packages:
<?php
namespace App\Services;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\Str;
class VideoAccessService
{
public function generateAccessToken(string $userId, string $videoId): string
{
// Create a signed URL with a 30-minute expiration
// tied to the specific user and video
$signedUrl = URL::temporarySignedRoute(
'video.proxy',
now()->addMinutes(30),
[
'user' => $userId,
'video' => $videoId,
'nonce' => Str::random(32),
]
);
// Optionally store the token in cache for revocation capability
cache()->set(
"video_access:{$userId}:{$videoId}",
true,
now()->addMinutes(30)
);
return $signedUrl;
}
} Step 2: Create the Proxy Route and Controller
<?php
// routes/web.php
Route::get('/video/proxy/{user}/{video}', 'VideoController@proxy')
->name('video.proxy')
->middleware('signed'); // Laravel's built-in signed URL validation <?php
namespace App\Http\Controllers;
use App\Models\Video;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
class VideoController extends Controller
{
public function proxy(Request $request, User $user, string $videoId)
{
// Verify user entitlement
if (!$user->hasAccessToVideo($videoId)) {
abort(403, 'Access revoked');
}
// Check cache for revocation
if (!Cache::get("video_access:{$user->id}:{$videoId}")) {
abort(410, 'Token expired or revoked');
}
// Verify it is the same user making the request
if ($request->user()->id !== $user->id) {
abort(403, 'Token belongs to another user');
}
// Generate a short-lived embed URL from your video provider
$embedUrl = $this->generateEmbedUrl($videoId, 5); // 5-minute TTL
// Delete the cache entry to make it single-use
Cache::forget("video_access:{$user->id}:{$videoId}");
return response()->json([
'embed_url' => $embedUrl,
'expires_at' => now()->addMinutes(5)->toIso8601String(),
]);
}
private function generateEmbedUrl(string $videoId, int $ttlMinutes): string
{
// Call your video provider's API to generate a time-limited embed URL
// Example with a generic API:
$response = Http::withToken(config('services.videoprovider.key'))
->post('https://api.videoprovider.com/embed', [
'video_id' => $videoId,
'expires_in' => $ttlMinutes * 60,
'domain_restriction' => config('app.url'),
]);
return $response->json('embed_url');
}
} Step 3: Frontend Integration
On the client side, request the token from your API, then use it to fetch the actual embed URL:
// Frontend JavaScript (or Vue/React)
async function loadVideo(videoId) {
// Step 1: Request an access token from your API
const tokenResponse = await fetch('/api/video/request-token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ video_id: videoId }),
});
const { token } = await tokenResponse.json();
// Step 2: Use the token to get the actual embed URL
const proxyResponse = await fetch(token);
const { embed_url, expires_at } = await proxyResponse.json();
// Step 3: Load the embed
const iframe = document.getElementById('video-player');
iframe.src = embed_url;
} Additional Security Layers
Domain Restriction
Configure your video hosting provider to only serve the embed from your domain. Most providers support this:
// Vimeo: Set allowed domains in the video settings
// UIshare: Pass domain_restriction in the API call IP Binding
Bind the token to the user's IP address at generation time and verify it at consumption time:
<?php
public function generateAccessToken(string $userId, string $videoId, string $ip): string
{
return URL::temporarySignedRoute(
'video.proxy',
now()->addMinutes(30),
[
'user' => $userId,
'video' => $videoId,
'ip' => $ip, // Bind to IP
'nonce' => Str::random(32),
]
);
}
// In the proxy controller:
if ($request->ip() !== $request->route('ip')) {
abort(403, 'IP address mismatch');
} User Agent Fingerprinting
For stricter security, combine IP binding with user agent fingerprinting. This makes it significantly harder for someone to share the token with another person.
Handling the Sharing Problem
The original question asked how to prevent users from sharing the embed link. Here is how each layer helps:
- Expiring tokens — the embed link is only valid for a few minutes, making shared links useless quickly
- Single-use tokens — once the embed is loaded, the token cannot be reused
- IP binding — the token only works from the original requester's IP
- Domain restriction — the embed only renders on your domain
- Revocation cache — you can invalidate all tokens for a user if you detect abuse
Conclusion
Protecting paid video content in a Laravel application does not require a third-party DRM service. By combining Laravel's built-in signed routes with short-lived tokens, IP binding, and domain restriction, you can build a robust access control system that prevents unauthorized sharing of your embed links.
The key principle: never return a static embed URL to the client. Always proxy through your backend with time-limited, revocable tokens.
Stefan
SEO engineer and Laravel developer. Building tools to help Laravel applications rank higher in search results.