Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
23 changes: 23 additions & 0 deletions database/src/tables/users/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Passkey>,
) -> 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")]
Expand Down
121 changes: 121 additions & 0 deletions server/src/http/cloud/add_passkey_finish.rs
Original file line number Diff line number Diff line change
@@ -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<Arc<Db>>,
State(web_auth): State<Arc<Webauthn>>,
State(sessions_cache): State<Arc<ApiSessionsCache>>,
Extension(user_id): Extension<UserId>,
Json(request): Json<HttpAddPasskeyFinishRequest>,
) -> Result<Json<HttpAddPasskeyFinishResponse>, (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 {}));
}
80 changes: 80 additions & 0 deletions server/src/http/cloud/add_passkey_start.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
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 log::error;
use std::sync::Arc;
use webauthn_rs::{
prelude::{CreationChallengeResponse, Uuid},
Webauthn,
};

pub type HttpAddPasskeyStartResponse = CreationChallengeResponse;

pub async fn add_passkey_start(
State(db): State<Arc<Db>>,
State(web_auth): State<Arc<Webauthn>>,
State(sessions_cache): State<Arc<ApiSessionsCache>>,
Extension(user_id): Extension<UserId>,
) -> Result<Json<HttpAddPasskeyStartResponse>, (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 {
user_id,
passkey_registration_state: reg_state,
created_at: get_timestamp_in_milliseconds(),
}),
None,
);

return Ok(Json(ccr));
}
125 changes: 125 additions & 0 deletions server/src/http/cloud/delete_passkey.rs
Original file line number Diff line number Diff line change
@@ -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<Arc<Db>>,
State(web_auth): State<Arc<Webauthn>>,
State(sessions_cache): State<Arc<ApiSessionsCache>>,
Extension(user_id): Extension<UserId>,
Json(payload): Json<HttpDeletePasskeyRequest>,
) -> 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(());
}
Loading