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

Delete passkey #157

Merged
merged 5 commits into from
Apr 5, 2024
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
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
Loading