1use crate::random_token;
2use base64::Engine as _;
3use origin_domain::Result;
4use sha2::{Digest, Sha256};
5
6#[derive(Clone)]
11pub struct Pkce {
12 verifier: String,
13 challenge: String,
14}
15
16impl Pkce {
17 pub fn generate() -> Result<Self> {
19 let verifier = random_token(32)?;
21 let challenge = Self::challenge_for(&verifier);
22
23 Ok(Self {
24 verifier,
25 challenge,
26 })
27 }
28
29 fn challenge_for(verifier: &str) -> String {
30 let digest = Sha256::digest(verifier.as_bytes());
31 base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(digest)
32 }
33
34 pub fn challenge(&self) -> &str {
36 &self.challenge
37 }
38
39 pub fn verifier(&self) -> &str {
41 &self.verifier
42 }
43}
44
45impl std::fmt::Debug for Pkce {
47 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48 f.debug_struct("Pkce")
49 .field("challenge", &self.challenge)
50 .field("verifier", &"***")
51 .finish()
52 }
53}
54
55#[cfg(test)]
56mod tests {
57 use super::*;
58
59 #[test]
60 fn the_challenge_is_the_sha256_of_the_verifier() {
61 let verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
63 assert_eq!(
64 Pkce::challenge_for(verifier),
65 "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"
66 );
67 }
68
69 #[test]
70 fn every_flow_gets_a_fresh_verifier() {
71 let first = Pkce::generate().unwrap();
72 let second = Pkce::generate().unwrap();
73 assert_ne!(first.verifier(), second.verifier());
74 assert_eq!(first.verifier().len(), 43);
75 }
76
77 #[test]
78 fn debug_output_never_contains_the_verifier() {
79 let pkce = Pkce::generate().unwrap();
80 assert!(!format!("{pkce:?}").contains(pkce.verifier()));
81 }
82}