Something went wrong. Try again.
AtAuth
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314//! Input validation utilities for AT Protocol identifiers.//!//! This module provides validation functions for DIDs (Decentralized Identifiers)//! and handles according to AT Protocol specifications.
use crate::error::{AuthError, AuthResult};use crate::{MAX_DID_LENGTH, MAX_HANDLE_LENGTH};
/// Validate a Decentralized Identifier (DID).////// DIDs must:/// - Start with "did:"/// - Be between 1 and 512 characters/// - Follow the DID syntax (did:method:identifier)////// # Examples////// ```rust/// use atauth::validate_did;////// // Valid DIDs/// assert!(validate_did("did:plc:z72i7hdynmk6r22z27h6tvur").is_ok());/// assert!(validate_did("did:web:example.com").is_ok());////// // Invalid DIDs/// assert!(validate_did("not-a-did").is_err());/// assert!(validate_did("").is_err());/// ```pub fn validate_did(did: &str) -> AuthResult<()> { // Check length if did.is_empty() { return Err(AuthError::InvalidDid("DID cannot be empty".to_string())); }
if did.len() > MAX_DID_LENGTH { return Err(AuthError::InvalidDid(format!( "DID exceeds maximum length of {} characters", MAX_DID_LENGTH ))); }
// Check prefix if !did.starts_with("did:") { return Err(AuthError::InvalidDid( "DID must start with 'did:'".to_string(), )); }
// Check for method component let parts: Vec<&str> = did.split(':').collect(); if parts.len() < 3 { return Err(AuthError::InvalidDid( "DID must have format 'did:method:identifier'".to_string(), )); }
// Validate method (second part) let method = parts[1]; if method.is_empty() { return Err(AuthError::InvalidDid( "DID method cannot be empty".to_string(), )); }
// Method should only contain lowercase letters if !method.chars().all(|c| c.is_ascii_lowercase()) { return Err(AuthError::InvalidDid( "DID method must contain only lowercase letters".to_string(), )); }
// Validate identifier (third part) let identifier = parts[2..].join(":"); if identifier.is_empty() { return Err(AuthError::InvalidDid( "DID identifier cannot be empty".to_string(), )); }
// Check for invalid characters in identifier // AT Protocol DIDs typically use alphanumeric characters plus some special chars if !identifier .chars() .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-' || c == '_' || c == ':') { return Err(AuthError::InvalidDid( "DID identifier contains invalid characters".to_string(), )); }
Ok(())}
/// Validate an AT Protocol handle.////// Handles must:/// - Contain at least one dot (domain separator)/// - Be between 1 and 256 characters/// - Follow DNS name conventions////// # Examples////// ```rust/// use atauth::validate_handle;////// // Valid handles/// assert!(validate_handle("alice.bsky.social").is_ok());/// assert!(validate_handle("bob.example.com").is_ok());////// // Invalid handles/// assert!(validate_handle("nodomain").is_err());/// assert!(validate_handle("").is_err());/// ```pub fn validate_handle(handle: &str) -> AuthResult<()> { // Check length if handle.is_empty() { return Err(AuthError::InvalidHandle( "Handle cannot be empty".to_string(), )); }
if handle.len() > MAX_HANDLE_LENGTH { return Err(AuthError::InvalidHandle(format!( "Handle exceeds maximum length of {} characters", MAX_HANDLE_LENGTH ))); }
// Check for domain separator if !handle.contains('.') { return Err(AuthError::InvalidHandle( "Handle must contain a domain (at least one dot)".to_string(), )); }
// Split into parts let parts: Vec<&str> = handle.split('.').collect();
// Check each label for (i, part) in parts.iter().enumerate() { if part.is_empty() { return Err(AuthError::InvalidHandle( "Handle contains empty label (consecutive dots)".to_string(), )); }
// Each label should be max 63 characters (DNS limit) if part.len() > 63 { return Err(AuthError::InvalidHandle( "Handle label exceeds 63 characters".to_string(), )); }
// First and last parts shouldn't start or end with hyphen if part.starts_with('-') || part.ends_with('-') { return Err(AuthError::InvalidHandle( "Handle labels cannot start or end with hyphen".to_string(), )); }
// Check valid characters (alphanumeric and hyphen) if !part.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') { return Err(AuthError::InvalidHandle( "Handle contains invalid characters".to_string(), )); }
// First part (username) should start with letter or number if i == 0 && !part .chars() .next() .map_or(false, |c| c.is_ascii_alphanumeric()) { return Err(AuthError::InvalidHandle( "Handle must start with letter or number".to_string(), )); } }
// TLD validation (last part should be at least 2 chars) if let Some(tld) = parts.last() { if tld.len() < 2 { return Err(AuthError::InvalidHandle( "Handle TLD must be at least 2 characters".to_string(), )); } }
Ok(())}
/// Validate a nonce string.////// Nonces should be:/// - Non-empty/// - Maximum 64 characters/// - Alphanumeric with hyphens and underscorespub fn validate_nonce(nonce: &str) -> AuthResult<()> { const MAX_NONCE_LENGTH: usize = 64;
if nonce.is_empty() { return Err(AuthError::InvalidPayload( "Nonce cannot be empty".to_string(), )); }
if nonce.len() > MAX_NONCE_LENGTH { return Err(AuthError::InvalidPayload(format!( "Nonce exceeds maximum length of {} characters", MAX_NONCE_LENGTH ))); }
if !nonce .chars() .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_') { return Err(AuthError::InvalidPayload( "Nonce contains invalid characters".to_string(), )); }
Ok(())}
#[cfg(test)]mod tests { use super::*;
// DID Tests #[test] fn test_valid_dids() { assert!(validate_did("did:plc:z72i7hdynmk6r22z27h6tvur").is_ok()); assert!(validate_did("did:web:example.com").is_ok()); assert!(validate_did("did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK").is_ok()); }
#[test] fn test_invalid_dids() { // Empty assert!(validate_did("").is_err());
// No prefix assert!(validate_did("plc:test").is_err());
// Wrong prefix assert!(validate_did("DID:plc:test").is_err());
// No method assert!(validate_did("did::test").is_err());
// No identifier assert!(validate_did("did:plc").is_err());
// Too long let long_did = format!("did:plc:{}", "a".repeat(600)); assert!(validate_did(&long_did).is_err()); }
// Handle Tests #[test] fn test_valid_handles() { assert!(validate_handle("alice.bsky.social").is_ok()); assert!(validate_handle("bob.example.com").is_ok()); assert!(validate_handle("user-123.test.org").is_ok()); assert!(validate_handle("a.bc").is_ok()); }
#[test] fn test_invalid_handles() { // Empty assert!(validate_handle("").is_err());
// No domain assert!(validate_handle("nodomain").is_err());
// Starts with hyphen assert!(validate_handle("-alice.example.com").is_err());
// Ends with hyphen assert!(validate_handle("alice-.example.com").is_err());
// Invalid characters assert!(validate_handle("alice@example.com").is_err());
// Empty label assert!(validate_handle("alice..example.com").is_err());
// TLD too short assert!(validate_handle("alice.a").is_err());
// Too long let long_handle = format!("{}.example.com", "a".repeat(300)); assert!(validate_handle(&long_handle).is_err()); }
// Nonce Tests #[test] fn test_valid_nonces() { assert!(validate_nonce("abc123").is_ok()); assert!(validate_nonce("test-nonce_123").is_ok()); assert!(validate_nonce("a").is_ok()); }
#[test] fn test_invalid_nonces() { assert!(validate_nonce("").is_err()); assert!(validate_nonce(&"a".repeat(100)).is_err()); assert!(validate_nonce("nonce with spaces").is_err()); assert!(validate_nonce("nonce!@#").is_err()); }}