rustauth-plugins 0.3.0

Official RustAuth plugin modules.
Documentation
use std::sync::Arc;

use http::{Method, StatusCode};
use rustauth_core::api::output::user_output_value;
use rustauth_core::api::{create_auth_endpoint, parse_request_body};

use super::{flow_error_response, json_response};
use crate::two_factor::backup_codes::{
    consume_backup_code, decode_backup_codes, encode_backup_codes, generate_backup_codes,
};
use crate::two_factor::errors::{error_message, error_response};
use crate::two_factor::flow::{current_session, validate_password, verify_context};
use crate::two_factor::options::TwoFactorOptions;
use crate::two_factor::payloads::{
    body_options, code_schema, password_schema, view_backup_codes_schema, BackupCodesBody,
    CodeBody, PasswordBody, TokenUserBody, ViewBackupCodesBody,
};
use crate::two_factor::store::{user_two_factor_enabled, TwoFactorStore};

pub(super) fn generate_backup_codes_endpoint(
    options: Arc<TwoFactorOptions>,
) -> rustauth_core::api::AsyncAuthEndpoint {
    create_auth_endpoint(
        "/two-factor/generate-backup-codes",
        Method::POST,
        body_options("generateBackupCodes", password_schema()),
        move |context, request| {
            let options = std::sync::Arc::clone(&options);
            async move {
                let body: PasswordBody = parse_request_body(&request)?;
                let (_session, user, cookies) = match current_session(&context, &request).await {
                    Ok(session) => session,
                    Err(error) => return flow_error_response(error),
                };
                let adapter = context.adapter_ref()?;
                if !user_two_factor_enabled(adapter, &user.id).await? {
                    return error_response(
                        StatusCode::BAD_REQUEST,
                        "TWO_FACTOR_NOT_ENABLED",
                        error_message("TWO_FACTOR_NOT_ENABLED"),
                    );
                }
                if let Err(error) = validate_password(
                    &context,
                    &user.id,
                    body.password.as_deref(),
                    options.allow_passwordless,
                )
                .await
                {
                    return flow_error_response(error);
                }
                let Some(record) = TwoFactorStore::new(adapter).find_by_user(&user.id).await?
                else {
                    return error_response(
                        StatusCode::BAD_REQUEST,
                        "TWO_FACTOR_NOT_ENABLED",
                        error_message("TWO_FACTOR_NOT_ENABLED"),
                    );
                };
                let codes = generate_backup_codes(&options.backup_codes);
                let encoded = encode_backup_codes(&codes, &context.secret, &options.backup_codes)?;
                TwoFactorStore::new(adapter)
                    .update_backup_codes_if_current(&record.id, &record.backup_codes, encoded)
                    .await?;
                json_response(
                    StatusCode::OK,
                    &BackupCodesBody {
                        status: true,
                        backup_codes: codes,
                    },
                    cookies,
                )
            }
        },
    )
}

pub(super) fn verify_backup_code_endpoint(
    options: Arc<TwoFactorOptions>,
) -> rustauth_core::api::AsyncAuthEndpoint {
    create_auth_endpoint(
        "/two-factor/verify-backup-code",
        Method::POST,
        body_options("verifyBackupCode", code_schema()),
        move |context, request| {
            let options = std::sync::Arc::clone(&options);
            async move {
                let body: CodeBody = parse_request_body(&request)?;
                let flow = match verify_context(&context, &request, &options).await {
                    Ok(flow) => flow,
                    Err(error) => return flow_error_response(error),
                };
                let adapter = context.adapter_ref()?;
                let Some(record) = TwoFactorStore::new(adapter)
                    .find_by_user(&flow.user.id)
                    .await?
                else {
                    return error_response(
                        StatusCode::BAD_REQUEST,
                        "BACKUP_CODES_NOT_ENABLED",
                        error_message("BACKUP_CODES_NOT_ENABLED"),
                    );
                };
                let codes = decode_backup_codes(
                    &record.backup_codes,
                    &context.secret,
                    &options.backup_codes,
                )?;
                let Some(updated) = consume_backup_code(&codes, &body.code) else {
                    return error_response(
                        StatusCode::UNAUTHORIZED,
                        "INVALID_BACKUP_CODE",
                        error_message("INVALID_BACKUP_CODE"),
                    );
                };
                let encoded =
                    encode_backup_codes(&updated, &context.secret, &options.backup_codes)?;
                if !TwoFactorStore::new(adapter)
                    .update_backup_codes_if_current(&record.id, &record.backup_codes, encoded)
                    .await?
                {
                    return error_response(
                        StatusCode::CONFLICT,
                        "INVALID_BACKUP_CODE",
                        "Failed to verify backup code. Please try again.",
                    );
                }
                if body.disable_session.unwrap_or(false) {
                    let user =
                        user_output_value(context.adapter_ref()?, &context, &flow.user).await?;
                    return json_response(
                        StatusCode::OK,
                        &TokenUserBody {
                            token: flow
                                .session
                                .as_ref()
                                .map(|session| session.token.clone())
                                .unwrap_or_default(),
                            user,
                        },
                        Vec::new(),
                    );
                }
                match flow.valid(&context, &options).await {
                    Ok(response) => Ok(response),
                    Err(error) => flow_error_response(error),
                }
            }
        },
    )
}

pub(super) fn view_backup_codes_endpoint(
    options: Arc<TwoFactorOptions>,
) -> rustauth_core::api::AsyncAuthEndpoint {
    create_auth_endpoint(
        "/two-factor/view-backup-codes",
        Method::POST,
        body_options("viewBackupCodes", view_backup_codes_schema()),
        move |context, request| {
            let options = std::sync::Arc::clone(&options);
            async move {
                let body: ViewBackupCodesBody = parse_request_body(&request)?;
                let adapter = context.adapter_ref()?;
                let Some(record) = TwoFactorStore::new(adapter)
                    .find_by_user(&body.user_id)
                    .await?
                else {
                    return error_response(
                        StatusCode::BAD_REQUEST,
                        "BACKUP_CODES_NOT_ENABLED",
                        error_message("BACKUP_CODES_NOT_ENABLED"),
                    );
                };
                let codes = decode_backup_codes(
                    &record.backup_codes,
                    &context.secret,
                    &options.backup_codes,
                )?;
                json_response(
                    StatusCode::OK,
                    &BackupCodesBody {
                        status: true,
                        backup_codes: codes,
                    },
                    Vec::new(),
                )
            }
        },
    )
}