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

Add passkey #159

Merged
merged 2 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));
}
2 changes: 2 additions & 0 deletions server/src/http/cloud/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
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;
pub mod delete_passkey;
Expand Down
10 changes: 10 additions & 0 deletions server/src/routes/cloud_router.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
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,
delete_passkey::delete_passkey,
Expand Down Expand Up @@ -163,5 +165,13 @@ pub fn private_router(state: ServerState) -> Router<ServerState> {
&HttpCloudEndpoint::DeletePasskey.to_string(),
post(delete_passkey),
)
.route(
&HttpCloudEndpoint::AddPasskeyStart.to_string(),
post(add_passkey_start),
)
.route(
&HttpCloudEndpoint::AddPasskeyFinish.to_string(),
post(add_passkey_finish),
)
.with_state(state)
}
6 changes: 6 additions & 0 deletions server/src/structs/cloud/cloud_http_endpoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ pub enum HttpCloudEndpoint {
GetPasskeyChallenge,
#[serde(rename = "/delete_passkey")]
DeletePasskey,
#[serde(rename = "/add_passkey_start")]
AddPasskeyStart,
#[serde(rename = "/add_passkey_finish")]
AddPasskeyFinish,
}

impl HttpCloudEndpoint {
Expand Down Expand Up @@ -98,6 +102,8 @@ 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(),
HttpCloudEndpoint::AddPasskeyFinish => "/add_passkey_finish".to_string(),
}
}
}
10 changes: 10 additions & 0 deletions server/src/structs/session_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ pub enum SessionCache {
VerifyPasskeyRegister(PasskeyVerification),
ResetPasskey(ResetPasskeyVerification),
Passkey2FA(webauthn_rs::prelude::PasskeyAuthentication),
VerifyAddPasskey(AddPasskeyVerification),
}

pub enum SessionsCacheKey {
Expand All @@ -17,6 +18,7 @@ pub enum SessionsCacheKey {
PasskeyVerification(String), // user email
ResetPasskeyVerification(String), // user email
Passkey2FA(String), // user id
AddPasskey(String), // user id
}

impl SessionsCacheKey {
Expand All @@ -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),
}
}
}
Expand Down Expand Up @@ -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 user_id: String,
pub passkey_registration_state: webauthn_rs::prelude::PasskeyRegistration,
pub created_at: u64,
}
Loading