use crate::error::{DiscordError, Result};
pub fn validate_token(token: &str) -> Result<()> {
let token = token.trim();
if token.is_empty() {
return Err(DiscordError::InvalidToken);
}
let parts: Vec<&str> = token.splitn(3, '.').collect();
match parts.as_slice() {
[first, rest] if *first == "mfa" => {
if rest.is_empty() || !is_base64url_like(rest) {
return Err(DiscordError::InvalidToken);
}
Ok(())
}
[id_b64, ts_b64, hmac] => {
if id_b64.is_empty() || ts_b64.is_empty() || hmac.is_empty() {
return Err(DiscordError::InvalidToken);
}
if !is_base64url_like(id_b64) {
return Err(DiscordError::InvalidToken);
}
match base64::Engine::decode(&base64::engine::general_purpose::STANDARD_NO_PAD, id_b64) {
Ok(bytes) => {
let s = String::from_utf8(bytes).map_err(|_| DiscordError::InvalidToken)?;
if s.is_empty() || !s.chars().all(|c| c.is_ascii_digit()) {
return Err(DiscordError::InvalidToken);
}
}
Err(_) => return Err(DiscordError::InvalidToken),
}
if !is_base64url_like(ts_b64) || !is_base64url_like(hmac) {
return Err(DiscordError::InvalidToken);
}
Ok(())
}
_ => Err(DiscordError::InvalidToken),
}
}
fn is_base64url_like(s: &str) -> bool {
!s.is_empty() && s.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '+' | '/' | '-' | '_' | '='))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_token_is_invalid() {
assert!(validate_token("").is_err());
}
#[test]
fn no_dots_is_invalid() {
assert!(validate_token("notavalidtoken").is_err());
}
#[test]
fn two_parts_non_mfa_is_invalid() {
assert!(validate_token("abc.def").is_err());
}
#[test]
fn mfa_token_valid() {
assert!(validate_token("mfa.abcABC0123456789_-").is_ok());
}
#[test]
fn mfa_token_empty_payload_invalid() {
assert!(validate_token("mfa.").is_err());
}
#[test]
fn well_formed_user_token() {
use base64::Engine;
let id_b64 = base64::engine::general_purpose::STANDARD_NO_PAD.encode("123456789");
let token = format!("{}.Gc5abc.SomeHmacHere", id_b64);
assert!(validate_token(&token).is_ok());
}
#[test]
fn non_numeric_id_is_invalid() {
use base64::Engine;
let id_b64 = base64::engine::general_purpose::STANDARD_NO_PAD.encode("not-a-number");
let token = format!("{}.Gc5abc.SomeHmacHere", id_b64);
assert!(validate_token(&token).is_err());
}
}