From 63cbb70dbc0637aa58f5881fb6592b53d4b70ae9 Mon Sep 17 00:00:00 2001 From: Mauro D Date: Sun, 14 May 2023 12:34:49 +0000 Subject: [PATCH] OAuth passing tests. --- crates/jmap/src/api/config.rs | 4 +- crates/jmap/src/api/http.rs | 67 ++-- crates/jmap/src/auth/oauth/user_code.rs | 2 +- crates/jmap/src/lib.rs | 2 +- crates/jmap/src/mailbox/query.rs | 7 +- tests/Cargo.toml | 2 + tests/src/jmap/{acl.rs => auth_acl.rs} | 2 +- tests/src/jmap/auth_limits.rs | 176 ++++++++++ tests/src/jmap/auth_oauth.rs | 421 ++++++++++++++++++++++++ tests/src/jmap/mod.rs | 30 +- 10 files changed, 667 insertions(+), 46 deletions(-) rename tests/src/jmap/{acl.rs => auth_acl.rs} (99%) create mode 100644 tests/src/jmap/auth_limits.rs create mode 100644 tests/src/jmap/auth_oauth.rs diff --git a/crates/jmap/src/api/config.rs b/crates/jmap/src/api/config.rs index c95b4868..f32cd63c 100644 --- a/crates/jmap/src/api/config.rs +++ b/crates/jmap/src/api/config.rs @@ -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 diff --git a/crates/jmap/src/api/http.rs b/crates/jmap/src/api/http.rs index b131a8ca..ce30af32 100644 --- a/crates/jmap/src/api/http.rs +++ b/crates/jmap/src/api/http.rs @@ -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() } } diff --git a/crates/jmap/src/auth/oauth/user_code.rs b/crates/jmap/src/auth/oauth/user_code.rs index 8de962b8..3d44dc7b 100644 --- a/crates/jmap/src/auth/oauth/user_code.rs +++ b/crates/jmap/src/auth/oauth/user_code.rs @@ -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::>(); diff --git a/crates/jmap/src/lib.rs b/crates/jmap/src/lib.rs index 9dba8192..d9e2d9cb 100644 --- a/crates/jmap/src/lib.rs +++ b/crates/jmap/src/lib.rs @@ -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), ), diff --git a/crates/jmap/src/mailbox/query.rs b/crates/jmap/src/mailbox/query.rs index 5d6594d7..471c92ad 100644 --- a/crates/jmap/src/mailbox/query.rs +++ b/crates/jmap/src/mailbox/query.rs @@ -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::>( account_id, diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 117646ee..8cdb1a55 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -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" diff --git a/tests/src/jmap/acl.rs b/tests/src/jmap/auth_acl.rs similarity index 99% rename from tests/src/jmap/acl.rs rename to tests/src/jmap/auth_acl.rs index ae28f3d4..fdbca2a6 100644 --- a/tests/src/jmap/acl.rs +++ b/tests/src/jmap/auth_acl.rs @@ -47,7 +47,7 @@ pub async fn test(server: Arc, 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 diff --git a/tests/src/jmap/auth_limits.rs b/tests/src/jmap/auth_limits.rs new file mode 100644 index 00000000..f58279ad --- /dev/null +++ b/tests/src/jmap/auth_limits.rs @@ -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, _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();*/ +} diff --git a/tests/src/jmap/auth_oauth.rs b/tests/src/jmap/auth_oauth.rs new file mode 100644 index 00000000..2fcb1502 --- /dev/null +++ b/tests/src/jmap/auth_oauth.rs @@ -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, _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::(&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::(&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::, None::>) + .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::(&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::(&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::(&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::(&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::, None::>) + .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::( + &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::(&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) -> 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(url: &str, params: &AHashMap) -> T { + serde_json::from_slice(&post_bytes(url, params).await).unwrap() +} + +async fn post_expect_redirect(url: &str, params: &AHashMap) -> 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(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, 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), + } +} diff --git a/tests/src/jmap/mod.rs b/tests/src/jmap/mod.rs index 519345ca..0e54cfb1 100644 --- a/tests/src/jmap/mod.rs +++ b/tests/src/jmap/mod.rs @@ -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();