use crate::authentication_helpers::{is_valid_email, json_type};
use alibi_core::error::codes;
use alibi_core::utils::json::JsValue;
use alibi_core::{AuthRequest, AuthResponse, types::ParsedRequestBody};
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value, json};
#[serde_with::skip_serializing_none]
#[derive(Debug, Serialize, Deserialize)]
pub struct SiweVerificationInput {
pub message: String,
pub signature: String,
pub email: Option<String>,
}
pub(super) type VerifyBody = SiweVerificationInput;
fn error(message: &str) -> AuthResponse {
AuthResponse::json(
400,
&json!({"message":message,"code":codes::VALIDATION_ERROR}),
)
.unwrap_or_else(|_| AuthResponse::text(400, "Invalid request body"))
}
const fn received(value: Option<&Value>) -> &'static str {
match value {
None => "undefined",
Some(Value::Null) => "null",
Some(Value::Array(_)) => "array",
Some(Value::Object(_)) => "object",
Some(Value::Bool(_)) => "boolean",
Some(Value::String(_)) => "string",
Some(Value::Number(_)) => "number",
}
}
fn body(request: &AuthRequest, optional: bool) -> Result<Map<String, Value>, AuthResponse> {
if request.body.is_some() {
let content_type = request
.header("content-type")
.map(String::as_str)
.unwrap_or_default();
let normalized = content_type.to_ascii_lowercase();
let media_type = normalized.split(';').next().unwrap_or_default().trim();
if !media_type.contains("application/json") {
let message = if normalized.is_empty() {
"Content-Type is required. Allowed types: application/json".to_owned()
} else {
format!(
"Content-Type \"{content_type}\" is not allowed. Allowed types: application/json"
)
};
return Err(AuthResponse::json(
415,
&json!({"message":message,"code":codes::UNSUPPORTED_MEDIA_TYPE}),
)
.unwrap_or_else(|_| AuthResponse::text(415, "Unsupported media type")));
}
}
if let Some(decoded) = request.extensions().get::<ParsedRequestBody>()
&& let ParsedRequestBody::Opaque(kind) = &*decoded
{
return if *kind == "ArrayBuffer" {
Ok(Map::new())
} else {
Err(error(&format!(
"[body] Invalid input: expected object, received {kind}"
)))
};
}
let parsed = request
.body
.as_ref()
.map(|_| request.body_as_json::<Value>())
.transpose()
.map_err(|_error| {
AuthResponse::json(
400,
&json!({"message":"Invalid JSON in request body","code":codes::BAD_REQUEST}),
)
.unwrap_or_else(|_| AuthResponse::text(400, "Invalid JSON in request body"))
})?;
match parsed {
None if optional => Ok(Map::new()),
Some(Value::Object(body)) => Ok(body),
value => Err(error(
&(format!(
"[body] Invalid input: expected object, received {}",
received(value.as_ref())
)),
)),
}
}
fn unknown_keys(body: &Map<String, Value>, known: &[&str]) -> Option<String> {
unknown_field_keys(body.keys().map(String::as_str), known)
}
pub(super) fn unknown_field_keys<'a>(
keys: impl Iterator<Item = &'a str>,
known: &[&str],
) -> Option<String> {
let mut keys: Vec<&str> = keys.filter(|key| !known.contains(key)).collect();
keys.sort_by_key(|key| {
key.parse::<u32>()
.ok()
.filter(|index| *index != u32::MAX && index.to_string() == *key)
.map_or((1, 0), |index| (0, index))
});
if keys.is_empty() {
return None;
}
let formatted = keys
.iter()
.map(|key| format!("\"{key}\""))
.collect::<Vec<_>>()
.join(", ");
Some(format!(
"[body] Unrecognized key{}: {formatted}",
if keys.len() == 1 { "" } else { "s" }
))
}
pub(super) fn nonce_body(request: &AuthRequest) -> Result<(), AuthResponse> {
let body = body(request, true)?;
if let Some(message) = unknown_keys(&body, &[]) {
return Err(error(&(message)));
}
Ok(())
}
pub(super) fn verify_body(
request: &AuthRequest,
anonymous: bool,
) -> Result<VerifyBody, AuthResponse> {
let body = JsValue::from(Value::Object(body(request, false)?));
verify_value(Some(&body), anonymous)
}
pub(super) fn verify_value(
value: Option<&JsValue>,
anonymous: bool,
) -> Result<VerifyBody, AuthResponse> {
let body = value.and_then(JsValue::as_object).ok_or_else(|| {
error(&format!(
"[body] Invalid input: expected object, received {}",
json_type(value)
))
})?;
let mut errors = Vec::new();
let mut aborted = false;
for field in ["message", "signature"] {
match body.get(field) {
Some(JsValue::String(value)) if value.is_empty() => errors.push(format!(
"[body.{field}] Too small: expected string to have >=1 characters"
)),
Some(JsValue::String(_)) => {}
value => {
aborted = true;
errors.push(format!(
"[body.{field}] Invalid input: expected string, received {}",
json_type(value)
));
}
}
}
match body.get("email") {
Some(JsValue::String(email)) if !is_valid_email(email) => {
errors.push("[body.email] Invalid email address".to_owned());
}
None | Some(JsValue::String(_)) => {}
value => {
aborted = true;
errors.push(format!(
"[body.email] Invalid input: expected string, received {}",
json_type(value)
));
}
}
if let Some(unknown) = unknown_field_keys(
body.keys().map(String::as_str),
&["message", "signature", "email"],
) {
errors.push(unknown);
aborted = true;
}
if !anonymous
&& !aborted
&& body
.get("email")
.and_then(JsValue::as_str)
.is_none_or(str::is_empty)
{
errors.push(
"[body.email] Email is required when the anonymous plugin option is disabled."
.to_owned(),
);
}
if !errors.is_empty() {
return Err(error(&(errors.join("; "))));
}
Ok(VerifyBody {
message: body
.get("message")
.and_then(JsValue::as_str)
.unwrap_or_default()
.to_owned(),
signature: body
.get("signature")
.and_then(JsValue::as_str)
.unwrap_or_default()
.to_owned(),
email: body
.get("email")
.and_then(JsValue::as_str)
.map(str::to_owned),
})
}