openauth-passkey 0.0.5

Server-side passkey plugin for OpenAuth.
Documentation
use openauth_core::error::OpenAuthError;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use url::Url;
use uuid::Uuid;
use webauthn_rs::prelude::{
    CreationChallengeResponse, Credential, DiscoverableAuthentication, DiscoverableKey,
    PasskeyAuthentication, PasskeyRegistration, PublicKeyCredential, RegisterPublicKeyCredential,
    RequestChallengeResponse, Webauthn, WebauthnBuilder,
};

use crate::options::{PasskeyRegistrationUser, RegistrationWebAuthnOptions};

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WebAuthnConfig {
    pub rp_id: String,
    pub rp_name: String,
    pub origins: Vec<String>,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PasskeyRegistrationStart {
    pub options: Value,
    pub state: Value,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PasskeyAuthenticationStart {
    pub options: Value,
    pub state: Value,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct VerifiedPasskeyCredential {
    pub credential_id: String,
    pub public_key: String,
    pub counter: u32,
    pub device_type: String,
    pub backed_up: bool,
    pub transports: Option<String>,
    pub aaguid: Option<String>,
    pub credential: Value,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct VerifiedAuthentication {
    pub credential: Option<Value>,
    pub new_counter: u32,
}

pub trait PasskeyWebAuthnBackend: Send + Sync {
    fn start_registration(
        &self,
        config: WebAuthnConfig,
        user: &PasskeyRegistrationUser,
        exclude_credentials: Vec<Value>,
        options: RegistrationWebAuthnOptions,
    ) -> Result<PasskeyRegistrationStart, OpenAuthError>;

    fn finish_registration(
        &self,
        config: WebAuthnConfig,
        response: Value,
        state: Value,
    ) -> Result<VerifiedPasskeyCredential, OpenAuthError> {
        let _ = (config, response, state);
        Err(OpenAuthError::Api(
            "passkey registration verification is not implemented".to_owned(),
        ))
    }

    fn start_authentication(
        &self,
        config: WebAuthnConfig,
        credentials: Vec<Value>,
        extensions: Option<Value>,
    ) -> Result<PasskeyAuthenticationStart, OpenAuthError>;

    fn finish_authentication(
        &self,
        config: WebAuthnConfig,
        response: Value,
        state: Value,
        credential: Option<Value>,
    ) -> Result<VerifiedAuthentication, OpenAuthError> {
        let _ = (config, response, state, credential);
        Err(OpenAuthError::Api(
            "passkey authentication verification is not implemented".to_owned(),
        ))
    }
}

#[derive(Debug, Clone, Copy)]
pub struct RealPasskeyWebAuthnBackend;

impl PasskeyWebAuthnBackend for RealPasskeyWebAuthnBackend {
    fn start_registration(
        &self,
        config: WebAuthnConfig,
        user: &PasskeyRegistrationUser,
        exclude_credentials: Vec<Value>,
        request_options: RegistrationWebAuthnOptions,
    ) -> Result<PasskeyRegistrationStart, OpenAuthError> {
        let webauthn = webauthn(&config)?;
        let exclude = exclude_credentials
            .into_iter()
            .map(|value| {
                serde_json::from_value::<Credential>(value).map(|credential| credential.cred_id)
            })
            .collect::<Result<Vec<_>, _>>()
            .map_err(|error| OpenAuthError::Api(error.to_string()))?;
        let user_id = stable_user_uuid(&user.id);
        let display_name = user.display_name.as_deref().unwrap_or(&user.name);
        let (options, state) = webauthn
            .start_passkey_registration(user_id, &user.name, display_name, Some(exclude))
            .map_err(|error| OpenAuthError::Api(error.to_string()))?;
        let mut options = option_value(options)?;
        apply_registration_request_options(&mut options, &request_options);
        Ok(PasskeyRegistrationStart {
            options,
            state: serde_json::to_value(state).map_err(json_error)?,
        })
    }

    fn finish_registration(
        &self,
        config: WebAuthnConfig,
        response: Value,
        state: Value,
    ) -> Result<VerifiedPasskeyCredential, OpenAuthError> {
        let webauthn = webauthn(&config)?;
        let response = serde_json::from_value::<RegisterPublicKeyCredential>(response)
            .map_err(|error| OpenAuthError::Api(error.to_string()))?;
        let state = serde_json::from_value::<PasskeyRegistration>(state).map_err(json_error)?;
        let passkey = webauthn
            .finish_passkey_registration(&response, &state)
            .map_err(|error| OpenAuthError::Api(error.to_string()))?;
        credential_output(passkey)
    }

    fn start_authentication(
        &self,
        config: WebAuthnConfig,
        credentials: Vec<Value>,
        extensions: Option<Value>,
    ) -> Result<PasskeyAuthenticationStart, OpenAuthError> {
        let webauthn = webauthn(&config)?;
        if credentials.is_empty() {
            let (options, state) = webauthn
                .start_discoverable_authentication()
                .map_err(|error| OpenAuthError::Api(error.to_string()))?;
            let mut options = auth_option_value(options)?;
            apply_authentication_request_options(&mut options, extensions);
            return Ok(PasskeyAuthenticationStart {
                options,
                state: serde_json::to_value(StoredAuthenticationState::Discoverable(state))
                    .map_err(json_error)?,
            });
        }
        let passkeys = credentials
            .into_iter()
            .map(credential_value_to_passkey)
            .collect::<Result<Vec<_>, _>>()?;
        let (options, state) = webauthn
            .start_passkey_authentication(&passkeys)
            .map_err(|error| OpenAuthError::Api(error.to_string()))?;
        let mut options = auth_option_value(options)?;
        apply_authentication_request_options(&mut options, extensions);
        Ok(PasskeyAuthenticationStart {
            options,
            state: serde_json::to_value(StoredAuthenticationState::Passkey(state))
                .map_err(json_error)?,
        })
    }

    fn finish_authentication(
        &self,
        config: WebAuthnConfig,
        response: Value,
        state: Value,
        credential: Option<Value>,
    ) -> Result<VerifiedAuthentication, OpenAuthError> {
        let webauthn = webauthn(&config)?;
        let response = serde_json::from_value::<PublicKeyCredential>(response)
            .map_err(|error| OpenAuthError::Api(error.to_string()))?;
        let state =
            serde_json::from_value::<StoredAuthenticationState>(state).map_err(json_error)?;
        let credential = credential.map(credential_value_to_passkey).transpose()?;
        let result = match state {
            StoredAuthenticationState::Passkey(state) => webauthn
                .finish_passkey_authentication(&response, &state)
                .map_err(|error| OpenAuthError::Api(error.to_string()))?,
            StoredAuthenticationState::Discoverable(state) => {
                let Some(credential) = credential.as_ref() else {
                    return Err(OpenAuthError::Api(
                        "passkey credential is required".to_owned(),
                    ));
                };
                let discoverable = DiscoverableKey::from(credential);
                webauthn
                    .finish_discoverable_authentication(&response, state, &[discoverable])
                    .map_err(|error| OpenAuthError::Api(error.to_string()))?
            }
        };
        let updated_credential = credential.and_then(|mut passkey| {
            passkey
                .update_credential(&result)
                .and_then(|changed| changed.then_some(passkey))
        });
        Ok(VerifiedAuthentication {
            credential: updated_credential
                .map(|passkey| serde_json::to_value(passkey).map_err(json_error))
                .transpose()?,
            new_counter: result.counter(),
        })
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
enum StoredAuthenticationState {
    Passkey(PasskeyAuthentication),
    Discoverable(DiscoverableAuthentication),
}

fn webauthn(config: &WebAuthnConfig) -> Result<Webauthn, OpenAuthError> {
    let primary_origin = config
        .origins
        .first()
        .ok_or_else(|| OpenAuthError::InvalidConfig("passkey origin is required".to_owned()))?;
    let primary =
        Url::parse(primary_origin).map_err(|error| OpenAuthError::Api(error.to_string()))?;
    let mut builder = WebauthnBuilder::new(&config.rp_id, &primary)
        .map_err(|error| OpenAuthError::Api(error.to_string()))?
        .rp_name(&config.rp_name)
        .allow_any_port(true);
    for origin in config.origins.iter().skip(1) {
        let origin = Url::parse(origin).map_err(|error| OpenAuthError::Api(error.to_string()))?;
        builder = builder.append_allowed_origin(&origin);
    }
    builder
        .build()
        .map_err(|error| OpenAuthError::Api(error.to_string()))
}

fn option_value(options: CreationChallengeResponse) -> Result<Value, OpenAuthError> {
    serde_json::to_value(options)
        .map(|mut value| value.pointer_mut("/publicKey").cloned().unwrap_or(value))
        .map_err(json_error)
}

fn auth_option_value(options: RequestChallengeResponse) -> Result<Value, OpenAuthError> {
    serde_json::to_value(options)
        .map(|mut value| value.pointer_mut("/publicKey").cloned().unwrap_or(value))
        .map_err(json_error)
}

fn apply_registration_request_options(
    options: &mut Value,
    request_options: &RegistrationWebAuthnOptions,
) {
    options["authenticatorSelection"] = request_options.authenticator_selection.to_json();
    if let Some(extensions) = &request_options.extensions {
        options["extensions"] = extensions.clone();
    }
}

fn apply_authentication_request_options(options: &mut Value, extensions: Option<Value>) {
    options["userVerification"] = Value::String("preferred".to_owned());
    if let Some(extensions) = extensions {
        options["extensions"] = extensions;
    }
}

fn credential_value_to_passkey(
    value: Value,
) -> Result<webauthn_rs::prelude::Passkey, OpenAuthError> {
    serde_json::from_value::<webauthn_rs::prelude::Passkey>(value).map_err(json_error)
}

fn credential_output(
    passkey: webauthn_rs::prelude::Passkey,
) -> Result<VerifiedPasskeyCredential, OpenAuthError> {
    let credential = Credential::from(passkey.clone());
    let credential_id = serde_json::to_value(&credential.cred_id)
        .and_then(serde_json::from_value::<String>)
        .unwrap_or_else(|_| format!("{:?}", credential.cred_id));
    let public_key = serde_json::to_string(&credential.cred).map_err(json_error)?;
    let transports = credential.transports.as_ref().map(|values| {
        values
            .iter()
            .map(|value| format!("{value:?}"))
            .collect::<Vec<_>>()
            .join(",")
    });
    Ok(VerifiedPasskeyCredential {
        credential_id,
        public_key,
        counter: credential.counter,
        device_type: if credential.backup_eligible {
            "multiDevice".to_owned()
        } else {
            "singleDevice".to_owned()
        },
        backed_up: credential.backup_state,
        transports,
        aaguid: None,
        credential: serde_json::to_value(passkey).map_err(json_error)?,
    })
}

fn stable_user_uuid(user_id: &str) -> Uuid {
    Uuid::new_v5(&Uuid::NAMESPACE_URL, user_id.as_bytes())
}

fn json_error(error: serde_json::Error) -> OpenAuthError {
    OpenAuthError::Api(error.to_string())
}