Security hardening: invited user onboarding flow (C1–N7)
- C1: verify.php now filters license='owner' — invite tokens no longer accepted - C1: onboarding API rejects non-owner sessions - C2: Existing-user invite requires explicit acceptance via accept_invite.php - C2: New accept_invite.php page and API engine added - C2: inviteUser() generates token + expiry for existing users; resendInvite() handles active users - C3: session_regenerate_id(true) before writing invite session keys on both invite pages - C4: invited_onboarding API wraps activation in transaction with SELECT FOR UPDATE; rowCount check added; SQLSTATE 23000 caught cleanly - C5: inviteUser() and resendInvite() two-table writes wrapped in transactions - M2: removeUser() wrapped in transaction with FOR UPDATE; clears default_company on active user removal - M4: Logged-in user guard added to invited_onboarding.php and accept_invite.php - M5: manage_users.php uses $server_url instead of HTTP_HOST for invite URLs - M6: Username regex enforces 3-32 chars; reserved name blocklist added - N5: searchUsers() changed from LIKE fuzzy search to exact email match only - N7: resendInvite() rate-limited to once per 60s via invite_resent_at column - Schema: company_map_user gains invite_expires_at and invite_resent_at columns Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
1904fea84c
commit
b4b1f5cbec
@@ -10,6 +10,34 @@
|
||||
exit;
|
||||
}
|
||||
|
||||
// Reject if a user is already logged in — opening an invite link in an active
|
||||
// session would bind a different account's identity into the current session.
|
||||
if (!empty($_SESSION['login_company_id'])) {
|
||||
require '../include_header.php';
|
||||
?>
|
||||
<body>
|
||||
<div class="container py-5" style="max-width:480px;">
|
||||
<div class="text-center mb-5">
|
||||
<a href="<?php echo $server_url?>login/index.php" class="d-inline-block mb-4">
|
||||
<img src="<?php echo $server_url?>assets/images/favicon.png" alt="" width="40"/>
|
||||
<span class="ms-2"><img src="<?php echo $server_url?>assets/images/logo.svg" alt=""></span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="card text-center">
|
||||
<div class="card-body p-5">
|
||||
<i class="ti ti-user-check text-warning mb-3" style="font-size:3rem;"></i>
|
||||
<h2 class="fs-4 mb-2">Already Signed In</h2>
|
||||
<p class="text-muted mb-4">You are already signed in. Please sign out first before accepting an invitation.</p>
|
||||
<a href="<?php echo $server_url?>login/index.php" class="btn btn-primary">Go to Dashboard</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
<?php
|
||||
exit;
|
||||
}
|
||||
|
||||
// Validate token — must match a pending invited user (license='user') that has not expired
|
||||
$sth = $pdo1->prepare(
|
||||
"SELECT u.user_id, u.email, u.verify_expires_at, c.company_name
|
||||
@@ -65,12 +93,12 @@
|
||||
exit;
|
||||
}
|
||||
|
||||
// Regenerate session ID before binding invite identity to prevent session fixation
|
||||
session_regenerate_id(true);
|
||||
|
||||
$_SESSION['invited_user_id'] = (int)$row['user_id'];
|
||||
$_SESSION['invited_token'] = $token;
|
||||
|
||||
if (empty($_SESSION['csrf_token'])) {
|
||||
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||
}
|
||||
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||
|
||||
$company_name = htmlspecialchars($row['company_name']);
|
||||
$invite_email = htmlspecialchars($row['email']);
|
||||
@@ -107,8 +135,8 @@
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Username <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" id="username" placeholder="Lowercase letters, numbers, underscores">
|
||||
<div class="form-text">Used to log in. Cannot be changed later.</div>
|
||||
<input type="text" class="form-control" id="username" placeholder="Lowercase letters, numbers, underscores" minlength="3" maxlength="32">
|
||||
<div class="form-text">3–32 characters. Used to log in. Cannot be changed later.</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Password <span class="text-danger">*</span></label>
|
||||
|
||||
Reference in New Issue
Block a user