#[macro_use]
extern crate rocket;
use passki::{
AttestationConveyancePreference, AuthenticationChallenge, AuthenticationCredential,
AuthenticationState, ClientData, Passki, RegistrationChallenge, RegistrationCredential,
RegistrationState, ResidentKeyRequirement, StoredPasskey, UserVerificationRequirement,
};
use rocket::http::Status;
use rocket::response::content::RawHtml;
use rocket::serde::json::Json;
use rocket::{Build, Rocket, State};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::sync::Mutex;
use uuid::Uuid;
struct AppError(String);
impl<'r> rocket::response::Responder<'r, 'static> for AppError {
fn respond_to(self, _req: &'r rocket::Request<'_>) -> rocket::response::Result<'static> {
rocket::Response::build()
.status(Status::BadRequest)
.sized_body(self.0.len(), std::io::Cursor::new(self.0))
.ok()
}
}
impl From<Box<dyn std::error::Error>> for AppError {
fn from(err: Box<dyn std::error::Error>) -> Self {
AppError(err.to_string())
}
}
impl From<uuid::Error> for AppError {
fn from(err: uuid::Error) -> Self {
AppError(err.to_string())
}
}
type AppResult<T> = Result<Json<T>, AppError>;
#[derive(Default)]
struct Store {
users: Mutex<HashMap<String, User>>,
pending_registrations: Mutex<HashMap<String, RegistrationState>>,
pending_authentications: 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>,
}
#[get("/")]
fn index() -> Result<RawHtml<String>, AppError> {
let html = fs::read_to_string("examples/index.html")
.map_err(|e| AppError(e.to_string()))?;
Ok(RawHtml(html))
}
#[post("/register/start", data = "<req>")]
fn register_start(
passki: &State<Passki>,
store: &State<Store>,
req: Json<RegisterStartRequest>,
) -> AppResult<RegistrationChallenge> {
let user_id = Uuid::new_v4().as_bytes().to_vec();
let existing = store.users.lock().unwrap()
.get(&req.username).map(|u| u.passkeys.clone());
let (challenge, reg_state) = passki.start_passkey_registration(
&user_id,
&req.username, &req.username, 60000, AttestationConveyancePreference::None, ResidentKeyRequirement::Preferred, UserVerificationRequirement::Preferred, existing.as_deref(), )?;
store.pending_registrations.lock().unwrap().insert(challenge.challenge.clone(), reg_state);
Ok(Json(challenge))
}
#[post("/register/finish", data = "<req>")]
fn register_finish(
passki: &State<Passki>,
store: &State<Store>,
req: Json<RegisterFinishRequest>,
) -> AppResult<ApiResponse> {
let client_data = ClientData::from_base64(&req.client_data_json)?;
let reg_state = store.pending_registrations.lock().unwrap()
.remove(&client_data.challenge)
.ok_or(AppError("No pending registration".into()))?;
let credential = RegistrationCredential {
credential_id: req.credential_id.clone(),
public_key: req.public_key.clone(),
client_data_json: req.client_data_json.clone(),
};
let passkey = passki.finish_passkey_registration(&credential, ®_state)?;
let user_id_bytes = Passki::base64_decode(®_state.user.id)?;
let user_id = Uuid::from_slice(&user_id_bytes)?;
let mut users = 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(Json(ApiResponse { success: true, message: "Registration successful".into(), username: None }))
}
#[post("/auth/start", data = "<req>")]
fn auth_start(
passki: &State<Passki>,
store: &State<Store>,
req: Json<AuthStartRequest>,
) -> AppResult<AuthenticationChallenge> {
let passkeys = if let Some(ref username) = req.username {
let users = store.users.lock().unwrap();
let user = users.get(username).ok_or(AppError("User not found".into()))?;
user.passkeys.clone()
} else {
vec![]
};
let (challenge, auth_state) = passki.start_passkey_authentication(
&passkeys,
60000, UserVerificationRequirement::Preferred, );
store.pending_authentications.lock().unwrap().insert(challenge.challenge.clone(), auth_state);
Ok(Json(challenge))
}
#[post("/auth/finish", data = "<req>")]
fn auth_finish(
passki: &State<Passki>,
store: &State<Store>,
req: Json<AuthFinishRequest>,
) -> AppResult<ApiResponse> {
let client_data = ClientData::from_base64(&req.client_data_json)?;
let auth_state = store.pending_authentications.lock().unwrap()
.remove(&client_data.challenge)
.ok_or(AppError("No pending authentication".into()))?;
let credential_id = Passki::base64_decode(&req.credential_id)?;
let mut users = 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(AppError("Unknown credential".into()))?;
let credential = AuthenticationCredential {
credential_id: req.credential_id.clone(),
authenticator_data: req.authenticator_data.clone(),
client_data_json: req.client_data_json.clone(),
signature: req.signature.clone(),
};
let result = passki.finish_passkey_authentication(&credential, &auth_state, passkey)?;
passkey.counter = result.counter;
Ok(Json(ApiResponse {
success: true,
message: format!("Welcome back, {}!", username),
username: Some(username),
}))
}
#[launch]
fn rocket() -> Rocket<Build> {
let passki = Passki::new(
"localhost",
"http://localhost:3000",
"Passkeys Demo",
);
let figment = rocket::Config::figment()
.merge(("port", 3000));
rocket::custom(figment)
.manage(passki)
.manage(Store::default())
.mount("/", routes![
index,
register_start,
register_finish,
auth_start,
auth_finish,
])
}