feat: Add support for importing email addresses from the IDP

This commit is contained in:
Ginger committed 2026-07-02 16:34:38 -04:00
1 parent 3282ba8eab
commit 786890baa0
4 files changed
+66 -5

No files matched your search

+17 -1
View File
@@ -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.
+14
View File
@@ -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<String>,
/// 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,
+26 -2
View File
@@ -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<config::Service>,
globals: Dep<globals::Service>,
media: Dep<media::Service>,
threepid: Dep<threepid::Service>,
users: Dep<users::Service>,
}
@@ -119,6 +122,7 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
config: args.depend::<config::Service>("config"),
globals: args.depend::<globals::Service>("globals"),
media: args.depend::<media::Service>("media"),
threepid: args.depend::<threepid::Service>("threepid"),
users: args.depend::<users::Service>("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();
+9 -2
View File
@@ -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