mellon 0.1.0

Library for adding contemporary authentication to rust-based websites.
Documentation
//! Authentication-related data that should be sent via REST

use argon2::{Argon2, PasswordHash, PasswordVerifier};
use jiff::Timestamp;
use otpauth::TOTP;
use serde::{Deserialize, Serialize};
use url::Url;
use webauthn_rs::{
    Webauthn, WebauthnBuilder,
    prelude::{PasskeyAuthentication, PublicKeyCredential, RequestChallengeResponse},
};

/// Indicate the required information for login
///
/// Should be sent from the server to the client.
#[derive(Default, Serialize, Deserialize)]
pub struct LoginChallenge {
    /// Is a password expected?
    pub password: bool,
    /// Is a TOTP code expected?
    pub totp: bool,
    /// WebAuthn challenge value, if it is required
    ///
    /// The value should be a `webauthn_rs::prelude::RequestChallengeResponse` but we use the `serde_json`
    /// type here for better compatibility with various payload serialization traits.
    /// Use the `.webauthn_key()` method to obtain a `RequestChallengeResponse` value.
    pub webauthn: Option<serde_json::Value>,
}

impl LoginChallenge {
    pub fn webauthn_key(&self) -> Option<RequestChallengeResponse> {
        self.webauthn
            .as_ref()
            .and_then(|w| serde_json::from_value(w.clone()).ok())
    }
}

/// The client's response to the login challenge
#[derive(Default)]
pub struct LoginChallengeResponse {
    password: Option<String>,
    totp: Option<u32>,
    /// The value here should be a `webauthn_rs::prelude::PublicKeyCredential`, but we use the `serd_json`
    /// type here for better compatibility with various payload serialization traits.
    /// You can use the [webauthn_pubkey] method to parse the value into a `PublicKeyCredential`.
    webauthn: Option<serde_json::Value>,
}

impl LoginChallengeResponse {
    pub fn verify_password(&self, stored: &PasswordHash) -> bool {
        if let Some(pwd) = &self.password {
            Argon2::default()
                .verify_password(pwd.as_bytes(), stored)
                .is_ok()
        } else {
            false
        }
    }

    pub fn verify_totp(&self, stored: &TOTP) -> bool {
        if let Some(code) = self.totp {
            let now: u64 = Timestamp::now().as_second().try_into().expect("after 1970");
            stored.verify(code, 30, now)
        } else {
            false
        }
    }

    pub fn verify_webauthn(&self, setup: &Webauthn, stored: &PasskeyAuthentication) -> bool {
        if let Some(pubkey_cred) = self.webauthn_pubkey() {
            setup
                .finish_passkey_authentication(&pubkey_cred, stored)
                .is_ok()
        } else {
            false // missing data
        }
    }

    pub fn webauthn_pubkey(&self) -> Option<PublicKeyCredential> {
        self.webauthn
            .as_ref()
            .and_then(|w| serde_json::from_value(w.clone()).ok())
    }
}

pub fn webauthn_setup(
    relaying_party: &str,
    effective_domain_name: &str,
    rp_origin: &Url,
) -> Webauthn {
    let mut builder = WebauthnBuilder::new(effective_domain_name, rp_origin).unwrap();
    builder = builder.rp_name(relaying_party);
    builder.build().unwrap()
}