Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: added totp login #228

Merged
merged 3 commits into from
Jan 19, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 73 additions & 3 deletions src/app/operations/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,53 @@ use anyhow::{Context, Result};
use crate::app::{file_manager, App};

impl App {
pub async fn user_login(&self, phone: Option<String>, code: String, email: bool) -> Result<()> {
pub async fn user_login(
&self,
phone: Option<String>,
code: String,
email: bool,
two_fa_code: Option<String>,
) -> Result<()> {
siddhart1o1 marked this conversation as resolved.
Show resolved Hide resolved
if let Some(phone) = phone {
let result = self
.client
.user_login_phone(phone.clone(), code.clone())
.await
.context("Failed to log in")?;

file_manager::save_data(file_manager::TOKEN_FILE_NAME, &result)
let auth_token = result.auth_token;
let totp_required = result.totp_required;

if totp_required {
let mut is_valid_2fa = false;
let mut final_two_fa_code = two_fa_code;

while !is_valid_2fa {
if final_two_fa_code.is_none() {
println!("Your account requires two-factor authentication. Please enter your TFA code:");
let mut input = String::new();
std::io::stdin()
.read_line(&mut input)
.expect("Failed to read line");
Comment on lines +30 to +32
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are the guarantees that this operation can never fail ?

final_two_fa_code = Some(input.trim().to_string());
}

is_valid_2fa = self
.client
.validate_totp_code(auth_token.clone(), final_two_fa_code.clone().unwrap())
.await
.context("something went wrong")?;

if !is_valid_2fa {
println!(
"The entered 2FA code is incorrect. Please enter the correct TFA code:"
);
final_two_fa_code = None;
}
}
}

file_manager::save_data(file_manager::TOKEN_FILE_NAME, &auth_token)
.context("Failed to save token")?;

println!("User logged in successfully!");
Expand All @@ -25,7 +63,39 @@ impl App {
.await
.context("Failed to log in")?;

file_manager::save_data(file_manager::TOKEN_FILE_NAME, &result)
let auth_token = result.auth_token;
let totp_required = result.totp_required;

if totp_required {
let mut is_valid_2fa = false;
let mut final_two_fa_code = two_fa_code;

while !is_valid_2fa {
if final_two_fa_code.is_none() {
println!("Your account requires two-factor authentication. Please enter your TFA code:");
let mut input = String::new();
std::io::stdin()
.read_line(&mut input)
.expect("Failed to read line");
final_two_fa_code = Some(input.trim().to_string());
}

is_valid_2fa = self
.client
.validate_totp_code(auth_token.clone(), final_two_fa_code.clone().unwrap())
.await
.context("something went wrong")?;

if !is_valid_2fa {
println!(
"The entered 2FA code is incorrect. Please enter the correct TFA code:"
);
final_two_fa_code = None;
}
}
}

file_manager::save_data(file_manager::TOKEN_FILE_NAME, &auth_token)
.context("Failed to save token")?;

println!("User logged in successfully!");
Expand Down
2 changes: 2 additions & 0 deletions src/cli/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ pub enum Command {
email: bool,
#[clap(short, long)]
code: String,
#[clap(short = 't', long = "two-fa-code", value_parser)]
two_fa_code: Option<String>,
},
/// Logout the current user by removing the auth token
Logout,
Expand Down
9 changes: 7 additions & 2 deletions src/cli/runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,13 @@ pub async fn run() -> anyhow::Result<()> {
Command::Globals => {
app.globals().await?;
}
Command::Login { phone, code, email } => {
app.user_login(phone, code, email).await?;
Command::Login {
phone,
code,
email,
two_fa_code,
} => {
app.user_login(phone, code, email, two_fa_code).await?;
}
Command::Logout => {
app.user_logout().await?;
Expand Down
1 change: 1 addition & 0 deletions src/client/gql/mutations/user_login.gql
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ mutation UserLogin($input: UserLoginInput!) {
message
}
authToken
totpRequired
}
}
83 changes: 73 additions & 10 deletions src/client/requests/auth.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use graphql_client::reqwest::post_graphql;
use reqwest::Client;
use reqwest::{Client, StatusCode};
use serde_json::{json, Value};

use crate::client::{
Expand All @@ -11,12 +11,17 @@ use crate::client::{
GaloyClient,
};

pub struct LoginResponse {
pub auth_token: String,
pub totp_required: bool,
}

impl GaloyClient {
pub async fn user_login_phone(
&self,
phone: String,
code: String,
) -> Result<String, ClientError> {
) -> Result<LoginResponse, ClientError> {
let input = UserLoginInput { phone, code };

let variables = user_login::Variables { input };
Expand All @@ -28,28 +33,34 @@ impl GaloyClient {

let response_data = response_body.data.ok_or(ApiError::IssueParsingResponse)?;

if let Some(auth_token) = response_data.user_login.auth_token {
Ok(auth_token)
let login_result = response_data.user_login;

if let (Some(auth_token), Some(totp_required)) =
(login_result.auth_token, login_result.totp_required)
{
Ok(LoginResponse {
auth_token,
totp_required,
})
} else {
let error_string: String = response_data
.user_login
let error_string: String = login_result
.errors
.iter()
.map(|error| format!("{:?}", error))
.collect::<Vec<String>>()
.join(", ");

return Err(ClientError::ApiError(ApiError::RequestFailedWithError(
Err(ClientError::ApiError(ApiError::RequestFailedWithError(
error_string,
)));
)))
}
}

pub async fn user_login_email(
&self,
email_login_id: String,
code: String,
) -> Result<String, ClientError> {
) -> Result<LoginResponse, ClientError> {
let endpoint = self.api.trim_end_matches("/graphql");
let url = format!("{}/auth/email/login", endpoint);
let request_body = json!({ "code": code, "emailLoginId": email_login_id });
Expand All @@ -71,7 +82,14 @@ impl GaloyClient {
.ok_or(ApiError::IssueParsingResponse)?
.to_string();

Ok(auth_token)
let totp_required = response_json["result"]["totpRequired"]
.as_bool()
.ok_or(ApiError::IssueParsingResponse)?;

Ok(LoginResponse {
auth_token,
totp_required,
})
}

pub async fn create_captcha_challenge(&self) -> Result<CaptchaChallenge, ClientError> {
Expand Down Expand Up @@ -110,4 +128,49 @@ impl GaloyClient {
.ok_or(ApiError::IssueParsingResponse)?;
Ok(email_login_id.to_string())
}

pub async fn validate_totp_code(
&self,
auth_token: String,
totp_code: String,
) -> Result<bool, ClientError> {
let endpoint = self.api.trim_end_matches("/graphql");
let url = format!("{}/auth/totp/validate", endpoint);
let request_body = json!({ "totpCode": totp_code, "authToken": auth_token });

let response = Client::new().post(&url).json(&request_body).send().await;

// TODO status code coming from backend are not appropriate need, will update this when correct status codes are added.
match response {
Ok(resp) => match resp.status() {
StatusCode::OK => Ok(true),
StatusCode::UNPROCESSABLE_ENTITY => Ok(false),
StatusCode::INTERNAL_SERVER_ERROR => {
let error_details = resp
.text()
.await
.unwrap_or_else(|_| "Unknown error".to_string());
if error_details.contains("Request failed with status code 400") {
Ok(false)
} else {
Err(ClientError::ApiError(ApiError::RequestFailedWithError(
error_details,
)))
}
}
_ => {
let status = resp.status();
Err(ClientError::ApiError(ApiError::RequestFailedWithError(
format!("Unexpected status code: {}", status),
)))
}
},
Err(e) => {
eprintln!("Network or other error: {}", e);
Err(ClientError::ApiError(ApiError::IssueGettingResponse(
anyhow::Error::new(e),
)))
}
}
}
}