use passki::{
AttestationConveyancePreference, AuthenticationCredential, AuthenticationState, ClientData,
Passki, RegistrationCredential, RegistrationState, ResidentKeyRequirement, StoredPasskey,
UserVerificationRequirement,
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::convert::Infallible;
use std::fs;
use std::sync::{Arc, Mutex};
use uuid::Uuid;
use warp::{Filter, Reply, http::StatusCode, reject::Reject, reply};
#[derive(Debug)]
struct AppError(String);
impl Reject for AppError {}
async fn handle_rejection(err: warp::Rejection) -> Result<impl Reply, Infallible> {
let message = if let Some(e) = err.find::<AppError>() {
e.0.clone()
} else {
"Internal server error".to_string()
};
Ok(reply::with_status(message, StatusCode::BAD_REQUEST))
}
#[derive(Clone, Default)]
struct Store {
users: Arc<Mutex<HashMap<String, User>>>,
pending_registrations: Arc<Mutex<HashMap<String, RegistrationState>>>,
pending_authentications: Arc<Mutex<HashMap<String, AuthenticationState>>>,
}
#[derive(Clone)]
#[allow(unused)]
struct User {
id: Uuid,
username: String,
display_name: String,
passkeys: Vec<StoredPasskey>,
}
#[derive(Deserialize)]
struct RegisterStartRequest {
username: String,
}
#[derive(Deserialize)]
struct RegisterFinishRequest {
credential_id: String,
public_key: String,
client_data_json: String,
}
#[derive(Deserialize, Default)]
struct AuthStartRequest {
#[serde(default)]
username: Option<String>,
}
#[derive(Deserialize)]
struct AuthFinishRequest {
credential_id: String,
authenticator_data: String,
client_data_json: String,
signature: String,
}
#[derive(Serialize)]
struct ApiResponse {
success: bool,
message: String,
#[serde(skip_serializing_if = "Option::is_none")]
username: Option<String>,
}
#[derive(Clone)]
struct AppState {
passki: Arc<Passki>,
store: Store,
}
fn with_state(
state: AppState,
) -> impl Filter<Extract = (AppState,), Error = Infallible> + Clone {
warp::any().map(move || state.clone())
}
async fn index() -> Result<impl Reply, warp::Rejection> {
let html = fs::read_to_string("examples/index.html")
.map_err(|e| warp::reject::custom(AppError(e.to_string())))?;
Ok(reply::html(html))
}
async fn register_start(
state: AppState,
req: RegisterStartRequest,
) -> Result<impl Reply, warp::Rejection> {
let user_id = Uuid::new_v4().as_bytes().to_vec();
let existing = state.store.users.lock().unwrap()
.get(&req.username).map(|u| u.passkeys.clone());
let (challenge, reg_state) = state.passki.start_passkey_registration(
&user_id,
&req.username, &req.username, 60000, AttestationConveyancePreference::None, ResidentKeyRequirement::Preferred, UserVerificationRequirement::Preferred, existing.as_deref(), ).map_err(|e| warp::reject::custom(AppError(e.to_string())))?;
state.store.pending_registrations.lock().unwrap().insert(challenge.challenge.clone(), reg_state);
Ok(reply::json(&challenge))
}
async fn register_finish(
state: AppState,
req: RegisterFinishRequest,
) -> Result<impl Reply, warp::Rejection> {
let client_data = ClientData::from_base64(&req.client_data_json)
.map_err(|e| warp::reject::custom(AppError(e.to_string())))?;
let reg_state = state.store.pending_registrations.lock().unwrap()
.remove(&client_data.challenge)
.ok_or_else(|| warp::reject::custom(AppError("No pending registration".into())))?;
let credential = RegistrationCredential {
credential_id: req.credential_id,
public_key: req.public_key,
client_data_json: req.client_data_json,
};
let passkey = state.passki.finish_passkey_registration(&credential, ®_state)
.map_err(|e| warp::reject::custom(AppError(e.to_string())))?;
let user_id_bytes = Passki::base64_decode(®_state.user.id)
.map_err(|e| warp::reject::custom(AppError(e.to_string())))?;
let user_id = Uuid::from_slice(&user_id_bytes)
.map_err(|e| warp::reject::custom(AppError(e.to_string())))?;
let mut users = state.store.users.lock().unwrap();
users
.entry(reg_state.user.name.clone())
.and_modify(|user| user.passkeys.push(passkey.clone()))
.or_insert(User {
id: user_id,
username: reg_state.user.name,
display_name: reg_state.user.display_name,
passkeys: vec![passkey],
});
Ok(reply::json(&ApiResponse { success: true, message: "Registration successful".into(), username: None }))
}
async fn auth_start(
state: AppState,
req: AuthStartRequest,
) -> Result<impl Reply, warp::Rejection> {
let passkeys = if let Some(ref username) = req.username {
let users = state.store.users.lock().unwrap();
let user = users.get(username)
.ok_or_else(|| warp::reject::custom(AppError("User not found".into())))?;
user.passkeys.clone()
} else {
vec![]
};
let (challenge, auth_state) = state.passki.start_passkey_authentication(
&passkeys,
60000, UserVerificationRequirement::Preferred, );
state.store.pending_authentications.lock().unwrap().insert(challenge.challenge.clone(), auth_state);
Ok(reply::json(&challenge))
}
async fn auth_finish(
state: AppState,
req: AuthFinishRequest,
) -> Result<impl Reply, warp::Rejection> {
let client_data = ClientData::from_base64(&req.client_data_json)
.map_err(|e| warp::reject::custom(AppError(e.to_string())))?;
let auth_state = state.store.pending_authentications.lock().unwrap()
.remove(&client_data.challenge)
.ok_or_else(|| warp::reject::custom(AppError("No pending authentication".into())))?;
let credential_id = Passki::base64_decode(&req.credential_id)
.map_err(|e| warp::reject::custom(AppError(e.to_string())))?;
let mut users = state.store.users.lock().unwrap();
let (username, passkey) = users.iter_mut()
.find_map(|(name, user)| {
user.passkeys.iter_mut()
.find(|pk| pk.credential_id == credential_id)
.map(|pk| (name.clone(), pk))
})
.ok_or_else(|| warp::reject::custom(AppError("Unknown credential".into())))?;
let credential = AuthenticationCredential {
credential_id: req.credential_id,
authenticator_data: req.authenticator_data,
client_data_json: req.client_data_json,
signature: req.signature,
};
let result = state.passki.finish_passkey_authentication(&credential, &auth_state, passkey)
.map_err(|e| warp::reject::custom(AppError(e.to_string())))?;
passkey.counter = result.counter;
Ok(reply::json(&ApiResponse {
success: true,
message: format!("Welcome back, {}!", username),
username: Some(username),
}))
}
#[tokio::main]
async fn main() {
let state = AppState {
passki: Arc::new(Passki::new(
"localhost",
"http://localhost:3000",
"Passkeys Demo",
)),
store: Store::default(),
};
let index = warp::path::end()
.and(warp::get())
.and_then(index);
let register_start = warp::path!("register" / "start")
.and(warp::post())
.and(with_state(state.clone()))
.and(warp::body::json())
.and_then(register_start);
let register_finish = warp::path!("register" / "finish")
.and(warp::post())
.and(with_state(state.clone()))
.and(warp::body::json())
.and_then(register_finish);
let auth_start = warp::path!("auth" / "start")
.and(warp::post())
.and(with_state(state.clone()))
.and(warp::body::json())
.and_then(auth_start);
let auth_finish = warp::path!("auth" / "finish")
.and(warp::post())
.and(with_state(state.clone()))
.and(warp::body::json())
.and_then(auth_finish);
let routes = index
.or(register_start)
.or(register_finish)
.or(auth_start)
.or(auth_finish)
.recover(handle_rejection);
println!("Server starting on http://localhost:3000");
warp::serve(routes).run(([0, 0, 0, 0], 3000)).await;
}