Compare commits

..

No commits in common. "d188fe49b52b0b39fc9f3f72c4e8a3a930ba0c90" and "317dbe1ed1faed866ef9df296c2987291bec4899" have entirely different histories.

7 changed files with 17 additions and 244 deletions

View File

@ -11,9 +11,7 @@ use crate::errors::ChorusResult;
use crate::gateway::Gateway; use crate::gateway::Gateway;
use crate::instance::{ChorusUser, Instance}; use crate::instance::{ChorusUser, Instance};
use crate::ratelimiter::ChorusRequest; use crate::ratelimiter::ChorusRequest;
use crate::types::{ use crate::types::{GatewayIdentifyPayload, LimitType, LoginResult, LoginSchema, User};
AuthenticatorType, GatewayIdentifyPayload, LimitType, LoginResult, LoginSchema, SendMfaSmsResponse, SendMfaSmsSchema, User, VerifyMFALoginResponse, VerifyMFALoginSchema
};
impl Instance { impl Instance {
/// Logs into an existing account on the spacebar server. /// Logs into an existing account on the spacebar server.
@ -34,7 +32,7 @@ impl Instance {
// instances' limits to pass them on as user_rate_limits later. // instances' limits to pass them on as user_rate_limits later.
let mut user = let mut user =
ChorusUser::shell(Arc::new(RwLock::new(self.clone())), "None".to_string()).await; ChorusUser::shell(Arc::new(RwLock::new(self.clone())), "None".to_string()).await;
let login_result = chorus_request let login_result = chorus_request
.deserialize_response::<LoginResult>(&mut user) .deserialize_response::<LoginResult>(&mut user)
.await?; .await?;
@ -50,59 +48,4 @@ impl Instance {
Ok(user) Ok(user)
} }
/// Verifies a multi-factor authentication login
///
/// # Reference
/// See <https://docs.discord.sex/authentication#verify-mfa-login>
pub async fn verify_mfa_login(
&mut self,
authenticator: AuthenticatorType,
schema: VerifyMFALoginSchema,
) -> ChorusResult<ChorusUser> {
let endpoint_url = self.urls.api.clone() + &authenticator.to_string();
let chorus_request = ChorusRequest {
request: Client::new()
.post(endpoint_url)
.header("Content-Type", "application/json")
.json(&schema),
limit_type: LimitType::AuthLogin,
};
let mut user =
ChorusUser::shell(Arc::new(RwLock::new(self.clone())), "None".to_string()).await;
match chorus_request.deserialize_response::<VerifyMFALoginResponse>(&mut user).await? {
VerifyMFALoginResponse::Success { token, user_settings: _ } => user.set_token(token),
VerifyMFALoginResponse::UserSuspended { suspended_user_token } => return Err(crate::errors::ChorusError::SuspendUser { token: suspended_user_token }),
}
let mut identify = GatewayIdentifyPayload::common();
identify.token = user.token();
user.gateway.send_identify(identify).await;
Ok(user)
}
/// Sends a multi-factor authentication code to the user's phone number
///
/// # Reference
/// See <https://docs.discord.sex/authentication#send-mfa-sms>
pub async fn send_mfa_sms(&mut self, schema: SendMfaSmsSchema) -> ChorusResult<SendMfaSmsResponse> {
let endpoint_url = self.urls.api.clone() + "/auth/mfa/sms/send";
let chorus_request = ChorusRequest {
request: Client::new()
.post(endpoint_url)
.header("Content-Type", "application/json")
.json(&schema),
limit_type: LimitType::Ip
};
let mut chorus_user = ChorusUser::shell(Arc::new(RwLock::new(self.clone())), "None".to_string()).await;
let send_mfa_sms_response = chorus_request.deserialize_response::<SendMfaSmsResponse>(&mut chorus_user).await?;
Ok(send_mfa_sms_response)
}
} }

View File

@ -48,9 +48,7 @@ custom_error! {
/// Invalid, insufficient or too many arguments provided. /// Invalid, insufficient or too many arguments provided.
InvalidArguments{error: String} = "Invalid arguments were provided. Error: {error}", InvalidArguments{error: String} = "Invalid arguments were provided. Error: {error}",
/// The request requires MFA verification /// The request requires MFA verification
MfaRequired {error: MfaRequiredSchema} = "Mfa verification is required to perform this action", MfaRequired {error: MfaRequiredSchema} = "Mfa verification is required to perform this action"
/// The user's account is suspended
SuspendUser { token: String } = "Your account has been suspended"
} }
impl From<reqwest::Error> for ChorusError { impl From<reqwest::Error> for ChorusError {
@ -155,3 +153,4 @@ custom_error! {
CannotBind{error: String} = "Cannot bind socket due to a UDP error: {error}", CannotBind{error: String} = "Cannot bind socket due to a UDP error: {error}",
CannotConnect{error: String} = "Cannot connect due to a UDP error: {error}", CannotConnect{error: String} = "Cannot connect due to a UDP error: {error}",
} }

View File

@ -236,14 +236,13 @@ impl ChorusUser {
} }
/// Sends a request to complete an MFA challenge. /// Sends a request to complete an MFA challenge.
/// # Reference
/// See <https://docs.discord.sex/authentication#verify-mfa>
/// ///
/// If successful, the MFA verification JWT returned is set on the current [ChorusUser] executing the /// If successful, the MFA verification JWT returned is set on the current [ChorusUser] executing the
/// request. /// request.
/// ///
/// The JWT token expires after 5 minutes. /// The JWT token expires after 5 minutes.
///
/// # Reference
/// See <https://docs.discord.sex/authentication#verify-mfa>
pub async fn complete_mfa_challenge(&mut self, mfa_verify_schema: MfaVerifySchema) -> ChorusResult<()> { pub async fn complete_mfa_challenge(&mut self, mfa_verify_schema: MfaVerifySchema) -> ChorusResult<()> {
let endpoint_url = self.belongs_to.read().unwrap().urls.api.clone() + "/mfa/finish"; let endpoint_url = self.belongs_to.read().unwrap().urls.api.clone() + "/mfa/finish";
let chorus_request = ChorusRequest { let chorus_request = ChorusRequest {

View File

@ -1,7 +1,3 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]

View File

@ -35,26 +35,3 @@ pub struct LoginSchema {
pub login_source: Option<String>, pub login_source: Option<String>,
pub gift_code_sku_id: Option<String>, pub gift_code_sku_id: Option<String>,
} }
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct VerifyMFALoginSchema {
pub ticket: String,
pub code: String,
pub login_source: Option<String>,
pub gift_code_sku_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum VerifyMFALoginResponse {
Success { token: String, user_settings: LoginSettings },
UserSuspended { suspended_user_token: String }
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct LoginSettings {
pub locale: String,
pub theme: String,
}

View File

@ -1,13 +1,13 @@
use std::fmt::Display; use std::fmt::Display;
use serde::{Deserialize, Serialize}; use serde::{Serialize, Deserialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub struct MfaRequiredSchema { pub struct MfaRequiredSchema {
pub message: String, pub message: String,
pub code: i32, pub code: i32,
pub mfa: MfaVerificationSchema, pub mfa: MfaVerificationSchema,
} }
impl Display for MfaRequiredSchema { impl Display for MfaRequiredSchema {
@ -24,14 +24,14 @@ impl Display for MfaRequiredSchema {
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub struct MfaVerificationSchema { pub struct MfaVerificationSchema {
pub ticket: String, pub ticket: String,
pub methods: Vec<MfaMethod>, pub methods: Vec<MfaMethod>
} }
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub struct MfaMethod { pub struct MfaMethod {
#[serde(rename = "type")] #[serde(rename = "type")]
pub kind: AuthenticatorType, pub kind: MfaType,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub challenge: Option<String>, pub challenge: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
@ -40,7 +40,7 @@ pub struct MfaMethod {
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum AuthenticatorType { pub enum MfaType {
TOTP, TOTP,
SMS, SMS,
Backup, Backup,
@ -48,27 +48,11 @@ pub enum AuthenticatorType {
Password, Password,
} }
impl Display for AuthenticatorType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
AuthenticatorType::TOTP => "totp",
AuthenticatorType::SMS => "sms",
AuthenticatorType::Backup => "backup",
AuthenticatorType::WebAuthn => "webauthn",
AuthenticatorType::Password => "password",
}
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub struct MfaVerifySchema { pub struct MfaVerifySchema {
pub ticket: String, pub ticket: String,
pub mfa_type: AuthenticatorType, pub mfa_type: MfaType,
pub data: String, pub data: String,
} }
@ -76,13 +60,3 @@ pub struct MfaVerifySchema {
pub struct MfaTokenSchema { pub struct MfaTokenSchema {
pub token: String, pub token: String,
} }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SendMfaSmsSchema {
pub ticket: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SendMfaSmsResponse {
pub phone: String,
}

View File

@ -2,9 +2,9 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this // License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/. // file, You can obtain one at http://mozilla.org/MPL/2.0/.
use std::{os::unix::fs::chroot, str::FromStr}; use std::str::FromStr;
use chorus::{instance::ChorusUser, types::{AuthenticatorType, LoginSchema, MfaVerifySchema, RegisterSchema, SendMfaSmsSchema}}; use chorus::types::{LoginSchema, RegisterSchema};
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
use wasm_bindgen_test::*; use wasm_bindgen_test::*;
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
@ -85,7 +85,9 @@ async fn test_login_with_token() {
.await .await
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
bundle.user.object.as_ref().unwrap().read().unwrap().id, bundle.user.object.as_ref().unwrap()
.read().unwrap()
.id,
other_user.object.unwrap().read().unwrap().id other_user.object.unwrap().read().unwrap().id
); );
assert_eq!(bundle.user.token, other_user.token); assert_eq!(bundle.user.token, other_user.token);
@ -105,120 +107,3 @@ async fn test_login_with_invalid_token() {
common::teardown(bundle).await; common::teardown(bundle).await;
} }
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
async fn test_complete_mfa_challenge_totp() {
let mut bundle = common::setup().await;
let token = "".to_string();
let mut chorus_user = bundle.instance.login_with_token(token).await
.unwrap();
let schema = MfaVerifySchema {
ticket: "".to_string(),
mfa_type: AuthenticatorType::TOTP,
data: "".to_string(),
};
let result = chorus_user.complete_mfa_challenge(schema)
.await;
assert!(result.is_ok())
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
async fn test_complete_mfa_challenge_sms() {
let mut bundle = common::setup().await;
let token = "".to_string();
let mut chorus_user = bundle.instance.login_with_token(token).await
.unwrap();
let schema = MfaVerifySchema {
ticket: "".to_string(),
mfa_type: AuthenticatorType::SMS,
data: "".to_string(),
};
let result = chorus_user.complete_mfa_challenge(schema)
.await;
assert!(result.is_ok())
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
async fn test_verify_mfa_login_webauthn() {
let mut bundle = common::setup().await;
let token = "".to_string();
let mut chorus_user = bundle.instance.login_with_token(token).await
.unwrap();
let schema = MfaVerifySchema {
ticket: "".to_string(),
mfa_type: AuthenticatorType::SMS,
data: "".to_string(),
};
let result = chorus_user.complete_mfa_challenge(schema)
.await;
assert!(result.is_ok())
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
async fn test_complete_mfa_challenge_backup() {
let mut bundle = common::setup().await;
let token = "".to_string();
let mut chorus_user = bundle.instance.login_with_token(token).await
.unwrap();
let schema = MfaVerifySchema {
ticket: "".to_string(),
mfa_type: AuthenticatorType::Backup,
data: "".to_string(),
};
let result = chorus_user.complete_mfa_challenge(schema)
.await;
assert!(result.is_ok())
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
async fn test_complete_mfa_challenge_password() {
let mut bundle = common::setup().await;
let token = "".to_string();
let mut chorus_user = bundle.instance.login_with_token(token).await
.unwrap();
let schema = MfaVerifySchema {
ticket: "".to_string(),
mfa_type: AuthenticatorType::Password,
data: "".to_string(),
};
let result = chorus_user.complete_mfa_challenge(schema)
.await;
assert!(result.is_ok())
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
async fn test_send_mfa_sms() {
let mut bundle = common::setup().await;
let schema = SendMfaSmsSchema { ticket: "".to_string() };
let result = bundle.instance.send_mfa_sms(schema).await;
assert!(result.is_ok())
}