Skip to content

Commit

Permalink
Merge pull request #167 from nightly-labs/login-with-passkey
Browse files Browse the repository at this point in the history
login with passkey
  • Loading branch information
Giems authored Apr 9, 2024
2 parents 9131b82 + 65d9b23 commit b0f5422
Show file tree
Hide file tree
Showing 10 changed files with 296 additions and 67 deletions.
59 changes: 30 additions & 29 deletions server/src/http/cloud/delete_passkey.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,36 @@ pub async fn delete_passkey(
Extension(user_id): Extension<UserId>,
Json(payload): Json<HttpDeletePasskeyRequest>,
) -> Result<(), (StatusCode, 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.passkey_verification_state,
) {
warn!(
"Failed to finish passkey authentication: {:?}, user_id: {}",
err, user_id
);
return Err((
StatusCode::BAD_REQUEST,
CloudApiErrors::InvalidPasskeyCredential.to_string(),
));
}

// Get user data
let user_data = match db.get_user_by_user_id(&user_id).await {
Ok(Some(user_data)) => user_data,
Expand Down Expand Up @@ -62,35 +92,6 @@ pub async fn delete_passkey(
}
};

// 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()
Expand Down
15 changes: 12 additions & 3 deletions server/src/http/cloud/get_passkey_challenge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ use crate::{
middlewares::auth_middleware::UserId,
structs::{
cloud::api_cloud_errors::CloudApiErrors,
session_cache::{ApiSessionsCache, SessionCache, SessionsCacheKey},
session_cache::{ApiSessionsCache, Passkey2FAVerification, SessionCache, SessionsCacheKey},
},
utils::get_timestamp_in_milliseconds,
};
use axum::{extract::State, http::StatusCode};
use axum::{Extension, Json};
Expand Down Expand Up @@ -53,14 +54,22 @@ pub async fn get_passkey_challenge(
};

// Save to cache passkey challenge request
let sessions_key = SessionsCacheKey::PasskeyVerification(user_id.clone()).to_string();
let sessions_key = SessionsCacheKey::Passkey2FA(user_id.clone()).to_string();

// Remove leftover session data
sessions_cache.remove(&sessions_key);

match web_auth.start_passkey_authentication(&passkey) {
Ok((rcr, auth_state)) => {
sessions_cache.set(sessions_key, SessionCache::Passkey2FA(auth_state), None);
sessions_cache.set(
sessions_key,
SessionCache::Passkey2FA(Passkey2FAVerification {
email: user_data.email.clone(),
passkey_verification_state: auth_state,
created_at: get_timestamp_in_milliseconds(),
}),
None,
);
return Ok(Json(rcr));
}
Err(_) => {
Expand Down
106 changes: 106 additions & 0 deletions server/src/http/cloud/login/login_with_passkey_finish.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
use crate::{
http::cloud::utils::{generate_tokens, validate_request},
structs::{
cloud::api_cloud_errors::CloudApiErrors,
session_cache::{ApiSessionsCache, SessionCache, SessionsCacheKey},
},
};
use axum::{
extract::{ConnectInfo, State},
http::StatusCode,
Json,
};
use database::db::Db;
use garde::Validate;
use log::{error, warn};
use serde::{Deserialize, Serialize};
use std::{net::SocketAddr, sync::Arc};
use ts_rs::TS;
use webauthn_rs::{prelude::PublicKeyCredential, Webauthn};

#[derive(Validate, Debug, Deserialize, Serialize)]
pub struct HttpLoginWithPasskeyFinishRequest {
#[garde(email)]
pub email: String,
#[garde(skip)]
pub passkey_credential: PublicKeyCredential,
#[garde(skip)]
pub enforce_ip: bool,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct HttpLoginWithPasskeyFinishResponse {
pub user_id: String,
pub auth_token: String,
pub refresh_token: String,
}

pub async fn login_with_passkey_finish(
ConnectInfo(ip): ConnectInfo<SocketAddr>,
State(db): State<Arc<Db>>,
State(web_auth): State<Arc<Webauthn>>,
State(sessions_cache): State<Arc<ApiSessionsCache>>,
Json(request): Json<HttpLoginWithPasskeyFinishRequest>,
) -> Result<Json<HttpLoginWithPasskeyFinishResponse>, (StatusCode, String)> {
// Validate request
validate_request(&request, &())?;

// Get session data
let sessions_key = SessionsCacheKey::PasskeyLogin(request.email.clone()).to_string();
let session_data = match sessions_cache.get(&sessions_key) {
Some(SessionCache::PasskeyLogin(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(
&request.passkey_credential,
&session_data.passkey_verification_state,
) {
warn!(
"Failed to finish passkey authentication: {:?}, user_email: {}",
err, request.email
);
return Err((
StatusCode::BAD_REQUEST,
CloudApiErrors::InvalidPasskeyCredential.to_string(),
));
}

// Get user data
let user = match db.get_user_by_email(&request.email).await {
Ok(Some(user)) => user,
Ok(None) => {
return Err((
StatusCode::BAD_REQUEST,
CloudApiErrors::UserDoesNotExist.to_string(),
));
}
Err(err) => {
error!("Failed to get user by email: {:?}", err);
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
CloudApiErrors::DatabaseError.to_string(),
));
}
};

// Generate tokens
let (auth_token, refresh_token) = generate_tokens(request.enforce_ip, ip, &user.user_id)?;

return Ok(Json(HttpLoginWithPasskeyFinishResponse {
auth_token,
refresh_token,
user_id: user.user_id,
}));
}
94 changes: 94 additions & 0 deletions server/src/http/cloud/login/login_with_passkey_start.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
use crate::{
http::cloud::utils::validate_request,
structs::{
cloud::api_cloud_errors::CloudApiErrors,
session_cache::{
ApiSessionsCache, PasskeyLoginVerification, SessionCache, SessionsCacheKey,
},
},
utils::get_timestamp_in_milliseconds,
};
use axum::{extract::State, http::StatusCode, Json};
use database::db::Db;
use garde::Validate;
use log::error;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use ts_rs::TS;
use webauthn_rs::{prelude::RequestChallengeResponse, Webauthn};

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS, Validate)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct HttpLoginWithPasskeyStartRequest {
#[garde(email)]
pub email: String,
}

pub type HttpLoginWithPasskeyStartResponse = RequestChallengeResponse;

pub async fn login_with_passkey_start(
State(db): State<Arc<Db>>,
State(web_auth): State<Arc<Webauthn>>,
State(sessions_cache): State<Arc<ApiSessionsCache>>,
Json(request): Json<HttpLoginWithPasskeyStartRequest>,
) -> Result<Json<HttpLoginWithPasskeyStartResponse>, (StatusCode, String)> {
// Validate request
validate_request(&request, &())?;

// Check if user exists
let user = match db.get_user_by_email(&request.email).await {
Ok(Some(user)) => user,
Ok(None) => {
return Err((
StatusCode::BAD_REQUEST,
CloudApiErrors::UserDoesNotExist.to_string(),
));
}
Err(err) => {
error!("Failed to get user by email: {:?}", err);
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
CloudApiErrors::DatabaseError.to_string(),
));
}
};

// Check if user has passkey
let passkeys = match user.passkeys {
Some(passkeys) => passkeys,
None => {
return Err((
StatusCode::BAD_REQUEST,
CloudApiErrors::PasswordNotSet.to_string(),
));
}
};

// Save to cache passkey challenge request
let sessions_key = SessionsCacheKey::PasskeyLogin(request.email.clone()).to_string();

// Remove leftover session data
sessions_cache.remove(&sessions_key);

match web_auth.start_passkey_authentication(&passkeys) {
Ok((rcr, auth_state)) => {
sessions_cache.set(
sessions_key,
SessionCache::PasskeyLogin(PasskeyLoginVerification {
email: request.email.clone(),
passkey_verification_state: auth_state,
created_at: get_timestamp_in_milliseconds(),
}),
None,
);
return Ok(Json(rcr));
}
Err(_) => {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
CloudApiErrors::WebAuthnError.to_string(),
));
}
};
}
16 changes: 8 additions & 8 deletions server/src/http/cloud/login/login_with_password.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,9 @@ mod tests {
use super::*;
use crate::{
http::cloud::register::{
register_with_password_finish::HttpVerifyRegisterWithPasswordRequest,
register_with_password_finish::HttpRegisterWithPasswordFinishRequest,
register_with_password_start::{
HttpRegisterWithPasswordRequest, HttpRegisterWithPasswordResponse,
HttpRegisterWithPasswordStartRequest, HttpRegisterWithPasswordStartResponse,
},
},
structs::cloud::cloud_http_endpoints::HttpCloudEndpoint,
Expand Down Expand Up @@ -129,7 +129,7 @@ mod tests {
let password = format!("Password123");

// Register user
let register_payload = HttpRegisterWithPasswordRequest {
let register_payload = HttpRegisterWithPasswordStartRequest {
email: email.to_string(),
password: password.to_string(),
};
Expand All @@ -151,12 +151,12 @@ mod tests {
// Send request
let register_response = test_app.clone().oneshot(req).await.unwrap();
// Validate response
convert_response::<HttpRegisterWithPasswordResponse>(register_response)
convert_response::<HttpRegisterWithPasswordStartResponse>(register_response)
.await
.unwrap();

// Validate register
let verify_register_payload = HttpVerifyRegisterWithPasswordRequest {
let verify_register_payload = HttpRegisterWithPasswordFinishRequest {
email: email.to_string(),
// Random valid code for testing
code: "123456".to_string(),
Expand Down Expand Up @@ -262,7 +262,7 @@ mod tests {
let password = format!("Password123");

// Register user
let register_payload = HttpRegisterWithPasswordRequest {
let register_payload = HttpRegisterWithPasswordStartRequest {
email: email.to_string(),
password: password.to_string(),
};
Expand All @@ -284,12 +284,12 @@ mod tests {
// Send request
let register_response = test_app.clone().oneshot(req).await.unwrap();
// Validate response
convert_response::<HttpRegisterWithPasswordResponse>(register_response)
convert_response::<HttpRegisterWithPasswordStartResponse>(register_response)
.await
.unwrap();

// Validate register
let verify_register_payload = HttpVerifyRegisterWithPasswordRequest {
let verify_register_payload = HttpRegisterWithPasswordFinishRequest {
email: email.to_string(),
// Random valid code for testing
code: "123456".to_string(),
Expand Down
2 changes: 2 additions & 0 deletions server/src/http/cloud/login/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
pub mod login_with_google;
pub mod login_with_passkey_finish;
pub mod login_with_passkey_start;
pub mod login_with_password;
Loading

0 comments on commit b0f5422

Please sign in to comment.