Compare commits
5 Commits
317dbe1ed1
...
d188fe49b5
Author | SHA1 | Date |
---|---|---|
xystrive | d188fe49b5 | |
xystrive | 6ef5221c2c | |
xystrive | cd637fbb6b | |
xystrive | bf2608fb39 | |
xystrive | 530ed90171 |
|
@ -11,7 +11,9 @@ 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::{GatewayIdentifyPayload, LimitType, LoginResult, LoginSchema, User};
|
use crate::types::{
|
||||||
|
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.
|
||||||
|
@ -32,7 +34,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?;
|
||||||
|
@ -48,4 +50,59 @@ 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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,7 +48,9 @@ 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 {
|
||||||
|
@ -153,4 +155,3 @@ 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}",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -236,13 +236,14 @@ 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 {
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
// 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)]
|
||||||
|
|
|
@ -35,3 +35,26 @@ 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,
|
||||||
|
}
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
|
|
||||||
use serde::{Serialize, Deserialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[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: MfaType,
|
pub kind: AuthenticatorType,
|
||||||
#[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 MfaType {
|
pub enum AuthenticatorType {
|
||||||
TOTP,
|
TOTP,
|
||||||
SMS,
|
SMS,
|
||||||
Backup,
|
Backup,
|
||||||
|
@ -48,11 +48,27 @@ pub enum MfaType {
|
||||||
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: MfaType,
|
pub mfa_type: AuthenticatorType,
|
||||||
pub data: String,
|
pub data: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,3 +76,13 @@ 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,
|
||||||
|
}
|
||||||
|
|
125
tests/auth.rs
125
tests/auth.rs
|
@ -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::str::FromStr;
|
use std::{os::unix::fs::chroot, str::FromStr};
|
||||||
|
|
||||||
use chorus::types::{LoginSchema, RegisterSchema};
|
use chorus::{instance::ChorusUser, types::{AuthenticatorType, LoginSchema, MfaVerifySchema, RegisterSchema, SendMfaSmsSchema}};
|
||||||
#[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,9 +85,7 @@ async fn test_login_with_token() {
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
bundle.user.object.as_ref().unwrap()
|
bundle.user.object.as_ref().unwrap().read().unwrap().id,
|
||||||
.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);
|
||||||
|
@ -107,3 +105,120 @@ 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())
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue