openauth-plugins 0.0.4

Official OpenAuth plugin modules.
Documentation
//! Plugin construction for Have I Been Pwned password checks.

use std::sync::Arc;

use openauth_core::plugin::{AuthPlugin, PluginErrorCode, PluginPasswordValidationRejection};

use super::checker::{
    sha1_prefix_suffix, HaveIBeenPwnedCheckError, HaveIBeenPwnedChecker,
    ReqwestHaveIBeenPwnedChecker,
};
use super::error::{CHECK_FAILED_MESSAGE, PASSWORD_COMPROMISED_CODE, PASSWORD_COMPROMISED_MESSAGE};
use super::options::HaveIBeenPwnedOptions;

pub const UPSTREAM_PLUGIN_ID: &str = "haveibeenpwned";
pub const RUNTIME_PLUGIN_ID: &str = "have-i-been-pwned";

pub fn have_i_been_pwned() -> AuthPlugin {
    have_i_been_pwned_with_options(HaveIBeenPwnedOptions::default())
}

pub fn have_i_been_pwned_with_options(options: HaveIBeenPwnedOptions) -> AuthPlugin {
    have_i_been_pwned_with_checker(options, Arc::new(ReqwestHaveIBeenPwnedChecker::new()))
}

pub fn have_i_been_pwned_with_checker(
    options: HaveIBeenPwnedOptions,
    checker: Arc<dyn HaveIBeenPwnedChecker>,
) -> AuthPlugin {
    let plugin_options = serde_json::json!({
        "enabled": options.enabled,
        "paths": options.paths,
        "customPasswordCompromisedMessage": options.custom_password_compromised_message,
    });
    let validation_options = options.clone();
    let validation_checker = Arc::clone(&checker);

    AuthPlugin::new(RUNTIME_PLUGIN_ID)
        .with_version(crate::VERSION)
        .with_options(plugin_options)
        .with_error_code(PluginErrorCode::new(
            PASSWORD_COMPROMISED_CODE,
            PASSWORD_COMPROMISED_MESSAGE,
        ))
        .with_password_validator(move |_context, input| {
            let options = validation_options.clone();
            let checker = Arc::clone(&validation_checker);
            Box::pin(async move {
                if !options.enabled || !options.paths.iter().any(|path| path == &input.path) {
                    return Ok(());
                }
                if input.password.is_empty() {
                    return Ok(());
                }
                let (prefix, suffix) = sha1_prefix_suffix(&input.password);
                let compromised = checker
                    .is_hash_suffix_compromised(&prefix, &suffix)
                    .await
                    .map_err(check_error_response)?;
                if compromised {
                    return Err(PluginPasswordValidationRejection::bad_request(
                        PASSWORD_COMPROMISED_CODE,
                        options
                            .custom_password_compromised_message
                            .unwrap_or_else(|| PASSWORD_COMPROMISED_MESSAGE.to_owned()),
                    ));
                }
                Ok(())
            })
        })
}

fn check_error_response(error: HaveIBeenPwnedCheckError) -> PluginPasswordValidationRejection {
    match error {
        HaveIBeenPwnedCheckError::HttpStatus(status) => {
            PluginPasswordValidationRejection::internal_server_error(format!(
                "Failed to check password. Status: {status}"
            ))
        }
        HaveIBeenPwnedCheckError::Transport(_) => {
            PluginPasswordValidationRejection::internal_server_error(CHECK_FAILED_MESSAGE)
        }
    }
}