Skip to main content

openauth_plugins/captcha/
mod.rs

1//! CAPTCHA plugin.
2
3mod error;
4mod ip;
5mod options;
6mod response;
7
8pub mod verify_handlers;
9
10pub use error::{CaptchaConfigError, CaptchaErrorCode};
11pub use options::{CaptchaOptions, CaptchaProvider, DEFAULT_ENDPOINTS};
12
13use std::sync::Arc;
14
15use openauth_core::plugin::{AuthPlugin, PluginErrorCode};
16use response::error_response;
17use verify_handlers::{verify_captcha, VerifyCaptchaInput};
18
19pub const UPSTREAM_PLUGIN_ID: &str = "captcha";
20
21/// Create the CAPTCHA plugin.
22pub fn captcha(options: CaptchaOptions) -> Result<AuthPlugin, CaptchaConfigError> {
23    options.validate()?;
24
25    let options = Arc::new(options.with_defaults());
26    let serialized_options = serde_json::to_value(options.as_ref())
27        .map_err(|error| CaptchaConfigError::SerializeOptions(error.to_string()))?;
28
29    let mut plugin = AuthPlugin::new(UPSTREAM_PLUGIN_ID)
30        .with_version(env!("CARGO_PKG_VERSION"))
31        .with_options(serialized_options)
32        .with_error_code(PluginErrorCode::new(
33            CaptchaErrorCode::VerificationFailed.as_str(),
34            CaptchaErrorCode::VerificationFailed.message(),
35        ))
36        .with_error_code(PluginErrorCode::new(
37            CaptchaErrorCode::MissingResponse.as_str(),
38            CaptchaErrorCode::MissingResponse.message(),
39        ))
40        .with_error_code(PluginErrorCode::new(
41            CaptchaErrorCode::UnknownError.as_str(),
42            CaptchaErrorCode::UnknownError.message(),
43        ));
44
45    for endpoint in options.endpoints.clone() {
46        let options = Arc::clone(&options);
47        plugin = plugin.with_async_middleware(endpoint, move |context, request| {
48            let options = Arc::clone(&options);
49            Box::pin(async move {
50                let Some(captcha_response) = request
51                    .headers()
52                    .get("x-captcha-response")
53                    .and_then(|value| value.to_str().ok())
54                    .map(str::trim)
55                    .filter(|value| !value.is_empty())
56                    .map(str::to_owned)
57                else {
58                    return error_response(CaptchaErrorCode::MissingResponse).map(Some);
59                };
60
61                let input = VerifyCaptchaInput {
62                    options: options.as_ref(),
63                    captcha_response: &captcha_response,
64                    remote_ip: ip::request_ip(context, request),
65                };
66
67                match verify_captcha(input).await {
68                    Ok(true) => Ok(None),
69                    Ok(false) => error_response(CaptchaErrorCode::VerificationFailed).map(Some),
70                    Err(_) => error_response(CaptchaErrorCode::UnknownError).map(Some),
71                }
72            })
73        });
74    }
75
76    Ok(plugin)
77}