spotify_cli/oauth/
pkce.rs1use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
10use rand::Rng;
11use sha2::{Digest, Sha256};
12
13use crate::constants::PKCE_VERIFIER_LENGTH;
14
15pub struct PkceChallenge {
20 pub verifier: String,
21 pub challenge: String,
22}
23
24impl PkceChallenge {
25 pub fn generate() -> Self {
27 let verifier = generate_verifier();
28 let challenge = generate_challenge(&verifier);
29
30 Self {
31 verifier,
32 challenge,
33 }
34 }
35}
36
37fn generate_verifier() -> String {
38 const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
39
40 let mut rng = rand::thread_rng();
41
42 (0..PKCE_VERIFIER_LENGTH)
43 .map(|_| {
44 let idx = rng.gen_range(0..CHARSET.len());
45 CHARSET[idx] as char
46 })
47 .collect()
48}
49
50fn generate_challenge(verifier: &str) -> String {
51 let mut hasher = Sha256::new();
52 hasher.update(verifier.as_bytes());
53 let hash = hasher.finalize();
54
55 URL_SAFE_NO_PAD.encode(hash)
56}
57
58#[cfg(test)]
59mod tests {
60 use super::*;
61
62 #[test]
63 fn verifier_has_correct_length() {
64 let pkce = PkceChallenge::generate();
65 assert_eq!(pkce.verifier.len(), PKCE_VERIFIER_LENGTH);
66 }
67
68 #[test]
69 fn verifier_uses_valid_characters() {
70 let pkce = PkceChallenge::generate();
71 let valid_chars: &str =
72 "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
73
74 for c in pkce.verifier.chars() {
75 assert!(valid_chars.contains(c));
76 }
77 }
78
79 #[test]
80 fn challenge_is_base64url_encoded() {
81 let pkce = PkceChallenge::generate();
82 assert!(URL_SAFE_NO_PAD.decode(&pkce.challenge).is_ok());
83 }
84
85 #[test]
86 fn challenge_is_sha256_of_verifier() {
87 let pkce = PkceChallenge::generate();
88 let expected = generate_challenge(&pkce.verifier);
89 assert_eq!(pkce.challenge, expected);
90 }
91}