1use base64::Engine as _;
2use base64::engine::general_purpose::URL_SAFE_NO_PAD;
3use secrecy::SecretString;
4use sha2::{Digest as _, Sha256};
5
6pub struct Pkce {
7 pub verifier: SecretString,
8 pub challenge: String,
9}
10
11impl Pkce {
12 pub fn generate() -> Self {
13 let verifier = random_token(32);
14 Self {
15 challenge: challenge_for(&verifier),
16 verifier: SecretString::from(verifier),
17 }
18 }
19}
20
21pub fn challenge_for(verifier: &str) -> String {
22 URL_SAFE_NO_PAD.encode(Sha256::digest(verifier.as_bytes()))
23}
24
25pub fn random_token(bytes: usize) -> String {
27 let mut buffer = vec![0u8; bytes];
28 rand::fill(buffer.as_mut_slice());
29 URL_SAFE_NO_PAD.encode(buffer)
30}
31
32#[cfg(test)]
33mod tests {
34 use secrecy::ExposeSecret as _;
35
36 use super::*;
37
38 #[test]
39 fn rfc_7636_example() {
40 assert_eq!(
41 challenge_for("dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"),
42 "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"
43 );
44 }
45
46 #[test]
47 fn generated_verifiers_are_long_random_and_match_their_challenge() {
48 let a = Pkce::generate();
49 let b = Pkce::generate();
50 assert_eq!(a.verifier.expose_secret().len(), 43);
51 assert_ne!(a.verifier.expose_secret(), b.verifier.expose_secret());
52 assert_eq!(a.challenge, challenge_for(a.verifier.expose_secret()));
53 }
54}