test: sign in url test (#75)

* test: sign in url test

* fix: Tungstenite allows remote attackers to cause a denial of service

* chore: enable feature

* chore: update
This commit is contained in:
Nathan.fooo 2023-09-24 13:03:24 +08:00 committed by GitHub
parent e3ff765137
commit 0883ae94b8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 90 additions and 140 deletions

9
Cargo.lock generated
View File

@ -839,6 +839,7 @@ dependencies = [
"opener",
"parking_lot",
"reqwest",
"scraper",
"serde",
"serde_json",
"serde_repr",
@ -4038,9 +4039,9 @@ dependencies = [
[[package]]
name = "tokio-tungstenite"
version = "0.20.0"
version = "0.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b2dbec703c26b00d74844519606ef15d09a7d6857860f84ad223dec002ddea2"
checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c"
dependencies = [
"futures-util",
"log",
@ -4182,9 +4183,9 @@ checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed"
[[package]]
name = "tungstenite"
version = "0.20.0"
version = "0.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e862a1c4128df0112ab625f55cd5c934bcb4312ba80b39ae4b4835a3fd58e649"
checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9"
dependencies = [
"byteorder",
"bytes",

View File

@ -80,11 +80,11 @@ once_cell = "1.7.2"
collab-plugins = { version = "0.1.0", features = ["sync_plugin"] }
collab-define = { version = "0.1.0" }
collab-sync-protocol = { version = "0.1.0" }
client-api = { path = "libs/client-api" }
tempfile = "3.4.0"
assert-json-diff = "2.0.2"
dotenv = "0.15.0"
scraper = "0.17.1"
client-api = { path = "libs/client-api", features = ["client-api-test"]}
[[bin]]
name = "appflowy_cloud"

View File

@ -22,11 +22,17 @@ parking_lot = "0.12.1"
tracing = { version = "0.1" }
thiserror = "1.0.39"
serde = { version = "1.0", features = ["derive"] }
tokio-tungstenite = { version = "0.20" }
tokio-tungstenite = { version = "0.20.1" }
tokio = { version = "1.26", features = ["full"] }
futures-util = "0.3.26"
tokio-retry = "0.3"
bytes = "1.0"
uuid = "1.4.1"
scraper = { version = "0.17.1", optional = true }
collab-sync-protocol = { version = "0.1.0" }
[dev-dependencies]
scraper = "0.17.1"
[features]
client-api-test = ["scraper"]

View File

@ -27,8 +27,8 @@ use storage_entity::{AFWorkspaces, QueryCollabParams};
use storage_entity::{DeleteCollabParams, RawData};
pub struct Client {
cloud_client: reqwest::Client,
gotrue_client: gotrue::api::Client,
pub(crate) cloud_client: reqwest::Client,
pub(crate) gotrue_client: gotrue::api::Client,
base_url: String,
ws_addr: String,
token: Arc<RwLock<ClientToken>>,

View File

@ -0,0 +1,51 @@
use crate::Client;
use gotrue::grant::{Grant, PasswordGrant};
use gotrue::params::GenerateLinkParams;
use gotrue_entity::GoTrueError;
use scraper::{Html, Selector};
impl Client {
pub async fn generate_sign_in_url(
&self,
admin_user_email: &str,
admin_user_password: &str,
user_email: &str,
) -> Result<String, GoTrueError> {
let admin_token = self
.gotrue_client
.token(&Grant::Password(PasswordGrant {
email: admin_user_email.to_string(),
password: admin_user_password.to_string(),
}))
.await?;
let admin_user_params: GenerateLinkParams = GenerateLinkParams {
email: user_email.to_string(),
..Default::default()
};
let link_resp = self
.gotrue_client
.generate_link(&admin_token.access_token, &admin_user_params)
.await?;
assert_eq!(link_resp.email, user_email);
let action_link = link_resp.action_link;
let resp = reqwest::Client::new().get(action_link).send().await?;
let resp_text = resp.text().await?;
Ok(extract_appflowy_sign_in_url(&resp_text))
}
}
pub fn extract_appflowy_sign_in_url(html_str: &str) -> String {
let fragment = Html::parse_fragment(html_str);
let selector = Selector::parse("a").unwrap();
fragment
.select(&selector)
.next()
.unwrap()
.value()
.attr("href")
.unwrap()
.to_string()
}

View File

@ -1,4 +1,8 @@
mod http;
#[cfg(feature = "client-api-test")]
pub mod http_test;
pub mod notify;
pub mod ws;

View File

@ -111,6 +111,7 @@ async fn retrieve_snapshot_data_handler(
Ok(Json(AppResponse::Ok().with_data(data)))
}
#[tracing::instrument(level = "debug", skip_all)]
async fn retrieve_snapshots_handler(
payload: Json<QueryObjectSnapshotParams>,
storage: Data<Storage<CollabStorageProxy>>,

View File

@ -41,6 +41,7 @@ async fn verify_handler(
Ok(AppResponse::Ok().with_data(resp).into())
}
#[tracing::instrument(level = "debug", skip(state))]
async fn profile_handler(
uuid: UserUuid,
state: Data<AppState>,
@ -59,6 +60,7 @@ async fn update_handler(
Ok(AppResponse::Ok().into())
}
#[tracing::instrument(level = "debug", skip_all)]
async fn login_handler(
req: Json<LoginRequest>,
state: Data<AppState>,
@ -83,6 +85,7 @@ async fn login_handler(
Ok(HttpResponse::Ok().json(resp))
}
#[tracing::instrument(level = "debug", skip(state))]
async fn logout_handler(req: HttpRequest, state: Data<AppState>) -> Result<HttpResponse> {
let logged_user = logged_user_from_request(&req, &state.config.application.server_key)?;
logout(logged_user, state.user.clone()).await;
@ -109,6 +112,7 @@ async fn register_handler(
Ok(HttpResponse::Ok().json(resp))
}
#[tracing::instrument(level = "debug", skip_all)]
async fn change_password_handler(
req: HttpRequest,
payload: Json<ChangePasswordRequest>,

View File

@ -1,34 +0,0 @@
use crate::client::utils::{register_deep_fake, LOCALHOST_URL};
use appflowy_cloud::client::http;
#[tokio::test]
async fn login_success() {
let c = http::Client::from(reqwest::Client::new(), LOCALHOST_URL);
let (email, _user, password) = register_deep_fake(&mut c).await;
let initial_token = c.logged_in_token().unwrap().to_string();
c.login(&email, &password).await.unwrap();
let relogin_token = c.logged_in_token().unwrap();
assert_ne!(&initial_token, relogin_token);
assert!(c.logged_in_token().is_some())
}
#[tokio::test]
async fn login_with_empty_email() {
let c = http::Client::from(reqwest::Client::new(), LOCALHOST_URL);
let (_email, _user, password) = register_deep_fake(&mut c).await;
assert!(c.login("", &password).await.is_err());
}
#[tokio::test]
async fn login_with_empty_password() {
let c = http::Client::from(reqwest::Client::new(), LOCALHOST_URL);
let (email, _user, _password) = register_deep_fake(&mut c).await;
assert!(c.login(&email, "").await.is_err());
}
#[tokio::test]
async fn login_with_unknown_user() {
let c = http::Client::from(reqwest::Client::new(), LOCALHOST_URL);
let token = c.login("unknown@appflowy.io", "Abc@123!").await;
assert!(token.is_err());
}

View File

@ -1,40 +0,0 @@
use crate::client::utils::{register_deep_fake, LOCALHOST_URL};
use appflowy_cloud::client::http;
#[tokio::test]
async fn change_password_with_unmatch_password() {
let c = http::Client::from(reqwest::Client::new(), LOCALHOST_URL);
let new_password = "HelloWord@1a";
let new_password_confirm = "HeloWord@1a";
let (_email, _user, password) = register_deep_fake(&mut c).await;
let res = c
.change_password(&password, new_password, new_password_confirm)
.await;
assert!(res.is_err())
}
#[tokio::test]
async fn login_failed_after_change_password() {
let c = http::Client::from(reqwest::Client::new(), LOCALHOST_URL);
let new_password = "HelloWord@1a";
let (email, _user, old_password) = register_deep_fake(&mut c).await;
let res = c
.change_password(&old_password, new_password, new_password)
.await;
assert!(res.is_ok());
let res = c.login(&email, &old_password).await;
assert!(res.is_err())
}
#[tokio::test]
async fn login_success_after_change_password() {
let c = http::Client::from(reqwest::Client::new(), LOCALHOST_URL);
let new_password = "HelloWord@1a";
let (email, _user, old_password) = register_deep_fake(&mut c).await;
let res = c
.change_password(&old_password, new_password, new_password)
.await;
assert!(res.is_ok());
let res = c.login(&email, new_password).await;
assert!(res.is_ok())
}

View File

@ -1,41 +0,0 @@
use crate::client::utils::timestamp_nano;
use crate::client::utils::LOCALHOST_URL;
use appflowy_cloud::client::http;
#[tokio::test]
async fn register_success() {
let c = http::Client::from(reqwest::Client::new(), LOCALHOST_URL);
let email = format!("deep_fake{}@appflowy.io", timestamp_nano());
c.register("user1", &email, "DeepFakePassword!123")
.await
.unwrap();
assert!(c.logged_in_token().is_some())
}
#[tokio::test]
async fn register_with_invalid_password() {
let c = http::Client::from(reqwest::Client::new(), LOCALHOST_URL);
let email = format!("deep_fake{}@appflowy.io", timestamp_nano());
let res = c.register("user1", &email, "123").await;
assert!(res.is_err());
assert!(c.logged_in_token().is_none())
}
#[tokio::test]
async fn register_with_invalid_name() {
let c = http::Client::from(reqwest::Client::new(), LOCALHOST_URL);
let email = format!("deep_fake{}@appflowy.io", timestamp_nano());
let res = c.register("", &email, "DeepFakePassword!123").await;
assert!(res.is_err());
assert!(c.logged_in_token().is_none())
}
#[tokio::test]
async fn register_with_invalid_email() {
let c = http::Client::from(reqwest::Client::new(), LOCALHOST_URL);
let res = c
.register("user1", "appflowy.io", "DeepFakePassword!123")
.await;
assert!(res.is_err());
assert!(c.logged_in_token().is_none())
}

View File

@ -1,6 +1,6 @@
use shared_entity::error_code::ErrorCode;
use crate::client::utils::{generate_unique_email, generate_unique_registered_user};
use crate::client::utils::{generate_unique_email, generate_unique_registered_user, ADMIN_USER};
use crate::client_api_client;
#[tokio::test]
@ -87,7 +87,7 @@ async fn sign_in_success() {
}
#[tokio::test]
async fn sign_in_with_url() {
async fn sign_in_with_invalid_url() {
let url_str = "appflowy-flutter://#access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2OTQ1ODIyMjMsInN1YiI6Ijk5MGM2NDNjLTMyMWEtNGNmMi04OWY1LTNhNmJhZGFjMTg5NCIsImVtYWlsIjoiNG5uaWhpbGF0ZWRAZ21haWwuY29tIiwicGhvbmUiOiIiLCJhcHBfbWV0YWRhdGEiOnsicHJvdmlkZXIiOiJnb29nbGUiLCJwcm92aWRlcnMiOlsiZ29vZ2xlIl19LCJ1c2VyX21ldGFkYXRhIjp7ImF2YXRhcl91cmwiOiJodHRwczovL2xoMy5nb29nbGV1c2VyY29udGVudC5jb20vYS9BQ2c4b2NJdGZpa28xX0lpMmZiNzM4VnpGekViLVBqT0NCY3FUQzdrNjVIX0hnRTQwOVk9czk2LWMiLCJlbWFpbCI6IjRubmloaWxhdGVkQGdtYWlsLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJmdWxsX25hbWUiOiJmdSB6aXhpYW5nIiwiaXNzIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vdXNlcmluZm8vdjIvbWUiLCJuYW1lIjoiZnUgeml4aWFuZyIsInBpY3R1cmUiOiJodHRwczovL2xoMy5nb29nbGV1c2VyY29udGVudC5jb20vYS9BQ2c4b2NJdGZpa28xX0lpMmZiNzM4VnpGekViLVBqT0NCY3FUQzdrNjVIX0hnRTQwOVk9czk2LWMiLCJwcm92aWRlcl9pZCI6IjEwMTQ5OTYxMDMxOTYxNjE0NTcyNSIsInN1YiI6IjEwMTQ5OTYxMDMxOTYxNjE0NTcyNSJ9LCJyb2xlIjoiIn0.I-7j-Tdj62P56zhzEqvBc7cHMldv5MA_MM7xtrBibbE&expires_in=3600&provider_token=ya29.a0AfB_byCovXs1CUiC9_f9VBTupQPsIxwh9aSlOg0PLYJvv1x1zvVfssrQfW6_Aq9no7EKpCzFUCLElOvK1Xz4x4K5r7tug79tr5b1yiOoUMWTeWTXyV61fZHQbZ9vscAiyKYtq5NqYTiytHcQEFlKr7UMfu6BTbKsUwaCgYKAaISARISFQGOcNnC0Vsx2QCAXgYO3XbfcF91WQ0169&refresh_token=Hi3Jc3I_pj9YrexcR91i5g&token_type=bearer";
let c = client_api_client();
match c.sign_in_url(url_str).await {
@ -100,3 +100,14 @@ async fn sign_in_with_url() {
},
}
}
#[tokio::test]
async fn sign_in_with_url() {
let c = client_api_client();
let user_email = generate_unique_email();
let url_str = c
.generate_sign_in_url(&ADMIN_USER.email, &ADMIN_USER.password, &user_email)
.await
.unwrap();
let _ = c.sign_in_url(&url_str).await.unwrap();
}

View File

@ -1,9 +1,9 @@
use client_api::http_test::extract_appflowy_sign_in_url;
use gotrue::{
api::Client,
grant::{Grant, PasswordGrant},
params::{AdminUserParams, GenerateLinkParams},
};
use scraper::{Html, Selector};
use crate::{
client::{
@ -95,16 +95,3 @@ async fn admin_generate_link_and_user_sign_in() {
let workspaces = client.workspaces().await.unwrap();
assert_eq!(workspaces.len(), 1);
}
fn extract_appflowy_sign_in_url(html_str: &str) -> String {
let fragment = Html::parse_fragment(html_str);
let selector = Selector::parse("a").unwrap();
fragment
.select(&selector)
.next()
.unwrap()
.value()
.attr("href")
.unwrap()
.to_string()
}