forge-core 0.9.0

Core types and traits for the Forge framework
Documentation
//! PKCE (Proof Key for Code Exchange) verification.
//!
//! Implements S256 challenge method per RFC 7636 and OAuth 2.1.
//! Plain method is intentionally unsupported (OAuth 2.1 mandates S256).

use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
use sha2::{Digest, Sha256};

/// Verify a PKCE code verifier against a stored code challenge (S256).
///
/// Computes `BASE64URL(SHA256(code_verifier))` and compares to `code_challenge`.
pub fn verify_s256(code_verifier: &str, code_challenge: &str) -> bool {
    let mut hasher = Sha256::new();
    hasher.update(code_verifier.as_bytes());
    let computed = URL_SAFE_NO_PAD.encode(hasher.finalize());
    constant_time_eq(computed.as_bytes(), code_challenge.as_bytes())
}

/// Compute S256 challenge from a verifier.
pub fn compute_s256_challenge(code_verifier: &str) -> String {
    let mut hasher = Sha256::new();
    hasher.update(code_verifier.as_bytes());
    URL_SAFE_NO_PAD.encode(hasher.finalize())
}

/// Constant-time byte comparison to prevent timing attacks.
fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
    if a.len() != b.len() {
        return false;
    }
    let mut diff = 0u8;
    for (x, y) in a.iter().zip(b.iter()) {
        diff |= x ^ y;
    }
    diff == 0
}

#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
    use super::*;

    #[test]
    fn test_s256_roundtrip() {
        let verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
        let challenge = compute_s256_challenge(verifier);
        assert!(verify_s256(verifier, &challenge));
    }

    #[test]
    fn test_s256_rfc7636_example() {
        // RFC 7636 Appendix B test vector
        let verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
        let expected_challenge = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM";
        assert!(verify_s256(verifier, expected_challenge));
    }

    #[test]
    fn test_s256_wrong_verifier() {
        let challenge = compute_s256_challenge("correct-verifier");
        assert!(!verify_s256("wrong-verifier", &challenge));
    }

    #[test]
    fn test_s256_empty_strings() {
        let challenge = compute_s256_challenge("");
        assert!(verify_s256("", &challenge));
        assert!(!verify_s256("notempty", &challenge));
    }
}