From dcdf68b774f6650100cc154889fcf378c52f9afc Mon Sep 17 00:00:00 2001 From: mdecimus Date: Fri, 16 May 2025 17:17:13 +0200 Subject: [PATCH] XOAUTH2 support (closes #1194 closes #1369) --- crates/common/src/auth/sasl.rs | 31 +------------------- crates/common/src/config/smtp/session.rs | 7 +++-- crates/imap-proto/src/protocol/capability.rs | 3 +- crates/imap/src/op/authenticate.rs | 2 +- crates/managesieve/src/op/authenticate.rs | 2 +- crates/managesieve/src/op/capability.rs | 4 +-- crates/pop3/src/op/authenticate.rs | 2 +- crates/pop3/src/op/mod.rs | 4 +-- crates/smtp/src/inbound/auth.rs | 22 ++------------ 9 files changed, 18 insertions(+), 59 deletions(-) diff --git a/crates/common/src/auth/sasl.rs b/crates/common/src/auth/sasl.rs index 2c0d8f1f..36579c53 100644 --- a/crates/common/src/auth/sasl.rs +++ b/crates/common/src/auth/sasl.rs @@ -30,36 +30,6 @@ pub fn sasl_decode_challenge_plain(challenge: &[u8]) -> Option Option> { - let mut b_username = Vec::new(); - let mut b_secret = Vec::new(); - let mut arg_num = 0; - let mut in_arg = false; - - for &ch in challenge { - if in_arg { - if ch != 1 { - if arg_num == 1 { - b_username.push(ch); - } else if arg_num == 2 { - b_secret.push(ch); - } - } else { - in_arg = false; - } - } else if ch == b'=' { - arg_num += 1; - in_arg = true; - } - } - match (String::from_utf8(b_username), String::from_utf8(b_secret)) { - (Ok(s_username), Ok(s_secret)) if !s_username.is_empty() => { - Some((s_username, s_secret).into()) - } - _ => None, - } -} - pub fn sasl_decode_challenge_oauth(challenge: &[u8]) -> Option> { extract_oauth_bearer(challenge).map(|s| Credentials::OAuthBearer { token: s.into() }) } @@ -86,6 +56,7 @@ fn extract_oauth_bearer(bytes: &[u8]) -> Option<&str> { None } + #[cfg(test)] mod tests { use super::*; diff --git a/crates/common/src/config/smtp/session.rs b/crates/common/src/config/smtp/session.rs index 4a605d2f..1ea9e234 100644 --- a/crates/common/src/config/smtp/session.rs +++ b/crates/common/src/config/smtp/session.rs @@ -667,8 +667,11 @@ impl Default for SessionConfig { mechanisms: IfBlock::new::( "session.auth.mechanisms", [ - ("local_port != 25 && is_tls", "[plain, login, oauthbearer]"), - ("local_port != 25", "[oauthbearer]"), + ( + "local_port != 25 && is_tls", + "[plain, login, oauthbearer, xoauth2]", + ), + ("local_port != 25", "[oauthbearer, xoauth2]"), ], "false", ), diff --git a/crates/imap-proto/src/protocol/capability.rs b/crates/imap-proto/src/protocol/capability.rs index c0ff30f0..5933af44 100644 --- a/crates/imap-proto/src/protocol/capability.rs +++ b/crates/imap-proto/src/protocol/capability.rs @@ -176,8 +176,9 @@ impl Capability { ]); } else { capabilities.extend([ - Capability::Auth(Mechanism::OAuthBearer), Capability::Auth(Mechanism::Plain), + Capability::Auth(Mechanism::OAuthBearer), + Capability::Auth(Mechanism::XOauth2), ]); } if offer_tls { diff --git a/crates/imap/src/op/authenticate.rs b/crates/imap/src/op/authenticate.rs index 0e0248c3..d7173d81 100644 --- a/crates/imap/src/op/authenticate.rs +++ b/crates/imap/src/op/authenticate.rs @@ -29,7 +29,7 @@ impl Session { let mut args = request.parse_authenticate()?; match args.mechanism { - Mechanism::Plain | Mechanism::OAuthBearer => { + Mechanism::Plain | Mechanism::OAuthBearer | Mechanism::XOauth2 => { if !args.params.is_empty() { let challenge = base64_decode(args.params.pop().unwrap().as_bytes()) .ok_or_else(|| { diff --git a/crates/managesieve/src/op/authenticate.rs b/crates/managesieve/src/op/authenticate.rs index b616d384..92680f88 100644 --- a/crates/managesieve/src/op/authenticate.rs +++ b/crates/managesieve/src/op/authenticate.rs @@ -37,7 +37,7 @@ impl Session { .collect(); let credentials = match mechanism { - Mechanism::Plain | Mechanism::OAuthBearer => { + Mechanism::Plain | Mechanism::OAuthBearer | Mechanism::XOauth2 => { if !params.is_empty() { base64_decode(params.pop().unwrap().as_bytes()) .and_then(|challenge| { diff --git a/crates/managesieve/src/op/capability.rs b/crates/managesieve/src/op/capability.rs index 33c1fb47..3588e7ee 100644 --- a/crates/managesieve/src/op/capability.rs +++ b/crates/managesieve/src/op/capability.rs @@ -22,9 +22,9 @@ impl Session { response.extend_from_slice(b"\"STARTTLS\"\r\n"); } if self.stream.is_tls() || self.server.core.imap.allow_plain_auth { - response.extend_from_slice(b"\"SASL\" \"PLAIN OAUTHBEARER\"\r\n"); + response.extend_from_slice(b"\"SASL\" \"PLAIN OAUTHBEARER XOAUTH2\"\r\n"); } else { - response.extend_from_slice(b"\"SASL\" \"OAUTHBEARER\"\r\n"); + response.extend_from_slice(b"\"SASL\" \"OAUTHBEARER XOAUTH2\"\r\n"); }; if let Some(sieve) = self.server diff --git a/crates/pop3/src/op/authenticate.rs b/crates/pop3/src/op/authenticate.rs index 7cc04c3f..e5294b41 100644 --- a/crates/pop3/src/op/authenticate.rs +++ b/crates/pop3/src/op/authenticate.rs @@ -27,7 +27,7 @@ impl Session { mut params: Vec, ) -> trc::Result<()> { match mechanism { - Mechanism::Plain | Mechanism::OAuthBearer => { + Mechanism::Plain | Mechanism::OAuthBearer | Mechanism::XOauth2 => { if !params.is_empty() { let credentials = base64_decode(params.pop().unwrap().as_bytes()) .and_then(|challenge| { diff --git a/crates/pop3/src/op/mod.rs b/crates/pop3/src/op/mod.rs index 26d4de4a..59046c26 100644 --- a/crates/pop3/src/op/mod.rs +++ b/crates/pop3/src/op/mod.rs @@ -19,9 +19,9 @@ pub mod list; impl Session { pub async fn handle_capa(&mut self) -> trc::Result<()> { let mechanisms = if self.stream.is_tls() || self.server.core.imap.allow_plain_auth { - vec![Mechanism::Plain, Mechanism::OAuthBearer] + vec![Mechanism::Plain, Mechanism::OAuthBearer, Mechanism::XOauth2] } else { - vec![Mechanism::OAuthBearer] + vec![Mechanism::OAuthBearer, Mechanism::XOauth2] }; trc::event!( diff --git a/crates/smtp/src/inbound/auth.rs b/crates/smtp/src/inbound/auth.rs index e11d3e13..b87079da 100644 --- a/crates/smtp/src/inbound/auth.rs +++ b/crates/smtp/src/inbound/auth.rs @@ -7,9 +7,7 @@ use common::{ auth::{ AuthRequest, - sasl::{ - sasl_decode_challenge_oauth, sasl_decode_challenge_plain, sasl_decode_challenge_xoauth, - }, + sasl::{sasl_decode_challenge_oauth, sasl_decode_challenge_plain}, }, listener::SessionStream, }; @@ -38,21 +36,13 @@ impl SaslToken { }, } .into(), - AUTH_OAUTHBEARER => SaslToken { + AUTH_OAUTHBEARER | AUTH_XOAUTH2 => SaslToken { mechanism, credentials: Credentials::OAuthBearer { token: String::new(), }, } .into(), - AUTH_XOAUTH2 => SaslToken { - mechanism, - credentials: Credentials::XOauth2 { - username: String::new(), - secret: String::new(), - }, - } - .into(), _ => None, } } @@ -96,17 +86,11 @@ impl Session { .await }; } - (AUTH_OAUTHBEARER, _) => { + (AUTH_OAUTHBEARER | AUTH_XOAUTH2, _) => { if let Some(credentials) = sasl_decode_challenge_oauth(&response) { return self.authenticate(credentials).await; } } - (AUTH_XOAUTH2, _) => { - if let Some(credentials) = sasl_decode_challenge_xoauth(&response) { - return self.authenticate(credentials).await; - } - } - _ => (), } }