Secure Team Invitations and Onboarding with Signed URLs in Laravel 11 & React
Onboarding team members into a SaaS platform requires a clean user experience and tight security. Allowing workspace owners to directly add emails works fine—until an invited user doesn't have an account yet, or an unaut
Onboarding team members into a SaaS platform requires a clean user experience and tight security. Allowing workspace owners to directly add emails works fine—until an invited user doesn't have an account yet, or an unauthorized link is intercepted.
In this tutorial, we’ll build a secure invitation workflow using Laravel 11 Signed URLs, tokenized invitation tables, and an auto-attachment session handler for new registrations.
1. Database Structure for Invitations
Create a dedicated team_invitations table to store pending invites:
Schema::create('team_invitations', function (Blueprint $table) {
$table->id();
$table->foreignId('team_id')->constrained()->cascadeOnDelete();
$table->string('email');
$table->string('role')->default('member');
$table->string('token', 32)->unique();
$table->timestamps();
});
2. Generating Tokenized Signed Links
When an owner sends an invite, generate a 32-character random token and build a signed URL that expires in 7 days:
use Illuminate\Support\Str;
use Illuminate\Support\Facades\URL;
$token = Str::random(32);
$invitation = TeamInvitation::create([
'team_id' => $currentTeam->id,
'email' => $request->email,
'role' => $request->role,
'token' => $token,
]);
// Build tamper-proof signed link
$acceptUrl = URL::signedRoute('invitations.accept', [
'token' => $invitation->token
], now()->addDays(7));
// Send mailable or queued notification with $acceptUrl...
3. Handling Unauthenticated & New Users
When a user clicks the invitation link, verify the signature and token in InvitationController.php:
public function accept(Request $request, string $token)
{
// 1. Verify URL signature
if (! $request->hasValidSignature()) {
abort(401, 'This invitation link has expired or is invalid.');
}
$invitation = TeamInvitation::where('token', $token)->firstOrFail();
// 2. If user is logged in, attach immediately
if (auth()->check()) {
$this->attachUserToTeam(auth()->user(), $invitation);
return redirect()->route('dashboard')->with('success', 'Joined workspace successfully!');
}
// 3. If unauthenticated, store token in session and send to registration
session(['pending_invitation_token' => $invitation->token]);
return redirect()->route('register')->with('info', 'Please create an account to accept your invitation.');
}
4. Auto-Attaching on Registration
Inside RegisteredUserController.php, check for the session token immediately after user creation:
$user = User::create([ ... ]);
event(new Registered($user));
Auth::login($user);
// Check for pending invitation token in session
if (session()->has('pending_invitation_token')) {
$token = session()->pull('pending_invitation_token');
$invitation = TeamInvitation::where('token',$token)->first();
if ($invitation) {
// Attach user with assigned pivot role
$user->teams()->attach($invitation->team_id, ['role' => $invitation->role]);$user->update(['current_team_id' => $invitation->team_id]);$invitation->delete();
}
}
return redirect()->route('dashboard');
Summary
Using signed URLs guarantees that invitation parameters cannot be modified in the browser address bar, while session bridging provides a seamless experience for brand-new users.
💡 Looking for a pre-built solution?
Get this entire invitation workflow along with multi-tenancy, team switching, and Stripe billing in the Pro Laravel 11 + React SaaS Starter Kit on Gumroad or explore the free open-source base on GitHub.
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.