use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
use sha2::{Digest, Sha256};
pub struct PkceChallenge {
pub verifier: String,
pub challenge: String,
}
impl PkceChallenge {
pub fn generate() -> Self {
let mut bytes = [0u8; 32];
rand::RngCore::fill_bytes(&mut rand::thread_rng(), &mut bytes);
let verifier = URL_SAFE_NO_PAD.encode(bytes);
let hash = Sha256::digest(verifier.as_bytes());
let challenge = URL_SAFE_NO_PAD.encode(hash);
Self {
verifier,
challenge,
}
}
pub fn verify(verifier: &str, challenge: &str) -> bool {
let hash = Sha256::digest(verifier.as_bytes());
let expected = URL_SAFE_NO_PAD.encode(hash);
expected == challenge
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_oauth_pkce_verifier_length() {
let pkce = PkceChallenge::generate();
assert_eq!(pkce.verifier.len(), 43);
}
#[test]
fn test_oauth_pkce_challenge_matches_verifier() {
let pkce = PkceChallenge::generate();
assert!(PkceChallenge::verify(&pkce.verifier, &pkce.challenge));
}
#[test]
fn test_oauth_pkce_unique() {
let a = PkceChallenge::generate();
let b = PkceChallenge::generate();
assert_ne!(a.verifier, b.verifier);
assert_ne!(a.challenge, b.challenge);
}
#[test]
fn test_oauth_pkce_challenge_is_base64url() {
let pkce = PkceChallenge::generate();
assert!(pkce
.challenge
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_'));
assert!(pkce
.verifier
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_'));
}
#[test]
fn test_oauth_pkce_wrong_verifier_fails() {
let pkce = PkceChallenge::generate();
assert!(!PkceChallenge::verify("wrong-verifier", &pkce.challenge));
}
}