Skip to main content

oauth_toolkit/
pkce.rs

1//! RFC 7636 PKCE (Proof Key for Code Exchange) support.
2
3use base64::Engine;
4use base64::engine::general_purpose::URL_SAFE_NO_PAD;
5use rand::Rng;
6use sha2::Digest;
7
8/// PKCE code challenge method.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum PkceMethod {
11    /// SHA-256 hash of the verifier (recommended).
12    S256,
13    /// Plain text (not recommended for production).
14    Plain,
15}
16
17/// Generate a PKCE code verifier and its S256 challenge.
18///
19/// Returns `(verifier, challenge)` where:
20/// - `verifier` is a 43-128 character random string (URL-safe base64)
21/// - `challenge` is `base64url(SHA256(verifier))`
22pub fn generate_pkce_pair() -> (String, String) {
23    let mut verifier_bytes = [0u8; 32];
24    rand::rng().fill(&mut verifier_bytes[..]);
25    let verifier = URL_SAFE_NO_PAD.encode(verifier_bytes);
26
27    let challenge = sha256_base64url(&verifier);
28    (verifier, challenge)
29}
30
31/// Verify a PKCE code challenge against the verifier.
32///
33/// Supports S256 (base64url(SHA256(verifier)) == challenge) and plain (verifier == challenge).
34pub fn verify_pkce(code_verifier: &str, stored_challenge: &str, method: &str) -> bool {
35    match method {
36        "S256" => {
37            let computed = sha256_base64url(code_verifier);
38            constant_time_eq(computed.as_bytes(), stored_challenge.as_bytes())
39        }
40        "plain" => constant_time_eq(code_verifier.as_bytes(), stored_challenge.as_bytes()),
41        _ => false,
42    }
43}
44
45fn sha256_base64url(input: &str) -> String {
46    let hash = sha2::Sha256::digest(input.as_bytes());
47    URL_SAFE_NO_PAD.encode(hash)
48}
49
50fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
51    if a.len() != b.len() {
52        return false;
53    }
54    a.iter()
55        .zip(b.iter())
56        .fold(0u8, |acc, (x, y)| acc | (x ^ y))
57        == 0
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63
64    #[test]
65    fn generate_pkce_pair_deterministic_challenge() {
66        let (v, c) = generate_pkce_pair();
67        let expected = sha256_base64url(&v);
68        assert_eq!(c, expected);
69    }
70
71    #[test]
72    fn verify_pkce_s256_correct() {
73        let (verifier, challenge) = generate_pkce_pair();
74        assert!(verify_pkce(&verifier, &challenge, "S256"));
75    }
76
77    #[test]
78    fn verify_pkce_s256_wrong_verifier() {
79        let (_, challenge) = generate_pkce_pair();
80        assert!(!verify_pkce("wrong-verifier", &challenge, "S256"));
81    }
82
83    #[test]
84    fn verify_pkce_plain_correct() {
85        assert!(verify_pkce("test", "test", "plain"));
86    }
87
88    #[test]
89    fn verify_pkce_plain_wrong() {
90        assert!(!verify_pkce("test", "wrong", "plain"));
91    }
92
93    #[test]
94    fn verify_pkce_unknown_method() {
95        assert!(!verify_pkce("a", "b", "unknown"));
96    }
97}