use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use async_trait::async_trait;
use axum::Json;
use axum::Router;
use axum::extract::State;
use axum::routing::post;
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
use serde::{Deserialize, Serialize};
use cheers::passkey::{
CreationChallengeResponse, Passkey, PasskeyAuthentication, PasskeyRegistration,
PasskeyRelyingParty, PasskeyUpdate, PublicKeyCredential, RegisterPublicKeyCredential,
RequestChallengeResponse, Uuid, apply_authentication_result, passkey_from_credential,
passkey_to_credential,
};
use cheers_core::{DeviceBinding, DeviceId, TokenMinter, UserId};
use cheers_server::{
PasskeyCredentialStore, RefreshStore, RevocationWriter, SessionAuthority, UserStore,
};
use crate::error::RouteError;
use crate::me::SessionRecorder;
use crate::session::SessionBody;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct StashedRegistration {
pub user_id: UserId,
pub device_id: DeviceId,
pub state: PasskeyRegistration,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct StashedAuthentication {
pub user_id: UserId,
pub state: PasskeyAuthentication,
}
#[async_trait]
pub trait PasskeyFlowStore: Send + Sync {
async fn put_registration(
&self,
flow_id: &str,
value: StashedRegistration,
) -> Result<(), String>;
async fn take_registration(
&self,
flow_id: &str,
) -> Result<Option<StashedRegistration>, String>;
async fn put_authentication(
&self,
flow_id: &str,
value: StashedAuthentication,
) -> Result<(), String>;
async fn take_authentication(
&self,
flow_id: &str,
) -> Result<Option<StashedAuthentication>, String>;
}
#[derive(Default, Debug)]
pub struct MemoryPasskeyFlowStore {
reg: Mutex<HashMap<String, StashedRegistration>>,
auth: Mutex<HashMap<String, StashedAuthentication>>,
}
impl MemoryPasskeyFlowStore {
pub fn new() -> Self {
Self::default()
}
}
#[async_trait]
impl PasskeyFlowStore for MemoryPasskeyFlowStore {
async fn put_registration(
&self,
flow_id: &str,
value: StashedRegistration,
) -> Result<(), String> {
self.reg.lock().unwrap().insert(flow_id.to_owned(), value);
Ok(())
}
async fn take_registration(
&self,
flow_id: &str,
) -> Result<Option<StashedRegistration>, String> {
Ok(self.reg.lock().unwrap().remove(flow_id))
}
async fn put_authentication(
&self,
flow_id: &str,
value: StashedAuthentication,
) -> Result<(), String> {
self.auth.lock().unwrap().insert(flow_id.to_owned(), value);
Ok(())
}
async fn take_authentication(
&self,
flow_id: &str,
) -> Result<Option<StashedAuthentication>, String> {
Ok(self.auth.lock().unwrap().remove(flow_id))
}
}
#[async_trait]
pub trait PasskeyRegistrationObserver: Send + Sync {
async fn record_registered(&self, user_id: &UserId, cred_id_len: usize, credential_count: usize);
}
#[derive(Debug, Clone, Copy, Default)]
pub struct NoPasskeyRegistrationObserver;
#[async_trait]
impl PasskeyRegistrationObserver for NoPasskeyRegistrationObserver {
async fn record_registered(&self, _user_id: &UserId, _cred_id_len: usize, _credential_count: usize) {}
}
pub struct PasskeyAuthState<M, R, U, W, P, F> {
pub relying_party: Arc<PasskeyRelyingParty>,
pub authority: Arc<SessionAuthority<M, R, U, W>>,
pub credentials: Arc<P>,
pub flows: Arc<F>,
pub recorder: Arc<dyn SessionRecorder>,
pub registration_observer: Arc<dyn PasskeyRegistrationObserver>,
}
impl<M, R, U, W, P, F> std::fmt::Debug for PasskeyAuthState<M, R, U, W, P, F> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PasskeyAuthState")
.field("rp_id", &self.relying_party.rp_id())
.finish_non_exhaustive()
}
}
pub fn router<M, R, U, W, P, F>(state: Arc<PasskeyAuthState<M, R, U, W, P, F>>) -> Router
where
M: TokenMinter + Send + Sync + 'static,
R: RefreshStore + Send + Sync + 'static,
U: UserStore + Send + Sync + 'static,
W: RevocationWriter + Send + Sync + 'static,
P: PasskeyCredentialStore + Send + Sync + 'static,
F: PasskeyFlowStore + Send + Sync + 'static,
{
Router::new()
.route(
"/passkey/register/start",
post(register_start::<M, R, U, W, P, F>),
)
.route(
"/passkey/register/finish",
post(register_finish::<M, R, U, W, P, F>),
)
.route(
"/passkey/authenticate/start",
post(authenticate_start::<M, R, U, W, P, F>),
)
.route(
"/passkey/authenticate/finish",
post(authenticate_finish::<M, R, U, W, P, F>),
)
.with_state(state)
}
#[derive(Debug, Clone, Deserialize)]
pub struct RegisterStartRequest {
pub user_id: String,
pub device_id: String,
pub user_name: String,
pub user_display_name: String,
}
#[derive(Debug, Clone, Serialize)]
#[non_exhaustive]
pub struct RegisterStartResponse {
pub flow_id: String,
pub challenge: CreationChallengeResponse,
}
pub async fn register_start<M, R, U, W, P, F>(
State(state): State<Arc<PasskeyAuthState<M, R, U, W, P, F>>>,
Json(req): Json<RegisterStartRequest>,
) -> Result<Json<RegisterStartResponse>, RouteError>
where
M: TokenMinter + Send + Sync + 'static,
R: RefreshStore + Send + Sync + 'static,
U: UserStore + Send + Sync + 'static,
W: RevocationWriter + Send + Sync + 'static,
P: PasskeyCredentialStore + Send + Sync + 'static,
F: PasskeyFlowStore + Send + Sync + 'static,
{
let user_id = UserId::new(req.user_id);
let device_id = DeviceId::new(req.device_id);
let existing = state.credentials.list_for_user(&user_id).await?;
let exclude: Vec<Passkey> = existing
.iter()
.map(passkey_from_credential)
.collect::<Result<_, _>>()?;
let user_handle = uuid_from_user_id(&user_id);
let (challenge, registration) = state.relying_party.start_registration(
user_handle,
&req.user_name,
&req.user_display_name,
&exclude,
)?;
let flow_id = random_flow_id();
let stashed = StashedRegistration {
user_id,
device_id,
state: registration,
};
state
.flows
.put_registration(&flow_id, stashed)
.await
.map_err(RouteError::Store)?;
Ok(Json(RegisterStartResponse { flow_id, challenge }))
}
#[derive(Debug, Clone, Deserialize)]
pub struct RegisterFinishRequest {
pub flow_id: String,
pub credential: RegisterPublicKeyCredential,
}
pub async fn register_finish<M, R, U, W, P, F>(
State(state): State<Arc<PasskeyAuthState<M, R, U, W, P, F>>>,
Json(req): Json<RegisterFinishRequest>,
) -> Result<Json<SessionBody>, RouteError>
where
M: TokenMinter + Send + Sync + 'static,
R: RefreshStore + Send + Sync + 'static,
U: UserStore + Send + Sync + 'static,
W: RevocationWriter + Send + Sync + 'static,
P: PasskeyCredentialStore + Send + Sync + 'static,
F: PasskeyFlowStore + Send + Sync + 'static,
{
let stashed = state
.flows
.take_registration(&req.flow_id)
.await
.map_err(RouteError::Store)?
.ok_or(RouteError::UnknownFlow)?;
let passkey = state
.relying_party
.finish_registration(&req.credential, &stashed.state)?;
let credential =
passkey_to_credential(stashed.user_id.clone(), stashed.device_id.clone(), &passkey)?;
state.credentials.put(&credential).await?;
let credential_count = state.credentials.list_for_user(&stashed.user_id).await?.len();
state
.registration_observer
.record_registered(&stashed.user_id, passkey.cred_id().len(), credential_count)
.await;
let now = now_unix();
let session = state
.authority
.establish(
stashed.user_id,
stashed.device_id,
DeviceBinding::Passkey,
now,
)
.await?;
state.recorder.record_new_session(&session).await?;
Ok(Json(SessionBody::from_new_session(session)))
}
#[derive(Debug, Clone, Deserialize)]
pub struct AuthenticateStartRequest {
pub user_id: String,
}
#[derive(Debug, Clone, Serialize)]
#[non_exhaustive]
pub struct AuthenticateStartResponse {
pub flow_id: String,
pub challenge: RequestChallengeResponse,
}
pub async fn authenticate_start<M, R, U, W, P, F>(
State(state): State<Arc<PasskeyAuthState<M, R, U, W, P, F>>>,
Json(req): Json<AuthenticateStartRequest>,
) -> Result<Json<AuthenticateStartResponse>, RouteError>
where
M: TokenMinter + Send + Sync + 'static,
R: RefreshStore + Send + Sync + 'static,
U: UserStore + Send + Sync + 'static,
W: RevocationWriter + Send + Sync + 'static,
P: PasskeyCredentialStore + Send + Sync + 'static,
F: PasskeyFlowStore + Send + Sync + 'static,
{
let user_id = UserId::new(req.user_id);
let credentials = state.credentials.list_for_user(&user_id).await?;
if credentials.is_empty() {
return Err(RouteError::UnknownCredential);
}
let passkeys: Vec<Passkey> = credentials
.iter()
.map(passkey_from_credential)
.collect::<Result<_, _>>()?;
let (challenge, authentication) = state.relying_party.start_authentication(&passkeys)?;
let flow_id = random_flow_id();
let stashed = StashedAuthentication {
user_id,
state: authentication,
};
state
.flows
.put_authentication(&flow_id, stashed)
.await
.map_err(RouteError::Store)?;
Ok(Json(AuthenticateStartResponse { flow_id, challenge }))
}
#[derive(Debug, Clone, Deserialize)]
pub struct AuthenticateFinishRequest {
pub flow_id: String,
pub credential: PublicKeyCredential,
}
pub async fn authenticate_finish<M, R, U, W, P, F>(
State(state): State<Arc<PasskeyAuthState<M, R, U, W, P, F>>>,
Json(req): Json<AuthenticateFinishRequest>,
) -> Result<Json<SessionBody>, RouteError>
where
M: TokenMinter + Send + Sync + 'static,
R: RefreshStore + Send + Sync + 'static,
U: UserStore + Send + Sync + 'static,
W: RevocationWriter + Send + Sync + 'static,
P: PasskeyCredentialStore + Send + Sync + 'static,
F: PasskeyFlowStore + Send + Sync + 'static,
{
let stashed = state
.flows
.take_authentication(&req.flow_id)
.await
.map_err(RouteError::Store)?
.ok_or(RouteError::UnknownFlow)?;
let result = state
.relying_party
.finish_authentication(&req.credential, &stashed.state)?;
let credentials = state.credentials.list_for_user(&stashed.user_id).await?;
let paired: Vec<(DeviceId, Passkey)> = credentials
.iter()
.map(|c| Ok::<_, RouteError>((c.device_id.clone(), passkey_from_credential(c)?)))
.collect::<Result<_, _>>()?;
let device_id = paired
.iter()
.find(|(_, p)| p.cred_id() == result.cred_id())
.map(|(d, _)| d.clone())
.ok_or(RouteError::UnknownCredential)?;
let mut passkeys: Vec<Passkey> = paired.iter().map(|(_, p)| p.clone()).collect();
match apply_authentication_result(&mut passkeys, &result) {
PasskeyUpdate::Updated(updated) => {
let cred =
passkey_to_credential(stashed.user_id.clone(), device_id.clone(), updated)?;
state.credentials.update(&cred).await?;
}
PasskeyUpdate::Unchanged(_) => {}
PasskeyUpdate::UnknownCredential => return Err(RouteError::UnknownCredential),
other => return Err(RouteError::Ceremony(format!("{other:?}"))),
}
let now = now_unix();
let session = state
.authority
.establish(stashed.user_id, device_id, DeviceBinding::Passkey, now)
.await?;
state.recorder.record_new_session(&session).await?;
Ok(Json(SessionBody::from_new_session(session)))
}
fn uuid_from_user_id(user_id: &UserId) -> Uuid {
let derived = uuid::Uuid::new_v5(&uuid::Uuid::NAMESPACE_OID, user_id.as_str().as_bytes());
Uuid::from_bytes(*derived.as_bytes())
}
fn random_flow_id() -> String {
let mut bytes = [0u8; 16];
getrandom::fill(&mut bytes).expect("OS CSPRNG must be available");
URL_SAFE_NO_PAD.encode(bytes)
}
fn now_unix() -> i64 {
use std::time::{SystemTime, UNIX_EPOCH};
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn uuid_from_user_id_is_stable_per_user() {
let a = uuid_from_user_id(&UserId::new("u-1"));
let b = uuid_from_user_id(&UserId::new("u-1"));
assert_eq!(a, b);
let c = uuid_from_user_id(&UserId::new("u-2"));
assert_ne!(a, c);
}
#[test]
fn random_flow_id_is_unique() {
let a = random_flow_id();
let b = random_flow_id();
assert_ne!(a, b);
assert_eq!(a.len(), 22);
}
}