php 106 lines · 4 tabs

Generate Temporary Signed S3 Download URLs for Private Files in Laravel

Shared by codesnips Aug 2026
4 tabs
<?php

namespace App\Models;

use App\Support\TransientUrl;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Storage;

class Document extends Model
{
    protected $fillable = [
        'user_id',
        'disk',
        'storage_path',
        'original_name',
        'mime_type',
    ];

    public function temporaryDownloadUrl(int $minutes = 5): TransientUrl
    {
        $expiresAt = now()->addMinutes($minutes);

        $url = Storage::disk($this->disk)->temporaryUrl(
            $this->storage_path,
            $expiresAt,
            [
                'ResponseContentDisposition' => 'attachment; filename="' . addslashes($this->original_name) . '"',
                'ResponseContentType' => $this->mime_type ?: 'application/octet-stream',
            ]
        );

        return new TransientUrl($url, $expiresAt);
    }

    public function authorizedFor(?User $user): bool
    {
        return $user !== null && $user->getKey() === $this->user_id;
    }
}
4 files · php Explain with highlit

This snippet shows how a Laravel application hands out short-lived, presigned download links for files kept in a private S3 bucket, without ever streaming the bytes through the application server. The core idea is that the object stays private at rest, and the browser is redirected to a URL that S3 itself has cryptographically signed with an expiry. Once the clock passes that expiry, the link becomes useless, which limits the blast radius of a leaked URL.

The Document model represents an uploaded file and carries the disk and storage_path where the object actually lives. Its temporaryDownloadUrl method delegates to Laravel's Storage facade via Storage::disk(...)->temporaryUrl(...), passing an expiry and a set of ResponseContentDisposition and ResponseContentType response overrides. Those override parameters are baked into the signature, so S3 will return the object with a friendly filename and a forced attachment disposition even though the stored key is opaque. The authorizedFor helper centralizes the ownership check so the URL is never minted for the wrong user.

The DocumentDownloadController is deliberately thin. It resolves the Document through route-model binding, runs authorization with authorize, and then either redirects to the presigned URL or returns it as JSON depending on the Accept header. Splitting these two response shapes lets the same endpoint serve both a plain browser navigation and an XHR-driven frontend that wants the raw link. The TransientUrl value object wraps the URL and its expires_at, so callers get a self-describing payload rather than a bare string, which is handy for clients that display a countdown or cache the link.

A key trade-off is expiry length: too short and legitimate downloads of large files fail mid-transfer, too long and a shared link stays live longer than intended. Five minutes is a common default. Also note temporaryUrl only works on drivers that support signing — the s3 driver does, but the local driver needs Storage::temporaryUrl support enabled or it throws. Because signing happens in-process with the stored credentials, no extra round trip to AWS is required, making these calls cheap enough to generate per request.


Related snips

ruby
payload = {
  sub: user.id,
  iss: 'https://auth.example.com',
  aud: 'codesnips-api',
  exp: 15.minutes.from_now.to_i,
  iat: Time.now.to_i,

JWT issuance and verification without common footguns

jwt authentication api
by Kai Nakamura 2 tabs
ruby
class SignupForm
  include ActiveModel::Model
  include ActiveModel::Attributes

  attribute :account_name, :string
  attribute :email, :string

Shallow Controller, Deep Params: Form Object Pattern

rails activemodel form-object
by codesnips 3 tabs
javascript
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["form"]
  static values = { delay: { type: Number, default: 250 } }

Debounced live search with Stimulus + Turbo Streams

rails hotwire stimulus
by codesnips 4 tabs
bash
#!/usr/bin/env bash
set -euo pipefail

export VAULT_ADDR="https://vault.internal:8200"
export VAULT_TOKEN="${VAULT_TOKEN:?missing VAULT_TOKEN}"

Secrets management with environment isolation and Vault

secrets-management vault environment-variables
by Kai Nakamura 1 tab
php
<?php

namespace App\Providers;

use App\Contracts\PaymentGateway;
use App\Services\StripePaymentGateway;

Laravel service container and dependency injection

laravel dependency-injection service-container
by Carlos Mendez 2 tabs
typescript
import { randomBytes, createHash } from "crypto";
import jwt from "jsonwebtoken";
import { RefreshTokenStore } from "./store";

const ACCESS_SECRET = process.env.ACCESS_SECRET!;
const ACCESS_TTL = "15m";

JWT access + refresh token rotation (conceptual)

security node jwt
by codesnips 3 tabs

Share this code

Here's the card — post it anywhere.

Generate Temporary Signed S3 Download URLs for Private Files in Laravel — share card
Link copied