Implement SSO/OIDC support. (closes #7)

Signed-off-by: Jason Volk <jason@zemos.net>
This commit is contained in:
Jason Volk
2025-12-23 14:55:29 +00:00
parent d665a34f30
commit 11309062a2
23 changed files with 1959 additions and 27 deletions

View File

@@ -3,7 +3,8 @@ pub mod manager;
pub mod proxy;
use std::{
collections::{BTreeMap, BTreeSet},
collections::{BTreeMap, BTreeSet, HashSet},
hash::{Hash, Hasher},
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
path::{Path, PathBuf},
};
@@ -16,7 +17,7 @@ use figment::providers::{Env, Format, Toml};
pub use figment::{Figment, value::Value as FigmentValue};
use regex::RegexSet;
use ruma::{
OwnedRoomOrAliasId, OwnedServerName, OwnedUserId, RoomVersionId,
OwnedMxcUri, OwnedRoomOrAliasId, OwnedServerName, OwnedUserId, RoomVersionId,
api::client::discovery::discover_support::ContactRole,
};
use serde::{Deserialize, de::IgnoredAny};
@@ -28,6 +29,7 @@ pub use self::{check::check, manager::Manager};
use crate::{
Result, err,
error::Error,
utils,
utils::{string::EMPTY, sys},
};
@@ -57,7 +59,7 @@ use crate::{
### https://tuwunel.chat/configuration.html
"#,
ignore = "catchall well_known tls blurhashing allow_invalid_tls_certificates ldap jwt \
appservice"
appservice identity_provider"
)]
pub struct Config {
/// The server_name is the pretty name of this server. It is used as a
@@ -2153,6 +2155,13 @@ pub struct Config {
#[serde(default)]
pub appservice: BTreeMap<String, AppService>,
// external structure; separate sections
#[serde(default)]
pub identity_provider: HashSet<IdentityProvider>,
#[serde(default)]
pub sso_aware_preferred: bool,
#[serde(flatten)]
#[allow(clippy::zero_sized_map_values)]
// this is a catchall, the map shouldn't be zero at runtime
@@ -2468,6 +2477,134 @@ pub struct JwtConfig {
pub validate_signature: bool,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
#[config_example_generator(
filename = "tuwunel-example.toml",
section = "[global.identity_provider]"
)]
pub struct IdentityProvider {
/// The brand-name of the service (e.g. Apple, Facebook, GitHub, GitLab,
/// Google) or the software (e.g. keycloak, MAS) providing the identity.
/// When a brand is recognized we apply certain defaults to this config
/// for your convenience. For certain brands we apply essential internal
/// workarounds specific to that provider; it is important to configure this
/// field properly when a provider needs to be recognized (like GitHub for
/// example). Several configured providers can share the same brand name. It
/// is not case-sensitive.
#[serde(deserialize_with = "utils::string::de::to_lowercase")]
pub brand: String,
/// The ID of your OAuth application which the provider generates upon
/// registration. This ID then uniquely identifies this configuration
/// instance itself, becoming the identity provider's ID and must be unique
/// and remain unchanged.
pub client_id: String,
/// Secret key the provider generated for you along with the `client_id`
/// above. Unlike the `client_id`, the `client_secret` can be changed here
/// whenever the provider regenerates one for you.
pub client_secret: String,
/// The callback URL configured when registering the OAuth application with
/// the provider. Tuwunel's callback URL must be strictly formatted exactly
/// as instructed. The URL host must point directly at the matrix server and
/// use the following path:
/// `/_matrix/client/unstable/login/sso/callback/<client_id>` where
/// `<client_id>` is the same one configured for this provider above.
pub callback_url: Option<Url>,
/// Optional display-name for this provider instance seen on the login page
/// by users. It defaults to `brand`. When configuring multiple providers
/// using the same `brand` this can be set to distinguish them.
pub name: Option<String>,
/// Optional icon for the provider. The canonical providers have a default
/// icon based on the `brand` supplied above when this is not supplied. Note
/// that it uses an MXC url which is curious in the auth-media era and may
/// not be reliable.
pub icon: Option<OwnedMxcUri>,
/// Optional list of scopes to authorize. An empty array does not impose any
/// restrictions from here, effectively defaulting to all scopes you
/// configured for the OAuth application at the provider. This setting
/// allows for restricting to a subset of those scopes for this instance.
/// Note the user can further restrict scopes during their authorization.
///
/// default: []
#[serde(default)]
pub scope: BTreeSet<String>,
/// List of userinfo claims which shape and restrict the way we compute a
/// Matrix UserId for new registrations. Reviewing Tuwunel's documentation
/// will be necessary for a complete description in detail. An empty array
/// imposes no restriction here, avoiding generated fallbacks as much as
/// possible. For simplicity we reserve a claim called "unique" which can be
/// listed alone to ensure *only* generated ID's are used for registrations.
///
/// default: []
#[serde(default)]
pub userid_claims: BTreeSet<String>,
/// Issuer URL the provider publishes for you. We have pre-supplied default
/// values for some of the canonical providers, making this field optional
/// based on the `brand` set above. Otherwise it is required for OIDC
/// discovery to acquire additional provider configuration, and it must be
/// correct to pass validations during various interactions.
pub issuer_url: Option<Url>,
/// Extra path components after the issuer_url leading to the location of
/// the `.well-known` directory used for discovery. This will be empty for
/// specification-compliant providers. We have supplied any known values
/// based on `brand` (e.g. `/login/oauth` for GitHub).
pub base_path: Option<String>,
/// Overrides the `.well-known` location where the provider's OIDC
/// configuration is found. It is very unlikely you will need to set this;
/// available for developers or special purposes only.
pub discovery_url: Option<Url>,
/// Overrides the authorize URL requested during the grant phase. This is
/// generally discovered or derived automatically, but may be required as a
/// workaround for any non-standard or undiscoverable provider.
pub authorization_url: Option<Url>,
/// Overrides the access token URL; the same caveats apply as with the other
/// URL overrides.
pub token_url: Option<Url>,
/// Overrides the revocation URL; the same caveats apply as with the other
/// URL overrides.
pub revocation_url: Option<Url>,
/// Overrides the introspection URL; the same caveats apply as with the
/// other URL overrides.
pub introspection_url: Option<Url>,
/// Overrides the userinfo URL; the same caveats apply as with the other URL
/// overrides.
pub userinfo_url: Option<Url>,
/// Whether to perform discovery and adjust this provider's configuration
/// accordingly. This defaults to true. When true, it is an error when
/// discovery fails and authorizations will not be attempted to the
/// provider.
#[serde(default = "true_fn")]
pub discovery: bool,
/// The duration in seconds before a grant authorization session expires.
#[serde(default = "default_sso_grant_session_duration")]
pub grant_session_duration: Option<u64>,
}
impl IdentityProvider {
#[must_use]
pub fn id(&self) -> &str { self.client_id.as_str() }
}
impl Hash for IdentityProvider {
fn hash<H: Hasher>(&self, state: &mut H) { self.id().hash(state) }
}
#[derive(Clone, Debug, Default, Deserialize)]
#[config_example_generator(
filename = "tuwunel-example.toml",
@@ -3016,3 +3153,5 @@ fn default_one_time_key_limit() -> usize { 256 }
fn default_max_make_join_attempts_per_join_attempt() -> usize { 48 }
fn default_max_join_attempts_per_join_request() -> usize { 3 }
fn default_sso_grant_session_duration() -> Option<u64> { Some(180) }