Skip to main content

oauth_toolkit/
pkce.rs

1//! RFC 7636 PKCE (Proof Key for Code Exchange) support.
2
3use base64::engine::general_purpose::URL_SAFE_NO_PAD;
4use base64::Engine;
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().zip(b.iter()).fold(0u8, |acc, (x, y)| acc | (x ^ y)) == 0
55}
56
57#[cfg(test)]
58mod tests {
59    use super::*;
60
61    #[test]
62    fn generate_pkce_pair_deterministic_challenge() {
63        let (v, c) = generate_pkce_pair();
64        let expected = sha256_base64url(&v);
65        assert_eq!(c, expected);
66    }
67
68    #[test]
69    fn verify_pkce_s256_correct() {
70        let (verifier, challenge) = generate_pkce_pair();
71        assert!(verify_pkce(&verifier, &challenge, "S256"));
72    }
73
74    #[test]
75    fn verify_pkce_s256_wrong_verifier() {
76        let (_, challenge) = generate_pkce_pair();
77        assert!(!verify_pkce("wrong-verifier", &challenge, "S256"));
78    }
79
80    #[test]
81    fn verify_pkce_plain_correct() {
82        assert!(verify_pkce("test", "test", "plain"));
83    }
84
85    #[test]
86    fn verify_pkce_plain_wrong() {
87        assert!(!verify_pkce("test", "wrong", "plain"));
88    }
89
90    #[test]
91    fn verify_pkce_unknown_method() {
92        assert!(!verify_pkce("a", "b", "unknown"));
93    }
94}