Skip to main content

rws_server/sso/
pkce.rs

1//! PKCE (Proof Key for Code Exchange) helpers — RFC 7636.
2//!
3//! Generate a [`PkceVerifier`] once per authorization request, store it in
4//! the pre-auth session, and pass the derived [`PkceChallenge`] in the
5//! authorization URL.  On the callback, supply the stored verifier string to
6//! the token endpoint via `code_verifier`.
7
8use rand_core::{OsRng, RngCore};
9use sha2::{Digest, Sha256};
10
11/// Cryptographically random code verifier (43–128 base64url chars, per RFC 7636).
12pub struct PkceVerifier(String);
13
14/// SHA-256 base64url challenge derived from the verifier.
15pub struct PkceChallenge(String);
16
17impl PkceVerifier {
18    /// Generate a fresh verifier with 32 random bytes (43-char base64url output).
19    pub fn new() -> Self {
20        let mut bytes = [0u8; 32];
21        OsRng.fill_bytes(&mut bytes);
22        PkceVerifier(base64url_encode(&bytes))
23    }
24
25    /// Return the verifier string.  Store this in the session before redirecting.
26    pub fn as_str(&self) -> &str {
27        &self.0
28    }
29
30    /// Derive the S256 challenge from this verifier.
31    pub fn challenge(&self) -> PkceChallenge {
32        let digest = Sha256::digest(self.0.as_bytes());
33        PkceChallenge(base64url_encode(&digest))
34    }
35}
36
37impl Default for PkceVerifier {
38    fn default() -> Self {
39        Self::new()
40    }
41}
42
43impl PkceChallenge {
44    /// Return the challenge string to include in the authorization URL.
45    pub fn as_str(&self) -> &str {
46        &self.0
47    }
48}
49
50// ── base64url helpers (no external dep) ──────────────────────────────────────
51
52const TABLE: &[u8; 64] =
53    b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
54
55/// Encode `bytes` as base64url without padding.
56#[doc(hidden)]
57pub fn base64url_encode(bytes: &[u8]) -> String {
58    let mut out = String::with_capacity(((bytes.len() + 2) / 3) * 4);
59    let mut i = 0;
60    while i + 3 <= bytes.len() {
61        let b0 = bytes[i] as usize;
62        let b1 = bytes[i + 1] as usize;
63        let b2 = bytes[i + 2] as usize;
64        out.push(TABLE[b0 >> 2] as char);
65        out.push(TABLE[((b0 & 3) << 4) | (b1 >> 4)] as char);
66        out.push(TABLE[((b1 & 0xf) << 2) | (b2 >> 6)] as char);
67        out.push(TABLE[b2 & 0x3f] as char);
68        i += 3;
69    }
70    let rem = bytes.len() - i;
71    if rem == 1 {
72        let b0 = bytes[i] as usize;
73        out.push(TABLE[b0 >> 2] as char);
74        out.push(TABLE[(b0 & 3) << 4] as char);
75    } else if rem == 2 {
76        let b0 = bytes[i] as usize;
77        let b1 = bytes[i + 1] as usize;
78        out.push(TABLE[b0 >> 2] as char);
79        out.push(TABLE[((b0 & 3) << 4) | (b1 >> 4)] as char);
80        out.push(TABLE[(b1 & 0xf) << 2] as char);
81    }
82    out
83}
84
85/// Decode a base64url string (with or without `=` padding; accepts `-`/`_`
86/// and `+`/`/` variants).
87#[doc(hidden)]
88pub fn base64url_decode(s: &str) -> Result<Vec<u8>, String> {
89    Ok(crate::core::base64::Decoder::EITHER.decode(s)?)
90}
91
92#[cfg(test)]
93mod tests {
94    #[test]
95    fn pkce_takes_either_alphabet() {
96        // PKCE / JWT: URL-safe or standard, unpadded.
97        assert_eq!(vec![0xfb, 0xff], super::base64url_decode("-_8").unwrap());
98        assert_eq!(vec![0xfb, 0xff], super::base64url_decode("+/8=").unwrap());
99        assert!(super::base64url_decode("a b").is_err());
100    }
101}