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 1/2] 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 2/2] 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, }