Files
wz-phone/crates/wzp-relay/src/auth.rs
2026-05-11 12:37:32 +04:00

104 lines
2.9 KiB
Rust

//! featherChat token authentication.
//!
//! When `--auth-url` is configured, the relay validates bearer tokens
//! against featherChat's `POST /v1/auth/validate` endpoint before
//! allowing clients to join rooms.
use serde::{Deserialize, Serialize};
use tracing::{info, warn};
/// Request body for featherChat token validation.
#[derive(Serialize)]
struct ValidateRequest {
token: String,
}
/// Response from featherChat token validation.
#[derive(Deserialize, Debug)]
pub struct ValidateResponse {
pub valid: bool,
pub fingerprint: Option<String>,
pub alias: Option<String>,
}
/// Validated client identity.
#[derive(Clone, Debug)]
pub struct AuthenticatedClient {
pub fingerprint: String,
pub alias: Option<String>,
}
/// Validate a bearer token against featherChat's auth endpoint.
///
/// Calls `POST {auth_url}` with `{ "token": "..." }`.
/// Returns the client identity if valid, or an error string.
pub async fn validate_token(auth_url: &str, token: &str) -> Result<AuthenticatedClient, String> {
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(5))
.build()
.map_err(|e| format!("http client error: {e}"))?;
let resp = client
.post(auth_url)
.json(&ValidateRequest {
token: token.to_string(),
})
.send()
.await
.map_err(|e| format!("auth request failed: {e}"))?;
if !resp.status().is_success() {
return Err(format!("auth endpoint returned {}", resp.status()));
}
let body: ValidateResponse = resp
.json()
.await
.map_err(|e| format!("invalid auth response: {e}"))?;
if body.valid {
let fingerprint = body
.fingerprint
.ok_or_else(|| "valid response missing fingerprint".to_string())?;
info!(%fingerprint, alias = ?body.alias, "token validated");
Ok(AuthenticatedClient {
fingerprint,
alias: body.alias,
})
} else {
warn!("token validation failed");
Err("invalid token".to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn validate_request_serializes() {
let req = ValidateRequest {
token: "abc123".to_string(),
};
let json = serde_json::to_string(&req).unwrap();
assert!(json.contains("abc123"));
}
#[test]
fn validate_response_deserializes() {
let json = r#"{"valid": true, "fingerprint": "abcd1234", "alias": "manwe"}"#;
let resp: ValidateResponse = serde_json::from_str(json).unwrap();
assert!(resp.valid);
assert_eq!(resp.fingerprint.unwrap(), "abcd1234");
assert_eq!(resp.alias.unwrap(), "manwe");
}
#[test]
fn invalid_response_deserializes() {
let json = r#"{"valid": false}"#;
let resp: ValidateResponse = serde_json::from_str(json).unwrap();
assert!(!resp.valid);
assert!(resp.fingerprint.is_none());
}
}