summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--crates/common/src/auth/mod.rs9
-rw-r--r--crates/common/src/auth/oauth/introspect.rs97
-rw-r--r--crates/common/src/auth/oauth/mod.rs89
-rw-r--r--crates/common/src/auth/oauth/token.rs163
-rw-r--r--crates/common/src/auth/sasl.rs131
-rw-r--r--crates/imap/src/op/authenticate.rs75
-rw-r--r--crates/jmap/src/api/http.rs22
-rw-r--r--crates/jmap/src/api/management/enterprise/telemetry.rs6
-rw-r--r--crates/jmap/src/auth/oauth/mod.rs12
-rw-r--r--crates/jmap/src/auth/oauth/token.rs71
-rw-r--r--crates/managesieve/src/op/authenticate.rs25
-rw-r--r--crates/pop3/src/op/authenticate.rs17
-rw-r--r--crates/smtp/src/inbound/auth.rs85
-rw-r--r--tests/src/imap/basic.rs4
-rw-r--r--tests/src/jmap/auth_oauth.rs101
-rw-r--r--tests/src/jmap/mod.rs14
16 files changed, 600 insertions, 321 deletions
diff --git a/crates/common/src/auth/mod.rs b/crates/common/src/auth/mod.rs
index 5a67a0e0..7b74cff8 100644
--- a/crates/common/src/auth/mod.rs
+++ b/crates/common/src/auth/mod.rs
@@ -11,6 +11,7 @@ use directory::{
};
use jmap_proto::types::collection::Collection;
use mail_send::Credentials;
+use oauth::GrantType;
use utils::map::{bitmap::Bitmap, ttl_dashmap::TtlMap, vec_map::VecMap};
use crate::Server;
@@ -18,6 +19,7 @@ use crate::Server;
pub mod access_token;
pub mod oauth;
pub mod roles;
+pub mod sasl;
#[derive(Debug, Clone, Default)]
pub struct AccessToken {
@@ -58,8 +60,11 @@ impl Server {
// Validate credentials
match &req.credentials {
Credentials::OAuthBearer { token } => {
- match self.validate_access_token("access_token", token).await {
- Ok((account_id, _, _)) => self.get_cached_access_token(account_id).await,
+ match self
+ .validate_access_token(GrantType::AccessToken.into(), token)
+ .await
+ {
+ Ok(token_into) => self.get_cached_access_token(token_into.account_id).await,
Err(err) => Err(err),
}
}
diff --git a/crates/common/src/auth/oauth/introspect.rs b/crates/common/src/auth/oauth/introspect.rs
new file mode 100644
index 00000000..f4e931b5
--- /dev/null
+++ b/crates/common/src/auth/oauth/introspect.rs
@@ -0,0 +1,97 @@
+/*
+ * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
+ */
+
+use serde::{Deserialize, Serialize};
+use trc::{AddContext, AuthEvent, EventType};
+
+use crate::{auth::AccessToken, Server};
+
+#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)]
+pub struct OAuthIntrospect {
+ #[serde(default)]
+ pub active: bool,
+
+ #[serde(default)]
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub scope: Option<String>,
+
+ #[serde(default)]
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub client_id: Option<String>,
+
+ #[serde(default)]
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub username: Option<String>,
+
+ #[serde(default)]
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub token_type: Option<String>,
+
+ #[serde(default)]
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub exp: Option<i64>,
+
+ #[serde(default)]
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub iat: Option<i64>,
+
+ #[serde(default)]
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub nbf: Option<i64>,
+
+ #[serde(default)]
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub sub: Option<String>,
+ /*#[serde(default)]
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub aud: Option<String>,
+
+ #[serde(default)]
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub iss: Option<String>,
+
+ #[serde(default)]
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub jti: Option<String>,*/
+}
+
+impl Server {
+ pub async fn introspect_access_token(
+ &self,
+ token: &str,
+ access_token: &AccessToken,
+ ) -> trc::Result<OAuthIntrospect> {
+ match self.validate_access_token(None, token).await {
+ Ok(token_info) => Ok(OAuthIntrospect {
+ active: true,
+ client_id: Some(token_info.client_id),
+ username: if access_token.primary_id() == token_info.account_id {
+ access_token.name.clone()
+ } else {
+ self.get_cached_access_token(token_info.account_id)
+ .await
+ .caused_by(trc::location!())?
+ .name
+ .clone()
+ }
+ .into(),
+ token_type: "bearer".to_string().into(),
+ exp: Some(token_info.expiry as i64),
+ iat: Some(token_info.issued_at as i64),
+ ..Default::default()
+ }),
+ Err(err)
+ if matches!(
+ err.inner,
+ EventType::Auth(AuthEvent::Error) | EventType::Auth(AuthEvent::TokenExpired)
+ ) =>
+ {
+ Ok(OAuthIntrospect::default())
+ }
+ Err(err) => Err(err),
+ }
+ }
+}
diff --git a/crates/common/src/auth/oauth/mod.rs b/crates/common/src/auth/oauth/mod.rs
index b58474e1..20078868 100644
--- a/crates/common/src/auth/oauth/mod.rs
+++ b/crates/common/src/auth/oauth/mod.rs
@@ -5,6 +5,7 @@
*/
pub mod crypto;
+pub mod introspect;
pub mod token;
pub const DEVICE_CODE_LEN: usize = 40;
@@ -14,68 +15,40 @@ pub const CLIENT_ID_MAX_LEN: usize = 20;
pub const USER_CODE_ALPHABET: &[u8] = b"ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; // No 0, O, I, 1
-pub fn extract_oauth_bearer(bytes: &[u8]) -> Option<&str> {
- let mut start_pos = 0;
- let eof = bytes.len().saturating_sub(1);
-
- for (pos, ch) in bytes.iter().enumerate() {
- let is_separator = *ch == 1;
- if is_separator || pos == eof {
- if bytes
- .get(start_pos..start_pos + 12)
- .map_or(false, |s| s.eq_ignore_ascii_case(b"auth=Bearer "))
- {
- return bytes
- .get(start_pos + 12..if is_separator { pos } else { bytes.len() })
- .and_then(|s| std::str::from_utf8(s).ok());
- }
+#[derive(Debug, Clone, Copy, Eq, PartialEq)]
+pub enum GrantType {
+ AccessToken,
+ RefreshToken,
+ LiveTracing,
+ LiveMetrics,
+}
- start_pos = pos + 1;
+impl GrantType {
+ pub fn as_str(&self) -> &'static str {
+ match self {
+ GrantType::AccessToken => "access_token",
+ GrantType::RefreshToken => "refresh_token",
+ GrantType::LiveTracing => "live_tracing",
+ GrantType::LiveMetrics => "live_metrics",
}
}
- None
-}
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn test_extract_oauth_bearer() {
- let input = b"auth=Bearer validtoken";
- let result = extract_oauth_bearer(input);
- assert_eq!(result, Some("validtoken"));
-
- let input = b"auth=Invalid validtoken";
- let result = extract_oauth_bearer(input);
- assert_eq!(result, None);
-
- let input = b"auth=Bearer";
- let result = extract_oauth_bearer(input);
- assert_eq!(result, None);
-
- let input = b"";
- let result = extract_oauth_bearer(input);
- assert_eq!(result, None);
-
- let input = b"auth=Bearer token1\x01auth=Bearer token2";
- let result = extract_oauth_bearer(input);
- assert_eq!(result, Some("token1"));
-
- let input = b"auth=Bearer VALIDTOKEN";
- let result = extract_oauth_bearer(input);
- assert_eq!(result, Some("VALIDTOKEN"));
-
- let input = b"auth=Bearer token with spaces";
- let result = extract_oauth_bearer(input);
- assert_eq!(result, Some("token with spaces"));
-
- let input = b"auth=Bearer token_with_special_chars!@#";
- let result = extract_oauth_bearer(input);
- assert_eq!(result, Some("token_with_special_chars!@#"));
+ pub fn id(&self) -> u8 {
+ match self {
+ GrantType::AccessToken => 0,
+ GrantType::RefreshToken => 1,
+ GrantType::LiveTracing => 2,
+ GrantType::LiveMetrics => 3,
+ }
+ }
- let input = "n,a=user@example.com,\x01host=server.example.com\x01port=143\x01auth=Bearer vF9dft4qmTc2Nvb3RlckBhbHRhdmlzdGEuY29tCg==\x01\x01";
- let result = extract_oauth_bearer(input.as_bytes());
- assert_eq!(result, Some("vF9dft4qmTc2Nvb3RlckBhbHRhdmlzdGEuY29tCg=="));
+ pub fn from_id(id: u8) -> Option<Self> {
+ match id {
+ 0 => Some(GrantType::AccessToken),
+ 1 => Some(GrantType::RefreshToken),
+ 2 => Some(GrantType::LiveTracing),
+ 3 => Some(GrantType::LiveMetrics),
+ _ => None,
+ }
}
}
diff --git a/crates/common/src/auth/oauth/token.rs b/crates/common/src/auth/oauth/token.rs
index 08904983..ec6cb946 100644
--- a/crates/common/src/auth/oauth/token.rs
+++ b/crates/common/src/auth/oauth/token.rs
@@ -13,63 +13,71 @@ use store::{
blake3,
rand::{thread_rng, Rng},
};
+use trc::AddContext;
use utils::codec::leb128::{Leb128Iterator, Leb128Vec};
use crate::Server;
-use super::{crypto::SymmetricEncrypt, CLIENT_ID_MAX_LEN, RANDOM_CODE_LEN};
+use super::{crypto::SymmetricEncrypt, GrantType, CLIENT_ID_MAX_LEN, RANDOM_CODE_LEN};
+
+pub struct TokenInfo {
+ pub grant_type: GrantType,
+ pub account_id: u32,
+ pub client_id: String,
+ pub expiry: u64,
+ pub issued_at: u64,
+ pub expires_in: u64,
+}
+
+const OAUTH_EPOCH: u64 = 946684800; // Jan 1, 2000
impl Server {
- pub async fn issue_custom_token(
+ pub async fn encode_access_token(
&self,
+ grant_type: GrantType,
account_id: u32,
- grant_type: &str,
client_id: &str,
expiry_in: u64,
) -> trc::Result<String> {
- self.encode_access_token(
- grant_type,
- account_id,
- &self
- .password_hash(account_id)
- .await
- .map_err(|err| trc::StoreEvent::UnexpectedError.into_err().details(err))?,
- client_id,
- expiry_in,
- )
- .map_err(|err| trc::StoreEvent::UnexpectedError.into_err().details(err))
- }
-
- pub fn encode_access_token(
- &self,
- grant_type: &str,
- account_id: u32,
- password_hash: &str,
- client_id: &str,
- expiry_in: u64,
- ) -> Result<String, &'static str> {
// Build context
if client_id.len() > CLIENT_ID_MAX_LEN {
- return Err("ClientId is too long");
+ return Err(trc::AuthEvent::Error
+ .into_err()
+ .details("Client id too long"));
}
- let key = self.core.jmap.oauth_key.clone();
+
+ // Include password hash if expiration is over 1 hour
+ let password_hash = if expiry_in > 3600 {
+ self.password_hash(account_id)
+ .await
+ .caused_by(trc::location!())?
+ } else {
+ String::new()
+ };
+
+ let key = &self.core.jmap.oauth_key;
let context = format!(
"{} {} {} {}",
- grant_type, client_id, account_id, password_hash
+ grant_type.as_str(),
+ client_id,
+ account_id,
+ password_hash
);
- let context_nonce = format!("{} nonce {}", grant_type, password_hash);
// Set expiration time
- let expiry = SystemTime::now()
- .duration_since(SystemTime::UNIX_EPOCH)
- .map(|d| d.as_secs())
- .unwrap_or(0)
- .saturating_sub(946684800) // Jan 1, 2000
- + expiry_in;
+ let issued_at = SystemTime::now()
+ .duration_since(SystemTime::UNIX_EPOCH)
+ .map_or(0, |d| d.as_secs())
+ .saturating_sub(OAUTH_EPOCH); // Jan 1, 2000
+ let expiry = issued_at + expiry_in;
// Calculate nonce
let mut hasher = blake3::Hasher::new();
- hasher.update(context_nonce.as_bytes());
+ if !password_hash.is_empty() {
+ hasher.update(password_hash.as_bytes());
+ }
+ hasher.update(grant_type.as_str().as_bytes());
+ hasher.update(issued_at.to_be_bytes().as_slice());
hasher.update(expiry.to_be_bytes().as_slice());
let nonce = hasher
.finalize()
@@ -82,8 +90,15 @@ impl Server {
// Encrypt random bytes
let mut token = SymmetricEncrypt::new(key.as_bytes(), &context)
.encrypt(&thread_rng().gen::<[u8; RANDOM_CODE_LEN]>(), &nonce)
- .map_err(|_| "Failed to encrypt token.")?;
+ .map_err(|_| {
+ trc::AuthEvent::Error
+ .into_err()
+ .ctx(trc::Key::Reason, "Failed to encrypt token")
+ .caused_by(trc::location!())
+ })?;
token.push_leb128(account_id);
+ token.push(grant_type.id());
+ token.push_leb128(issued_at);
token.push_leb128(expiry);
token.extend_from_slice(client_id.as_bytes());
@@ -92,9 +107,9 @@ impl Server {
pub async fn validate_access_token(
&self,
- grant_type: &str,
+ expected_grant_type: Option<GrantType>,
token_: &str,
- ) -> trc::Result<(u32, String, u64)> {
+ ) -> trc::Result<TokenInfo> {
// Base64 decode token
let token = base64_decode(token_.as_bytes()).ok_or_else(|| {
trc::AuthEvent::Error
@@ -103,12 +118,14 @@ impl Server {
.caused_by(trc::location!())
.details(token_.to_string())
})?;
- let (account_id, expiry, client_id) = token
+ let (account_id, grant_type, issued_at, expiry, client_id) = token
.get((RANDOM_CODE_LEN + SymmetricEncrypt::ENCRYPT_TAG_LEN)..)
.and_then(|bytes| {
let mut bytes = bytes.iter();
(
bytes.next_leb128()?,
+ GrantType::from_id(bytes.next().copied()?)?,
+ bytes.next_leb128::<u64>()?,
bytes.next_leb128::<u64>()?,
bytes.copied().map(char::from).collect::<String>(),
)
@@ -125,30 +142,45 @@ impl Server {
// Validate expiration
let now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
- .map(|d| d.as_secs())
- .unwrap_or(0)
- .saturating_sub(946684800); // Jan 1, 2000
- if expiry <= now {
+ .map_or(0, |d| d.as_secs())
+ .saturating_sub(OAUTH_EPOCH); // Jan 1, 2000
+ if expiry <= now || issued_at > now {
return Err(trc::AuthEvent::TokenExpired.into_err());
}
+ // Validate grant type
+ if expected_grant_type.map_or(false, |g| g != grant_type) {
+ return Err(trc::AuthEvent::Error
+ .into_err()
+ .details("Invalid grant type"));
+ }
+
// Obtain password hash
- let password_hash = self
- .password_hash(account_id)
- .await
- .map_err(|err| trc::AuthEvent::Error.into_err().ctx(trc::Key::Details, err))?;
+ let password_hash = if expiry - issued_at > 3600 {
+ self.password_hash(account_id)
+ .await
+ .map_err(|err| trc::AuthEvent::Error.into_err().ctx(trc::Key::Details, err))?
+ } else {
+ String::new()
+ };
// Build context
let key = self.core.jmap.oauth_key.clone();
let context = format!(
"{} {} {} {}",
- grant_type, client_id, account_id, password_hash
+ grant_type.as_str(),
+ client_id,
+ account_id,
+ password_hash
);
- let context_nonce = format!("{} nonce {}", grant_type, password_hash);
// Calculate nonce
let mut hasher = blake3::Hasher::new();
- hasher.update(context_nonce.as_bytes());
+ if !password_hash.is_empty() {
+ hasher.update(password_hash.as_bytes());
+ }
+ hasher.update(grant_type.as_str().as_bytes());
+ hasher.update(issued_at.to_be_bytes().as_slice());
hasher.update(expiry.to_be_bytes().as_slice());
let nonce = hasher
.finalize()
@@ -173,27 +205,46 @@ impl Server {
})?;
// Success
- Ok((account_id, client_id, expiry - now))
+ Ok(TokenInfo {
+ grant_type,
+ account_id,
+ client_id,
+ expiry: expiry + OAUTH_EPOCH,
+ issued_at: issued_at + OAUTH_EPOCH,
+ expires_in: expiry - now,
+ })
}
- pub async fn password_hash(&self, account_id: u32) -> Result<String, &'static str> {
+ pub async fn password_hash(&self, account_id: u32) -> trc::Result<String> {
if account_id != u32::MAX {
self.core
.storage
.directory
.query(QueryBy::Id(account_id), false)
.await
- .map_err(|_| "Temporary lookup error")?
- .ok_or("Account no longer exists")?
+ .caused_by(trc::location!())?
+ .ok_or_else(|| {
+ trc::AuthEvent::Error
+ .into_err()
+ .details("Account no longer exists")
+ })?
.take_str_array(PrincipalField::Secrets)
.unwrap_or_default()
.into_iter()
.next()
- .ok_or("Failed to obtain password hash")
+ .ok_or(
+ trc::AuthEvent::Error
+ .into_err()
+ .details("Account does not contain secrets")
+ .caused_by(trc::location!()),
+ )
} else if let Some((_, secret)) = &self.core.jmap.fallback_admin {
Ok(secret.clone())
} else {
- Err("Invalid account id.")
+ Err(trc::AuthEvent::Error
+ .into_err()
+ .details("Invalid account ID")
+ .caused_by(trc::location!()))
}
}
}
diff --git a/crates/common/src/auth/sasl.rs b/crates/common/src/auth/sasl.rs
new file mode 100644
index 00000000..92ba84b6
--- /dev/null
+++ b/crates/common/src/auth/sasl.rs
@@ -0,0 +1,131 @@
+/*
+ * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
+ */
+
+use mail_send::Credentials;
+
+pub fn sasl_decode_challenge_plain(challenge: &[u8]) -> Option<Credentials<String>> {
+ let mut username = Vec::new();
+ let mut secret = Vec::new();
+ let mut arg_num = 0;
+ for &ch in challenge {
+ if ch != 0 {
+ if arg_num == 1 {
+ username.push(ch);
+ } else if arg_num == 2 {
+ secret.push(ch);
+ }
+ } else {
+ arg_num += 1;
+ }
+ }
+
+ match (String::from_utf8(username), String::from_utf8(secret)) {
+ (Ok(username), Ok(secret)) if !username.is_empty() && !secret.is_empty() => {
+ Some((username, secret).into())
+ }
+ _ => None,
+ }
+}
+
+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() })
+}
+
+fn extract_oauth_bearer(bytes: &[u8]) -> Option<&str> {
+ let mut start_pos = 0;
+ let eof = bytes.len().saturating_sub(1);
+
+ for (pos, ch) in bytes.iter().enumerate() {
+ let is_separator = *ch == 1;
+ if is_separator || pos == eof {
+ if bytes
+ .get(start_pos..start_pos + 12)
+ .map_or(false, |s| s.eq_ignore_ascii_case(b"auth=Bearer "))
+ {
+ return bytes
+ .get(start_pos + 12..if is_separator { pos } else { bytes.len() })
+ .and_then(|s| std::str::from_utf8(s).ok());
+ }
+
+ start_pos = pos + 1;
+ }
+ }
+
+ None
+}
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_extract_oauth_bearer() {
+ let input = b"auth=Bearer validtoken";
+ let result = extract_oauth_bearer(input);
+ assert_eq!(result, Some("validtoken"));
+
+ let input = b"auth=Invalid validtoken";
+ let result = extract_oauth_bearer(input);
+ assert_eq!(result, None);
+
+ let input = b"auth=Bearer";
+ let result = extract_oauth_bearer(input);
+ assert_eq!(result, None);
+
+ let input = b"";
+ let result = extract_oauth_bearer(input);
+ assert_eq!(result, None);
+
+ let input = b"auth=Bearer token1\x01auth=Bearer token2";
+ let result = extract_oauth_bearer(input);
+ assert_eq!(result, Some("token1"));
+
+ let input = b"auth=Bearer VALIDTOKEN";
+ let result = extract_oauth_bearer(input);
+ assert_eq!(result, Some("VALIDTOKEN"));
+
+ let input = b"auth=Bearer token with spaces";
+ let result = extract_oauth_bearer(input);
+ assert_eq!(result, Some("token with spaces"));
+
+ let input = b"auth=Bearer token_with_special_chars!@#";
+ let result = extract_oauth_bearer(input);
+ assert_eq!(result, Some("token_with_special_chars!@#"));
+
+ let input = "n,a=user@example.com,\x01host=server.example.com\x01port=143\x01auth=Bearer vF9dft4qmTc2Nvb3RlckBhbHRhdmlzdGEuY29tCg==\x01\x01";
+ let result = extract_oauth_bearer(input.as_bytes());
+ assert_eq!(result, Some("vF9dft4qmTc2Nvb3RlckBhbHRhdmlzdGEuY29tCg=="));
+ }
+}
diff --git a/crates/imap/src/op/authenticate.rs b/crates/imap/src/op/authenticate.rs
index 3858b25c..c4140ec8 100644
--- a/crates/imap/src/op/authenticate.rs
+++ b/crates/imap/src/op/authenticate.rs
@@ -4,7 +4,13 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
-use common::{auth::AuthRequest, listener::SessionStream};
+use common::{
+ auth::{
+ sasl::{sasl_decode_challenge_oauth, sasl_decode_challenge_plain},
+ AuthRequest,
+ },
+ listener::SessionStream,
+};
use directory::Permission;
use imap_proto::{
protocol::{authenticate::Mechanism, capability::Capability},
@@ -35,14 +41,14 @@ impl<T: SessionStream> Session<T> {
})?;
let credentials = if args.mechanism == Mechanism::Plain {
- decode_challenge_plain(&challenge)
+ sasl_decode_challenge_plain(&challenge)
} else {
- decode_challenge_oauth(&challenge)
+ sasl_decode_challenge_oauth(&challenge)
}
- .map_err(|err| {
+ .ok_or_else(|| {
trc::AuthEvent::Error
.into_err()
- .details(err)
+ .details("Invalid SASL challenge.")
.id(args.tag.clone())
})?;
@@ -152,62 +158,3 @@ impl<T: SessionStream> Session<T> {
.await
}
}
-
-pub fn decode_challenge_plain(challenge: &[u8]) -> Result<Credentials<String>, &'static str> {
- let mut username = Vec::new();
- let mut secret = Vec::new();
- let mut arg_num = 0;
- for &ch in challenge {
- if ch != 0 {
- if arg_num == 1 {
- username.push(ch);
- } else if arg_num == 2 {
- secret.push(ch);
- }
- } else {
- arg_num += 1;
- }
- }
-
- match (String::from_utf8(username), String::from_utf8(secret)) {
- (Ok(username), Ok(secret)) if !username.is_empty() && !secret.is_empty() => {
- Ok((username, secret).into())
- }
- _ => Err("Invalid AUTH=PLAIN challenge."),
- }
-}
-
-pub fn decode_challenge_oauth(challenge: &[u8]) -> Result<Credentials<String>, &'static str> {
- let mut saw_marker = true;
- for (pos, &ch) in challenge.iter().enumerate() {
- if saw_marker {
- if challenge
- .get(pos..)
- .map_or(false, |b| b.starts_with(b"auth=Bearer "))
- {
- let pos = pos + 12;
- return Ok(Credentials::OAuthBearer {
- token: String::from_utf8(
- challenge
- .get(
- pos..pos
- + challenge
- .get(pos..)
- .and_then(|c| c.iter().position(|&ch| ch == 0x01))
- .unwrap_or(challenge.len()),
- )
- .ok_or("Failed to find end of bearer token")?
- .to_vec(),
- )
- .map_err(|_| "Bearer token is not a valid UTF-8 string.")?,
- });
- } else {
- saw_marker = false;
- }
- } else if ch == 0x01 {
- saw_marker = true;
- }
- }
-
- Err("Failed to find 'auth=Bearer' in challenge.")
-}
diff --git a/crates/jmap/src/api/http.rs b/crates/jmap/src/api/http.rs
index d3dd0938..9de2e2ab 100644
--- a/crates/jmap/src/api/http.rs
+++ b/crates/jmap/src/api/http.rs
@@ -7,7 +7,7 @@
use std::{borrow::Cow, net::IpAddr, sync::Arc};
use common::{
- auth::AccessToken,
+ auth::{oauth::GrantType, AccessToken},
core::BuildServer,
expr::{functions::ResolveVariable, *},
ipc::StateEvent,
@@ -285,6 +285,15 @@ impl ParseHttp for Server {
.handle_token_request(&mut req, session.session_id)
.await;
}
+ ("introspect", &Method::POST) => {
+ // Authenticate request
+ let (_in_flight, access_token) =
+ self.authenticate_headers(&req, &session).await?;
+
+ return self
+ .handle_token_introspect(&mut req, &access_token, session.session_id)
+ .await;
+ }
(_, &Method::OPTIONS) => {
return Ok(StatusCode::NO_CONTENT.into_http_response());
}
@@ -321,21 +330,22 @@ impl ParseHttp for Server {
.strip_prefix("/api/telemetry/")
.and_then(|p| {
p.strip_prefix("traces/live/")
- .map(|t| ("traces", "live_tracing", t))
+ .map(|t| ("traces", GrantType::LiveTracing, t))
.or_else(|| {
p.strip_prefix("metrics/live/")
- .map(|t| ("metrics", "live_metrics", t))
+ .map(|t| ("metrics", GrantType::LiveMetrics, t))
})
})
{
- let (account_id, _, _) =
- self.validate_access_token(grant_type, token).await?;
+ let token_info = self
+ .validate_access_token(grant_type.into(), token)
+ .await?;
return self
.handle_telemetry_api_request(
&req,
vec!["", live_path, "live"],
- &AccessToken::from_id(account_id)
+ &AccessToken::from_id(token_info.account_id)
.with_permission(Permission::MetricsLive)
.with_permission(Permission::TracingLive),
)
diff --git a/crates/jmap/src/api/management/enterprise/telemetry.rs b/crates/jmap/src/api/management/enterprise/telemetry.rs
index 376837fc..0ae46fe9 100644
--- a/crates/jmap/src/api/management/enterprise/telemetry.rs
+++ b/crates/jmap/src/api/management/enterprise/telemetry.rs
@@ -14,7 +14,7 @@ use std::{
};
use common::{
- auth::AccessToken,
+ auth::{oauth::GrantType, AccessToken},
telemetry::{
metrics::store::{Metric, MetricsStore},
tracers::store::{TracingQuery, TracingStore},
@@ -355,7 +355,7 @@ impl TelemetryApi for Server {
// Issue a live telemetry token valid for 60 seconds
Ok(JsonResponse::new(json!({
- "data": self.issue_custom_token(account_id, "live_tracing", "web", 60).await?,
+ "data": self.encode_access_token(GrantType::LiveTracing, account_id, "web", 60).await?,
}))
.into_http_response())
}
@@ -366,7 +366,7 @@ impl TelemetryApi for Server {
// Issue a live telemetry token valid for 60 seconds
Ok(JsonResponse::new(json!({
- "data": self.issue_custom_token(account_id, "live_metrics", "web", 60).await?,
+ "data": self.encode_access_token(GrantType::LiveMetrics, account_id, "web", 60).await?,
}))
.into_http_response())
}
diff --git a/crates/jmap/src/auth/oauth/mod.rs b/crates/jmap/src/auth/oauth/mod.rs
index f56585eb..eaa8c0c1 100644
--- a/crates/jmap/src/auth/oauth/mod.rs
+++ b/crates/jmap/src/auth/oauth/mod.rs
@@ -140,11 +140,12 @@ pub enum ErrorType {
pub struct OAuthMetadata {
pub issuer: String,
pub token_endpoint: String,
- pub grant_types_supported: Vec<String>,
+ pub authorization_endpoint: String,
pub device_authorization_endpoint: String,
+ pub introspection_endpoint: String,
+ pub grant_types_supported: Vec<String>,
pub response_types_supported: Vec<String>,
pub scopes_supported: Vec<String>,
- pub authorization_endpoint: String,
}
impl OAuthMetadata {
@@ -152,16 +153,17 @@ impl OAuthMetadata {
let base_url = base_url.as_ref();
OAuthMetadata {
issuer: base_url.into(),
- authorization_endpoint: format!("{}/authorize/code", base_url),
- token_endpoint: format!("{}/auth/token", base_url),
+ authorization_endpoint: format!("{base_url}/authorize/code",),
+ token_endpoint: format!("{base_url}/auth/token"),
grant_types_supported: vec![
"authorization_code".to_string(),
"implicit".to_string(),
"urn:ietf:params:oauth:grant-type:device_code".to_string(),
],
- device_authorization_endpoint: format!("{}/auth/device", base_url),
+ device_authorization_endpoint: format!("{base_url}/auth/device"),
response_types_supported: vec!["code".to_string(), "code token".to_string()],
scopes_supported: vec!["offline_access".to_string()],
+ introspection_endpoint: format!("{base_url}/auth/introspect"),
}
}
}
diff --git a/crates/jmap/src/auth/oauth/token.rs b/crates/jmap/src/auth/oauth/token.rs
index 277a94ae..49046746 100644
--- a/crates/jmap/src/auth/oauth/token.rs
+++ b/crates/jmap/src/auth/oauth/token.rs
@@ -4,7 +4,10 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
-use common::Server;
+use common::{
+ auth::{oauth::GrantType, AccessToken},
+ Server,
+};
use hyper::StatusCode;
use std::future::Future;
use store::write::Bincode;
@@ -22,12 +25,19 @@ pub trait TokenHandler: Sync + Send {
session_id: u64,
) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
+ fn handle_token_introspect(
+ &self,
+ req: &mut HttpRequest,
+ access_token: &AccessToken,
+ session_id: u64,
+ ) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
+
fn issue_token(
&self,
account_id: u32,
client_id: &str,
with_refresh_token: bool,
- ) -> impl Future<Output = Result<OAuthResponse, &'static str>> + Send;
+ ) -> impl Future<Output = trc::Result<OAuthResponse>> + Send;
}
impl TokenHandler for Server {
@@ -139,14 +149,15 @@ impl TokenHandler for Server {
} else if grant_type.eq_ignore_ascii_case("refresh_token") {
if let Some(refresh_token) = params.get("refresh_token") {
response = match self
- .validate_access_token("refresh_token", refresh_token)
+ .validate_access_token(GrantType::RefreshToken.into(), refresh_token)
.await
{
- Ok((account_id, client_id, time_left)) => self
+ Ok(token_info) => self
.issue_token(
- account_id,
- &client_id,
- time_left <= self.core.jmap.oauth_expiry_refresh_token_renew,
+ token_info.account_id,
+ &token_info.client_id,
+ token_info.expires_in
+ <= self.core.jmap.oauth_expiry_refresh_token_renew,
)
.await
.map(TokenResponse::Granted)
@@ -180,32 +191,52 @@ impl TokenHandler for Server {
.into_http_response())
}
+ async fn handle_token_introspect(
+ &self,
+ req: &mut HttpRequest,
+ access_token: &AccessToken,
+ session_id: u64,
+ ) -> trc::Result<HttpResponse> {
+ // Parse token
+ let token = FormData::from_request(req, 1024, session_id)
+ .await?
+ .remove("token")
+ .ok_or_else(|| {
+ trc::ResourceEvent::BadParameters
+ .into_err()
+ .details("Client ID is missing.")
+ })?;
+
+ self.introspect_access_token(&token, access_token)
+ .await
+ .map(|response| JsonResponse::new(response).into_http_response())
+ }
+
async fn issue_token(
&self,
account_id: u32,
client_id: &str,
with_refresh_token: bool,
- ) -> Result<OAuthResponse, &'static str> {
- let password_hash = self.password_hash(account_id).await?;
-
+ ) -> trc::Result<OAuthResponse> {
Ok(OAuthResponse {
- access_token: self.encode_access_token(
- "access_token",
- account_id,
- &password_hash,
- client_id,
- self.core.jmap.oauth_expiry_token,
- )?,
+ access_token: self
+ .encode_access_token(
+ GrantType::AccessToken,
+ account_id,
+ client_id,
+ self.core.jmap.oauth_expiry_token,
+ )
+ .await?,
token_type: "bearer".to_string(),
expires_in: self.core.jmap.oauth_expiry_token,
refresh_token: if with_refresh_token {
self.encode_access_token(
- "refresh_token",
+ GrantType::RefreshToken,
account_id,
- &password_hash,
client_id,
self.core.jmap.oauth_expiry_refresh_token,
- )?
+ )
+ .await?
.into()
} else {
None
diff --git a/crates/managesieve/src/op/authenticate.rs b/crates/managesieve/src/op/authenticate.rs
index eaec3c48..893a33a1 100644
--- a/crates/managesieve/src/op/authenticate.rs
+++ b/crates/managesieve/src/op/authenticate.rs
@@ -5,12 +5,14 @@
*/
use common::{
- auth::AuthRequest,
+ auth::{
+ sasl::{sasl_decode_challenge_oauth, sasl_decode_challenge_plain},
+ AuthRequest,
+ },
listener::{limiter::ConcurrencyLimiter, SessionStream},
ConcurrencyLimiters,
};
use directory::Permission;
-use imap::op::authenticate::{decode_challenge_oauth, decode_challenge_plain};
use imap_proto::{
protocol::authenticate::Mechanism,
receiver::{self, Request},
@@ -39,18 +41,19 @@ impl<T: SessionStream> Session<T> {
let credentials = match mechanism {
Mechanism::Plain | Mechanism::OAuthBearer => {
if !params.is_empty() {
- let challenge =
- base64_decode(params.pop().unwrap().as_bytes()).ok_or_else(|| {
+ base64_decode(params.pop().unwrap().as_bytes())
+ .and_then(|challenge| {
+ if mechanism == Mechanism::Plain {
+ sasl_decode_challenge_plain(&challenge)
+ } else {
+ sasl_decode_challenge_oauth(&challenge)
+ }
+ })
+ .ok_or_else(|| {
trc::AuthEvent::Error
.into_err()
.details("Failed to decode challenge.")
- })?;
- (if mechanism == Mechanism::Plain {
- decode_challenge_plain(&challenge)
- } else {
- decode_challenge_oauth(&challenge)
- }
- .map_err(|err| trc::AuthEvent::Error.into_err().details(err)))?
+ })?
} else {
self.receiver.request = receiver::Request {
tag: String::new(),
diff --git a/crates/pop3/src/op/authenticate.rs b/crates/pop3/src/op/authenticate.rs
index 4c70afa5..795043ce 100644
--- a/crates/pop3/src/op/authenticate.rs
+++ b/crates/pop3/src/op/authenticate.rs
@@ -5,12 +5,14 @@
*/
use common::{
- auth::AuthRequest,
+ auth::{
+ sasl::{sasl_decode_challenge_oauth, sasl_decode_challenge_plain},
+ AuthRequest,
+ },
listener::{limiter::ConcurrencyLimiter, SessionStream},
ConcurrencyLimiters,
};
use directory::Permission;
-use imap::op::authenticate::{decode_challenge_oauth, decode_challenge_plain};
use jmap::auth::rate_limit::RateLimiter;
use mail_parser::decoders::base64::base64_decode;
use mail_send::Credentials;
@@ -31,15 +33,18 @@ impl<T: SessionStream> Session<T> {
Mechanism::Plain | Mechanism::OAuthBearer => {
if !params.is_empty() {
let credentials = base64_decode(params.pop().unwrap().as_bytes())
- .ok_or("Failed to decode challenge.")
.and_then(|challenge| {
if mechanism == Mechanism::Plain {
- decode_challenge_plain(&challenge)
+ sasl_decode_challenge_plain(&challenge)
} else {
- decode_challenge_oauth(&challenge)
+ sasl_decode_challenge_oauth(&challenge)
}
})
- .map_err(|err| trc::AuthEvent::Error.into_err().details(err))?;
+ .ok_or_else(|| {
+ trc::AuthEvent::Error
+ .into_err()
+ .details("Invalid SASL challenge")
+ })?;
self.handle_auth(credentials).await
} else {
diff --git a/crates/smtp/src/inbound/auth.rs b/crates/smtp/src/inbound/auth.rs
index 88b32d64..3a728f1a 100644
--- a/crates/smtp/src/inbound/auth.rs
+++ b/crates/smtp/src/inbound/auth.rs
@@ -5,7 +5,12 @@
*/
use common::{
- auth::{oauth::extract_oauth_bearer, AuthRequest},
+ auth::{
+ sasl::{
+ sasl_decode_challenge_oauth, sasl_decode_challenge_plain, sasl_decode_challenge_xoauth,
+ },
+ AuthRequest,
+ },
listener::SessionStream,
};
use directory::Permission;
@@ -74,30 +79,9 @@ impl<T: SessionStream> Session<T> {
}
} else if let Some(response) = base64_decode(response) {
match (token.mechanism, &mut token.credentials) {
- (AUTH_PLAIN, Credentials::Plain { username, secret }) => {
- let mut b_username = Vec::new();
- let mut b_secret = Vec::new();
- let mut arg_num = 0;
- for ch in response {
- if ch != 0 {
- if arg_num == 1 {
- b_username.push(ch);
- } else if arg_num == 2 {
- b_secret.push(ch);
- }
- } else {
- arg_num += 1;
- }
- }
- match (String::from_utf8(b_username), String::from_utf8(b_secret)) {
- (Ok(s_username), Ok(s_secret)) if !s_username.is_empty() => {
- *username = s_username;
- *secret = s_secret;
- return self
- .authenticate(std::mem::take(&mut token.credentials))
- .await;
- }
- _ => (),
+ (AUTH_PLAIN, _) => {
+ if let Some(credentials) = sasl_decode_challenge_plain(&response) {
+ return self.authenticate(credentials).await;
}
}
(AUTH_LOGIN, Credentials::Plain { username, secret }) => {
@@ -111,45 +95,14 @@ impl<T: SessionStream> Session<T> {
.await
};
}
- (AUTH_OAUTHBEARER, Credentials::OAuthBearer { token: token_ }) => {
- if let Some(bearer) = extract_oauth_bearer(&response) {
- *token_ = bearer.to_string();
- return self
- .authenticate(std::mem::take(&mut token.credentials))
- .await;
+ (AUTH_OAUTHBEARER, _) => {
+ if let Some(credentials) = sasl_decode_challenge_oauth(&response) {
+ return self.authenticate(credentials).await;
}
}
- (AUTH_XOAUTH2, Credentials::XOauth2 { username, secret }) => {
- 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 response {
- 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() => {
- *username = s_username;
- *secret = s_secret;
- return self
- .authenticate(std::mem::take(&mut token.credentials))
- .await;
- }
- _ => (),
+ (AUTH_XOAUTH2, _) => {
+ if let Some(credentials) = sasl_decode_challenge_xoauth(&response) {
+ return self.authenticate(credentials).await;
}
}
@@ -210,7 +163,13 @@ impl<T: SessionStream> Session<T> {
.await;
}
trc::EventType::Security(trc::SecurityEvent::Unauthorized) => {
- self.write(b"550 5.7.1 Your account is not authorized to use this service.\r\n")
+ self.write(
+ concat!(
+ "550 5.7.1 Your account is not authorized ",
+ "to use this service.\r\n"
+ )
+ .as_bytes(),
+ )
.await?;
return Ok(false);
}
diff --git a/tests/src/imap/basic.rs b/tests/src/imap/basic.rs
index acaedb86..f3509f4d 100644
--- a/tests/src/imap/basic.rs
+++ b/tests/src/imap/basic.rs
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
-use imap::op::authenticate::decode_challenge_oauth;
+use common::auth::sasl::sasl_decode_challenge_oauth;
use imap_proto::ResponseType;
use mail_parser::decoders::base64::base64_decode;
use mail_send::Credentials;
@@ -44,7 +44,7 @@ fn decode_challenge() {
assert!(
Credentials::OAuthBearer {
token: "vF9dft4qmTc2Nvb3RlckBhbHRhdmlzdGEuY29tCg==".to_string()
- } == decode_challenge_oauth(
+ } == sasl_decode_challenge_oauth(
&base64_decode(
concat!(
"bixhPXVzZXJAZXhhbXBsZS5jb20sAWhv",
diff --git a/tests/src/jmap/auth_oauth.rs b/tests/src/jmap/auth_oauth.rs
index 3f18a0cd..7b74be57 100644
--- a/tests/src/jmap/auth_oauth.rs
+++ b/tests/src/jmap/auth_oauth.rs
@@ -8,6 +8,8 @@ use std::time::{Duration, Instant};
use base64::{engine::general_purpose, Engine};
use bytes::Bytes;
+use common::auth::oauth::introspect::OAuthIntrospect;
+use imap_proto::ResponseType;
use jmap::auth::oauth::{
DeviceAuthResponse, ErrorType, OAuthCodeRequest, OAuthMetadata, TokenResponse,
};
@@ -21,6 +23,10 @@ use store::ahash::AHashMap;
use crate::{
directory::internal::TestInternalDirectory,
+ imap::{
+ pop::{self, Pop3Connection},
+ ImapConnection, Type,
+ },
jmap::{
assert_is_empty, delivery::SmtpConnection, mailbox::destroy_all_mailboxes, ManagementApi,
},
@@ -108,7 +114,8 @@ pub async fn test(params: &mut JMAPTest) {
// Obtain token
token_params.insert("redirect_uri".to_string(), "https://localhost".to_string());
- let (token, _, _) = unwrap_token_response(post(&metadata.token_endpoint, &token_params).await);
+ let (token, refresh_token, _) =
+ unwrap_token_response(post(&metadata.token_endpoint, &token_params).await);
// Connect to account using token and attempt to search
let john_client = Client::new()
@@ -125,27 +132,62 @@ pub async fn test(params: &mut JMAPTest) {
.ids()
.is_empty());
+ // Introspect token
+ let access_introspect: OAuthIntrospect = post_with_auth::<OAuthIntrospect>(
+ &metadata.introspection_endpoint,
+ token.as_str().into(),
+ &AHashMap::from_iter([("token".to_string(), token.to_string())]),
+ )
+ .await;
+ assert_eq!(access_introspect.username.unwrap(), "jdoe@example.com");
+ assert_eq!(access_introspect.token_type.unwrap(), "bearer");
+ assert_eq!(access_introspect.client_id.unwrap(), "OAuthyMcOAuthFace");
+ assert!(access_introspect.active);
+ let refresh_introspect = post_with_auth::<OAuthIntrospect>(
+ &metadata.introspection_endpoint,
+ token.as_str().into(),
+ &AHashMap::from_iter([("token".to_string(), refresh_token.unwrap())]),
+ )
+ .await;
+ assert_eq!(refresh_introspect.username.unwrap(), "jdoe@example.com");
+ assert_eq!(refresh_introspect.client_id.unwrap(), "OAuthyMcOAuthFace");
+ assert!(refresh_introspect.active);
+ assert_eq!(
+ refresh_introspect.iat.unwrap(),
+ access_introspect.iat.unwrap()
+ );
+
// Try SMTP OAUTHBEARER auth
+ let oauth_bearer_invalid_sasl = general_purpose::STANDARD.encode(format!(
+ "n,a={},\u{1}auth=Bearer {}\u{1}\u{1}",
+ "user@domain", "invalid_token"
+ ));
+ let oauth_bearer_sasl = general_purpose::STANDARD.encode(format!(
+ "n,a={},\u{1}auth=Bearer {}\u{1}\u{1}",
+ "user@domain", token
+ ));
let mut smtp = SmtpConnection::connect().await;
- smtp.send(&format!(
- "AUTH OAUTHBEARER {}",
- general_purpose::STANDARD.encode(format!(
- "n,a={},\u{1}auth=Bearer {}\u{1}\u{1}",
- "user@domain", "invalid_token"
- ))
- ))
- .await;
+ smtp.send(&format!("AUTH OAUTHBEARER {oauth_bearer_invalid_sasl}",))
+ .await;
smtp.read(1, 4).await;
- smtp.send(&format!(
- "AUTH OAUTHBEARER {}",
- general_purpose::STANDARD.encode(format!(
- "n,a={},\u{1}auth=Bearer {}\u{1}\u{1}",
- "user@domain", token
- ))
- ))
- .await;
+ smtp.send(&format!("AUTH OAUTHBEARER {oauth_bearer_sasl}",))
+ .await;
smtp.read(1, 2).await;
+ // Try IMAP OAUTHBEARER auth
+ let mut imap = ImapConnection::connect(b"_x ").await;
+ imap.assert_read(Type::Untagged, ResponseType::Ok).await;
+ imap.send(&format!("AUTHENTICATE OAUTHBEARER {oauth_bearer_sasl}"))
+ .await;
+ imap.assert_read(Type::Tagged, ResponseType::Ok).await;
+
+ // Try POP3 OAUTHBEARER auth
+ let mut pop3 = Pop3Connection::connect().await;
+ pop3.assert_read(pop::ResponseType::Ok).await;
+ pop3.send(&format!("AUTH OAUTHBEARER {oauth_bearer_sasl}"))
+ .await;
+ pop3.assert_read(pop::ResponseType::Ok).await;
+
// ------------------------
// Device code flow
// ------------------------
@@ -315,13 +357,23 @@ pub async fn test(params: &mut JMAPTest) {
assert_is_empty(server).await;
}
-async fn post_bytes(url: &str, params: &AHashMap<String, String>) -> Bytes {
- reqwest::Client::builder()
+async fn post_bytes(
+ url: &str,
+ auth_token: Option<&str>,
+ params: &AHashMap<String, String>,
+) -> Bytes {
+ let mut client = reqwest::Client::builder()
.timeout(Duration::from_millis(500))
.danger_accept_invalid_certs(true)
.build()
.unwrap_or_default()
- .post(url)
+ .post(url);
+
+ if let Some(auth_token) = auth_token {
+ client = client.bearer_auth(auth_token);
+ }
+
+ client
.form(params)
.send()
.await
@@ -332,7 +384,14 @@ async fn post_bytes(url: &str, params: &AHashMap<String, String>) -> Bytes {
}
async fn post<T: DeserializeOwned>(url: &str, params: &AHashMap<String, String>) -> T {
- serde_json::from_slice(&post_bytes(url, params).await).unwrap()
+ post_with_auth(url, None, params).await
+}
+async fn post_with_auth<T: DeserializeOwned>(
+ url: &str,
+ auth_token: Option<&str>,
+ params: &AHashMap<String, String>,
+) -> T {
+ serde_json::from_slice(&post_bytes(url, auth_token, params).await).unwrap()
}
async fn get_bytes(url: &str) -> Bytes {
diff --git a/tests/src/jmap/mod.rs b/tests/src/jmap/mod.rs
index ad708d52..ee06ef75 100644
--- a/tests/src/jmap/mod.rs
+++ b/tests/src/jmap/mod.rs
@@ -105,6 +105,12 @@ greeting = 'Test LMTP instance'
protocol = 'lmtp'
tls.implicit = false
+[server.listener.pop3]
+bind = ["127.0.0.1:4110"]
+protocol = "pop3"
+max-connections = 81920
+tls.implicit = true
+
[server.socket]
reuse-addr = true
@@ -319,7 +325,7 @@ pub async fn jmap_tests() {
)
.await;
- webhooks::test(&mut params).await;
+ /*webhooks::test(&mut params).await;
email_query::test(&mut params, delete).await;
email_get::test(&mut params).await;
email_set::test(&mut params).await;
@@ -333,9 +339,9 @@ pub async fn jmap_tests() {
mailbox::test(&mut params).await;
delivery::test(&mut params).await;
auth_acl::test(&mut params).await;
- auth_limits::test(&mut params).await;
+ auth_limits::test(&mut params).await;*/
auth_oauth::test(&mut params).await;
- event_source::test(&mut params).await;
+ /*event_source::test(&mut params).await;
push_subscription::test(&mut params).await;
sieve_script::test(&mut params).await;
vacation_response::test(&mut params).await;
@@ -346,7 +352,7 @@ pub async fn jmap_tests() {
blob::test(&mut params).await;
permissions::test(&params).await;
purge::test(&mut params).await;
- enterprise::test(&mut params).await;
+ enterprise::test(&mut params).await;*/
if delete {
params.temp_dir.delete();