Skip to main content

alibi_plugins/siwe/
endpoint.rs

1//! SIWE nonce operations use the real issuer without constructing an HTTP request.
2use super::{SiwePlugin, SiweVerificationInput, validation};
3use crate::authentication_helpers::json_type;
4use crate::endpoint::{definition, error_response};
5use alibi_core::endpoint::{
6    EndpointCall, EndpointDefinition, EndpointInput, EndpointResponse, ServerEndpoint,
7};
8use alibi_core::utils::json::JsValue;
9use alibi_core::{AuthContext, AuthResult, AuthSchema, HttpMethod};
10use serde::{Deserialize, Serialize};
11
12/// An issued, stored ERC-4361 challenge.
13#[derive(Debug, Serialize, Deserialize)]
14pub struct SiweNonceResponse {
15    pub nonce: String,
16}
17
18/// Wallet identity selected by a successful SIWE proof.
19#[derive(Debug, Serialize, Deserialize)]
20#[serde(rename_all = "camelCase")]
21pub struct SiweVerifiedUser {
22    pub id: String,
23    pub wallet_address: String,
24    pub chain_id: f64,
25}
26/// Result of signature verification and session issuance.
27#[derive(Debug, Serialize, Deserialize)]
28pub struct SiweVerificationResponse {
29    pub success: bool,
30    pub token: String,
31    pub user: SiweVerifiedUser,
32}
33
34pub(super) fn definitions() -> Vec<EndpointDefinition> {
35    vec![
36        definition(
37            "verifySiweMessage",
38            "verifySiweMessage",
39            Some("/siwe/verify"),
40            HttpMethod::Post,
41        ),
42        definition(
43            "getSiweNonce",
44            "getSiweNonce",
45            Some("/siwe/nonce"),
46            HttpMethod::Post,
47        ),
48        definition(
49            "getNonce",
50            "getNonce",
51            Some("/siwe/get-nonce"),
52            HttpMethod::Post,
53        ),
54    ]
55}
56
57pub(super) fn validate(plugin: &SiwePlugin, call: &EndpointCall) -> AuthResult<EndpointInput> {
58    if call.operation_id() == "verifySiweMessage" {
59        _ = validation::verify_value(call.body(), plugin.config.anonymous)
60            .map_err(error_response)?;
61        if call.request().is_none() {
62            return Err(crate::endpoint::validation("Request is required"));
63        }
64        return Ok(EndpointInput {
65            body: call.body().cloned(),
66            query: call.query().cloned(),
67        });
68    }
69    if let Some(value) = call.body() {
70        let object = value.as_object().ok_or_else(|| {
71            crate::endpoint::validation(format!(
72                "[body] Invalid input: expected object, received {}",
73                json_type(Some(value)),
74            ))
75        })?;
76        if let Some(message) =
77            validation::unknown_field_keys(object.keys().map(String::as_str), &[])
78        {
79            return Err(crate::endpoint::validation(message));
80        }
81    }
82    Ok(EndpointInput {
83        body: call.body().cloned(),
84        query: call.query().cloned(),
85    })
86}
87
88impl SiwePlugin {
89    /// Verify a signed proof using the original request supplied in call options.
90    /// # Errors
91    /// Returns a serialization error if the input cannot be represented.
92    pub fn verify_message_endpoint(
93        input: &SiweVerificationInput,
94    ) -> AuthResult<ServerEndpoint<SiweVerificationResponse>> {
95        ServerEndpoint::new("siwe", "verifySiweMessage").with_body(input)
96    }
97    /// Generate and persist a nonce through installed server dispatch hooks.
98    #[must_use]
99    pub fn get_siwe_nonce_endpoint() -> ServerEndpoint<SiweNonceResponse> {
100        ServerEndpoint::new("siwe", "getSiweNonce")
101    }
102    /// Generate and persist a nonce through the public legacy alias.
103    #[must_use]
104    pub fn get_nonce_endpoint() -> ServerEndpoint<SiweNonceResponse> {
105        ServerEndpoint::new("siwe", "getNonce")
106    }
107}
108
109pub(super) async fn execute<S: AuthSchema>(
110    plugin: &SiwePlugin,
111    call: &EndpointCall,
112    ctx: &AuthContext<S>,
113) -> AuthResult<EndpointResponse> {
114    let output = if call.operation_id() == "verifySiweMessage" {
115        let request = call
116            .request()
117            .ok_or_else(|| crate::endpoint::validation("Request is required"))?;
118        plugin
119            .verify_response(request, ctx, call.body_as()?)
120            .await?
121    } else {
122        plugin.issue_nonce(ctx).await?
123    };
124    let body = alibi_core::utils::json::from_slice::<JsValue>(&output.body)?;
125    let mut response = if output.status >= 400 {
126        EndpointResponse::error(error_response(output.clone())).with_error_body(body)
127    } else {
128        EndpointResponse::value(body)
129    };
130    response.set_status(Some(output.status));
131    response.merge_headers(output.headers);
132    Ok(response)
133}