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>> {
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::*;

View file

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

View file

@ -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 {

View file

@ -29,7 +29,7 @@ impl<T: SessionStream> Session<T> {
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(|| {

View file

@ -37,7 +37,7 @@ impl<T: SessionStream> Session<T> {
.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| {

View file

@ -22,9 +22,9 @@ impl<T: SessionStream> Session<T> {
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

View file

@ -27,7 +27,7 @@ impl<T: SessionStream> Session<T> {
mut params: Vec<String>,
) -> 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| {

View file

@ -19,9 +19,9 @@ pub mod list;
impl<T: SessionStream> Session<T> {
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!(

View file

@ -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<T: SessionStream> Session<T> {
.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;
}
}
_ => (),
}
}