feat(magiclink) prepare magic link support (login via email)

imortant on security side: magic link  will be enabled only for users who don't have password nor OIDC
This commit is contained in:
Edouard Vanbelle
2026-06-01 21:57:07 +02:00
parent 2011d19e71
commit c3fa1b3e93
15 changed files with 1076 additions and 56 deletions
+42
View File
@@ -684,6 +684,34 @@ impl SmtpConfig {
}
}
/// Magic-link authentication configuration. Knobs that are specific to
/// the invite-by-email / login-via-email flow.
#[derive(Debug, Clone)]
pub struct MagicLinkConfig {
/// How long a freshly-minted magic-link token stays valid before the
/// background sweeper marks it expired. Default: 24 hours.
pub ttl_hours: u64,
/// Kill switch for the whole magic-link flow. When `false`:
/// - `POST /api/grants` rejects `subject.type = "email"` for unknown
/// email addresses (no lazy external-user creation).
/// - `POST /api/auth/magic-link/send` returns the uniform stub
/// response without actually issuing a token.
///
/// This is the coarser "turn it all off" switch; the future
/// `OXICLOUD_EXTERNAL_EMAIL_DOMAINS` allowlist is the fine-grained
/// version.
pub allow_external_users: bool,
}
impl Default for MagicLinkConfig {
fn default() -> Self {
Self {
ttl_hours: 24,
allow_external_users: true,
}
}
}
/// Feature configuration (feature flags)
#[derive(Debug, Clone)]
pub struct FeaturesConfig {
@@ -747,6 +775,8 @@ pub struct AppConfig {
pub nextcloud: NextcloudConfig,
/// Outbound SMTP configuration (magic-link invitations, etc.)
pub smtp: SmtpConfig,
/// Magic-link authentication configuration (TTL, external-users kill switch)
pub magic_link: MagicLinkConfig,
}
impl Default for AppConfig {
@@ -768,6 +798,7 @@ impl Default for AppConfig {
wopi: WopiConfig::default(),
nextcloud: NextcloudConfig::default(),
smtp: SmtpConfig::default(),
magic_link: MagicLinkConfig::default(),
}
}
}
@@ -1262,6 +1293,17 @@ impl AppConfig {
);
}
// Magic-link configuration
if let Ok(v) = env::var("OXICLOUD_MAGIC_LINK_TTL_HOURS")
&& let Ok(h) = v.parse::<u64>()
&& h > 0
{
config.magic_link.ttl_hours = h;
}
if let Ok(v) = env::var("OXICLOUD_ALLOW_EXTERNAL_USERS") {
config.magic_link.allow_external_users = v.parse::<bool>().unwrap_or(true);
}
config
}
+23 -10
View File
@@ -711,17 +711,29 @@ impl AppServiceFactory {
// delete (with audit) —
// replaces the silent FK
// CASCADE.
// 5. ExternalIdentityLifecycleHook — STUB. No-op for every
// event today; the
// magic-link / OIDC-only /
// OCM PR will fill it in
// to populate
// `auth.user_external_identity`.
// 5. ExternalIdentityLifecycleHook — audit + magic-link
// token cleanup. Logs an
// audit event for any
// external user that gets
// created or logs in;
// transactionally clears
// outstanding magic-link
// tokens on delete (so a
// new user reusing the
// same id can never
// inherit an old token).
// Last in the chain so it
// observes the latest user
// state before the chain
// commits.
// observes the latest
// user state before the
// chain commits.
let session_repo_for_hook = Arc::new(SessionPgRepository::new(pool.clone()));
let magic_link_repo: Arc<
dyn crate::domain::repositories::magic_link_token_repository::MagicLinkTokenRepository,
> = Arc::new(
crate::infrastructure::repositories::pg::MagicLinkTokenPgRepository::new(
pool.clone(),
),
);
let user_lifecycle = Arc::new(
crate::application::services::user_lifecycle_service::UserLifecycleService::new()
.with_hook(Arc::new(
@@ -743,7 +755,8 @@ impl AppServiceFactory {
),
))
.with_hook(Arc::new(
crate::application::services::external_identity_service::ExternalIdentityLifecycleHook,
crate::application::services::external_identity_service::ExternalIdentityLifecycleHook::new()
.with_magic_link_repo(magic_link_repo.clone()),
)),
);