XOAUTH2 support (closes #1194 closes #1369)

This commit is contained in:
mdecimus 2025-05-16 17:17:13 +02:00
parent f667da0d4f
commit dcdf68b774
9 changed files with 18 additions and 59 deletions

View file

@ -30,36 +30,6 @@ pub fn sasl_decode_challenge_plain(challenge: &[u8]) -> Option<Credentials<Strin
} }
} }
pub fn sasl_decode_challenge_xoauth(challenge: &[u8]) -> Option<Credentials<String>> {
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<Credentials<String>> { pub fn sasl_decode_challenge_oauth(challenge: &[u8]) -> Option<Credentials<String>> {
extract_oauth_bearer(challenge).map(|s| Credentials::OAuthBearer { token: s.into() }) extract_oauth_bearer(challenge).map(|s| Credentials::OAuthBearer { token: s.into() })
} }
@ -86,6 +56,7 @@ fn extract_oauth_bearer(bytes: &[u8]) -> Option<&str> {
None None
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View file

@ -667,8 +667,11 @@ impl Default for SessionConfig {
mechanisms: IfBlock::new::<Mechanism>( mechanisms: IfBlock::new::<Mechanism>(
"session.auth.mechanisms", "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", "false",
), ),

View file

@ -176,8 +176,9 @@ impl Capability {
]); ]);
} else { } else {
capabilities.extend([ capabilities.extend([
Capability::Auth(Mechanism::OAuthBearer),
Capability::Auth(Mechanism::Plain), Capability::Auth(Mechanism::Plain),
Capability::Auth(Mechanism::OAuthBearer),
Capability::Auth(Mechanism::XOauth2),
]); ]);
} }
if offer_tls { if offer_tls {

View file

@ -29,7 +29,7 @@ impl<T: SessionStream> Session<T> {
let mut args = request.parse_authenticate()?; let mut args = request.parse_authenticate()?;
match args.mechanism { match args.mechanism {
Mechanism::Plain | Mechanism::OAuthBearer => { Mechanism::Plain | Mechanism::OAuthBearer | Mechanism::XOauth2 => {
if !args.params.is_empty() { if !args.params.is_empty() {
let challenge = base64_decode(args.params.pop().unwrap().as_bytes()) let challenge = base64_decode(args.params.pop().unwrap().as_bytes())
.ok_or_else(|| { .ok_or_else(|| {

View file

@ -37,7 +37,7 @@ impl<T: SessionStream> Session<T> {
.collect(); .collect();
let credentials = match mechanism { let credentials = match mechanism {
Mechanism::Plain | Mechanism::OAuthBearer => { Mechanism::Plain | Mechanism::OAuthBearer | Mechanism::XOauth2 => {
if !params.is_empty() { if !params.is_empty() {
base64_decode(params.pop().unwrap().as_bytes()) base64_decode(params.pop().unwrap().as_bytes())
.and_then(|challenge| { .and_then(|challenge| {

View file

@ -22,9 +22,9 @@ impl<T: SessionStream> Session<T> {
response.extend_from_slice(b"\"STARTTLS\"\r\n"); response.extend_from_slice(b"\"STARTTLS\"\r\n");
} }
if self.stream.is_tls() || self.server.core.imap.allow_plain_auth { 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 { } else {
response.extend_from_slice(b"\"SASL\" \"OAUTHBEARER\"\r\n"); response.extend_from_slice(b"\"SASL\" \"OAUTHBEARER XOAUTH2\"\r\n");
}; };
if let Some(sieve) = if let Some(sieve) =
self.server self.server

View file

@ -27,7 +27,7 @@ impl<T: SessionStream> Session<T> {
mut params: Vec<String>, mut params: Vec<String>,
) -> trc::Result<()> { ) -> trc::Result<()> {
match mechanism { match mechanism {
Mechanism::Plain | Mechanism::OAuthBearer => { Mechanism::Plain | Mechanism::OAuthBearer | Mechanism::XOauth2 => {
if !params.is_empty() { if !params.is_empty() {
let credentials = base64_decode(params.pop().unwrap().as_bytes()) let credentials = base64_decode(params.pop().unwrap().as_bytes())
.and_then(|challenge| { .and_then(|challenge| {

View file

@ -19,9 +19,9 @@ pub mod list;
impl<T: SessionStream> Session<T> { impl<T: SessionStream> Session<T> {
pub async fn handle_capa(&mut self) -> trc::Result<()> { pub async fn handle_capa(&mut self) -> trc::Result<()> {
let mechanisms = if self.stream.is_tls() || self.server.core.imap.allow_plain_auth { 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 { } else {
vec![Mechanism::OAuthBearer] vec![Mechanism::OAuthBearer, Mechanism::XOauth2]
}; };
trc::event!( trc::event!(

View file

@ -7,9 +7,7 @@
use common::{ use common::{
auth::{ auth::{
AuthRequest, AuthRequest,
sasl::{ sasl::{sasl_decode_challenge_oauth, sasl_decode_challenge_plain},
sasl_decode_challenge_oauth, sasl_decode_challenge_plain, sasl_decode_challenge_xoauth,
},
}, },
listener::SessionStream, listener::SessionStream,
}; };
@ -38,21 +36,13 @@ impl SaslToken {
}, },
} }
.into(), .into(),
AUTH_OAUTHBEARER => SaslToken { AUTH_OAUTHBEARER | AUTH_XOAUTH2 => SaslToken {
mechanism, mechanism,
credentials: Credentials::OAuthBearer { credentials: Credentials::OAuthBearer {
token: String::new(), token: String::new(),
}, },
} }
.into(), .into(),
AUTH_XOAUTH2 => SaslToken {
mechanism,
credentials: Credentials::XOauth2 {
username: String::new(),
secret: String::new(),
},
}
.into(),
_ => None, _ => None,
} }
} }
@ -96,17 +86,11 @@ impl<T: SessionStream> Session<T> {
.await .await
}; };
} }
(AUTH_OAUTHBEARER, _) => { (AUTH_OAUTHBEARER | AUTH_XOAUTH2, _) => {
if let Some(credentials) = sasl_decode_challenge_oauth(&response) { if let Some(credentials) = sasl_decode_challenge_oauth(&response) {
return self.authenticate(credentials).await; return self.authenticate(credentials).await;
} }
} }
(AUTH_XOAUTH2, _) => {
if let Some(credentials) = sasl_decode_challenge_xoauth(&response) {
return self.authenticate(credentials).await;
}
}
_ => (), _ => (),
} }
} }