#![allow(non_snake_case)] use std::collections::HashMap; use std::sync::Arc; use std::time::Duration; use axum::debug_handler; use axum::extract::State; use axum::{ extract::{Json, Path}, http::StatusCode, response::IntoResponse, }; use std::time::Instant; use tower_cookies::Cookies; use tower_sessions::Session; use tracing::*; /* * Webauthn RS auth handlers. * These files use webauthn to process the data received from each route, and are closely tied to axum */ // 1. Import the prelude - this contains everything needed for the server to function. use webauthn_rs::prelude::*; use crate::config::{COOKIE_NAME, SESSION_ACTIVE_TIME}; use crate::ServerState; // 2. The first step a client (user) will carry out is requesting a credential to be // registered. We need to provide a challenge for this. The work flow will be: // // ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ // │ Authenticator │ │ Browser │ │ Site │ // └───────────────┘ └───────────────┘ └───────────────┘ // │ │ │ // │ │ 1. Start Reg │ // │ │─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─▶│ // │ │ │ // │ │ 2. Challenge │ // │ │◀ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┤ // │ │ │ // │ 3. Select Token │ │ // ─ ─ ─│◀ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─│ │ // 4. Verify │ │ │ │ // │ 4. Yield PubKey │ │ // └ ─ ─▶│─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─▶ │ // │ │ │ // │ │ 5. Send Reg Opts │ // │ │─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─▶│─ ─ ─ // │ │ │ │ 5. Verify // │ │ │ PubKey // │ │ │◀─ ─ ┘ // │ │ │─ ─ ─ // │ │ │ │ 6. Persist // │ │ │ Credential // │ │ │◀─ ─ ┘ // │ │ │ // │ │ │ // // In this step, we are responding to the start reg(istration) request, and providing // the challenge to the browser. // TODO - Improve error handling and messages fn auth_user(cookie_content:String,session_table:&mut HashMap) -> bool { let Ok(uuid) = Uuid::parse_str(&cookie_content) else { return false; }; let Some(expire) = session_table.get(&uuid) else { return false; }; if *expire <= Instant::now() { return false; } session_table.insert(uuid, Instant::now()+Duration::from_secs(*SESSION_ACTIVE_TIME)); tracing::info!("valid cookie {}",uuid); return true; } #[debug_handler] pub async fn start_register( State(state): State>, cookies: Cookies, session: Session, Path(username): Path, ) -> Result, String> { tracing::info!("Start register"); // todo!("Auth User"); let Some(cookie_content) = cookies.get(&COOKIE_NAME) else { return Err(WebauthnError::AuthenticationFailure.to_string()); }; let mut session_table = state.session.lock().await; if !auth_user(cookie_content.value().to_string(), &mut session_table){ return Err(WebauthnError::AuthenticationFailure.to_string()); } // Remove any previous registrations that may have occurred from the session. let _ = session .remove::<(String, Uuid, PasskeyRegistration)>("reg_state") .await .map_err(|_| WebauthnError::AuthenticationFailure.to_string())?; let pool = &state.db; let username = username.clone(); let (user_id, exclude_credentials): (Uuid, Option>) = match sqlx::query!("SELECT key FROM users WHERE name = $1;", username) .fetch_optional(pool) .await .map_err(|_| WebauthnError::AuthenticationFailure.to_string())? { Some(record) => { let uid = record.KEY.clone(); let records = sqlx::query!( "SELECT credential FROM CREDENTIALS WHERE user_id = $1;", uid ) .fetch_all(pool) .await .map_err(|_| WebauthnError::AuthenticationFailure.to_string())?; ( Uuid::parse_str(&record.KEY) .map_err(|_| WebauthnError::AuthenticationFailure.to_string())?, Some( records .iter() .map(|record| serde_json::from_str::(&record.CREDENTIAL)) .collect::, _>>() .map_err(|_| WebauthnError::CredentialPersistenceError.to_string())? .iter() .map(|passkey| passkey.cred_id().clone()) .collect(), ), ) } None => (Uuid::new_v4(), None), }; let res = match state.webauthn.start_passkey_registration( user_id, &username, &username, exclude_credentials, ) { Ok((ccr, reg_state)) => { // Note that due to the session store in use being a server side memory store, this is // safe to store the reg_state into the session since it is not client controlled and // not open to replay attacks. If this was a cookie store, this would be UNSAFE. session .insert("reg_state", (username, user_id, reg_state)) .await .map_err(|_| WebauthnError::ChallengePersistenceError.to_string())?; Json(ccr) } Err(e) => { debug!("challenge_register -> {:?}", e); return Err(WebauthnError::ChallengePersistenceError.to_string()); } }; Ok(res) } // 3. The browser has completed it's steps and the user has created a public key // on their device. Now we have the registration options sent to us, and we need // to verify these and persist them. pub async fn finish_register( State(state): State>, session: Session, Json(reg): Json, ) -> Result { info!("Confirming registration...."); let pool = &state.db; let Ok(Some((user_name, user_id, reg_state))) = session .get::<(String, Uuid, PasskeyRegistration)>("reg_state") .await else { return Err(WebauthnError::AuthenticationFailure.to_string()); //Corrupt Session }; let _ = session.remove::<(Uuid, PasskeyAuthentication)>("reg_state").await; let res = match state.webauthn.finish_passkey_registration(®, ®_state) { Ok(key) => { info!("Passkey is okay"); let uid = &user_id.to_string(); let username = &user_name.to_string(); // Check if the user_id already exists let record = sqlx::query!( "SELECT COUNT(KEY) AS count FROM users WHERE KEY = $1;", uid ) .fetch_one(pool) .await .map_err(|_| WebauthnError::AuthenticationFailure.to_string())?; // If the user doesn't exist, insert them into the users table if record.count == 0 && sqlx::query!( "INSERT INTO users(KEY, NAME) VALUES($1, $2);", uid, username ) .execute(pool) .await .map_err(|_| WebauthnError::UserNotPresent.to_string())? .rows_affected() != 1 { return Err(WebauthnError::AuthenticationFailure.to_string()); } // Serialise the key let serialised_key = serde_json::ser::to_string(&key) .map_err(|_| WebauthnError::CredentialPersistenceError.to_string())?; // Insert the key into the auth table if sqlx::query!( "INSERT INTO CREDENTIALS(user_id, credential) VALUES($1, $2);", uid, serialised_key ) .execute(pool) .await .map_err(|_| WebauthnError::UserNotPresent.to_string())? .rows_affected() != 1 { return Err(WebauthnError::AuthenticationFailure.to_string()); } StatusCode::OK } Err(e) => { debug!("challenge_register -> {:?}", e); StatusCode::BAD_REQUEST } }; Ok(res) } // 4. Now that our public key has been registered, we can authenticate a user and verify // that they are the holder of that security token. The work flow is similar to registration. // // ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ // │ Authenticator │ │ Browser │ │ Site │ // └───────────────┘ └───────────────┘ └───────────────┘ // │ │ │ // │ │ 1. Start Auth │ // │ │─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─▶│ // │ │ │ // │ │ 2. Challenge │ // │ │◀ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┤ // │ │ │ // │ 3. Select Token │ │ // ─ ─ ─│◀ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─│ │ // 4. Verify │ │ │ │ // │ 4. Yield Sig │ │ // └ ─ ─▶│─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─▶ │ // │ │ 5. Send Auth │ // │ │ Opts │ // │ │─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─▶│─ ─ ─ // │ │ │ │ 5. Verify // │ │ │ Sig // │ │ │◀─ ─ ┘ // │ │ │ // │ │ │ // // The user indicates the wish to start authentication and we need to provide a challenge. pub async fn start_authentication( State(state): State>, session: Session, Path(user_name): Path, ) -> Result { info!("Start Authentication"); let pool = &state.db; // Remove any previous authentication that may have occurred from the session. let _ = session.remove::<(Uuid, PasskeyAuthentication)>("auth_state"); let user_id = sqlx::query!("SELECT KEY FROM users WHERE NAME = $1;", user_name) .fetch_one(pool) .await .map_err(|_| WebauthnError::UserNotPresent.to_string())? .KEY; let records = sqlx::query!( "SELECT credential FROM CREDENTIALS WHERE user_id = $1;", user_id ) .fetch_all(pool) .await .map_err(|_| WebauthnError::AuthenticationFailure.to_string())?; if records.is_empty() { return Err(WebauthnError::CredentialNotFound.to_string()); } let allow_credentials: Vec = records .iter() .map(|record| serde_json::de::from_str::(&record.CREDENTIAL)) .collect::, _>>() .map_err(|_| WebauthnError::CredentialPersistenceError.to_string())?; let res = match state .webauthn .start_passkey_authentication(&allow_credentials) { Ok((rcr, auth_state)) => { // Note that due to the session store in use being a server side memory store, this is // safe to store the auth_state into the session since it is not client controlled and // not open to replay attacks. If this was a cookie store, this would be UNSAFE. session .insert("auth_state", (&user_id, auth_state)) .await .map_err(|_| WebauthnError::AuthenticationFailure.to_string())?; Json(rcr) } Err(e) => { debug!("challenge_authenticate -> {:?}", e); return Err(WebauthnError::MismatchedChallenge.to_string()); } }; Ok(res) } // 5. The browser and user have completed their part of the processing. Only in the // case that the webauthn authenticate call returns Ok, is authentication considered // a success. If the browser does not complete this call, or *any* error occurs, // this is an authentication failure. pub async fn finish_authentication( State(state): State>, session: Session, Json(auth): Json, ) -> Result { let pool = &state.db; let (user_id, auth_state): (Uuid, PasskeyAuthentication) = session .get("auth_state") .await .unwrap() .ok_or(WebauthnError::AuthenticationFailure.to_string())?; let _ = session.remove::<(Uuid, PasskeyAuthentication)>("auth_state"); let res = match state .webauthn .finish_passkey_authentication(&auth, &auth_state) { Ok(auth_result) => { let uid = user_id.clone().to_string(); let records = sqlx::query!( "SELECT CREDENTIAL FROM CREDENTIALS WHERE USER_ID = $1;", uid ) .fetch_all(pool) .await .map_err(|_| WebauthnError::AuthenticatorDataMissingExtension.to_string())?; if records.is_empty() { return Err(WebauthnError::UserNotPresent.to_string()); } for record in records { let mut credential = serde_json::from_str::(&record.CREDENTIAL) .map_err(|_| WebauthnError::CredentialExistCheckError.to_string())?; if credential.cred_id() == auth_result.cred_id() { credential.update_credential(&auth_result); let credential = serde_json::to_string(&credential) .map_err(|_| WebauthnError::CredentialPersistenceError.to_string())?; if sqlx::query!( "UPDATE CREDENTIALS SET credential = $1 WHERE user_id = $2 AND credential = $3;", credential, uid, record.CREDENTIAL ) .execute(pool) .await.is_ok_and(|e|e.rows_affected() !=1 ) { return Err(WebauthnError::AuthenticationFailure.to_string()); } break; } } let user_name = sqlx::query!( "SELECT NAME FROM users WHERE KEY = $1;", uid ) .fetch_one(pool) .await .map_err(|_| WebauthnError::AuthenticationFailure.to_string())?; // Add our own values to the session session .insert("user_id", user_id) .await .map_err(|_| WebauthnError::InvalidUserUniqueId.to_string())?; session .insert("user_name", user_name.NAME) .await .map_err(|_| WebauthnError::InvalidUsername.to_string())?; StatusCode::OK } Err(e) => { debug!("challenge_register -> {:?}", e); StatusCode::BAD_REQUEST } }; info!("Authentication Successful!"); Ok(res) }