fix(auth): validate invitation magic link tokens

Accept invitation links across configured public origins while still
rejecting stored invitations whose token no longer matches.
This commit is contained in:
Andras Bacsai
2026-06-12 16:17:45 +02:00
parent 442ed98169
commit 4f509c02be
5 changed files with 99 additions and 6 deletions
+15 -3
View File
@@ -98,7 +98,7 @@ class Controller extends BaseController
public function link()
{
$token = request()->get('token');
if ($token) {
if (is_string($token) && $token !== '') {
try {
$decrypted = Crypt::decryptString($token);
} catch (DecryptException) {
@@ -126,9 +126,8 @@ class Controller extends BaseController
$invitation = TeamInvitation::query()
->where('email', $email)
->when($invitationUuid, fn ($query) => $query->where('uuid', $invitationUuid))
->where('link', request()->fullUrl())
->first();
if (! $invitation || ! $invitation->isValid()) {
if (! $invitation || ! $this->invitationLinkMatchesToken($invitation, $token) || ! $invitation->isValid()) {
return redirect()->route('login')->with('error', 'Invitation has expired or been revoked.');
}
@@ -152,6 +151,19 @@ class Controller extends BaseController
return redirect()->route('login')->with('error', 'Invalid credentials.');
}
private function invitationLinkMatchesToken(TeamInvitation $invitation, string $token): bool
{
$query = parse_url($invitation->link, PHP_URL_QUERY);
if (! is_string($query)) {
return false;
}
parse_str($query, $parameters);
$storedToken = $parameters['token'] ?? null;
return is_string($storedToken) && hash_equals($storedToken, $token);
}
public function showInvitation()
{
$invitationUuid = request()->route('uuid');