feat: add LOGIN_DOMAIN env var to normalize emails during external login

Add LOGIN_DOMAIN environment variable that appends a domain to emails
missing one during external login (OAuth/SAML/SCIM). When set, emails
without '@' will have '@{LOGIN_DOMAIN}' appended.

Example: LOGIN_DOMAIN=example.com transforms "john" to "john@example.com"

Also includes a migration to lowercase existing emails in critical tables:
- password (primary user identity)
- usr (workspace users)
- email_to_igroup (instance group memberships)
- token (active sessions)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Ruben Fiszel
2026-01-27 00:01:35 +00:00
co-authored by Claude Opus 4.5
parent 2002e99a43
commit 89210fc736
3 changed files with 73 additions and 0 deletions
@@ -0,0 +1,3 @@
-- This migration cannot be reversed as the original email casing is not preserved
-- Emails will remain lowercased
SELECT 1;
@@ -0,0 +1,14 @@
-- Normalize emails to lowercase for consistency with external login normalization
-- This migration lowercases email columns in critical authentication tables
-- Lowercase emails in password table (primary user identity)
UPDATE password SET email = LOWER(email) WHERE email != LOWER(email);
-- Lowercase emails in usr table (workspace users)
UPDATE usr SET email = LOWER(email) WHERE email != LOWER(email);
-- Lowercase emails in email_to_igroup table (instance group memberships)
UPDATE email_to_igroup SET email = LOWER(email) WHERE email != LOWER(email);
-- Lowercase emails in token table (active sessions)
UPDATE token SET email = LOWER(email) WHERE email != LOWER(email);
+56
View File
@@ -28,6 +28,24 @@ lazy_static::lazy_static! {
.and_then(|x| x.parse::<bool>().ok())
.unwrap_or(false));
/// Domain to append to emails missing a domain during external login (OAuth/SAML).
/// If set, emails without '@' will have '@{LOGIN_DOMAIN}' appended.
/// Example: LOGIN_DOMAIN=example.com transforms "john" to "john@example.com"
pub static ref LOGIN_DOMAIN: Option<String> = std::env::var("LOGIN_DOMAIN").ok();
}
/// Normalize an email from external login by appending LOGIN_DOMAIN if the email is missing a domain.
/// Returns the email lowercased.
pub fn normalize_external_email(email: &str) -> String {
let email = email.trim();
if email.contains('@') {
email.to_lowercase()
} else if let Some(domain) = LOGIN_DOMAIN.as_ref() {
format!("{}@{}", email, domain).to_lowercase()
} else {
email.to_lowercase()
}
}
#[derive(Serialize)]
@@ -41,3 +59,41 @@ pub enum InstanceEvent {
UserInvitedWorkspace { workspace: String, email: String },
UserJoinedWorkspace { workspace: String, email: String, username: String },
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_normalize_external_email_with_domain() {
// Email already has domain - should just lowercase
assert_eq!(
normalize_external_email("John.Doe@Example.COM"),
"john.doe@example.com"
);
assert_eq!(
normalize_external_email("user@domain.org"),
"user@domain.org"
);
}
#[test]
fn test_normalize_external_email_trims_whitespace() {
assert_eq!(
normalize_external_email(" user@example.com "),
"user@example.com"
);
assert_eq!(normalize_external_email(" john "), "john");
}
#[test]
fn test_normalize_external_email_without_domain_no_env() {
// When LOGIN_DOMAIN is not set, email without @ stays as-is (lowercased)
// Note: This test's behavior depends on whether LOGIN_DOMAIN env var is set
// In the test environment, it's typically not set
let result = normalize_external_email("JohnDoe");
// Result will either be "johndoe" or "johndoe@{LOGIN_DOMAIN}" depending on env
assert!(result.starts_with("johndoe"));
assert_eq!(result, result.to_lowercase());
}
}