rustauth-plugins 0.3.0

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

use http::{Method, StatusCode};
use rustauth_core::api::{create_auth_endpoint, parse_request_body};
use rustauth_core::outbound::dispatch_outbound;
use rustauth_core::verification::{CreateVerificationInput, UpdateVerificationInput};
use time::OffsetDateTime;

use super::errors::{error_message, error_response};
use super::flow::verify_context;
use super::options::{TwoFactorOptions, TwoFactorOtpMessage};
use super::otp::{generate_otp, store_otp, verify_stored_otp};
use super::payloads::{body_options, code_schema, optional_trust_schema, CodeBody, StatusBody};
use super::routes::{flow_error_response, json_response};
use super::store::{update_user_two_factor_enabled, user_two_factor_enabled};

pub(super) fn send_otp_endpoint(
    options: Arc<TwoFactorOptions>,
) -> rustauth_core::api::AsyncAuthEndpoint {
    create_auth_endpoint(
        "/two-factor/send-otp",
        Method::POST,
        body_options("sendTwoFactorOtp", optional_trust_schema()),
        move |context, request| {
            let options = std::sync::Arc::clone(&options);
            async move {
                let Some(send_otp) = &options.otp.send_otp else {
                    return error_response(
                        StatusCode::BAD_REQUEST,
                        "OTP_NOT_CONFIGURED",
                        error_message("OTP_NOT_CONFIGURED"),
                    );
                };
                let flow = match verify_context(&context, &request, &options).await {
                    Ok(flow) => flow,
                    Err(error) => return flow_error_response(error),
                };
                let code = generate_otp(options.otp.digits);
                let stored = store_otp(&code, &context.secret, &options.otp).await?;
                context
                    .verifications()?
                    .create_verification(CreateVerificationInput::new(
                        format!("2fa-otp-{}", flow.key),
                        format!("{stored}:0"),
                        OffsetDateTime::now_utc() + options.otp.period,
                    ))
                    .await?;
                dispatch_outbound(
                    &context,
                    send_otp(TwoFactorOtpMessage {
                        user: flow.user,
                        otp: code,
                        request,
                    }),
                );
                json_response(StatusCode::OK, &StatusBody { status: true }, Vec::new())
            }
        },
    )
}

pub(super) fn verify_otp_endpoint(
    options: Arc<TwoFactorOptions>,
) -> rustauth_core::api::AsyncAuthEndpoint {
    create_auth_endpoint(
        "/two-factor/verify-otp",
        Method::POST,
        body_options("verifyTwoFactorCode", code_schema()),
        move |context, request| {
            let options = std::sync::Arc::clone(&options);
            async move {
                if options.otp.send_otp.is_none() {
                    return error_response(
                        StatusCode::BAD_REQUEST,
                        "OTP_NOT_CONFIGURED",
                        error_message("OTP_NOT_CONFIGURED"),
                    );
                }
                let body: CodeBody = parse_request_body(&request)?;
                let mut flow = match verify_context(&context, &request, &options).await {
                    Ok(flow) => flow,
                    Err(error) => return flow_error_response(error),
                };
                let identifier = format!("2fa-otp-{}", flow.key);
                let store = context.verifications()?;
                let Some(record) = store.find_verification(&identifier).await? else {
                    return error_response(
                        StatusCode::BAD_REQUEST,
                        "OTP_HAS_EXPIRED",
                        error_message("OTP_HAS_EXPIRED"),
                    );
                };
                let (stored, counter) = record
                    .value
                    .split_once(':')
                    .unwrap_or((record.value.as_str(), "0"));
                let attempts = counter.parse::<u32>().unwrap_or(0);
                if attempts >= options.otp.allowed_attempts {
                    store.delete_verification(&identifier).await?;
                    return error_response(
                        StatusCode::BAD_REQUEST,
                        "TOO_MANY_ATTEMPTS_REQUEST_NEW_CODE",
                        error_message("TOO_MANY_ATTEMPTS_REQUEST_NEW_CODE"),
                    );
                }
                if !verify_stored_otp(stored, &body.code, &context.secret, &options.otp).await? {
                    store
                        .update_verification(
                            &identifier,
                            UpdateVerificationInput::new()
                                .value(format!("{stored}:{}", attempts + 1)),
                        )
                        .await?;
                    return error_response(
                        StatusCode::UNAUTHORIZED,
                        "INVALID_CODE",
                        error_message("INVALID_CODE"),
                    );
                }
                if !user_two_factor_enabled(context.adapter_ref()?, &flow.user.id).await? {
                    update_user_two_factor_enabled(&context, &flow.user.id, true).await?;
                }
                flow.trust_device = body.trust_device.unwrap_or(false);
                match flow.valid(&context, &options).await {
                    Ok(response) => Ok(response),
                    Err(error) => flow_error_response(error),
                }
            }
        },
    )
}