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

login with passkey #167

Merged
merged 1 commit into from
Apr 9, 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
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
Loading