-
Notifications
You must be signed in to change notification settings - Fork 16
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #156 from nightly-labs/reset-passkey
passkey reset rust implementation
- Loading branch information
Showing
13 changed files
with
777 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 {})); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(()); | ||
} |
Oops, something went wrong.