fix: handle SSO email collision by linking existing local users
All checks were successful
CI Pipeline / Rust Format Check (push) Successful in 6s
CI Pipeline / Clippy Lints (push) Successful in 52s
CI Pipeline / Rust Unit Tests (push) Successful in 1m9s
CI Pipeline / Security Audit (push) Successful in 4s
CI Pipeline / Frontend Lint & Type Check (push) Successful in 13s
CI Pipeline / Build .deb & Release (push) Has been skipped
All checks were successful
CI Pipeline / Rust Format Check (push) Successful in 6s
CI Pipeline / Clippy Lints (push) Successful in 52s
CI Pipeline / Rust Unit Tests (push) Successful in 1m9s
CI Pipeline / Security Audit (push) Successful in 4s
CI Pipeline / Frontend Lint & Type Check (push) Successful in 13s
CI Pipeline / Build .deb & Release (push) Has been skipped
When a user already exists with auth_provider=local and the same email, the SSO callback now links the existing user to the new SSO provider instead of failing with a unique constraint violation on email. Flow: 1) Try exact match (email+auth_provider), 2) If not found, try email-only lookup, 3) If found with different provider, link by updating auth_provider, azure_oid, oidc_sub, 4) If not found, create new user as before.
This commit is contained in:
@ -416,6 +416,7 @@ async fn sso_callback(
|
||||
_ => "oidc",
|
||||
};
|
||||
|
||||
// First try exact match: email AND auth_provider
|
||||
let user_opt: Option<DbUserForSso> = match sqlx::query_as(
|
||||
r#"SELECT id, username, display_name, role, is_active, mfa_enabled
|
||||
FROM users WHERE email = $1 AND auth_provider = $2"#,
|
||||
@ -438,6 +439,70 @@ async fn sso_callback(
|
||||
},
|
||||
Some(u) => u,
|
||||
None => {
|
||||
// Try to find existing user by email alone (may have different auth_provider)
|
||||
let existing_user: Option<DbUserForSso> = match sqlx::query_as(
|
||||
r#"SELECT id, username, display_name, role, is_active, mfa_enabled
|
||||
FROM users WHERE email = $1"#,
|
||||
)
|
||||
.bind(&email)
|
||||
.fetch_optional(&state.db)
|
||||
.await
|
||||
{
|
||||
Ok(o) => o,
|
||||
Err(e) => {
|
||||
tracing::error!(error = %e, "Failed to look up existing user by email");
|
||||
return Err(error_redirect("internal_error", "Database error"));
|
||||
},
|
||||
};
|
||||
|
||||
match existing_user {
|
||||
Some(existing) if !existing.is_active => {
|
||||
return Err(error_redirect("account_disabled", "Account is disabled"));
|
||||
},
|
||||
Some(existing) => {
|
||||
// Link existing local user to SSO provider
|
||||
tracing::info!(user_id = %existing.id, "Linking existing user to SSO provider");
|
||||
if let Err(e) = sqlx::query(
|
||||
"UPDATE users SET auth_provider = $1, azure_oid = COALESCE(azure_oid, $2), oidc_sub = COALESCE(oidc_sub, $3) WHERE id = $4",
|
||||
)
|
||||
.bind(auth_provider)
|
||||
.bind(if azure_oid.is_empty() { None } else { Some(azure_oid.as_str()) })
|
||||
.bind(if provider_subject.is_empty() { None } else { Some(provider_subject.as_str()) })
|
||||
.bind(existing.id)
|
||||
.execute(&state.db)
|
||||
.await
|
||||
{
|
||||
tracing::error!(error = %e, "Failed to link user to SSO provider");
|
||||
return Err(error_redirect("internal_error", "Failed to link SSO account"));
|
||||
}
|
||||
|
||||
log_event(
|
||||
&state.db,
|
||||
AuditAction::UserCreated,
|
||||
None,
|
||||
Some(auth_provider),
|
||||
Some("user"),
|
||||
Some(&existing.id.to_string()),
|
||||
json!({ "action": "sso_link", "auth_provider": auth_provider, "email": email }),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
DbUserForSso {
|
||||
id: existing.id,
|
||||
username: existing.username.clone(),
|
||||
display_name: name
|
||||
.is_empty()
|
||||
.then(|| existing.display_name.clone())
|
||||
.unwrap_or(name),
|
||||
role: existing.role.clone(),
|
||||
is_active: existing.is_active,
|
||||
mfa_enabled: existing.mfa_enabled,
|
||||
}
|
||||
},
|
||||
None => {
|
||||
// No existing user - create new one
|
||||
let id: Uuid = match sqlx::query_scalar(
|
||||
r#"INSERT INTO users (username, display_name, email, role, auth_provider, azure_oid, oidc_sub)
|
||||
VALUES ($1, $2, $3, 'operator', $4, $5, $6)
|
||||
@ -481,6 +546,8 @@ async fn sso_callback(
|
||||
mfa_enabled: false,
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// Update last_login_at and provider subject IDs
|
||||
|
||||
Reference in New Issue
Block a user