use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
use sha2::{Digest, Sha256};
#[derive(Debug, Clone)]
pub struct PkceChallenge {
pub verifier: String,
pub challenge: String,
}
impl PkceChallenge {
pub fn generate() -> Self {
let random_bytes: [u8; 32] = rand::random();
let verifier = URL_SAFE_NO_PAD.encode(random_bytes);
let mut hasher = Sha256::new();
hasher.update(verifier.as_bytes());
let challenge = URL_SAFE_NO_PAD.encode(hasher.finalize());
Self {
verifier,
challenge,
}
}
pub fn challenge_method() -> &'static str {
"S256"
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pkce_generation() {
let pkce = PkceChallenge::generate();
assert_eq!(pkce.verifier.len(), 43);
assert_eq!(pkce.challenge.len(), 43);
assert_ne!(pkce.verifier, pkce.challenge);
}
#[test]
fn test_pkce_uniqueness() {
let pkce1 = PkceChallenge::generate();
let pkce2 = PkceChallenge::generate();
assert_ne!(pkce1.verifier, pkce2.verifier);
assert_ne!(pkce1.challenge, pkce2.challenge);
}
#[test]
fn test_pkce_verifier_challenge_relationship() {
let pkce = PkceChallenge::generate();
let mut hasher = Sha256::new();
hasher.update(pkce.verifier.as_bytes());
let expected_challenge = URL_SAFE_NO_PAD.encode(hasher.finalize());
assert_eq!(pkce.challenge, expected_challenge);
}
#[test]
fn test_challenge_method() {
assert_eq!(PkceChallenge::challenge_method(), "S256");
}
#[test]
fn test_pkce_base64url_format() {
let pkce = PkceChallenge::generate();
let valid_chars = |s: &str| {
s.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
};
assert!(valid_chars(&pkce.verifier));
assert!(valid_chars(&pkce.challenge));
}
}