mirror of
https://github.com/stalwartlabs/mail-server.git
synced 2025-10-06 18:45:45 +08:00
OAuth passing tests.
This commit is contained in:
parent
0959a6d737
commit
63cbb70dbc
10 changed files with 667 additions and 46 deletions
|
@ -65,9 +65,9 @@ impl crate::Config {
|
|||
.property("jmap.session.cache.ttl")?
|
||||
.unwrap_or(Duration::from_secs(3600)),
|
||||
rate_authenticated: settings
|
||||
.property_or_static("jmap.rate-limit.authenticated.rate", "1000/1s")?,
|
||||
.property_or_static("jmap.rate-limit.account.rate", "1000/1s")?,
|
||||
rate_authenticate_req: settings
|
||||
.property_or_static("jmap.rate-limit.authenticate.rate", "10/1s")?,
|
||||
.property_or_static("jmap.rate-limit.authentication.rate", "10/1s")?,
|
||||
rate_anonymous: settings
|
||||
.property_or_static("jmap.rate-limit.anonymous.rate", "100/1s")?,
|
||||
rate_use_forwarded: settings
|
||||
|
|
|
@ -86,11 +86,11 @@ impl JMAP {
|
|||
Ok(None) => RequestError::not_found().into_http_response(),
|
||||
Err(err) => {
|
||||
tracing::error!(event = "error",
|
||||
context = "blob_store",
|
||||
account_id = account_id.document_id(),
|
||||
blob_id = ?blob_id,
|
||||
error = ?err,
|
||||
"Failed to download blob");
|
||||
context = "blob_store",
|
||||
account_id = account_id.document_id(),
|
||||
blob_id = ?blob_id,
|
||||
error = ?err,
|
||||
"Failed to download blob");
|
||||
RequestError::internal_server_error().into_http_response()
|
||||
}
|
||||
};
|
||||
|
@ -161,48 +161,40 @@ impl JMAP {
|
|||
|
||||
match (path.next().unwrap_or(""), req.method()) {
|
||||
("", &Method::GET) => {
|
||||
// Limit anonymous requests
|
||||
if let Err(err) = self.is_anonymous_allowed(remote_addr) {
|
||||
return err.into_http_response();
|
||||
return match self.is_anonymous_allowed(remote_addr) {
|
||||
Ok(_) => self.handle_user_device_auth(req).await,
|
||||
Err(err) => err.into_http_response(),
|
||||
}
|
||||
todo!()
|
||||
}
|
||||
("", &Method::POST) => {
|
||||
// Limit authentication requests
|
||||
if let Err(err) = self.is_auth_allowed(remote_addr) {
|
||||
return err.into_http_response();
|
||||
return match self.is_auth_allowed(remote_addr) {
|
||||
Ok(_) => self.handle_user_device_auth_post(req).await,
|
||||
Err(err) => err.into_http_response(),
|
||||
}
|
||||
|
||||
todo!()
|
||||
}
|
||||
("code", &Method::GET) => {
|
||||
// Limit anonymous requests
|
||||
if let Err(err) = self.is_anonymous_allowed(remote_addr) {
|
||||
return err.into_http_response();
|
||||
return match self.is_anonymous_allowed(remote_addr) {
|
||||
Ok(_) => self.handle_user_code_auth(req).await,
|
||||
Err(err) => err.into_http_response(),
|
||||
}
|
||||
todo!()
|
||||
}
|
||||
("code", &Method::POST) => {
|
||||
// Limit authentication requests
|
||||
if let Err(err) = self.is_auth_allowed(remote_addr) {
|
||||
return err.into_http_response();
|
||||
return match self.is_auth_allowed(remote_addr) {
|
||||
Ok(_) => self.handle_user_code_auth_post(req).await,
|
||||
Err(err) => err.into_http_response(),
|
||||
}
|
||||
|
||||
todo!()
|
||||
}
|
||||
("device", &Method::POST) => {
|
||||
// Limit anonymous requests
|
||||
if let Err(err) = self.is_anonymous_allowed(remote_addr) {
|
||||
return err.into_http_response();
|
||||
return match self.is_anonymous_allowed(remote_addr) {
|
||||
Ok(_) => self.handle_device_auth(req, instance).await,
|
||||
Err(err) => err.into_http_response(),
|
||||
}
|
||||
todo!()
|
||||
}
|
||||
("token", &Method::POST) => {
|
||||
// Limit anonymous requests
|
||||
if let Err(err) = self.is_anonymous_allowed(remote_addr) {
|
||||
return err.into_http_response();
|
||||
return match self.is_anonymous_allowed(remote_addr) {
|
||||
Ok(_) => self.handle_token_request(req).await,
|
||||
Err(err) => err.into_http_response(),
|
||||
}
|
||||
todo!()
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
@ -306,7 +298,7 @@ pub async fn fetch_body(
|
|||
let mut bytes = Vec::with_capacity(1024);
|
||||
while let Some(Ok(frame)) = req.frame().await {
|
||||
if let Some(data) = frame.data_ref() {
|
||||
if bytes.len() + data.len() < max_size {
|
||||
if bytes.len() + data.len() <= max_size {
|
||||
bytes.extend_from_slice(data);
|
||||
} else {
|
||||
return Err(RequestError::limit(RequestLimitError::Size));
|
||||
|
@ -409,8 +401,15 @@ impl ToHttpResponse for UploadResponse {
|
|||
|
||||
impl ToHttpResponse for RequestError {
|
||||
fn into_http_response(self) -> HttpResponse {
|
||||
JsonResponse::with_status(StatusCode::from_u16(self.status).unwrap(), self)
|
||||
.into_http_response()
|
||||
hyper::Response::builder()
|
||||
.status(StatusCode::from_u16(self.status).unwrap())
|
||||
.header(header::CONTENT_TYPE, "application/problem+json")
|
||||
.body(
|
||||
Full::new(Bytes::from(serde_json::to_string(&self).unwrap()))
|
||||
.map_err(|never| match never {})
|
||||
.boxed(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ use super::{
|
|||
|
||||
impl JMAP {
|
||||
// Code authorization flow, handles an authorization request
|
||||
pub async fn handle_user_code_auth(req: &mut HttpRequest) -> HttpResponse {
|
||||
pub async fn handle_user_code_auth(&self, req: &mut HttpRequest) -> HttpResponse {
|
||||
let params = form_urlencoded::parse(req.uri().query().unwrap_or_default().as_bytes())
|
||||
.into_owned()
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
|
|
@ -199,7 +199,7 @@ impl JMAP {
|
|||
),
|
||||
rate_limit_auth: LruCache::with_capacity(
|
||||
config
|
||||
.property("jmap.rate-limit.authenticated.size")
|
||||
.property("jmap.rate-limit.account.size")
|
||||
.failed("Invalid property")
|
||||
.unwrap_or(1024),
|
||||
),
|
||||
|
|
|
@ -23,6 +23,7 @@ impl JMAP {
|
|||
let sort_as_tree = request.arguments.sort_as_tree.unwrap_or(false);
|
||||
let filter_as_tree = request.arguments.filter_as_tree.unwrap_or(false);
|
||||
let mut filters = Vec::with_capacity(request.filter.len());
|
||||
let mailbox_ids = self.mailbox_get_or_create(account_id).await?;
|
||||
|
||||
for cond in std::mem::take(&mut request.filter) {
|
||||
match cond {
|
||||
|
@ -100,11 +101,7 @@ impl JMAP {
|
|||
&& (paginate.is_some()
|
||||
|| (response.total.map_or(false, |total| total > 0) && filter_as_tree))
|
||||
{
|
||||
for document_id in self
|
||||
.get_document_ids(account_id, Collection::Mailbox)
|
||||
.await?
|
||||
.unwrap_or_default()
|
||||
{
|
||||
for document_id in mailbox_ids {
|
||||
let parent_id = self
|
||||
.get_property::<Object<Value>>(
|
||||
account_id,
|
||||
|
|
|
@ -20,4 +20,6 @@ serde = { version = "1.0", features = ["derive"]}
|
|||
serde_json = "1.0"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
reqwest = { version = "0.11", default-features = false, features = ["rustls-tls"]}
|
||||
bytes = "1.4.0"
|
||||
|
||||
|
|
|
@ -47,7 +47,7 @@ pub async fn test(server: Arc<JMAP>, admin_client: &mut Client) {
|
|||
server
|
||||
.auth_db
|
||||
.execute(
|
||||
"INSERT INTO users (login, secret, name) VALUES (?, ?, ?)",
|
||||
"INSERT OR REPLACE INTO users (login, secret, name) VALUES (?, ?, ?)",
|
||||
vec![login.to_string(), secret.to_string(), name.to_string()].into_iter()
|
||||
)
|
||||
.await
|
176
tests/src/jmap/auth_limits.rs
Normal file
176
tests/src/jmap/auth_limits.rs
Normal file
|
@ -0,0 +1,176 @@
|
|||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
use jmap::JMAP;
|
||||
use jmap_client::{
|
||||
client::{Client, Credentials},
|
||||
mailbox::{self},
|
||||
};
|
||||
use jmap_proto::types::id::Id;
|
||||
|
||||
pub async fn test(server: Arc<JMAP>, _client: &mut Client) {
|
||||
println!("Running Authorization tests...");
|
||||
|
||||
// Create test account
|
||||
assert!(
|
||||
server
|
||||
.auth_db
|
||||
.execute(
|
||||
"INSERT OR REPLACE INTO users (login, secret, name) VALUES (?, ?, ?)",
|
||||
vec![
|
||||
"jdoe@example.com".to_string(),
|
||||
"12345".to_string(),
|
||||
"John Doe".to_string()
|
||||
]
|
||||
.into_iter()
|
||||
)
|
||||
.await
|
||||
);
|
||||
let account_id = Id::from(1u64).to_string();
|
||||
|
||||
// Wait for rate limit to be restored after running previous tests
|
||||
//tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
|
||||
// Incorrect passwords should be rejected with a 401 error
|
||||
assert!(matches!(
|
||||
Client::new()
|
||||
.credentials(Credentials::basic("jdoe@example.com", "abcde"))
|
||||
.accept_invalid_certs(true)
|
||||
.connect("https://127.0.0.1:8899")
|
||||
.await,
|
||||
Err(jmap_client::Error::Problem(err)) if err.status() == Some(401)));
|
||||
|
||||
// Requests should be rate limited
|
||||
let mut n_401 = 0;
|
||||
let mut n_429 = 0;
|
||||
for n in 0..110 {
|
||||
if let Err(jmap_client::Error::Problem(problem)) = Client::new()
|
||||
.credentials(Credentials::basic(
|
||||
"not_an_account@example.com",
|
||||
&format!("brute_force{}", n),
|
||||
))
|
||||
.accept_invalid_certs(true)
|
||||
.connect("https://127.0.0.1:8899")
|
||||
.await
|
||||
{
|
||||
if problem.status().unwrap() == 401 {
|
||||
n_401 += 1;
|
||||
if n_401 > 100 {
|
||||
panic!("Rate limiter failed.");
|
||||
}
|
||||
} else if problem.status().unwrap() == 429 {
|
||||
n_429 += 1;
|
||||
if n_429 > 11 {
|
||||
panic!("Rate limiter too restrictive.");
|
||||
}
|
||||
} else {
|
||||
panic!("Unexpected error status {}", problem.status().unwrap());
|
||||
}
|
||||
} else {
|
||||
panic!("Unexpected response.");
|
||||
}
|
||||
}
|
||||
|
||||
// Limit should be restored after 1 second
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
|
||||
// Login with the correct credentials
|
||||
let client = Client::new()
|
||||
.credentials(Credentials::basic("jdoe@example.com", "12345"))
|
||||
.accept_invalid_certs(true)
|
||||
.connect("https://127.0.0.1:8899")
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(client.session().username(), "jdoe@example.com");
|
||||
assert_eq!(
|
||||
client.session().account(&account_id).unwrap().name(),
|
||||
"jdoe@example.com"
|
||||
);
|
||||
assert!(client.session().account(&account_id).unwrap().is_personal());
|
||||
|
||||
// Uploads up to 50000000 bytes should be allowed
|
||||
assert_eq!(
|
||||
client
|
||||
.upload(None, vec![b'A'; 5000000], None)
|
||||
.await
|
||||
.unwrap()
|
||||
.size(),
|
||||
5000000
|
||||
);
|
||||
assert!(client
|
||||
.upload(None, vec![b'A'; 5000001], None)
|
||||
.await
|
||||
.is_err());
|
||||
|
||||
// Users should be allowed to create identities only
|
||||
// using email addresses associated to their principal
|
||||
let implement = "true";
|
||||
/*client
|
||||
.identity_create("John Doe", "jdoe@example.com")
|
||||
.await
|
||||
.unwrap()
|
||||
.take_id();
|
||||
client
|
||||
.identity_create("John Doe (secondary)", "john.doe@example.com")
|
||||
.await
|
||||
.unwrap()
|
||||
.take_id();
|
||||
assert!(matches!(
|
||||
client
|
||||
.identity_create("John the Spammer", "spammy@mcspamface.com")
|
||||
.await,
|
||||
Err(jmap_client::Error::Set(SetError {
|
||||
type_: SetErrorType::InvalidProperties,
|
||||
..
|
||||
}))
|
||||
));*/
|
||||
|
||||
// Concurrent requests check
|
||||
let client = Arc::new(client);
|
||||
for _ in 0..8 {
|
||||
let client_ = client.clone();
|
||||
tokio::spawn(async move {
|
||||
client_
|
||||
.mailbox_query(
|
||||
mailbox::query::Filter::name("__sleep").into(),
|
||||
[mailbox::query::Comparator::name()].into(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
});
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
assert!(matches!(
|
||||
client
|
||||
.mailbox_query(
|
||||
mailbox::query::Filter::name("__sleep").into(),
|
||||
[mailbox::query::Comparator::name()].into(),
|
||||
)
|
||||
.await,
|
||||
Err(jmap_client::Error::Problem(err)) if err.status() == Some(400)));
|
||||
|
||||
// Wait for sleep to be done
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
|
||||
// Concurrent upload test
|
||||
for _ in 0..4 {
|
||||
let client_ = client.clone();
|
||||
tokio::spawn(async move {
|
||||
client_.upload(None, b"sleep".to_vec(), None).await.unwrap();
|
||||
});
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
assert!(matches!(
|
||||
client.upload(None, b"sleep".to_vec(), None).await,
|
||||
Err(jmap_client::Error::Problem(err)) if err.status() == Some(400)));
|
||||
|
||||
// Destroy test accounts
|
||||
let implement = "true";
|
||||
/*admin_client
|
||||
.set_default_account_id(Id::new(SUPERUSER_ID as u64))
|
||||
.principal_destroy(&account_id)
|
||||
.await
|
||||
.unwrap();
|
||||
admin_client.principal_destroy(&domain_id).await.unwrap();
|
||||
server.store.principal_purge().unwrap();
|
||||
server.store.assert_is_empty();*/
|
||||
}
|
421
tests/src/jmap/auth_oauth.rs
Normal file
421
tests/src/jmap/auth_oauth.rs
Normal file
|
@ -0,0 +1,421 @@
|
|||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
use bytes::Bytes;
|
||||
use jmap::{
|
||||
auth::oauth::{DeviceAuthResponse, ErrorType, OAuthMetadata, TokenResponse},
|
||||
JMAP,
|
||||
};
|
||||
use jmap_client::{
|
||||
client::{Client, Credentials},
|
||||
mailbox::query::Filter,
|
||||
};
|
||||
use jmap_proto::types::id::Id;
|
||||
use reqwest::{header, redirect::Policy};
|
||||
use serde::de::DeserializeOwned;
|
||||
use store::ahash::AHashMap;
|
||||
|
||||
pub async fn test(server: Arc<JMAP>, _client: &mut Client) {
|
||||
println!("Running OAuth tests...");
|
||||
|
||||
// Create test account
|
||||
assert!(
|
||||
server
|
||||
.auth_db
|
||||
.execute(
|
||||
"INSERT OR REPLACE INTO users (login, secret, name) VALUES (?, ?, ?)",
|
||||
vec![
|
||||
"jdoe@example.com".to_string(),
|
||||
"abcde".to_string(),
|
||||
"John Doe".to_string()
|
||||
]
|
||||
.into_iter()
|
||||
)
|
||||
.await
|
||||
);
|
||||
let john_id = Id::from(1u64).to_string();
|
||||
|
||||
// Obtain OAuth metadata
|
||||
let metadata: OAuthMetadata =
|
||||
get("https://127.0.0.1:8899/.well-known/oauth-authorization-server").await;
|
||||
//println!("OAuth metadata: {:#?}", metadata);
|
||||
|
||||
// ------------------------
|
||||
// Authorization code flow
|
||||
// ------------------------
|
||||
|
||||
// Build authorization request
|
||||
let auth_endpoint = format!(
|
||||
"{}?response_type=token&client_id=OAuthyMcOAuthFace&state=xyz&redirect_uri=https://localhost",
|
||||
metadata.authorization_endpoint
|
||||
);
|
||||
let mut auth_request = AHashMap::from_iter([
|
||||
("email".to_string(), "jdoe@example.com".to_string()),
|
||||
("password".to_string(), "wrong_pass".to_string()),
|
||||
(
|
||||
"code".to_string(),
|
||||
parse_code_input(get_bytes(&auth_endpoint).await),
|
||||
),
|
||||
]);
|
||||
|
||||
// Exceeding the max failed attempts should redirect with an access_denied code
|
||||
assert_eq!(
|
||||
post_expect_redirect(&metadata.authorization_endpoint, &auth_request).await,
|
||||
"https://localhost?error=access_denied&state=xyz"
|
||||
);
|
||||
|
||||
// Authenticate with the correct password
|
||||
auth_request.insert("password".to_string(), "abcde".to_string());
|
||||
auth_request.insert(
|
||||
"code".to_string(),
|
||||
parse_code_input(get_bytes(&auth_endpoint).await),
|
||||
);
|
||||
let code = parse_code_redirect(
|
||||
post_expect_redirect(&metadata.authorization_endpoint, &auth_request).await,
|
||||
"xyz",
|
||||
);
|
||||
|
||||
// Both client_id and redirect_uri have to match
|
||||
let mut token_params = AHashMap::from_iter([
|
||||
("client_id".to_string(), "invalid_client".to_string()),
|
||||
("redirect_uri".to_string(), "https://localhost".to_string()),
|
||||
("grant_type".to_string(), "authorization_code".to_string()),
|
||||
("code".to_string(), code),
|
||||
]);
|
||||
assert_eq!(
|
||||
post::<TokenResponse>(&metadata.token_endpoint, &token_params).await,
|
||||
TokenResponse::Error {
|
||||
error: ErrorType::InvalidClient
|
||||
}
|
||||
);
|
||||
token_params.insert("client_id".to_string(), "OAuthyMcOAuthFace".to_string());
|
||||
token_params.insert(
|
||||
"redirect_uri".to_string(),
|
||||
"https://some-other.url".to_string(),
|
||||
);
|
||||
assert_eq!(
|
||||
post::<TokenResponse>(&metadata.token_endpoint, &token_params).await,
|
||||
TokenResponse::Error {
|
||||
error: ErrorType::InvalidClient
|
||||
}
|
||||
);
|
||||
|
||||
// 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);
|
||||
|
||||
// Connect to account using token and attempt to search
|
||||
let john_client = Client::new()
|
||||
.credentials(Credentials::bearer(&token))
|
||||
.accept_invalid_certs(true)
|
||||
.connect("https://127.0.0.1:8899")
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(john_client.default_account_id(), john_id);
|
||||
assert!(!john_client
|
||||
.mailbox_query(None::<Filter>, None::<Vec<_>>)
|
||||
.await
|
||||
.unwrap()
|
||||
.ids()
|
||||
.is_empty());
|
||||
|
||||
// ------------------------
|
||||
// Device code flow
|
||||
// ------------------------
|
||||
|
||||
// Request a device code
|
||||
let device_code_params = AHashMap::from_iter([("client_id".to_string(), "1234".to_string())]);
|
||||
let device_response: DeviceAuthResponse =
|
||||
post(&metadata.device_authorization_endpoint, &device_code_params).await;
|
||||
//println!("Device response: {:#?}", device_response);
|
||||
|
||||
// Status should be pending
|
||||
let mut token_params = AHashMap::from_iter([
|
||||
("client_id".to_string(), "1234".to_string()),
|
||||
(
|
||||
"grant_type".to_string(),
|
||||
"urn:ietf:params:oauth:grant-type:device_code".to_string(),
|
||||
),
|
||||
(
|
||||
"device_code".to_string(),
|
||||
device_response.device_code.to_string(),
|
||||
),
|
||||
]);
|
||||
assert_eq!(
|
||||
post::<TokenResponse>(&metadata.token_endpoint, &token_params).await,
|
||||
TokenResponse::Error {
|
||||
error: ErrorType::AuthorizationPending
|
||||
}
|
||||
);
|
||||
|
||||
// Invalidate the code by having too many unsuccessful attempts
|
||||
assert_client_auth(
|
||||
"jdoe@example.com",
|
||||
"wrongpass",
|
||||
&device_response,
|
||||
"Incorrect",
|
||||
)
|
||||
.await;
|
||||
assert_client_auth(
|
||||
"jdoe@example.com",
|
||||
"wrongpass",
|
||||
&device_response,
|
||||
"Invalid or expired authentication code.",
|
||||
)
|
||||
.await;
|
||||
assert_eq!(
|
||||
post::<TokenResponse>(&metadata.token_endpoint, &token_params).await,
|
||||
TokenResponse::Error {
|
||||
error: ErrorType::AccessDenied
|
||||
}
|
||||
);
|
||||
|
||||
// Request a new device code
|
||||
let device_response: DeviceAuthResponse =
|
||||
post(&metadata.device_authorization_endpoint, &device_code_params).await;
|
||||
token_params.insert(
|
||||
"device_code".to_string(),
|
||||
device_response.device_code.to_string(),
|
||||
);
|
||||
|
||||
// Let the code expire and make sure it's invalidated
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
assert_client_auth(
|
||||
"jdoe@example.com",
|
||||
"abcde",
|
||||
&device_response,
|
||||
"Invalid or expired authentication code.",
|
||||
)
|
||||
.await;
|
||||
assert_eq!(
|
||||
post::<TokenResponse>(&metadata.token_endpoint, &token_params).await,
|
||||
TokenResponse::Error {
|
||||
error: ErrorType::ExpiredToken
|
||||
}
|
||||
);
|
||||
|
||||
// Authenticate account using a valid code
|
||||
let device_response: DeviceAuthResponse =
|
||||
post(&metadata.device_authorization_endpoint, &device_code_params).await;
|
||||
token_params.insert(
|
||||
"device_code".to_string(),
|
||||
device_response.device_code.to_string(),
|
||||
);
|
||||
assert_client_auth("jdoe@example.com", "abcde", &device_response, "successful").await;
|
||||
|
||||
// Obtain token
|
||||
let (token, refresh_token, _) =
|
||||
unwrap_token_response(post(&metadata.token_endpoint, &token_params).await);
|
||||
let refresh_token = refresh_token.unwrap();
|
||||
|
||||
// Authorization codes can only be used once
|
||||
assert_eq!(
|
||||
post::<TokenResponse>(&metadata.token_endpoint, &token_params).await,
|
||||
TokenResponse::Error {
|
||||
error: ErrorType::ExpiredToken
|
||||
}
|
||||
);
|
||||
|
||||
// Connect to account using token and attempt to search
|
||||
let john_client = Client::new()
|
||||
.credentials(Credentials::bearer(&token))
|
||||
.accept_invalid_certs(true)
|
||||
.connect("https://127.0.0.1:8899")
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(john_client.default_account_id(), john_id);
|
||||
assert!(!john_client
|
||||
.mailbox_query(None::<Filter>, None::<Vec<_>>)
|
||||
.await
|
||||
.unwrap()
|
||||
.ids()
|
||||
.is_empty());
|
||||
|
||||
// Connecting using the refresh token should not work
|
||||
assert_unauthorized("https://127.0.0.1:8899", &refresh_token).await;
|
||||
|
||||
// Refreshing a token using the access token should not work
|
||||
assert_eq!(
|
||||
post::<TokenResponse>(
|
||||
&metadata.token_endpoint,
|
||||
&AHashMap::from_iter([
|
||||
("client_id".to_string(), "1234".to_string()),
|
||||
("grant_type".to_string(), "refresh_token".to_string()),
|
||||
("refresh_token".to_string(), token),
|
||||
]),
|
||||
)
|
||||
.await,
|
||||
TokenResponse::Error {
|
||||
error: ErrorType::InvalidGrant
|
||||
}
|
||||
);
|
||||
|
||||
// Refreshing the access token before expiration should not include a new refresh token
|
||||
let refresh_params = AHashMap::from_iter([
|
||||
("client_id".to_string(), "1234".to_string()),
|
||||
("grant_type".to_string(), "refresh_token".to_string()),
|
||||
("refresh_token".to_string(), refresh_token),
|
||||
]);
|
||||
let (token, new_refresh_token, _) =
|
||||
unwrap_token_response(post(&metadata.token_endpoint, &refresh_params).await);
|
||||
assert_eq!(new_refresh_token, None);
|
||||
|
||||
// Wait 1 second and make sure the access token expired
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
assert_unauthorized("https://127.0.0.1:8899", &token).await;
|
||||
|
||||
// Wait another second for the refresh token to be about to expire
|
||||
// and expect a new refresh token
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
let (_, new_refresh_token, _) =
|
||||
unwrap_token_response(post(&metadata.token_endpoint, &refresh_params).await);
|
||||
//println!("New refresh token: {:?}", new_refresh_token);
|
||||
assert_ne!(new_refresh_token, None);
|
||||
|
||||
// Wait another second and make sure the refresh token expired
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
assert_eq!(
|
||||
post::<TokenResponse>(&metadata.token_endpoint, &refresh_params).await,
|
||||
TokenResponse::Error {
|
||||
error: ErrorType::InvalidGrant
|
||||
}
|
||||
);
|
||||
|
||||
// Destroy test accounts
|
||||
let cleanup = "true";
|
||||
/*for principal_id in [john_id, domain_id] {
|
||||
admin_client.principal_destroy(&principal_id).await.unwrap();
|
||||
}
|
||||
server.store.principal_purge().unwrap();
|
||||
server.store.assert_is_empty();*/
|
||||
}
|
||||
|
||||
async fn post_bytes(url: &str, params: &AHashMap<String, String>) -> Bytes {
|
||||
reqwest::Client::builder()
|
||||
.timeout(Duration::from_millis(500))
|
||||
.danger_accept_invalid_certs(true)
|
||||
.build()
|
||||
.unwrap_or_default()
|
||||
.post(url)
|
||||
.form(params)
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.bytes()
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
async fn post<T: DeserializeOwned>(url: &str, params: &AHashMap<String, String>) -> T {
|
||||
serde_json::from_slice(&post_bytes(url, params).await).unwrap()
|
||||
}
|
||||
|
||||
async fn post_expect_redirect(url: &str, params: &AHashMap<String, String>) -> String {
|
||||
let response = reqwest::Client::builder()
|
||||
.timeout(Duration::from_millis(500))
|
||||
.danger_accept_invalid_certs(true)
|
||||
.redirect(Policy::none())
|
||||
.build()
|
||||
.unwrap_or_default()
|
||||
.post(url)
|
||||
.form(params)
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
response
|
||||
.headers()
|
||||
.get(header::LOCATION)
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_string()
|
||||
}
|
||||
|
||||
async fn get_bytes(url: &str) -> Bytes {
|
||||
reqwest::Client::builder()
|
||||
.timeout(Duration::from_millis(500))
|
||||
.danger_accept_invalid_certs(true)
|
||||
.build()
|
||||
.unwrap_or_default()
|
||||
.get(url)
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.bytes()
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
async fn get<T: DeserializeOwned>(url: &str) -> T {
|
||||
serde_json::from_slice(&get_bytes(url).await).unwrap()
|
||||
}
|
||||
|
||||
async fn assert_client_auth(
|
||||
email: &str,
|
||||
pass: &str,
|
||||
device_response: &DeviceAuthResponse,
|
||||
expect: &str,
|
||||
) {
|
||||
let html_response = String::from_utf8_lossy(
|
||||
&post_bytes(
|
||||
&device_response.verification_uri,
|
||||
&AHashMap::from_iter([
|
||||
("email".to_string(), email.to_string()),
|
||||
("password".to_string(), pass.to_string()),
|
||||
("code".to_string(), device_response.user_code.to_string()),
|
||||
]),
|
||||
)
|
||||
.await,
|
||||
)
|
||||
.into_owned();
|
||||
assert!(html_response.contains(expect), "{:#?}", html_response);
|
||||
}
|
||||
|
||||
async fn assert_unauthorized(base_url: &str, token: &str) {
|
||||
match Client::new()
|
||||
.credentials(Credentials::bearer(token))
|
||||
.accept_invalid_certs(true)
|
||||
.connect(base_url)
|
||||
.await
|
||||
{
|
||||
Ok(_) => panic!("Expected unauthorized access."),
|
||||
Err(err) => {
|
||||
let err = err.to_string();
|
||||
assert!(err.contains("Unauthorized"), "{}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_code_input(bytes: Bytes) -> String {
|
||||
let html = String::from_utf8_lossy(&bytes).into_owned();
|
||||
if let Some((_, code)) = html.split_once("name=\"code\" value=\"") {
|
||||
if let Some((code, _)) = code.split_once('\"') {
|
||||
return code.to_string();
|
||||
}
|
||||
}
|
||||
panic!("Could not parse code input: {}", html);
|
||||
}
|
||||
|
||||
fn parse_code_redirect(uri: String, state: &str) -> String {
|
||||
if let Some(code) = uri.strip_prefix("https://localhost?code=") {
|
||||
if let Some(code) = code.strip_suffix(&format!("&state={}", state)) {
|
||||
return code.to_string();
|
||||
}
|
||||
}
|
||||
panic!("Invalid redirect URI: {}", uri);
|
||||
}
|
||||
|
||||
fn unwrap_token_response(response: TokenResponse) -> (String, Option<String>, u64) {
|
||||
match response {
|
||||
TokenResponse::Granted {
|
||||
access_token,
|
||||
token_type,
|
||||
expires_in,
|
||||
refresh_token,
|
||||
..
|
||||
} => {
|
||||
assert_eq!(token_type, "bearer");
|
||||
(access_token, refresh_token, expires_in)
|
||||
}
|
||||
TokenResponse::Error { error } => panic!("Expected granted, got {:?}", error),
|
||||
}
|
||||
}
|
|
@ -7,7 +7,9 @@ use tokio::sync::watch;
|
|||
|
||||
use crate::{add_test_certs, store::TempDir};
|
||||
|
||||
pub mod acl;
|
||||
pub mod auth_acl;
|
||||
pub mod auth_limits;
|
||||
pub mod auth_oauth;
|
||||
pub mod email_changes;
|
||||
pub mod email_copy;
|
||||
pub mod email_get;
|
||||
|
@ -48,6 +50,19 @@ private-key = 'file://{PK}'
|
|||
[jmap.protocol]
|
||||
set.max-objects = 100000
|
||||
|
||||
[jmap.protocol.request]
|
||||
max-concurrent = 8
|
||||
max-concurrent-total = 512
|
||||
|
||||
[jmap.protocol.upload]
|
||||
max-size = 5000000
|
||||
max-concurrent = 4
|
||||
|
||||
[jmap.rate-limit]
|
||||
account.rate = '100/1m'
|
||||
authentication.rate = '100/1m'
|
||||
anonymous.rate = '1000/1m'
|
||||
|
||||
[jmap.auth.database]
|
||||
type = 'sql'
|
||||
address = 'sqlite::memory:'
|
||||
|
@ -58,6 +73,15 @@ login-by-uid = 'SELECT login FROM users WHERE ROWID - 1 = ?'
|
|||
secret-by-uid = 'SELECT secret FROM users WHERE ROWID - 1 = ?'
|
||||
gids-by-uid = 'SELECT gid FROM groups WHERE uid = ?'
|
||||
|
||||
[oauth]
|
||||
key = 'parerga_und_paralipomena'
|
||||
max-auth-attempts = 1
|
||||
|
||||
[oauth.expiry]
|
||||
user-code = '1s'
|
||||
token = '1s'
|
||||
refresh-token = '3s'
|
||||
refresh-token-renew = '2s'
|
||||
";
|
||||
|
||||
#[tokio::test]
|
||||
|
@ -82,7 +106,9 @@ pub async fn jmap_tests() {
|
|||
//thread_get::test(params.server.clone(), &mut params.client).await;
|
||||
//thread_merge::test(params.server.clone(), &mut params.client).await;
|
||||
//mailbox::test(params.server.clone(), &mut params.client).await;
|
||||
acl::test(params.server.clone(), &mut params.client).await;
|
||||
//auth_acl::test(params.server.clone(), &mut params.client).await;
|
||||
//auth_limits::test(params.server.clone(), &mut params.client).await;
|
||||
auth_oauth::test(params.server.clone(), &mut params.client).await;
|
||||
|
||||
if delete {
|
||||
params.temp_dir.delete();
|
||||
|
|
Loading…
Add table
Reference in a new issue