alibi_plugins/siwe/
validation.rs1use crate::authentication_helpers::{is_valid_email, json_type};
2use alibi_core::utils::json::JsValue;
3use alibi_core::{AuthRequest, AuthResponse, types::ParsedRequestBody};
4use serde::{Deserialize, Serialize};
5use serde_json::{Map, Value, json};
6
7#[serde_with::skip_serializing_none]
9#[derive(Debug, Serialize, Deserialize)]
10pub struct SiweVerificationInput {
11 pub message: String,
12 pub signature: String,
13 pub email: Option<String>,
14}
15
16pub(super) type VerifyBody = SiweVerificationInput;
17
18fn error(message: &str) -> AuthResponse {
19 AuthResponse::json(400, &json!({"message":message,"code":"VALIDATION_ERROR"}))
20 .unwrap_or_else(|_| AuthResponse::text(400, "Invalid request body"))
21}
22
23const fn received(value: Option<&Value>) -> &'static str {
24 match value {
25 None => "undefined",
26 Some(Value::Null) => "null",
27 Some(Value::Array(_)) => "array",
28 Some(Value::Object(_)) => "object",
29 Some(Value::Bool(_)) => "boolean",
30 Some(Value::String(_)) => "string",
31 Some(Value::Number(_)) => "number",
32 }
33}
34
35fn body(request: &AuthRequest, optional: bool) -> Result<Map<String, Value>, AuthResponse> {
36 if request.body.is_some() {
37 let content_type = request
38 .header("content-type")
39 .map(String::as_str)
40 .unwrap_or_default();
41 let normalized = content_type.to_ascii_lowercase();
42 let media_type = normalized.split(';').next().unwrap_or_default().trim();
43 if !media_type.contains("application/json") {
46 let message = if normalized.is_empty() {
47 "Content-Type is required. Allowed types: application/json".to_owned()
48 } else {
49 format!(
50 "Content-Type \"{content_type}\" is not allowed. Allowed types: application/json"
51 )
52 };
53 return Err(AuthResponse::json(
54 415,
55 &json!({"message":message,"code":"UNSUPPORTED_MEDIA_TYPE"}),
56 )
57 .unwrap_or_else(|_| AuthResponse::text(415, "Unsupported media type")));
58 }
59 }
60 if let Some(decoded) = request.extensions().get::<ParsedRequestBody>()
64 && let ParsedRequestBody::Opaque(kind) = &*decoded
65 {
66 return if *kind == "ArrayBuffer" {
67 Ok(Map::new())
68 } else {
69 Err(error(&format!(
70 "[body] Invalid input: expected object, received {kind}"
71 )))
72 };
73 }
74 let parsed = request
75 .body
76 .as_ref()
77 .map(|_| request.body_as_json::<Value>())
78 .transpose()
79 .map_err(|_error| {
80 AuthResponse::json(
81 400,
82 &json!({"message":"Invalid JSON in request body","code":"BAD_REQUEST"}),
83 )
84 .unwrap_or_else(|_| AuthResponse::text(400, "Invalid JSON in request body"))
85 })?;
86 match parsed {
87 None if optional => Ok(Map::new()),
88 Some(Value::Object(body)) => Ok(body),
89 value => Err(error(
90 &(format!(
91 "[body] Invalid input: expected object, received {}",
92 received(value.as_ref())
93 )),
94 )),
95 }
96}
97
98fn unknown_keys(body: &Map<String, Value>, known: &[&str]) -> Option<String> {
99 unknown_field_keys(body.keys().map(String::as_str), known)
100}
101
102pub(super) fn unknown_field_keys<'a>(
103 keys: impl Iterator<Item = &'a str>,
104 known: &[&str],
105) -> Option<String> {
106 let mut keys: Vec<&str> = keys.filter(|key| !known.contains(key)).collect();
107 keys.sort_by_key(|key| {
110 key.parse::<u32>()
111 .ok()
112 .filter(|index| *index != u32::MAX && index.to_string() == *key)
113 .map_or((1, 0), |index| (0, index))
114 });
115 if keys.is_empty() {
116 return None;
117 }
118 let formatted = keys
119 .iter()
120 .map(|key| format!("\"{key}\""))
121 .collect::<Vec<_>>()
122 .join(", ");
123 Some(format!(
124 "[body] Unrecognized key{}: {formatted}",
125 if keys.len() == 1 { "" } else { "s" }
126 ))
127}
128
129pub(super) fn nonce_body(request: &AuthRequest) -> Result<(), AuthResponse> {
130 let body = body(request, true)?;
131 if let Some(message) = unknown_keys(&body, &[]) {
132 return Err(error(&(message)));
133 }
134 Ok(())
135}
136
137pub(super) fn verify_body(
138 request: &AuthRequest,
139 anonymous: bool,
140) -> Result<VerifyBody, AuthResponse> {
141 let body = JsValue::from(Value::Object(body(request, false)?));
142 verify_value(Some(&body), anonymous)
143}
144
145pub(super) fn verify_value(
146 value: Option<&JsValue>,
147 anonymous: bool,
148) -> Result<VerifyBody, AuthResponse> {
149 let body = value.and_then(JsValue::as_object).ok_or_else(|| {
150 error(&format!(
151 "[body] Invalid input: expected object, received {}",
152 json_type(value)
153 ))
154 })?;
155 let mut errors = Vec::new();
156 let mut aborted = false;
157 for field in ["message", "signature"] {
158 match body.get(field) {
159 Some(JsValue::String(value)) if value.is_empty() => errors.push(format!(
160 "[body.{field}] Too small: expected string to have >=1 characters"
161 )),
162 Some(JsValue::String(_)) => {}
163 value => {
164 aborted = true;
165 errors.push(format!(
166 "[body.{field}] Invalid input: expected string, received {}",
167 json_type(value)
168 ));
169 }
170 }
171 }
172 match body.get("email") {
173 Some(JsValue::String(email)) if !is_valid_email(email) => {
174 errors.push("[body.email] Invalid email address".to_owned());
175 }
176 None | Some(JsValue::String(_)) => {}
177 value => {
178 aborted = true;
179 errors.push(format!(
180 "[body.email] Invalid input: expected string, received {}",
181 json_type(value)
182 ));
183 }
184 }
185 if let Some(unknown) = unknown_field_keys(
186 body.keys().map(String::as_str),
187 &["message", "signature", "email"],
188 ) {
189 errors.push(unknown);
190 aborted = true;
191 }
192 if !anonymous
193 && !aborted
194 && body
195 .get("email")
196 .and_then(JsValue::as_str)
197 .is_none_or(str::is_empty)
198 {
199 errors.push(
200 "[body.email] Email is required when the anonymous plugin option is disabled."
201 .to_owned(),
202 );
203 }
204 if !errors.is_empty() {
205 return Err(error(&(errors.join("; "))));
206 }
207 Ok(VerifyBody {
208 message: body
209 .get("message")
210 .and_then(JsValue::as_str)
211 .unwrap_or_default()
212 .to_owned(),
213 signature: body
214 .get("signature")
215 .and_then(JsValue::as_str)
216 .unwrap_or_default()
217 .to_owned(),
218 email: body
219 .get("email")
220 .and_then(JsValue::as_str)
221 .map(str::to_owned),
222 })
223}