From ed4015f550667bafb60a9fb61f9d0f5d829c2bd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CGiems=E2=80=9D?= <“hubert.wabia@gmail.com”> Date: Fri, 5 Apr 2024 09:36:08 +0200 Subject: [PATCH 1/5] passkey reset rust implementation --- database/src/tables/users/update.rs | 23 +++ .../src/http/cloud/reset_credentials/mod.rs | 2 + .../reset_credentials/reset_passkey_finish.rs | 151 ++++++++++++++++++ .../reset_credentials/reset_passkey_start.rs | 123 ++++++++++++++ server/src/routes/cloud_router.rs | 9 ++ server/src/structs/cloud/api_cloud_errors.rs | 2 + .../src/structs/cloud/cloud_http_endpoints.rs | 6 + server/src/structs/session_cache.rs | 11 ++ 8 files changed, 327 insertions(+) create mode 100644 server/src/http/cloud/reset_credentials/reset_passkey_finish.rs create mode 100644 server/src/http/cloud/reset_credentials/reset_passkey_start.rs diff --git a/database/src/tables/users/update.rs b/database/src/tables/users/update.rs index e4dcf3ef..5a2abe57 100644 --- a/database/src/tables/users/update.rs +++ b/database/src/tables/users/update.rs @@ -63,6 +63,29 @@ impl Db { Err(e) => Err(e).map_err(|e| e.into()), } } + + pub async fn update_passkeys( + &self, + user_email: &String, + passkeys: &Vec, + ) -> Result<(), DbError> { + let serialized_passkey = serde_json::to_string(passkeys).map_err(|e| { + DbError::DatabaseError(format!("Failed to serialize passkey: {}", e.to_string())) + })?; + + let query_body = format!("UPDATE {USERS_TABLE_NAME} SET passkeys = $1 WHERE email = $2"); + + let query_result = query(&query_body) + .bind(&serialized_passkey) + .bind(user_email) + .execute(&self.connection_pool) + .await; + + match query_result { + Ok(_) => Ok(()), + Err(e) => Err(e).map_err(|e| e.into()), + } + } } #[cfg(feature = "cloud_db_tests")] diff --git a/server/src/http/cloud/reset_credentials/mod.rs b/server/src/http/cloud/reset_credentials/mod.rs index b0dd735c..32322733 100644 --- a/server/src/http/cloud/reset_credentials/mod.rs +++ b/server/src/http/cloud/reset_credentials/mod.rs @@ -1,2 +1,4 @@ +pub mod reset_passkey_finish; +pub mod reset_passkey_start; pub mod reset_password_finish; pub mod reset_password_start; diff --git a/server/src/http/cloud/reset_credentials/reset_passkey_finish.rs b/server/src/http/cloud/reset_credentials/reset_passkey_finish.rs new file mode 100644 index 00000000..3fb4e8e4 --- /dev/null +++ b/server/src/http/cloud/reset_credentials/reset_passkey_finish.rs @@ -0,0 +1,151 @@ +use crate::{ + env::is_env_production, + http::cloud::utils::{custom_validate_verification_code, validate_request}, + structs::{ + cloud::api_cloud_errors::CloudApiErrors, + session_cache::{ApiSessionsCache, SessionCache, SessionsCacheKey}, + }, +}; +use axum::{extract::State, http::StatusCode, Json}; +use database::db::Db; +use garde::Validate; +use log::{error, warn}; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; +use ts_rs::TS; +use webauthn_rs::prelude::RegisterPublicKeyCredential; +use webauthn_rs::Webauthn; + +#[derive(Validate, Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct HttpResetPasskeyFinishRequest { + #[garde(email)] + pub email: String, + #[garde(skip)] + pub credential: RegisterPublicKeyCredential, + #[garde(custom(custom_validate_verification_code))] + pub code: String, +} + +#[derive(Validate, Clone, Debug, Deserialize, Serialize, TS)] +#[ts(export)] +pub struct HttpResetPasskeyFinishResponse {} + +pub async fn reset_passkey_finish( + State(db): State>, + State(web_auth): State>, + State(sessions_cache): State>, + Json(request): Json, +) -> Result, (StatusCode, String)> { + // Validate request + validate_request(&request, &())?; + + // Get cache data + let sessions_key = + SessionsCacheKey::ResetPasskeyVerification(request.email.clone()).to_string(); + let session_data = match sessions_cache.get(&sessions_key) { + Some(SessionCache::ResetPasskey(session)) => session, + _ => { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + CloudApiErrors::InternalServerError.to_string(), + )); + } + }; + + // Remove leftover session data + sessions_cache.remove(&sessions_key); + + // validate code only on production + if is_env_production() { + // Validate the code + if session_data.code != request.code { + return Err(( + StatusCode::BAD_REQUEST, + CloudApiErrors::InvalidVerificationCode.to_string(), + )); + } + } + + // Validate passkey reset + let passkey = match web_auth.finish_passkey_registration( + &request.credential, + &session_data.passkey_registration_state, + ) { + Ok(sk) => sk, + Err(err) => { + error!( + "Failed to finish passkey reset: {:?}, user_email: {}", + err, &request.email + ); + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + CloudApiErrors::WebAuthnError.to_string(), + )); + } + }; + + // Validate new passkey + // Get user data + let user_data = match db.get_user_by_email(&request.email).await { + Ok(Some(user_data)) => user_data, + Ok(None) => { + warn!("Reaching this place [Passkey reset finish, user does not exist] should not be possible as we have already checked it during reset start method, user email: {}", &request.email); + return Err(( + StatusCode::BAD_REQUEST, + CloudApiErrors::UserDoesNotExist.to_string(), + )); + } + Err(err) => { + error!( + "Failed to get user data: {:?}, user_email: {}", + err, &request.email + ); + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + CloudApiErrors::DatabaseError.to_string(), + )); + } + }; + + // Check if user has already added this passkey + let mut passkeys = match user_data.passkeys { + Some(passkeys) => { + if passkeys.contains(&passkey) { + return Err(( + StatusCode::BAD_REQUEST, + CloudApiErrors::PasskeyAlreadyExists.to_string(), + )); + } + + passkeys + } + None => { + warn!("Reaching this place [Passkey reset finish, user does not have passkey] should not be possible as we have already checked it during reset start method, user email: {}", &request.email); + return Err(( + StatusCode::BAD_REQUEST, + CloudApiErrors::UserDoesNotHavePasskey.to_string(), + )); + } + }; + + // Add new passkey + passkeys.push(passkey); + + // Update passkeys in database + match db.update_passkeys(&request.email, &passkeys).await { + Ok(_) => { + return Ok(Json(HttpResetPasskeyFinishResponse {})); + } + Err(err) => { + error!( + "Failed to update user passkeys: {:?}, user_email: {}", + err, &request.email + ); + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + CloudApiErrors::DatabaseError.to_string(), + )); + } + } +} diff --git a/server/src/http/cloud/reset_credentials/reset_passkey_start.rs b/server/src/http/cloud/reset_credentials/reset_passkey_start.rs new file mode 100644 index 00000000..1e222e0c --- /dev/null +++ b/server/src/http/cloud/reset_credentials/reset_passkey_start.rs @@ -0,0 +1,123 @@ +use crate::{ + env::is_env_production, + http::cloud::utils::{generate_verification_code, validate_request}, + mailer::{ + mail_requests::{EmailConfirmationRequest, SendEmailRequest}, + mailer::Mailer, + }, + structs::{ + cloud::api_cloud_errors::CloudApiErrors, + session_cache::{ApiSessionsCache, PasskeyVerification, 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::{CreationChallengeResponse, Uuid}; +use webauthn_rs::Webauthn; + +#[derive(Validate, Clone, Debug, Deserialize, Serialize, TS)] +#[ts(export)] +#[serde(rename_all = "camelCase")] +pub struct HttpResetPasskeyStartRequest { + #[garde(email)] + pub email: String, +} + +pub type HttpResetPasskeyStartResponse = CreationChallengeResponse; + +pub async fn reset_passkey_start( + State(db): State>, + State(web_auth): State>, + State(mailer): State>, + State(sessions_cache): State>, + Json(request): Json, +) -> Result, (StatusCode, String)> { + // Validate request + validate_request(&request, &())?; + + // Check if user already exists + match db.get_user_by_email(&request.email).await { + Ok(Some(user_data)) => { + // Check if user has a passkey + if user_data.passkeys.is_none() { + return Err(( + StatusCode::BAD_REQUEST, + CloudApiErrors::UserDoesNotHavePasskey.to_string(), + )); + } + } + Ok(None) => { + return Err(( + StatusCode::BAD_REQUEST, + CloudApiErrors::UserDoesNotExist.to_string(), + )) + } + Err(err) => { + error!("Failed to check if user exists: {:?}", err); + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + CloudApiErrors::DatabaseError.to_string(), + )); + } + } + + // Save to cache register request + let sessions_key = SessionsCacheKey::PasskeyVerification(request.email.clone()).to_string(); + + // Remove leftover session data + sessions_cache.remove(&sessions_key); + + // Generate challenge + let temp_user_id = Uuid::new_v4(); + let res = + web_auth.start_passkey_registration(temp_user_id, &request.email, &request.email, None); + + let (ccr, reg_state) = match res { + Ok((ccr, reg_state)) => (ccr, reg_state), + Err(_) => { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + CloudApiErrors::WebAuthnError.to_string(), + )) + } + }; + + // Generate email verification code + let code = generate_verification_code(); + + // Send email with code, only for PROD + if is_env_production() { + let request = SendEmailRequest::EmailConfirmation(EmailConfirmationRequest { + email: request.email.clone(), + code: code.clone(), + }); + + if let Some(err) = mailer.handle_email_request(&request).error_message { + error!("Failed to send email: {:?}, request: {:?}", err, request); + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + CloudApiErrors::InternalServerError.to_string(), + )); + } + } + + // Save the challenge to the cache + sessions_cache.set( + sessions_key, + SessionCache::VerifyPasskeyRegister(PasskeyVerification { + email: request.email.clone(), + passkey_registration_state: reg_state, + code, + created_at: get_timestamp_in_milliseconds(), + }), + None, + ); + + return Ok(Json(ccr)); +} diff --git a/server/src/routes/cloud_router.rs b/server/src/routes/cloud_router.rs index bfec1457..b3d5c922 100644 --- a/server/src/routes/cloud_router.rs +++ b/server/src/routes/cloud_router.rs @@ -24,6 +24,7 @@ use crate::{ register_new_team::register_new_team, remove_user_from_team::remove_user_from_team, reset_credentials::{ + reset_passkey_finish::reset_passkey_finish, reset_passkey_start::reset_passkey_start, reset_password_finish::reset_password_finish, reset_password_start::reset_password_start, }, @@ -85,6 +86,14 @@ pub fn public_router(state: ServerState) -> Router { &HttpCloudEndpoint::RegisterWithPasskeyFinish.to_string(), post(register_with_passkey_finish), ) + .route( + &HttpCloudEndpoint::ResetPasskeyStart.to_string(), + post(reset_passkey_start), + ) + .route( + &HttpCloudEndpoint::ResetPasskeyFinish.to_string(), + post(reset_passkey_finish), + ) .route(&HttpCloudEndpoint::Events.to_string(), post(events)) .with_state(state) } diff --git a/server/src/structs/cloud/api_cloud_errors.rs b/server/src/structs/cloud/api_cloud_errors.rs index 13fa3194..2c24d78d 100644 --- a/server/src/structs/cloud/api_cloud_errors.rs +++ b/server/src/structs/cloud/api_cloud_errors.rs @@ -40,4 +40,6 @@ pub enum CloudApiErrors { DomainVerificationNotStarted, WebAuthnError, PasswordNotSet, + UserDoesNotHavePasskey, + PasskeyAlreadyExists, } diff --git a/server/src/structs/cloud/cloud_http_endpoints.rs b/server/src/structs/cloud/cloud_http_endpoints.rs index aa68a3b6..86738556 100644 --- a/server/src/structs/cloud/cloud_http_endpoints.rs +++ b/server/src/structs/cloud/cloud_http_endpoints.rs @@ -50,6 +50,10 @@ pub enum HttpCloudEndpoint { RegisterWithPasskeyStart, #[serde(rename = "/register_with_passkey_finish")] RegisterWithPasskeyFinish, + #[serde(rename = "/reset_passkey_start")] + ResetPasskeyStart, + #[serde(rename = "/reset_passkey_finish")] + ResetPasskeyFinish, } impl HttpCloudEndpoint { @@ -86,6 +90,8 @@ impl HttpCloudEndpoint { HttpCloudEndpoint::RegisterWithPasskeyFinish => { "/register_with_passkey_finish".to_string() } + HttpCloudEndpoint::ResetPasskeyStart => "/reset_passkey_start".to_string(), + HttpCloudEndpoint::ResetPasskeyFinish => "/reset_passkey_finish".to_string(), } } } diff --git a/server/src/structs/session_cache.rs b/server/src/structs/session_cache.rs index 262b8e53..6607f132 100644 --- a/server/src/structs/session_cache.rs +++ b/server/src/structs/session_cache.rs @@ -7,12 +7,14 @@ pub enum SessionCache { VerifyRegister(RegisterVerification), ResetPassword(ResetPasswordVerification), VerifyPasskeyRegister(PasskeyVerification), + ResetPasskey(ResetPasskeyVerification), } pub enum SessionsCacheKey { RegisterVerification(String), // user email ResetPasswordVerification(String), // user email PasskeyVerification(String), // user email + ResetPasskeyVerification(String), // user email } impl SessionsCacheKey { @@ -21,6 +23,7 @@ impl SessionsCacheKey { SessionsCacheKey::RegisterVerification(email) => format!("reg_ver_{}", email), SessionsCacheKey::ResetPasswordVerification(email) => format!("pass_res_{}", email), SessionsCacheKey::PasskeyVerification(email) => format!("pass_reg_{}", email), + SessionsCacheKey::ResetPasskeyVerification(email) => format!("pass_res_{}", email), } } } @@ -48,3 +51,11 @@ pub struct PasskeyVerification { pub passkey_registration_state: webauthn_rs::prelude::PasskeyRegistration, pub created_at: u64, } + +#[derive(Debug, Clone)] +pub struct ResetPasskeyVerification { + pub email: String, + pub code: String, + pub passkey_registration_state: webauthn_rs::prelude::PasskeyRegistration, + pub created_at: u64, +} From 241f06e7cb3caaf47772ccc9c9a580e0484e7f62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CGiems=E2=80=9D?= <“hubert.wabia@gmail.com”> Date: Fri, 5 Apr 2024 10:21:17 +0200 Subject: [PATCH 2/5] passkey challenge --- .../src/http/cloud/get_passkey_challenge.rs | 70 +++++++++++++++++++ server/src/http/cloud/mod.rs | 1 + server/src/structs/session_cache.rs | 3 + 3 files changed, 74 insertions(+) create mode 100644 server/src/http/cloud/get_passkey_challenge.rs diff --git a/server/src/http/cloud/get_passkey_challenge.rs b/server/src/http/cloud/get_passkey_challenge.rs new file mode 100644 index 00000000..18adae15 --- /dev/null +++ b/server/src/http/cloud/get_passkey_challenge.rs @@ -0,0 +1,70 @@ +use axum::{extract::State, http::StatusCode}; +use axum::{Extension, Json}; +use database::db::Db; +use log::error; +use std::sync::Arc; +use webauthn_rs::prelude::RequestChallengeResponse; +use webauthn_rs::Webauthn; + +use crate::middlewares::auth_middleware::UserId; +use crate::structs::cloud::api_cloud_errors::CloudApiErrors; +use crate::structs::session_cache::{ApiSessionsCache, SessionCache, SessionsCacheKey}; + +pub type TwoFactorWithPasskeyStartResponse = RequestChallengeResponse; + +pub async fn get_passkey_challenge( + State(db): State>, + State(web_auth): State>, + State(sessions_cache): State>, + Extension(user_id): Extension, +) -> Result, (StatusCode, String)> { + // Get user data + let user_data = match db.get_user_by_user_id(&user_id).await { + Ok(Some(user_data)) => user_data, + Ok(None) => { + return Err(( + StatusCode::BAD_REQUEST, + CloudApiErrors::UserDoesNotExist.to_string(), + )) + } + Err(err) => { + error!( + "Failed to check if user exists: {:?}, user_id: {}", + err, user_id + ); + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + CloudApiErrors::DatabaseError.to_string(), + )); + } + }; + + let passkey = match user_data.passkeys { + Some(passkey) => passkey, + None => { + return Err(( + StatusCode::BAD_REQUEST, + CloudApiErrors::UserDoesNotHavePasskey.to_string(), + )); + } + }; + + // Save to cache passkey challenge request + let sessions_key = SessionsCacheKey::PasskeyVerification(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); + return Ok(Json(rcr)); + } + Err(_) => { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + CloudApiErrors::WebAuthnError.to_string(), + )); + } + }; +} diff --git a/server/src/http/cloud/mod.rs b/server/src/http/cloud/mod.rs index 374108c0..ec9c53c5 100644 --- a/server/src/http/cloud/mod.rs +++ b/server/src/http/cloud/mod.rs @@ -4,6 +4,7 @@ pub mod cancel_user_team_invite; pub mod domains; pub mod events; pub mod get_events; +pub mod get_passkey_challenge; pub mod get_team_user_invites; pub mod get_user_joined_teams; pub mod get_user_team_invites; diff --git a/server/src/structs/session_cache.rs b/server/src/structs/session_cache.rs index 6607f132..f34698f5 100644 --- a/server/src/structs/session_cache.rs +++ b/server/src/structs/session_cache.rs @@ -8,6 +8,7 @@ pub enum SessionCache { ResetPassword(ResetPasswordVerification), VerifyPasskeyRegister(PasskeyVerification), ResetPasskey(ResetPasskeyVerification), + Passkey2FA(webauthn_rs::prelude::PasskeyAuthentication), } pub enum SessionsCacheKey { @@ -15,6 +16,7 @@ pub enum SessionsCacheKey { ResetPasswordVerification(String), // user email PasskeyVerification(String), // user email ResetPasskeyVerification(String), // user email + Passkey2FA(String), // user id } impl SessionsCacheKey { @@ -24,6 +26,7 @@ impl SessionsCacheKey { SessionsCacheKey::ResetPasswordVerification(email) => format!("pass_res_{}", email), SessionsCacheKey::PasskeyVerification(email) => format!("pass_reg_{}", email), SessionsCacheKey::ResetPasskeyVerification(email) => format!("pass_res_{}", email), + SessionsCacheKey::Passkey2FA(user_id) => format!("pass_chal_{}", user_id), } } } From 26a10906550a48f31a7866403e1e6cf4ce4557bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CGiems=E2=80=9D?= <“hubert.wabia@gmail.com”> Date: Fri, 5 Apr 2024 10:47:58 +0200 Subject: [PATCH 3/5] delete passkey, passkey challenge --- server/src/http/cloud/delete_passkey.rs | 125 ++++++++++++++++++ .../src/http/cloud/get_passkey_challenge.rs | 15 ++- server/src/http/cloud/mod.rs | 1 + server/src/routes/cloud_router.rs | 10 ++ server/src/structs/cloud/api_cloud_errors.rs | 2 + .../src/structs/cloud/cloud_http_endpoints.rs | 6 + 6 files changed, 153 insertions(+), 6 deletions(-) create mode 100644 server/src/http/cloud/delete_passkey.rs diff --git a/server/src/http/cloud/delete_passkey.rs b/server/src/http/cloud/delete_passkey.rs new file mode 100644 index 00000000..ed0ebcef --- /dev/null +++ b/server/src/http/cloud/delete_passkey.rs @@ -0,0 +1,125 @@ +use crate::{ + middlewares::auth_middleware::UserId, + structs::{ + cloud::api_cloud_errors::CloudApiErrors, + session_cache::{ApiSessionsCache, SessionCache, SessionsCacheKey}, + }, +}; +use axum::{extract::State, http::StatusCode, Extension, Json}; +use database::db::Db; +use garde::Validate; +use log::{error, warn}; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; +use webauthn_rs::prelude::PublicKeyCredential; +use webauthn_rs::Webauthn; + +#[derive(Validate, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct HttpDeletePasskeyRequest { + #[garde(skip)] + pub passkey_id: String, + #[garde(skip)] + pub passkey_credential: PublicKeyCredential, +} + +pub async fn delete_passkey( + State(db): State>, + State(web_auth): State>, + State(sessions_cache): State>, + Extension(user_id): Extension, + Json(payload): Json, +) -> Result<(), (StatusCode, String)> { + // Get user data + let user_data = match db.get_user_by_user_id(&user_id).await { + Ok(Some(user_data)) => user_data, + Ok(None) => { + return Err(( + StatusCode::BAD_REQUEST, + CloudApiErrors::UserDoesNotExist.to_string(), + )) + } + Err(err) => { + error!( + "Failed to check if user exists: {:?}, user_id: {}", + err, user_id + ); + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + CloudApiErrors::DatabaseError.to_string(), + )); + } + }; + + // Get user passkeys + let mut passkeys = match user_data.passkeys { + Some(passkey) => passkey, + None => { + return Err(( + StatusCode::BAD_REQUEST, + CloudApiErrors::UserDoesNotHavePasskey.to_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) + { + 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() + .position(|x| x.cred_id().to_string() == payload.passkey_id) + { + Some(index) => { + // Remove passkey + passkeys.remove(index); + } + None => { + return Err(( + StatusCode::BAD_REQUEST, + CloudApiErrors::PasskeyDoesNotExist.to_string(), + )) + } + } + + // Update user passkeys in database + if let Err(err) = db.update_passkeys(&user_id, &passkeys).await { + error!( + "Failed to update user passkeys: {:?}, user_id: {}", + err, user_id + ); + + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + CloudApiErrors::DatabaseError.to_string(), + )); + } + + return Ok(()); +} diff --git a/server/src/http/cloud/get_passkey_challenge.rs b/server/src/http/cloud/get_passkey_challenge.rs index 18adae15..f249d368 100644 --- a/server/src/http/cloud/get_passkey_challenge.rs +++ b/server/src/http/cloud/get_passkey_challenge.rs @@ -1,3 +1,10 @@ +use crate::{ + middlewares::auth_middleware::UserId, + structs::{ + cloud::api_cloud_errors::CloudApiErrors, + session_cache::{ApiSessionsCache, SessionCache, SessionsCacheKey}, + }, +}; use axum::{extract::State, http::StatusCode}; use axum::{Extension, Json}; use database::db::Db; @@ -6,18 +13,14 @@ use std::sync::Arc; use webauthn_rs::prelude::RequestChallengeResponse; use webauthn_rs::Webauthn; -use crate::middlewares::auth_middleware::UserId; -use crate::structs::cloud::api_cloud_errors::CloudApiErrors; -use crate::structs::session_cache::{ApiSessionsCache, SessionCache, SessionsCacheKey}; - -pub type TwoFactorWithPasskeyStartResponse = RequestChallengeResponse; +pub type HttpTwoFactorWithPasskeyStartResponse = RequestChallengeResponse; pub async fn get_passkey_challenge( State(db): State>, State(web_auth): State>, State(sessions_cache): State>, Extension(user_id): Extension, -) -> Result, (StatusCode, String)> { +) -> Result, (StatusCode, String)> { // Get user data let user_data = match db.get_user_by_user_id(&user_id).await { Ok(Some(user_data)) => user_data, diff --git a/server/src/http/cloud/mod.rs b/server/src/http/cloud/mod.rs index ec9c53c5..11ead83a 100644 --- a/server/src/http/cloud/mod.rs +++ b/server/src/http/cloud/mod.rs @@ -1,6 +1,7 @@ pub mod accept_team_invite; pub mod cancel_team_user_invite; pub mod cancel_user_team_invite; +pub mod delete_passkey; pub mod domains; pub mod events; pub mod get_events; diff --git a/server/src/routes/cloud_router.rs b/server/src/routes/cloud_router.rs index b3d5c922..4946b80c 100644 --- a/server/src/routes/cloud_router.rs +++ b/server/src/routes/cloud_router.rs @@ -3,12 +3,14 @@ use crate::{ accept_team_invite::accept_team_invite, cancel_team_user_invite::cancel_team_user_invite, cancel_user_team_invite::cancel_user_team_invite, + delete_passkey::delete_passkey, domains::{ remove_whitelisted_domain::remove_whitelisted_domain, verify_domain_finish::verify_domain_finish, verify_domain_start::verify_domain_start, }, events::events::events, get_events::get_events, + get_passkey_challenge::get_passkey_challenge, get_team_user_invites::get_team_user_invites, get_user_joined_teams::get_user_joined_teams, get_user_team_invites::get_user_team_invites, @@ -153,5 +155,13 @@ pub fn private_router(state: ServerState) -> Router { &HttpCloudEndpoint::RemoveWhitelistedDomain.to_string(), post(remove_whitelisted_domain), ) + .route( + &HttpCloudEndpoint::GetPasskeyChallenge.to_string(), + get(get_passkey_challenge), + ) + .route( + &HttpCloudEndpoint::DeletePasskey.to_string(), + post(delete_passkey), + ) .with_state(state) } diff --git a/server/src/structs/cloud/api_cloud_errors.rs b/server/src/structs/cloud/api_cloud_errors.rs index 2c24d78d..7852a592 100644 --- a/server/src/structs/cloud/api_cloud_errors.rs +++ b/server/src/structs/cloud/api_cloud_errors.rs @@ -42,4 +42,6 @@ pub enum CloudApiErrors { PasswordNotSet, UserDoesNotHavePasskey, PasskeyAlreadyExists, + InvalidPasskeyCredential, + PasskeyDoesNotExist, } diff --git a/server/src/structs/cloud/cloud_http_endpoints.rs b/server/src/structs/cloud/cloud_http_endpoints.rs index 86738556..1f8f56f2 100644 --- a/server/src/structs/cloud/cloud_http_endpoints.rs +++ b/server/src/structs/cloud/cloud_http_endpoints.rs @@ -54,6 +54,10 @@ pub enum HttpCloudEndpoint { ResetPasskeyStart, #[serde(rename = "/reset_passkey_finish")] ResetPasskeyFinish, + #[serde(rename = "/get_passkey_challenge")] + GetPasskeyChallenge, + #[serde(rename = "/delete_passkey")] + DeletePasskey, } impl HttpCloudEndpoint { @@ -92,6 +96,8 @@ impl HttpCloudEndpoint { } HttpCloudEndpoint::ResetPasskeyStart => "/reset_passkey_start".to_string(), HttpCloudEndpoint::ResetPasskeyFinish => "/reset_passkey_finish".to_string(), + HttpCloudEndpoint::DeletePasskey => "/delete_passkey".to_string(), + HttpCloudEndpoint::GetPasskeyChallenge => "/get_passkey_challenge".to_string(), } } } From 4049e7caa888b99b7a71e9604727d930ababe9b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CGiems=E2=80=9D?= <“hubert.wabia@gmail.com”> Date: Fri, 5 Apr 2024 11:21:31 +0200 Subject: [PATCH 4/5] add passkey start --- server/src/http/cloud/add_passkey_start.rs | 91 +++++++++++++++++++ server/src/http/cloud/mod.rs | 1 + server/src/routes/cloud_router.rs | 5 + .../src/structs/cloud/cloud_http_endpoints.rs | 3 + server/src/structs/session_cache.rs | 10 ++ 5 files changed, 110 insertions(+) create mode 100644 server/src/http/cloud/add_passkey_start.rs diff --git a/server/src/http/cloud/add_passkey_start.rs b/server/src/http/cloud/add_passkey_start.rs new file mode 100644 index 00000000..856174c2 --- /dev/null +++ b/server/src/http/cloud/add_passkey_start.rs @@ -0,0 +1,91 @@ +use crate::{ + middlewares::auth_middleware::UserId, + structs::{ + cloud::api_cloud_errors::CloudApiErrors, + session_cache::{AddPasskeyVerification, ApiSessionsCache, SessionCache, SessionsCacheKey}, + }, + utils::get_timestamp_in_milliseconds, +}; +use axum::{extract::State, http::StatusCode, Extension, 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::{CreationChallengeResponse, Uuid}, + Webauthn, +}; + +#[derive(Validate, Clone, Debug, Deserialize, Serialize, TS)] +#[ts(export)] +pub struct HttpAddPasskeyStartRequest { + #[garde(email)] + pub email: String, +} + +pub type HttpAddPasskeyStartResponse = CreationChallengeResponse; + +pub async fn add_passkey_start( + State(db): State>, + State(web_auth): State>, + State(sessions_cache): State>, + Extension(user_id): Extension, + Json(request): Json, +) -> Result, (StatusCode, String)> { + // Get user data + let user_data = match db.get_user_by_user_id(&user_id).await { + Ok(Some(user_data)) => user_data, + Ok(None) => { + return Err(( + StatusCode::BAD_REQUEST, + CloudApiErrors::UserDoesNotExist.to_string(), + )) + } + Err(err) => { + error!( + "Failed to check if user exists: {:?}, user_id: {}", + err, user_id + ); + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + CloudApiErrors::DatabaseError.to_string(), + )); + } + }; + + // Save to cache passkey challenge request + let sessions_key = SessionsCacheKey::AddPasskey(user_id.clone()).to_string(); + + // Remove leftover session data + sessions_cache.remove(&sessions_key); + + // Generate challenge + let temp_user_id = Uuid::new_v4(); + let res = + web_auth.start_passkey_registration(temp_user_id, &user_data.email, &user_data.email, None); + + let (ccr, reg_state) = match res { + Ok((ccr, reg_state)) => (ccr, reg_state), + Err(_) => { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + CloudApiErrors::WebAuthnError.to_string(), + )) + } + }; + + // Save the challenge to the cache + sessions_cache.set( + sessions_key, + SessionCache::VerifyAddPasskey(AddPasskeyVerification { + email: request.email.clone(), + passkey_registration_state: reg_state, + created_at: get_timestamp_in_milliseconds(), + }), + None, + ); + + return Ok(Json(ccr)); +} diff --git a/server/src/http/cloud/mod.rs b/server/src/http/cloud/mod.rs index 11ead83a..d557dd4e 100644 --- a/server/src/http/cloud/mod.rs +++ b/server/src/http/cloud/mod.rs @@ -1,4 +1,5 @@ pub mod accept_team_invite; +pub mod add_passkey_start; pub mod cancel_team_user_invite; pub mod cancel_user_team_invite; pub mod delete_passkey; diff --git a/server/src/routes/cloud_router.rs b/server/src/routes/cloud_router.rs index 4946b80c..89514f53 100644 --- a/server/src/routes/cloud_router.rs +++ b/server/src/routes/cloud_router.rs @@ -1,6 +1,7 @@ use crate::{ http::cloud::{ accept_team_invite::accept_team_invite, + add_passkey_start::add_passkey_start, cancel_team_user_invite::cancel_team_user_invite, cancel_user_team_invite::cancel_user_team_invite, delete_passkey::delete_passkey, @@ -163,5 +164,9 @@ pub fn private_router(state: ServerState) -> Router { &HttpCloudEndpoint::DeletePasskey.to_string(), post(delete_passkey), ) + .route( + &HttpCloudEndpoint::AddPasskeyStart.to_string(), + post(add_passkey_start), + ) .with_state(state) } diff --git a/server/src/structs/cloud/cloud_http_endpoints.rs b/server/src/structs/cloud/cloud_http_endpoints.rs index 1f8f56f2..d17833da 100644 --- a/server/src/structs/cloud/cloud_http_endpoints.rs +++ b/server/src/structs/cloud/cloud_http_endpoints.rs @@ -58,6 +58,8 @@ pub enum HttpCloudEndpoint { GetPasskeyChallenge, #[serde(rename = "/delete_passkey")] DeletePasskey, + #[serde(rename = "/add_passkey_start")] + AddPasskeyStart, } impl HttpCloudEndpoint { @@ -98,6 +100,7 @@ impl HttpCloudEndpoint { HttpCloudEndpoint::ResetPasskeyFinish => "/reset_passkey_finish".to_string(), HttpCloudEndpoint::DeletePasskey => "/delete_passkey".to_string(), HttpCloudEndpoint::GetPasskeyChallenge => "/get_passkey_challenge".to_string(), + HttpCloudEndpoint::AddPasskeyStart => "/add_passkey_start".to_string(), } } } diff --git a/server/src/structs/session_cache.rs b/server/src/structs/session_cache.rs index f34698f5..fdf1107f 100644 --- a/server/src/structs/session_cache.rs +++ b/server/src/structs/session_cache.rs @@ -9,6 +9,7 @@ pub enum SessionCache { VerifyPasskeyRegister(PasskeyVerification), ResetPasskey(ResetPasskeyVerification), Passkey2FA(webauthn_rs::prelude::PasskeyAuthentication), + VerifyAddPasskey(AddPasskeyVerification), } pub enum SessionsCacheKey { @@ -17,6 +18,7 @@ pub enum SessionsCacheKey { PasskeyVerification(String), // user email ResetPasskeyVerification(String), // user email Passkey2FA(String), // user id + AddPasskey(String), // user email } impl SessionsCacheKey { @@ -27,6 +29,7 @@ impl SessionsCacheKey { SessionsCacheKey::PasskeyVerification(email) => format!("pass_reg_{}", email), SessionsCacheKey::ResetPasskeyVerification(email) => format!("pass_res_{}", email), SessionsCacheKey::Passkey2FA(user_id) => format!("pass_chal_{}", user_id), + SessionsCacheKey::AddPasskey(email) => format!("add_pass_{}", email), } } } @@ -62,3 +65,10 @@ pub struct ResetPasskeyVerification { pub passkey_registration_state: webauthn_rs::prelude::PasskeyRegistration, pub created_at: u64, } + +#[derive(Debug, Clone)] +pub struct AddPasskeyVerification { + pub email: String, + pub passkey_registration_state: webauthn_rs::prelude::PasskeyRegistration, + pub created_at: u64, +} From a0bf068a0c73bd0ba71460f90f06c6a14b91df90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CGiems=E2=80=9D?= <“hubert.wabia@gmail.com”> Date: Fri, 5 Apr 2024 11:50:00 +0200 Subject: [PATCH 5/5] add passkey finish --- server/src/http/cloud/add_passkey_finish.rs | 121 ++++++++++++++++++ server/src/http/cloud/add_passkey_start.rs | 13 +- server/src/http/cloud/mod.rs | 1 + server/src/routes/cloud_router.rs | 5 + .../src/structs/cloud/cloud_http_endpoints.rs | 3 + server/src/structs/session_cache.rs | 4 +- 6 files changed, 133 insertions(+), 14 deletions(-) create mode 100644 server/src/http/cloud/add_passkey_finish.rs diff --git a/server/src/http/cloud/add_passkey_finish.rs b/server/src/http/cloud/add_passkey_finish.rs new file mode 100644 index 00000000..1a49743b --- /dev/null +++ b/server/src/http/cloud/add_passkey_finish.rs @@ -0,0 +1,121 @@ +use crate::{ + middlewares::auth_middleware::UserId, + structs::{ + cloud::api_cloud_errors::CloudApiErrors, + session_cache::{ApiSessionsCache, SessionCache, SessionsCacheKey}, + }, +}; +use axum::{extract::State, http::StatusCode, Extension, Json}; +use database::db::Db; +use garde::Validate; +use log::{error, warn}; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; +use ts_rs::TS; +use webauthn_rs::{prelude::RegisterPublicKeyCredential, Webauthn}; + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct HttpAddPasskeyFinishRequest { + pub credential: RegisterPublicKeyCredential, +} + +#[derive(Validate, Clone, Debug, Deserialize, Serialize, TS)] +#[ts(export)] +pub struct HttpAddPasskeyFinishResponse {} + +pub async fn add_passkey_finish( + State(db): State>, + State(web_auth): State>, + State(sessions_cache): State>, + Extension(user_id): Extension, + Json(request): Json, +) -> Result, (StatusCode, String)> { + // Get cache data + let sessions_key = SessionsCacheKey::AddPasskey(user_id.clone()).to_string(); + let session_data = match sessions_cache.get(&sessions_key) { + Some(SessionCache::VerifyAddPasskey(session)) => session, + _ => { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + CloudApiErrors::InternalServerError.to_string(), + )); + } + }; + + // Remove leftover session data + sessions_cache.remove(&sessions_key); + + // Validate new passkey registration + let passkey = match web_auth.finish_passkey_registration( + &request.credential, + &session_data.passkey_registration_state, + ) { + Ok(sk) => sk, + Err(err) => { + warn!( + "Failed to finish adding new passkey: {:?}, user_id: {}", + err, &user_id + ); + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + CloudApiErrors::WebAuthnError.to_string(), + )); + } + }; + + // Validate new passkey + // Get user data + let user_data = match db.get_user_by_user_id(&user_id).await { + Ok(Some(user_data)) => user_data, + Ok(None) => { + return Err(( + StatusCode::BAD_REQUEST, + CloudApiErrors::UserDoesNotExist.to_string(), + )); + } + Err(err) => { + error!("Failed to get user data: {:?}, user_id: {}", err, &user_id); + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + CloudApiErrors::DatabaseError.to_string(), + )); + } + }; + + // Check if user has already added this passkey + let mut passkeys = match user_data.passkeys { + Some(passkeys) => { + if passkeys.contains(&passkey) { + return Err(( + StatusCode::BAD_REQUEST, + CloudApiErrors::PasskeyAlreadyExists.to_string(), + )); + } + + passkeys + } + None => { + return Err(( + StatusCode::BAD_REQUEST, + CloudApiErrors::UserDoesNotHavePasskey.to_string(), + )); + } + }; + + // Add new passkey + passkeys.push(passkey); + + // Update passkeys in database + if let Err(err) = db.update_passkeys(&user_data.email, &passkeys).await { + error!( + "Failed to update user passkeys: {:?}, user_email: {}", + err, &user_data.email + ); + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + CloudApiErrors::DatabaseError.to_string(), + )); + } + + return Ok(Json(HttpAddPasskeyFinishResponse {})); +} diff --git a/server/src/http/cloud/add_passkey_start.rs b/server/src/http/cloud/add_passkey_start.rs index 856174c2..3b67db47 100644 --- a/server/src/http/cloud/add_passkey_start.rs +++ b/server/src/http/cloud/add_passkey_start.rs @@ -8,23 +8,13 @@ use crate::{ }; use axum::{extract::State, http::StatusCode, Extension, 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::{CreationChallengeResponse, Uuid}, Webauthn, }; -#[derive(Validate, Clone, Debug, Deserialize, Serialize, TS)] -#[ts(export)] -pub struct HttpAddPasskeyStartRequest { - #[garde(email)] - pub email: String, -} - pub type HttpAddPasskeyStartResponse = CreationChallengeResponse; pub async fn add_passkey_start( @@ -32,7 +22,6 @@ pub async fn add_passkey_start( State(web_auth): State>, State(sessions_cache): State>, Extension(user_id): Extension, - Json(request): Json, ) -> Result, (StatusCode, String)> { // Get user data let user_data = match db.get_user_by_user_id(&user_id).await { @@ -80,7 +69,7 @@ pub async fn add_passkey_start( sessions_cache.set( sessions_key, SessionCache::VerifyAddPasskey(AddPasskeyVerification { - email: request.email.clone(), + user_id, passkey_registration_state: reg_state, created_at: get_timestamp_in_milliseconds(), }), diff --git a/server/src/http/cloud/mod.rs b/server/src/http/cloud/mod.rs index d557dd4e..6c08e460 100644 --- a/server/src/http/cloud/mod.rs +++ b/server/src/http/cloud/mod.rs @@ -1,4 +1,5 @@ pub mod accept_team_invite; +pub mod add_passkey_finish; pub mod add_passkey_start; pub mod cancel_team_user_invite; pub mod cancel_user_team_invite; diff --git a/server/src/routes/cloud_router.rs b/server/src/routes/cloud_router.rs index 89514f53..3c0ae4bd 100644 --- a/server/src/routes/cloud_router.rs +++ b/server/src/routes/cloud_router.rs @@ -1,6 +1,7 @@ use crate::{ http::cloud::{ accept_team_invite::accept_team_invite, + add_passkey_finish::add_passkey_finish, add_passkey_start::add_passkey_start, cancel_team_user_invite::cancel_team_user_invite, cancel_user_team_invite::cancel_user_team_invite, @@ -168,5 +169,9 @@ pub fn private_router(state: ServerState) -> Router { &HttpCloudEndpoint::AddPasskeyStart.to_string(), post(add_passkey_start), ) + .route( + &HttpCloudEndpoint::AddPasskeyFinish.to_string(), + post(add_passkey_finish), + ) .with_state(state) } diff --git a/server/src/structs/cloud/cloud_http_endpoints.rs b/server/src/structs/cloud/cloud_http_endpoints.rs index d17833da..80984919 100644 --- a/server/src/structs/cloud/cloud_http_endpoints.rs +++ b/server/src/structs/cloud/cloud_http_endpoints.rs @@ -60,6 +60,8 @@ pub enum HttpCloudEndpoint { DeletePasskey, #[serde(rename = "/add_passkey_start")] AddPasskeyStart, + #[serde(rename = "/add_passkey_finish")] + AddPasskeyFinish, } impl HttpCloudEndpoint { @@ -101,6 +103,7 @@ impl HttpCloudEndpoint { HttpCloudEndpoint::DeletePasskey => "/delete_passkey".to_string(), HttpCloudEndpoint::GetPasskeyChallenge => "/get_passkey_challenge".to_string(), HttpCloudEndpoint::AddPasskeyStart => "/add_passkey_start".to_string(), + HttpCloudEndpoint::AddPasskeyFinish => "/add_passkey_finish".to_string(), } } } diff --git a/server/src/structs/session_cache.rs b/server/src/structs/session_cache.rs index fdf1107f..4136b388 100644 --- a/server/src/structs/session_cache.rs +++ b/server/src/structs/session_cache.rs @@ -18,7 +18,7 @@ pub enum SessionsCacheKey { PasskeyVerification(String), // user email ResetPasskeyVerification(String), // user email Passkey2FA(String), // user id - AddPasskey(String), // user email + AddPasskey(String), // user id } impl SessionsCacheKey { @@ -68,7 +68,7 @@ pub struct ResetPasskeyVerification { #[derive(Debug, Clone)] pub struct AddPasskeyVerification { - pub email: String, + pub user_id: String, pub passkey_registration_state: webauthn_rs::prelude::PasskeyRegistration, pub created_at: u64, }