Match OIDC users by sub claim, auto-enroll, sync admin from groups
Switch from email-based OIDC matching to the stable `sub` claim. Existing users are migrated by email on first login, new users are auto-enrolled from OIDC claims, and admin status is synced from the IdP's groups claim. Also expose oidc_sub on the admin edit-user page. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -29,7 +29,7 @@ class OIDCLogin
|
||||
|
||||
$oidc = new OpenIDConnectClient(OIDC_PROVIDER_URL, OIDC_CLIENT_ID, OIDC_CLIENT_SECRET);
|
||||
$oidc->setRedirectURL(BASEURL . '/oidclogin/');
|
||||
$oidc->addScope(['openid', 'email']);
|
||||
$oidc->addScope(['openid', 'email', 'profile', 'groups']);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -42,22 +42,62 @@ class OIDCLogin
|
||||
exit;
|
||||
}
|
||||
|
||||
$email = $oidc->requestUserInfo('email');
|
||||
if (empty($email))
|
||||
// Get the stable subject identifier from the ID token.
|
||||
$sub = $oidc->getVerifiedClaims('sub');
|
||||
if (empty($sub))
|
||||
{
|
||||
$_SESSION['login_msg'] = ['', 'No email address received from OIDC provider.', 'danger'];
|
||||
$_SESSION['login_msg'] = ['', 'No subject identifier received from OIDC provider.', 'danger'];
|
||||
header('Location: ' . BASEURL . '/login/');
|
||||
exit;
|
||||
}
|
||||
|
||||
$user = Member::fromEmailAddress($email);
|
||||
// Step 1: Look up user by oidc_sub.
|
||||
$user = Member::fromOidcSub($sub);
|
||||
|
||||
// Step 2: One-time email migration — link existing account by email.
|
||||
if ($user === null || $user === false)
|
||||
{
|
||||
$_SESSION['login_msg'] = ['', 'No account found for this email address. Please contact an administrator.', 'danger'];
|
||||
header('Location: ' . BASEURL . '/login/');
|
||||
exit;
|
||||
$email = $oidc->requestUserInfo('email');
|
||||
if (!empty($email))
|
||||
{
|
||||
$user = Member::fromEmailAddress($email);
|
||||
if ($user !== null && $user !== false)
|
||||
$user->update(['oidc_sub' => $sub]);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: Auto-enroll — create a new user from OIDC claims.
|
||||
if ($user === null || $user === false)
|
||||
{
|
||||
$first_name = $oidc->requestUserInfo('given_name') ?: '';
|
||||
$last_name = $oidc->requestUserInfo('family_name') ?: '';
|
||||
$email = $oidc->requestUserInfo('email') ?: $sub . '@oidc.placeholder';
|
||||
|
||||
$slug = $this->generateSlug($first_name, $last_name);
|
||||
|
||||
$user = Member::createNew([
|
||||
'first_name' => $first_name ?: 'OIDC',
|
||||
'surname' => $last_name ?: 'User',
|
||||
'slug' => $slug,
|
||||
'emailaddress' => $email,
|
||||
'oidc_sub' => $sub,
|
||||
]);
|
||||
|
||||
if ($user === false)
|
||||
{
|
||||
$_SESSION['login_msg'] = ['', 'Failed to create account. Please contact an administrator.', 'danger'];
|
||||
header('Location: ' . BASEURL . '/login/');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: Sync admin status from groups claim.
|
||||
$groups = $oidc->requestUserInfo('groups');
|
||||
$should_be_admin = is_array($groups) && in_array(OIDC_ADMIN_GROUP, $groups);
|
||||
if ($should_be_admin !== $user->isAdmin())
|
||||
$user->update(['is_admin' => $should_be_admin ? 1 : 0]);
|
||||
|
||||
// Set session and redirect.
|
||||
$_SESSION['user_id'] = $user->getUserId();
|
||||
|
||||
if (!empty($_SESSION['oidc_redirect_url']))
|
||||
@@ -77,4 +117,36 @@ class OIDCLogin
|
||||
|
||||
exit;
|
||||
}
|
||||
|
||||
private function generateSlug($first_name, $last_name)
|
||||
{
|
||||
$base = trim($first_name . ' ' . $last_name);
|
||||
if (empty($base))
|
||||
$base = 'user';
|
||||
|
||||
$slug = strtolower(preg_replace('/[^a-zA-Z0-9]+/', '-', $base));
|
||||
$slug = trim($slug, '-');
|
||||
if (empty($slug))
|
||||
$slug = 'user';
|
||||
|
||||
// Check if slug is available.
|
||||
$candidate = $slug;
|
||||
for ($i = 0; $i < 10; $i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
Member::fromSlug($candidate);
|
||||
// Slug taken — append random suffix.
|
||||
$candidate = $slug . '-' . bin2hex(random_bytes(3));
|
||||
}
|
||||
catch (NotFoundException $e)
|
||||
{
|
||||
// Slug is available.
|
||||
return $candidate;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: use sub-based slug.
|
||||
return 'user-' . bin2hex(random_bytes(4));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user