Skip to content

Commit

Permalink
Merge pull request #156 from nightly-labs/reset-passkey
Browse files Browse the repository at this point in the history
passkey reset rust implementation
  • Loading branch information
Giems authored Apr 5, 2024
2 parents 5b0373a + 20b1434 commit e45360d
Show file tree
Hide file tree
Showing 13 changed files with 777 additions and 0 deletions.
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

0 comments on commit e45360d

Please sign in to comment.