From 786890baa059e7d127b0e45c81dad87f3ea1d54f Mon Sep 17 00:00:00 2001 From: Ginger Date: Wed, 1 Jul 2026 15:40:23 -0400 Subject: [PATCH] feat: Add support for importing email addresses from the IDP --- conduwuit-example.toml | 18 +++++++++++++++++- src/core/config/mod.rs | 14 ++++++++++++++ src/service/oidc/mod.rs | 28 ++++++++++++++++++++++++++-- src/service/threepid/mod.rs | 11 +++++++++-- 4 files changed, 66 insertions(+), 5 deletions(-) diff --git a/conduwuit-example.toml b/conduwuit-example.toml index 427131c64..cd93719ae 100644 --- a/conduwuit-example.toml +++ b/conduwuit-example.toml @@ -2068,12 +2068,28 @@ # #preferred_username_claim = "preferred_username" +# The claim which will be used to set the user's email address, +# either on initial registration or on every login depending on +# the value of `profile_key_import_mode`. Continuwuity assumes that +# the IDP has taken care of verifying that the user controls the email +# address it provides. +# +# This option does nothing if SMTP is not configured. +# +# If this option is set, and `profile_key_import_mode` is `on_login`, +# users will not be able to change their email addresses themselves. +# +#email_claim = "email" + # Defines how claims returned from the IDP should be mapped to a user's # profile data. The profile field named in each key will be set from the # claim named in the corresponding value when the user first registers, # and possibly on subsequent logins as well, depending on the value of # `profile_key_import_mode` (see below). # +# Per-room overrides to the user's display name or avatar will be +# preserved by the import process. +# # SECURITY NOTE: If the `avatar_url` field is set, Continuwuity will # perform a HTTP GET to the URL in the mapped claim and use the returned # file as the user's profile picture. Make sure your users are not able @@ -2084,7 +2100,7 @@ # When profile keys should be imported from the IDP's claims. # # - "on_registration": Listed keys will be imported once, when the user -# registers. +# logs in for the first time and their shadow account is created. # - "on_login": Listed keys will be imported every time the user logs in. # Additionally, users will not be able to manually edit any listed keys # through their Matrix client. diff --git a/src/core/config/mod.rs b/src/core/config/mod.rs index 5f67e4623..0b9342713 100644 --- a/src/core/config/mod.rs +++ b/src/core/config/mod.rs @@ -2503,6 +2503,20 @@ pub struct OidcConfig { #[serde(default = "default_preferred_username_claim")] pub preferred_username_claim: String, + /// The claim which will be used to set the user's email address, + /// either on initial registration or on every login depending on + /// the value of `profile_key_import_mode`. Continuwuity assumes that + /// the IDP has taken care of verifying that the user controls the email + /// address it provides. + /// + /// This option does nothing if SMTP is not configured. + /// + /// If this option is set, and `profile_key_import_mode` is `on_login`, + /// users will not be able to change their email addresses themselves. + /// + /// default: "email" + pub email_claim: Option, + /// Defines how claims returned from the IDP should be mapped to a user's /// profile data. The profile field named in each key will be set from the /// claim named in the corresponding value when the user first registers, diff --git a/src/service/oidc/mod.rs b/src/service/oidc/mod.rs index 9b288c24a..15fa8ba5b 100644 --- a/src/service/oidc/mod.rs +++ b/src/service/oidc/mod.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, sync::Arc, time::Duration}; +use std::{collections::HashMap, str::FromStr, sync::Arc, time::Duration}; use async_trait::async_trait; use conduwuit::{ @@ -7,6 +7,7 @@ debug, err, error, info, warn, }; use database::{Deserialized, Map}; +use lettre::Address; use openidconnect::{ AdditionalClaims, AuthorizationCode, CsrfToken, EmptyExtraTokenFields, EndpointMaybeSet, EndpointNotSet, EndpointSet, IdTokenClaims, IdTokenFields, IssuerUrl, Nonce, @@ -33,6 +34,7 @@ use crate::{ Dep, config, globals, media, oauth::grant::AuthorizationCodeResponse, + threepid, users::{self, AccountStatus, ProfileFieldChange}, }; @@ -51,6 +53,7 @@ struct Services { config: Dep, globals: Dep, media: Dep, + threepid: Dep, users: Dep, } @@ -119,6 +122,7 @@ fn build(args: crate::Args<'_>) -> Result> { config: args.depend::("config"), globals: args.depend::("globals"), media: args.depend::("media"), + threepid: args.depend::("threepid"), users: args.depend::("users"), }, runtime: args.server.runtime().clone(), @@ -316,7 +320,7 @@ pub async fn complete_session( your homeserver's administrator." })? } else { - error!("No preferred username claim was present"); + error!("Preferred username claim was not present or was not a string"); return Err(Self::SERVER_MISCONFIGURED); }; @@ -356,6 +360,26 @@ pub async fn complete_session( OidcProfileKeyImportMode::OnRegistration ) && new_account_registered) { + if let Some(email_claim) = &config.email_claim { + if let Some(email) = claims.email().map(|email| email.as_str()) + && let Ok(address) = Address::from_str(email) + { + if let Err(err) = self + .services + .threepid + .associate_localpart_email(user_id.localpart(), &address) + .await + { + warn!(?email_claim, ?address, "Failed to associate email address: {err}"); + } + } else { + warn!( + ?email_claim, + "Email claim was not present or was not a valid email address" + ); + } + } + let user_id = user_id.clone(); let subject = claims.subject().to_string(); let profile_key_map = config.profile_key_map.clone(); diff --git a/src/service/threepid/mod.rs b/src/service/threepid/mod.rs index 5fbebab31..98d2387e7 100644 --- a/src/service/threepid/mod.rs +++ b/src/service/threepid/mod.rs @@ -1,6 +1,6 @@ use std::{borrow::Cow, collections::HashMap, sync::Arc}; -use conduwuit::{Err, Error, Result, result::FlatOk}; +use conduwuit::{Err, Error, Result, config::OidcProfileKeyImportMode, result::FlatOk}; use database::{Deserialized, Map}; use governor::{DefaultKeyedRateLimiter, Quota, RateLimiter}; use lettre::{Address, message::Mailbox}; @@ -87,7 +87,14 @@ impl Service { /// Check if users are required to have an email address. pub fn email_requirement(&self) -> EmailRequirement { if let Some(smtp) = &self.services.config.smtp { - if smtp.require_email_for_registration || smtp.require_email_for_token_registration { + if let Some(oidc) = &self.services.config.oauth.oidc + && matches!(oidc.profile_key_import_mode, OidcProfileKeyImportMode::OnLogin) + && oidc.email_claim.is_some() + { + EmailRequirement::Unavailable + } else if smtp.require_email_for_registration + || smtp.require_email_for_token_registration + { EmailRequirement::Required } else { EmailRequirement::Optional