Skip to main content

Authorizing a gated resource without login

This is a passwordless login package, but the same single-use, hashed-at-rest token can authorize access to one specific resource — a one-time file download, a gated view — without logging anyone in and without a serialized payload. Mint a token, put the raw token on your own route, and consume it there.

Mint the token

use EmailMagicLink\Contracts\TokenStore;
use Illuminate\Support\Facades\URL;

// Mint a single-use token for the user — nothing is sent.
$issued = app(TokenStore::class)->issue($user, config('auth.defaults.guard'), 'link');

// Build a link to YOUR OWN route with the raw token. It is a 256-bit unguessable
// secret; sign the route as well for URL-level expiry and tamper-resistance.
$url = URL::temporarySignedRoute('invoices.download', $issued->record->expires_at, [
'token' => $issued->plaintext,
]);
// deliver $url however you like — email, SMS, chat …

Consume it on your own route

// On your route, authorize by consuming the token — without logging anyone in.
Route::get('/invoices/download', function (Request $request) {
$token = (string) $request->query('token');
$result = app(TokenStore::class)->claimLink($token);

abort_unless($result->successful, 403);

// The claim is atomic and single-use; serve the resource for the token's user.
return Storage::download(invoicePathFor($result->token->user_id));
})->middleware('signed')->name('invoices.download');

Why this works

Because you call claimLink() yourself instead of the bundled consume flow, no session is created — a successful claim is simply your authorization to serve the resource. The token is consumed atomically, stored only as a hash, and expires on its own; a second visit fails exactly like any spent link.

Variations

  • Allow a bounded number of downloads by minting with issue($user, $guard, 'link', maxUses: 3) — the same atomic decrement described under multi-use links.
  • Prefer a short code to a URL token with EmailMagicLink::issueCode($user), which returns a raw code you can pass around instead. Claim it with claimCode($user, $code, $guard).
  • Add a shared secret by minting with a passphrase and verifying it before you serve — see passphrase-gated links.

The TokenStore signatures and what each guarantees are in the contract reference.