Skip to content

Commit

Permalink
Merge pull request #159 from nightly-labs/add-passkey
Browse files Browse the repository at this point in the history
Add passkey
  • Loading branch information
Giems authored Apr 5, 2024
2 parents 26a1090 + a0bf068 commit 796bd60
Show file tree
Hide file tree
Showing 6 changed files with 229 additions and 0 deletions.
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,
}

0 comments on commit 796bd60

Please sign in to comment.