mirror of
https://github.com/stalwartlabs/mail-server.git
synced 2025-10-09 20:15:47 +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")?
|
.property("jmap.session.cache.ttl")?
|
||||||
.unwrap_or(Duration::from_secs(3600)),
|
.unwrap_or(Duration::from_secs(3600)),
|
||||||
rate_authenticated: settings
|
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
|
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
|
rate_anonymous: settings
|
||||||
.property_or_static("jmap.rate-limit.anonymous.rate", "100/1s")?,
|
.property_or_static("jmap.rate-limit.anonymous.rate", "100/1s")?,
|
||||||
rate_use_forwarded: settings
|
rate_use_forwarded: settings
|
||||||
|
|
|
@ -86,11 +86,11 @@ impl JMAP {
|
||||||
Ok(None) => RequestError::not_found().into_http_response(),
|
Ok(None) => RequestError::not_found().into_http_response(),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
tracing::error!(event = "error",
|
tracing::error!(event = "error",
|
||||||
context = "blob_store",
|
context = "blob_store",
|
||||||
account_id = account_id.document_id(),
|
account_id = account_id.document_id(),
|
||||||
blob_id = ?blob_id,
|
blob_id = ?blob_id,
|
||||||
error = ?err,
|
error = ?err,
|
||||||
"Failed to download blob");
|
"Failed to download blob");
|
||||||
RequestError::internal_server_error().into_http_response()
|
RequestError::internal_server_error().into_http_response()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -161,48 +161,40 @@ impl JMAP {
|
||||||
|
|
||||||
match (path.next().unwrap_or(""), req.method()) {
|
match (path.next().unwrap_or(""), req.method()) {
|
||||||
("", &Method::GET) => {
|
("", &Method::GET) => {
|
||||||
// Limit anonymous requests
|
return match self.is_anonymous_allowed(remote_addr) {
|
||||||
if let Err(err) = self.is_anonymous_allowed(remote_addr) {
|
Ok(_) => self.handle_user_device_auth(req).await,
|
||||||
return err.into_http_response();
|
Err(err) => err.into_http_response(),
|
||||||
}
|
}
|
||||||
todo!()
|
|
||||||
}
|
}
|
||||||
("", &Method::POST) => {
|
("", &Method::POST) => {
|
||||||
// Limit authentication requests
|
return match self.is_auth_allowed(remote_addr) {
|
||||||
if let Err(err) = self.is_auth_allowed(remote_addr) {
|
Ok(_) => self.handle_user_device_auth_post(req).await,
|
||||||
return err.into_http_response();
|
Err(err) => err.into_http_response(),
|
||||||
}
|
}
|
||||||
|
|
||||||
todo!()
|
|
||||||
}
|
}
|
||||||
("code", &Method::GET) => {
|
("code", &Method::GET) => {
|
||||||
// Limit anonymous requests
|
return match self.is_anonymous_allowed(remote_addr) {
|
||||||
if let Err(err) = self.is_anonymous_allowed(remote_addr) {
|
Ok(_) => self.handle_user_code_auth(req).await,
|
||||||
return err.into_http_response();
|
Err(err) => err.into_http_response(),
|
||||||
}
|
}
|
||||||
todo!()
|
|
||||||
}
|
}
|
||||||
("code", &Method::POST) => {
|
("code", &Method::POST) => {
|
||||||
// Limit authentication requests
|
return match self.is_auth_allowed(remote_addr) {
|
||||||
if let Err(err) = self.is_auth_allowed(remote_addr) {
|
Ok(_) => self.handle_user_code_auth_post(req).await,
|
||||||
return err.into_http_response();
|
Err(err) => err.into_http_response(),
|
||||||
}
|
}
|
||||||
|
|
||||||
todo!()
|
|
||||||
}
|
}
|
||||||
("device", &Method::POST) => {
|
("device", &Method::POST) => {
|
||||||
// Limit anonymous requests
|
return match self.is_anonymous_allowed(remote_addr) {
|
||||||
if let Err(err) = self.is_anonymous_allowed(remote_addr) {
|
Ok(_) => self.handle_device_auth(req, instance).await,
|
||||||
return err.into_http_response();
|
Err(err) => err.into_http_response(),
|
||||||
}
|
}
|
||||||
todo!()
|
|
||||||
}
|
}
|
||||||
("token", &Method::POST) => {
|
("token", &Method::POST) => {
|
||||||
// Limit anonymous requests
|
return match self.is_anonymous_allowed(remote_addr) {
|
||||||
if let Err(err) = self.is_anonymous_allowed(remote_addr) {
|
Ok(_) => self.handle_token_request(req).await,
|
||||||
return err.into_http_response();
|
Err(err) => err.into_http_response(),
|
||||||
}
|
}
|
||||||
todo!()
|
|
||||||
}
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
|
@ -306,7 +298,7 @@ pub async fn fetch_body(
|
||||||
let mut bytes = Vec::with_capacity(1024);
|
let mut bytes = Vec::with_capacity(1024);
|
||||||
while let Some(Ok(frame)) = req.frame().await {
|
while let Some(Ok(frame)) = req.frame().await {
|
||||||
if let Some(data) = frame.data_ref() {
|
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);
|
bytes.extend_from_slice(data);
|
||||||
} else {
|
} else {
|
||||||
return Err(RequestError::limit(RequestLimitError::Size));
|
return Err(RequestError::limit(RequestLimitError::Size));
|
||||||
|
@ -409,8 +401,15 @@ impl ToHttpResponse for UploadResponse {
|
||||||
|
|
||||||
impl ToHttpResponse for RequestError {
|
impl ToHttpResponse for RequestError {
|
||||||
fn into_http_response(self) -> HttpResponse {
|
fn into_http_response(self) -> HttpResponse {
|
||||||
JsonResponse::with_status(StatusCode::from_u16(self.status).unwrap(), self)
|
hyper::Response::builder()
|
||||||
.into_http_response()
|
.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 {
|
impl JMAP {
|
||||||
// Code authorization flow, handles an authorization request
|
// 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())
|
let params = form_urlencoded::parse(req.uri().query().unwrap_or_default().as_bytes())
|
||||||
.into_owned()
|
.into_owned()
|
||||||
.collect::<HashMap<_, _>>();
|
.collect::<HashMap<_, _>>();
|
||||||
|
|
|
@ -199,7 +199,7 @@ impl JMAP {
|
||||||
),
|
),
|
||||||
rate_limit_auth: LruCache::with_capacity(
|
rate_limit_auth: LruCache::with_capacity(
|
||||||
config
|
config
|
||||||
.property("jmap.rate-limit.authenticated.size")
|
.property("jmap.rate-limit.account.size")
|
||||||
.failed("Invalid property")
|
.failed("Invalid property")
|
||||||
.unwrap_or(1024),
|
.unwrap_or(1024),
|
||||||
),
|
),
|
||||||
|
|
|
@ -23,6 +23,7 @@ impl JMAP {
|
||||||
let sort_as_tree = request.arguments.sort_as_tree.unwrap_or(false);
|
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 filter_as_tree = request.arguments.filter_as_tree.unwrap_or(false);
|
||||||
let mut filters = Vec::with_capacity(request.filter.len());
|
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) {
|
for cond in std::mem::take(&mut request.filter) {
|
||||||
match cond {
|
match cond {
|
||||||
|
@ -100,11 +101,7 @@ impl JMAP {
|
||||||
&& (paginate.is_some()
|
&& (paginate.is_some()
|
||||||
|| (response.total.map_or(false, |total| total > 0) && filter_as_tree))
|
|| (response.total.map_or(false, |total| total > 0) && filter_as_tree))
|
||||||
{
|
{
|
||||||
for document_id in self
|
for document_id in mailbox_ids {
|
||||||
.get_document_ids(account_id, Collection::Mailbox)
|
|
||||||
.await?
|
|
||||||
.unwrap_or_default()
|
|
||||||
{
|
|
||||||
let parent_id = self
|
let parent_id = self
|
||||||
.get_property::<Object<Value>>(
|
.get_property::<Object<Value>>(
|
||||||
account_id,
|
account_id,
|
||||||
|
|
|
@ -20,4 +20,6 @@ serde = { version = "1.0", features = ["derive"]}
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
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
|
server
|
||||||
.auth_db
|
.auth_db
|
||||||
.execute(
|
.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()
|
vec![login.to_string(), secret.to_string(), name.to_string()].into_iter()
|
||||||
)
|
)
|
||||||
.await
|
.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};
|
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_changes;
|
||||||
pub mod email_copy;
|
pub mod email_copy;
|
||||||
pub mod email_get;
|
pub mod email_get;
|
||||||
|
@ -48,6 +50,19 @@ private-key = 'file://{PK}'
|
||||||
[jmap.protocol]
|
[jmap.protocol]
|
||||||
set.max-objects = 100000
|
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]
|
[jmap.auth.database]
|
||||||
type = 'sql'
|
type = 'sql'
|
||||||
address = 'sqlite::memory:'
|
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 = ?'
|
secret-by-uid = 'SELECT secret FROM users WHERE ROWID - 1 = ?'
|
||||||
gids-by-uid = 'SELECT gid FROM groups WHERE uid = ?'
|
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]
|
#[tokio::test]
|
||||||
|
@ -82,7 +106,9 @@ pub async fn jmap_tests() {
|
||||||
//thread_get::test(params.server.clone(), &mut params.client).await;
|
//thread_get::test(params.server.clone(), &mut params.client).await;
|
||||||
//thread_merge::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;
|
//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 {
|
if delete {
|
||||||
params.temp_dir.delete();
|
params.temp_dir.delete();
|
||||||
|
|
Loading…
Add table
Reference in a new issue