From 65d9b236faba2c88750cfb05d5f6ff786481a78b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CGiems=E2=80=9D?= <“hubert.wabia@gmail.com”> Date: Tue, 9 Apr 2024 10:53:41 +0200 Subject: [PATCH] login with passkey --- server/src/http/cloud/delete_passkey.rs | 59 +++++----- .../src/http/cloud/get_passkey_challenge.rs | 15 ++- .../cloud/login/login_with_passkey_finish.rs | 106 ++++++++++++++++++ .../cloud/login/login_with_passkey_start.rs | 94 ++++++++++++++++ .../http/cloud/login/login_with_password.rs | 16 +-- server/src/http/cloud/login/mod.rs | 2 + .../register/register_with_password_finish.rs | 32 +++--- .../register/register_with_password_start.rs | 12 +- server/src/structs/session_cache.rs | 19 +++- server/src/test_utils.rs | 8 +- 10 files changed, 296 insertions(+), 67 deletions(-) create mode 100644 server/src/http/cloud/login/login_with_passkey_finish.rs create mode 100644 server/src/http/cloud/login/login_with_passkey_start.rs diff --git a/server/src/http/cloud/delete_passkey.rs b/server/src/http/cloud/delete_passkey.rs index ed0ebcef..2b1db990 100644 --- a/server/src/http/cloud/delete_passkey.rs +++ b/server/src/http/cloud/delete_passkey.rs @@ -30,6 +30,36 @@ pub async fn delete_passkey( Extension(user_id): Extension, Json(payload): Json, ) -> Result<(), (StatusCode, String)> { + // Get cache data + let sessions_key = SessionsCacheKey::Passkey2FA(user_id.clone()).to_string(); + let session_data = match sessions_cache.get(&sessions_key) { + Some(SessionCache::Passkey2FA(session)) => session, + _ => { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + CloudApiErrors::InternalServerError.to_string(), + )); + } + }; + + // Remove leftover session data + sessions_cache.remove(&sessions_key); + + // Finish passkey authentication + if let Err(err) = web_auth.finish_passkey_authentication( + &payload.passkey_credential, + &session_data.passkey_verification_state, + ) { + warn!( + "Failed to finish passkey authentication: {:?}, user_id: {}", + err, user_id + ); + return Err(( + StatusCode::BAD_REQUEST, + CloudApiErrors::InvalidPasskeyCredential.to_string(), + )); + } + // Get user data let user_data = match db.get_user_by_user_id(&user_id).await { Ok(Some(user_data)) => user_data, @@ -62,35 +92,6 @@ pub async fn delete_passkey( } }; - // Get cache data - let sessions_key = SessionsCacheKey::Passkey2FA(user_id.clone()).to_string(); - let session_data = match sessions_cache.get(&sessions_key) { - Some(SessionCache::Passkey2FA(session)) => session, - _ => { - return Err(( - StatusCode::INTERNAL_SERVER_ERROR, - CloudApiErrors::InternalServerError.to_string(), - )); - } - }; - - // Remove leftover session data - sessions_cache.remove(&sessions_key); - - // Finish passkey authentication - if let Err(err) = - web_auth.finish_passkey_authentication(&payload.passkey_credential, &session_data) - { - warn!( - "Failed to finish passkey authentication: {:?}, user_id: {}", - err, user_id - ); - return Err(( - StatusCode::BAD_REQUEST, - CloudApiErrors::InvalidPasskeyCredential.to_string(), - )); - } - // Remove passkey match passkeys .iter() diff --git a/server/src/http/cloud/get_passkey_challenge.rs b/server/src/http/cloud/get_passkey_challenge.rs index f249d368..a478cbfb 100644 --- a/server/src/http/cloud/get_passkey_challenge.rs +++ b/server/src/http/cloud/get_passkey_challenge.rs @@ -2,8 +2,9 @@ use crate::{ middlewares::auth_middleware::UserId, structs::{ cloud::api_cloud_errors::CloudApiErrors, - session_cache::{ApiSessionsCache, SessionCache, SessionsCacheKey}, + session_cache::{ApiSessionsCache, Passkey2FAVerification, SessionCache, SessionsCacheKey}, }, + utils::get_timestamp_in_milliseconds, }; use axum::{extract::State, http::StatusCode}; use axum::{Extension, Json}; @@ -53,14 +54,22 @@ pub async fn get_passkey_challenge( }; // Save to cache passkey challenge request - let sessions_key = SessionsCacheKey::PasskeyVerification(user_id.clone()).to_string(); + let sessions_key = SessionsCacheKey::Passkey2FA(user_id.clone()).to_string(); // Remove leftover session data sessions_cache.remove(&sessions_key); match web_auth.start_passkey_authentication(&passkey) { Ok((rcr, auth_state)) => { - sessions_cache.set(sessions_key, SessionCache::Passkey2FA(auth_state), None); + sessions_cache.set( + sessions_key, + SessionCache::Passkey2FA(Passkey2FAVerification { + email: user_data.email.clone(), + passkey_verification_state: auth_state, + created_at: get_timestamp_in_milliseconds(), + }), + None, + ); return Ok(Json(rcr)); } Err(_) => { diff --git a/server/src/http/cloud/login/login_with_passkey_finish.rs b/server/src/http/cloud/login/login_with_passkey_finish.rs new file mode 100644 index 00000000..14a0803e --- /dev/null +++ b/server/src/http/cloud/login/login_with_passkey_finish.rs @@ -0,0 +1,106 @@ +use crate::{ + http::cloud::utils::{generate_tokens, validate_request}, + structs::{ + cloud::api_cloud_errors::CloudApiErrors, + session_cache::{ApiSessionsCache, SessionCache, SessionsCacheKey}, + }, +}; +use axum::{ + extract::{ConnectInfo, State}, + http::StatusCode, + Json, +}; +use database::db::Db; +use garde::Validate; +use log::{error, warn}; +use serde::{Deserialize, Serialize}; +use std::{net::SocketAddr, sync::Arc}; +use ts_rs::TS; +use webauthn_rs::{prelude::PublicKeyCredential, Webauthn}; + +#[derive(Validate, Debug, Deserialize, Serialize)] +pub struct HttpLoginWithPasskeyFinishRequest { + #[garde(email)] + pub email: String, + #[garde(skip)] + pub passkey_credential: PublicKeyCredential, + #[garde(skip)] + pub enforce_ip: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)] +#[ts(export)] +#[serde(rename_all = "camelCase")] +pub struct HttpLoginWithPasskeyFinishResponse { + pub user_id: String, + pub auth_token: String, + pub refresh_token: String, +} + +pub async fn login_with_passkey_finish( + ConnectInfo(ip): ConnectInfo, + State(db): State>, + State(web_auth): State>, + State(sessions_cache): State>, + Json(request): Json, +) -> Result, (StatusCode, String)> { + // Validate request + validate_request(&request, &())?; + + // Get session data + let sessions_key = SessionsCacheKey::PasskeyLogin(request.email.clone()).to_string(); + let session_data = match sessions_cache.get(&sessions_key) { + Some(SessionCache::PasskeyLogin(session)) => session, + _ => { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + CloudApiErrors::InternalServerError.to_string(), + )); + } + }; + + // Remove leftover session data + sessions_cache.remove(&sessions_key); + + // Finish passkey authentication + if let Err(err) = web_auth.finish_passkey_authentication( + &request.passkey_credential, + &session_data.passkey_verification_state, + ) { + warn!( + "Failed to finish passkey authentication: {:?}, user_email: {}", + err, request.email + ); + return Err(( + StatusCode::BAD_REQUEST, + CloudApiErrors::InvalidPasskeyCredential.to_string(), + )); + } + + // Get user data + let user = match db.get_user_by_email(&request.email).await { + Ok(Some(user)) => user, + Ok(None) => { + return Err(( + StatusCode::BAD_REQUEST, + CloudApiErrors::UserDoesNotExist.to_string(), + )); + } + Err(err) => { + error!("Failed to get user by email: {:?}", err); + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + CloudApiErrors::DatabaseError.to_string(), + )); + } + }; + + // Generate tokens + let (auth_token, refresh_token) = generate_tokens(request.enforce_ip, ip, &user.user_id)?; + + return Ok(Json(HttpLoginWithPasskeyFinishResponse { + auth_token, + refresh_token, + user_id: user.user_id, + })); +} diff --git a/server/src/http/cloud/login/login_with_passkey_start.rs b/server/src/http/cloud/login/login_with_passkey_start.rs new file mode 100644 index 00000000..d9669087 --- /dev/null +++ b/server/src/http/cloud/login/login_with_passkey_start.rs @@ -0,0 +1,94 @@ +use crate::{ + http::cloud::utils::validate_request, + structs::{ + cloud::api_cloud_errors::CloudApiErrors, + session_cache::{ + ApiSessionsCache, PasskeyLoginVerification, SessionCache, SessionsCacheKey, + }, + }, + utils::get_timestamp_in_milliseconds, +}; +use axum::{extract::State, http::StatusCode, Json}; +use database::db::Db; +use garde::Validate; +use log::error; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; +use ts_rs::TS; +use webauthn_rs::{prelude::RequestChallengeResponse, Webauthn}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS, Validate)] +#[ts(export)] +#[serde(rename_all = "camelCase")] +pub struct HttpLoginWithPasskeyStartRequest { + #[garde(email)] + pub email: String, +} + +pub type HttpLoginWithPasskeyStartResponse = RequestChallengeResponse; + +pub async fn login_with_passkey_start( + State(db): State>, + State(web_auth): State>, + State(sessions_cache): State>, + Json(request): Json, +) -> Result, (StatusCode, String)> { + // Validate request + validate_request(&request, &())?; + + // Check if user exists + let user = match db.get_user_by_email(&request.email).await { + Ok(Some(user)) => user, + Ok(None) => { + return Err(( + StatusCode::BAD_REQUEST, + CloudApiErrors::UserDoesNotExist.to_string(), + )); + } + Err(err) => { + error!("Failed to get user by email: {:?}", err); + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + CloudApiErrors::DatabaseError.to_string(), + )); + } + }; + + // Check if user has passkey + let passkeys = match user.passkeys { + Some(passkeys) => passkeys, + None => { + return Err(( + StatusCode::BAD_REQUEST, + CloudApiErrors::PasswordNotSet.to_string(), + )); + } + }; + + // Save to cache passkey challenge request + let sessions_key = SessionsCacheKey::PasskeyLogin(request.email.clone()).to_string(); + + // Remove leftover session data + sessions_cache.remove(&sessions_key); + + match web_auth.start_passkey_authentication(&passkeys) { + Ok((rcr, auth_state)) => { + sessions_cache.set( + sessions_key, + SessionCache::PasskeyLogin(PasskeyLoginVerification { + email: request.email.clone(), + passkey_verification_state: auth_state, + created_at: get_timestamp_in_milliseconds(), + }), + None, + ); + return Ok(Json(rcr)); + } + Err(_) => { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + CloudApiErrors::WebAuthnError.to_string(), + )); + } + }; +} diff --git a/server/src/http/cloud/login/login_with_password.rs b/server/src/http/cloud/login/login_with_password.rs index 652fa4a7..d2e5e258 100644 --- a/server/src/http/cloud/login/login_with_password.rs +++ b/server/src/http/cloud/login/login_with_password.rs @@ -98,9 +98,9 @@ mod tests { use super::*; use crate::{ http::cloud::register::{ - register_with_password_finish::HttpVerifyRegisterWithPasswordRequest, + register_with_password_finish::HttpRegisterWithPasswordFinishRequest, register_with_password_start::{ - HttpRegisterWithPasswordRequest, HttpRegisterWithPasswordResponse, + HttpRegisterWithPasswordStartRequest, HttpRegisterWithPasswordStartResponse, }, }, structs::cloud::cloud_http_endpoints::HttpCloudEndpoint, @@ -129,7 +129,7 @@ mod tests { let password = format!("Password123"); // Register user - let register_payload = HttpRegisterWithPasswordRequest { + let register_payload = HttpRegisterWithPasswordStartRequest { email: email.to_string(), password: password.to_string(), }; @@ -151,12 +151,12 @@ mod tests { // Send request let register_response = test_app.clone().oneshot(req).await.unwrap(); // Validate response - convert_response::(register_response) + convert_response::(register_response) .await .unwrap(); // Validate register - let verify_register_payload = HttpVerifyRegisterWithPasswordRequest { + let verify_register_payload = HttpRegisterWithPasswordFinishRequest { email: email.to_string(), // Random valid code for testing code: "123456".to_string(), @@ -262,7 +262,7 @@ mod tests { let password = format!("Password123"); // Register user - let register_payload = HttpRegisterWithPasswordRequest { + let register_payload = HttpRegisterWithPasswordStartRequest { email: email.to_string(), password: password.to_string(), }; @@ -284,12 +284,12 @@ mod tests { // Send request let register_response = test_app.clone().oneshot(req).await.unwrap(); // Validate response - convert_response::(register_response) + convert_response::(register_response) .await .unwrap(); // Validate register - let verify_register_payload = HttpVerifyRegisterWithPasswordRequest { + let verify_register_payload = HttpRegisterWithPasswordFinishRequest { email: email.to_string(), // Random valid code for testing code: "123456".to_string(), diff --git a/server/src/http/cloud/login/mod.rs b/server/src/http/cloud/login/mod.rs index 436da735..5fd936e1 100644 --- a/server/src/http/cloud/login/mod.rs +++ b/server/src/http/cloud/login/mod.rs @@ -1,2 +1,4 @@ pub mod login_with_google; +pub mod login_with_passkey_finish; +pub mod login_with_passkey_start; pub mod login_with_password; diff --git a/server/src/http/cloud/register/register_with_password_finish.rs b/server/src/http/cloud/register/register_with_password_finish.rs index 38f9e22e..e548ebd0 100644 --- a/server/src/http/cloud/register/register_with_password_finish.rs +++ b/server/src/http/cloud/register/register_with_password_finish.rs @@ -18,7 +18,7 @@ use uuid7::uuid7; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS, Validate)] #[ts(export)] #[serde(rename_all = "camelCase")] -pub struct HttpVerifyRegisterWithPasswordRequest { +pub struct HttpRegisterWithPasswordFinishRequest { #[garde(email)] pub email: String, #[garde(custom(custom_validate_verification_code))] @@ -27,13 +27,13 @@ pub struct HttpVerifyRegisterWithPasswordRequest { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)] #[ts(export)] -pub struct HttpVerifyRegisterWithPasswordResponse {} +pub struct HttpRegisterWithPasswordFinishResponse {} pub async fn register_with_password_finish( State(db): State>, State(sessions_cache): State>, - Json(request): Json, -) -> Result, (StatusCode, String)> { + Json(request): Json, +) -> Result, (StatusCode, String)> { // Validate request validate_request(&request, &())?; @@ -83,7 +83,7 @@ pub async fn register_with_password_finish( )); } - return Ok(Json(HttpVerifyRegisterWithPasswordResponse {})); + return Ok(Json(HttpRegisterWithPasswordFinishResponse {})); } #[cfg(feature = "cloud_db_tests")] @@ -92,7 +92,7 @@ mod tests { use super::*; use crate::{ http::cloud::register::register_with_password_start::{ - HttpRegisterWithPasswordRequest, HttpRegisterWithPasswordResponse, + HttpRegisterWithPasswordStartRequest, HttpRegisterWithPasswordStartResponse, }, structs::cloud::cloud_http_endpoints::HttpCloudEndpoint, test_utils::test_utils::{ @@ -122,7 +122,7 @@ mod tests { let password = format!("Password123"); // Register user - let register_payload = HttpRegisterWithPasswordRequest { + let register_payload = HttpRegisterWithPasswordStartRequest { email: email.to_string(), password: password.to_string(), }; @@ -144,12 +144,12 @@ mod tests { // Send request let register_response = test_app.clone().oneshot(req).await.unwrap(); // Validate response - convert_response::(register_response) + convert_response::(register_response) .await .unwrap(); // Validate register - let verify_register_payload = HttpVerifyRegisterWithPasswordRequest { + let verify_register_payload = HttpRegisterWithPasswordFinishRequest { email: email.to_string(), // Random valid code for testing code: "123456".to_string(), @@ -182,7 +182,7 @@ mod tests { let password = format!("Password123"); // Register user - let register_payload = HttpRegisterWithPasswordRequest { + let register_payload = HttpRegisterWithPasswordStartRequest { email: email.to_string(), password: password.to_string(), }; @@ -204,7 +204,7 @@ mod tests { // Send request let register_response = test_app.clone().oneshot(req).await.unwrap(); // Validate response, should be an error - convert_response::(register_response) + convert_response::(register_response) .await .unwrap_err(); @@ -212,7 +212,7 @@ mod tests { let email = format!("@gmail.com"); // Register user - let register_payload = HttpRegisterWithPasswordRequest { + let register_payload = HttpRegisterWithPasswordStartRequest { email: email.to_string(), password: password.to_string(), }; @@ -234,7 +234,7 @@ mod tests { // Send request let register_response = test_app.clone().oneshot(req).await.unwrap(); // Validate response, should be an error - convert_response::(register_response) + convert_response::(register_response) .await .unwrap_err(); } @@ -251,7 +251,7 @@ mod tests { { let app = test_app.clone(); - let payload = HttpRegisterWithPasswordRequest { + let payload = HttpRegisterWithPasswordStartRequest { email: "test@test.com".to_string(), password: "dfsdsfa2asdada".to_string(), }; @@ -274,7 +274,7 @@ mod tests { } { let app = test_app.clone(); - let payload = HttpRegisterWithPasswordRequest { + let payload = HttpRegisterWithPasswordStartRequest { email: "test@test.com".to_string(), password: "dA4ds".to_string(), }; @@ -296,7 +296,7 @@ mod tests { } { let app = test_app.clone(); - let payload = HttpRegisterWithPasswordRequest { + let payload = HttpRegisterWithPasswordStartRequest { email: "test@test.com".to_string(), password: "Ab1aaaaaa¡".to_string(), }; diff --git a/server/src/http/cloud/register/register_with_password_start.rs b/server/src/http/cloud/register/register_with_password_start.rs index 81d404f9..4e9c2444 100644 --- a/server/src/http/cloud/register/register_with_password_start.rs +++ b/server/src/http/cloud/register/register_with_password_start.rs @@ -25,7 +25,7 @@ use ts_rs::TS; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS, Validate)] #[ts(export)] #[serde(rename_all = "camelCase")] -pub struct HttpRegisterWithPasswordRequest { +pub struct HttpRegisterWithPasswordStartRequest { #[garde(email)] pub email: String, #[garde(custom(custom_validate_new_password))] @@ -34,14 +34,14 @@ pub struct HttpRegisterWithPasswordRequest { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)] #[ts(export)] -pub struct HttpRegisterWithPasswordResponse {} +pub struct HttpRegisterWithPasswordStartResponse {} pub async fn register_with_password_start( State(db): State>, State(sessions_cache): State>, State(mailer): State>, - Json(request): Json, -) -> Result, (StatusCode, String)> { + Json(request): Json, +) -> Result, (StatusCode, String)> { // Validate request validate_request(&request, &())?; @@ -109,10 +109,10 @@ pub async fn register_with_password_start( )); } None => { - return Ok(Json(HttpRegisterWithPasswordResponse {})); + return Ok(Json(HttpRegisterWithPasswordStartResponse {})); } } } - Ok(Json(HttpRegisterWithPasswordResponse {})) + Ok(Json(HttpRegisterWithPasswordStartResponse {})) } diff --git a/server/src/structs/session_cache.rs b/server/src/structs/session_cache.rs index 4136b388..7b6f8b9b 100644 --- a/server/src/structs/session_cache.rs +++ b/server/src/structs/session_cache.rs @@ -8,8 +8,9 @@ pub enum SessionCache { ResetPassword(ResetPasswordVerification), VerifyPasskeyRegister(PasskeyVerification), ResetPasskey(ResetPasskeyVerification), - Passkey2FA(webauthn_rs::prelude::PasskeyAuthentication), + Passkey2FA(Passkey2FAVerification), VerifyAddPasskey(AddPasskeyVerification), + PasskeyLogin(PasskeyLoginVerification), } pub enum SessionsCacheKey { @@ -19,6 +20,7 @@ pub enum SessionsCacheKey { ResetPasskeyVerification(String), // user email Passkey2FA(String), // user id AddPasskey(String), // user id + PasskeyLogin(String), // user email } impl SessionsCacheKey { @@ -30,6 +32,7 @@ impl SessionsCacheKey { SessionsCacheKey::ResetPasskeyVerification(email) => format!("pass_res_{}", email), SessionsCacheKey::Passkey2FA(user_id) => format!("pass_chal_{}", user_id), SessionsCacheKey::AddPasskey(email) => format!("add_pass_{}", email), + SessionsCacheKey::PasskeyLogin(email) => format!("pass_login_{}", email), } } } @@ -72,3 +75,17 @@ pub struct AddPasskeyVerification { pub passkey_registration_state: webauthn_rs::prelude::PasskeyRegistration, pub created_at: u64, } + +#[derive(Debug, Clone)] +pub struct Passkey2FAVerification { + pub email: String, + pub passkey_verification_state: webauthn_rs::prelude::PasskeyAuthentication, + pub created_at: u64, +} + +#[derive(Debug, Clone)] +pub struct PasskeyLoginVerification { + pub email: String, + pub passkey_verification_state: webauthn_rs::prelude::PasskeyAuthentication, + pub created_at: u64, +} diff --git a/server/src/test_utils.rs b/server/src/test_utils.rs index 16bce4e3..d949c325 100644 --- a/server/src/test_utils.rs +++ b/server/src/test_utils.rs @@ -19,8 +19,8 @@ pub mod test_utils { invite_user_to_team::{HttpInviteUserToTeamRequest, HttpInviteUserToTeamResponse}, login::login_with_password::{HttpLoginRequest, HttpLoginResponse}, register::{ - register_with_password_finish::HttpVerifyRegisterWithPasswordRequest, - register_with_password_start::HttpRegisterWithPasswordRequest, + register_with_password_finish::HttpRegisterWithPasswordFinishRequest, + register_with_password_start::HttpRegisterWithPasswordStartRequest, }, register_new_app::{HttpRegisterNewAppRequest, HttpRegisterNewAppResponse}, register_new_team::{HttpRegisterNewTeamRequest, HttpRegisterNewTeamResponse}, @@ -119,7 +119,7 @@ pub mod test_utils { let password = format!("Password123"); // Register user - let register_payload = HttpRegisterWithPasswordRequest { + let register_payload = HttpRegisterWithPasswordStartRequest { email: email.to_string(), password: password.to_string(), }; @@ -143,7 +143,7 @@ pub mod test_utils { assert_eq!(register_response.status(), StatusCode::OK); // Validate register - let verify_register_payload = HttpVerifyRegisterWithPasswordRequest { + let verify_register_payload = HttpRegisterWithPasswordFinishRequest { email: email.to_string(), // Random valid code for testing code: "123456".to_string(),