imanghafoori/laravel-tokenize-login
Token-based, passwordless login for Laravel: generate secure, expiring tokens and signed URLs that authenticate users without passwords. Ideal for magic-link sign-in, one-time access, and API-style flows, with drop-in integration into Laravel's auth system and configurable token lifetimes.
Install and publish.
composer require imanghafoori/laravel-tokenized-login
php artisan vendor:publish --provider="Imanghafoori\\TokenizedLogin\\TokenizedLoginServiceProvider"
Publish both the config (config/tokenized-login.php) and the migration. Run php artisan migrate so the token column/table exists before you wire the UI.
Look here first, in this order:
config/tokenized-login.php — token lifetime, length, and the token_sender class key are the knobs you will actually touch.vendor/imanghafoori/laravel-tokenized-login/src/ — the TokenSender contract and the trait are tiny; read them before customizing. Confirm exact method names (sendLoginToken, verifyLoginToken, etc.) in the source rather than trusting a blog post.routes/ file — this is where the "request token" and "verify token" endpoints live, if they ship enabled by default. If not, you write the controller yourself and the package only gives you the trait + contract.Bind your sender. The package does not send mail or SMS by itself; it delegates. Create app/TokenSenders/EmailTokenSender.php implementing the package contract, then bind it in AppServiceProvider::register() (or point the config key at the class, depending on which the published config exposes).
Add the trait to your User model and make sure the columns the trait expects (token, token_created_at, or whatever the migration adds) are not in $guarded/are in $fillable as required.
First end-to-end use case: a two-step login page. Step 1 collects the email, calls the send endpoint (or $user->sendLoginToken() directly), step 2 collects the token, calls verify, then Auth::login($user) on success and redirect. Get this working on a single guard before you touch anything else.
Two-step controller, written by you. Skip the package's routes and own the flow end-to-end:
public function send(Request $request) {
$request->validate(['email' => 'required|email']);
$user = User::where('email', $request->email)->first();
if ($user) { $user->sendLoginToken(); }
return back()->with('status', 'If that account exists, a code is on its way.');
}
public function verify(Request $request) {
$request->validate(['email' => 'required|email', 'token' => 'required|string']);
$user = User::where('email', $request->email)->first();
abort_unless($user && $user->verifyLoginToken($request->token), 422);
Auth::login($user, $request->boolean('remember'));
$request->session()->regenerate();
return redirect()->intended('/dashboard');
}
Owning the controller means you control throttling, enumeration messages, and where the user lands — all things you will need to tune.
Swap the sender per channel. Implement EmailTokenSender and SmsTokenSender both against the same contract, then choose at runtime (config is static, so the cleaner pattern is to bind a CompositeTokenSender that inspects $user->phone/$user->email and dispatches). This keeps you on one trait method while supporting multiple login identifiers.
Multi-guard. The trait works on any authenticatable model, so Auth::guard('admin')->login($admin) after a successful verify is enough — you don't need separate endpoints per guard if the guard is inferred from the request's route group.
Rate limit outside the package. Add ->middleware('throttle:5,1') to the send route and throttle:10,1 to verify. The package predates the RateLimiter facade's current ergonomics and likely does not throttle by default; brute-forcing a 5-digit token is trivial without this.
Extend via events, not edits. If you need audit logging of login attempts, wrap the send/verify calls in your controller with your own event dispatch — do not fork the trait. The trait is a single file; keeping it pristine means composer update stays safe.
Testing. If your sender wraps Mail::to()->send() or a notification, Notification::fake() / Mail::fake() is enough. If it wraps an SMS SDK, bind a fake implementation of the contract in TestCase::setUp() so tests never hit the network. Assert both the happy path and that a re-used token is rejected.
hash_equals on verify. Either way, verify the comparison is timing-safe — == on a 5-digit code is a real weakness at scale.$user->clearLoginToken() (or equivalent) call in your controller immediately after Auth::login().token_lifetime is almost certainly in minutes, not seconds. Grep the trait for ->addMinutes / ->addSeconds before you set it to 300 thinking "5 minutes."/login. If the package registers routes, they may shadow or be shadowed by your app's auth routes. Run php artisan route:list | grep -i token and reconcile before debugging any 404/405.illuminate/* constraint in your own composer.json or fork the repo; expect composer update to complain. Zero dependents means no one is filing these bugs upstream — pin the version and treat upgrades as a manual review.Auth::login() before session regenerate. Call $request->session()->regenerate() on successful verify to prevent session fixation; the trait won't do this for you.Log::error() the exception — if the package catches sender exceptions internally, a misconfigured mailer looks identical to a successful send, and you'll chase the wrong bug for an hour.User methods (sendLoginToken, verifyLoginToken). Run php -l / a quick php artisan tinker to confirm; if they clash, alias with insteadof in your model.vendor/.How can I help you explore Laravel packages today?