Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Handoff Laravel Package

clarkewing/handoff

A Laravel package for handing off work or state between users, jobs, or requests. Provides a straightforward API to pass things along cleanly without custom glue code. Lightweight, framework-native, and quick to set up.

View on GitHub
Deep Wiki
Context7

Getting Started

Handoff lets a Laravel app delegate authentication to a single "central" domain instead of maintaining sessions on every subdomain/tenant host. v0.2.0 is the first release that supports unauthenticated visitors (guests), so it is now usable as the entry point for login rather than only as a session-bridge for already-authenticated users.

Minimal setup:

composer require clarkewing/handoff
php artisan vendor:publish --tag="handoff-config"

Then in config/handoff.php, set the two values that everything else derives from:

  • the central domain that owns the login form (e.g. auth.example.com)
  • the return/handoff domain where the callback lands (e.g. app.example.com)

Register the middleware on the routes that require a session — typically a web group or a tenant route group — and add the package's route registration (service provider / Route::handoff() depending on the version) so the /handoff endpoint exists on the correct host.

First use case to try: hit a protected page on the secondary domain while logged out. You should be bounced to the central domain's login, authenticate there, and land back on the original URL with a valid local session. If that round trip works, the rest is configuration.

Look first at the published config file and the registered route list (php artisan route:list --path=handoff) — the new domain constraint means the route only appears for one host, which is the usual source of "why is this 404ing" confusion.

Implementation Patterns

Middleware on the consuming domain. The primary pattern is unchanged: protect routes with the handle package middleware, and let it decide between "already have a session → continue" and "no session → redirect to central domain with a signed return payload."

Route::middleware(['web', 'handoff'])->group(function () {
    Route::get('/dashboard', DashboardController::class);
});

Guest login flow (new in v0.2.0). Previously the package assumed an authenticated user on the central domain. The guest flow means an anonymous visitor can be pushed to the central domain, complete login there, and be handed back — no separate "log in over here first" instruction needed. Wire it by using the guest variant of the handoff redirect on your login route instead of Laravel's default redirect()->route('login'):

Route::get('/login', fn () => redirect()->away(
    handoff()->guestRedirectUrl(request()->fullUrl())
))->name('login');

Keep request()->fullUrl() intact through the round trip — the intended-destination behaviour is what makes the flow feel seamless, and it is the part most likely to be silently dropped if you build the redirect URL by hand.

/handoff domain constraint. The callback route is now constrained to a single host. That is deliberate: it stops the token-accepting endpoint from being reachable on tenant domains (where an attacker-controlled host could otherwise collect handoff tokens) and removes the need for you to hand-write Route::domain(...) guards. Practically, it means:

  • Local dev needs the configured domain in /etc/hosts (e.g. 127.0.0.1 app.test auth.test), not localhost.
  • Test suites must issue requests with an explicit host, e.g. $this->get('https://app.test/handoff?...'), or the route won't resolve.
  • If you run tenants on wildcard subdomains, do not set the handoff domain to a wildcard — it should be one fixed host.

Central-domain responsibilities. On the auth host you own the login controller (Fortify, Breeze, or hand-rolled). After a successful Auth::attempt(), hand off back to the originating domain with the package's redirect helper rather than a plain redirect()->intended(). Treat the central domain as stateless with respect to the tenant: authenticate, mint the handoff, redirect, forget.

Multi-guard / multi-tenant setups. If different domains map to different guards or tenant databases, resolve the guard before the handoff redirect and encode the tenant identifier in the return payload so the callback can restore the right connection. Don't infer the tenant from the Referer header.

Gotchas and Tips

Route caching bakes in the domain. php artisan route:cache stores the resolved domain constraint. Change handoff.domain and forget to re-cache and you'll get a 404 on /handoff with no obvious error. Always route:clear && route:cache in deploy scripts.

Session domain mismatches. SESSION_DOMAIN must be consistent with how you want cookies scoped. If you set it to .example.com, the tenant already shares a cookie with the central domain and the handoff becomes partly redundant — and worse, cookie collisions across tenants. Scope sessions per-host and let handoff do the work.

Signed URL expiry and single use. Handoff tokens should be short-lived (seconds, not minutes) and consumed once. If you see intermittent "already authenticated but bounced back to login" reports, it's almost always a reused token or a redirect chain that fires the callback twice (e.g. a trailing-slash redirect in front of the handoff route). Confirm the callback URL is canonical and registered exactly once.

HTTPS and secure cookies. Cross-domain redirects with Secure + SameSite=Lax cookies behave differently over plain HTTP. Local dev over http://auth.test will work while staging silently fails, or vice versa. Test the flow on the real scheme early.

Loop detection. A misconfigured central domain that itself has the handoff middleware applied will redirect to itself forever. The middleware must be absent (or explicitly disabled) on the auth host's login routes.

Debugging. Log the generated handoff URL and the validated payload (destination host, user id, expiry) at debug level. Nearly every failure is one of: wrong domain in config, expired signature, or the destination host not matching an allow-list. Fail closed on an unrecognised destination host rather than redirecting to it.

Extension points. The published config and the redirect/payload generation are the seams to reach for: subclass or rebind the handoff service if you need to carry extra context (impersonation flags, locale, tenant id), and listen for the package's authentication event if you need to audit cross-domain logins. Prefer extending the payload over adding query parameters to the redirect URL, which will invalidate the signature.

Upgrade note. v0.2.0 is additive — the guest flow and domain constraint don't break the v0.1.x authenticated-user path, but if you had manually registered a /handoff route or hand-rolled a Route::domain() wrapper, remove it. Two definitions of the same path on different hosts is a confusing failure mode, and the built-in constraint is the safer default.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings