1use base64::Engine;
26use base64::engine::general_purpose::URL_SAFE_NO_PAD;
27use rand::Rng;
28use sha2::Digest;
29
30pub const MIN_VERIFIER_LEN: usize = 43;
32pub const MAX_VERIFIER_LEN: usize = 128;
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum PkceMethod {
41 S256,
44 Plain,
47}
48
49impl PkceMethod {
50 pub fn as_str(self) -> &'static str {
52 match self {
53 Self::S256 => "S256",
54 Self::Plain => "plain",
55 }
56 }
57
58 pub fn parse(value: &str) -> Result<Self, PkceError> {
64 match value {
65 "S256" => Ok(Self::S256),
66 "plain" => Ok(Self::Plain),
67 other => Err(PkceError::UnknownMethod(other.to_string())),
68 }
69 }
70}
71
72#[derive(Debug, Clone, PartialEq, Eq)]
74pub enum PkceError {
75 UnknownMethod(String),
78 MethodNotPermitted {
81 method: PkceMethod,
83 },
84 MalformedVerifier(String),
86 Mismatch,
88}
89
90impl std::fmt::Display for PkceError {
91 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92 match self {
93 Self::UnknownMethod(m) => {
94 write!(
95 f,
96 "unknown code_challenge_method {m:?}; RFC 7636 defines plain and S256"
97 )
98 }
99 Self::MethodNotPermitted { method } => write!(
100 f,
101 "code_challenge_method {:?} is refused; OAuth 2.1 section 7.5.2 forbids it",
102 method.as_str()
103 ),
104 Self::MalformedVerifier(why) => {
105 write!(f, "code_verifier rejected: {why}")
106 }
107 Self::Mismatch => write!(f, "code_verifier does not match the stored challenge"),
108 }
109 }
110}
111
112impl std::error::Error for PkceError {}
113
114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
116pub struct PkcePolicy {
117 pub plain: bool,
119}
120
121impl Default for PkcePolicy {
122 fn default() -> Self {
124 Self { plain: false }
125 }
126}
127
128impl PkcePolicy {
129 pub fn allow_plain() -> Self {
134 Self { plain: true }
135 }
136
137 pub fn permits(&self, method: PkceMethod) -> bool {
139 match method {
140 PkceMethod::S256 => true,
141 PkceMethod::Plain => self.plain,
142 }
143 }
144}
145
146pub fn generate_pkce_pair() -> (String, String) {
153 let mut verifier_bytes = [0u8; 32];
154 rand::rng().fill(&mut verifier_bytes[..]);
155 let verifier = URL_SAFE_NO_PAD.encode(verifier_bytes);
156
157 let challenge = sha256_base64url(&verifier);
158 (verifier, challenge)
159}
160
161pub fn validate_verifier(code_verifier: &str) -> Result<(), PkceError> {
167 let len = code_verifier.chars().count();
168 if !(MIN_VERIFIER_LEN..=MAX_VERIFIER_LEN).contains(&len) {
169 return Err(PkceError::MalformedVerifier(format!(
170 "{len} characters, RFC 7636 section 4.1 requires {MIN_VERIFIER_LEN}-{MAX_VERIFIER_LEN}"
171 )));
172 }
173 if let Some(bad) = code_verifier
174 .chars()
175 .find(|c| !(c.is_ascii_alphanumeric() || matches!(c, '-' | '.' | '_' | '~')))
176 {
177 return Err(PkceError::MalformedVerifier(format!(
178 "{bad:?} is not in the unreserved set [A-Za-z0-9-._~]"
179 )));
180 }
181 Ok(())
182}
183
184pub fn verify(
191 code_verifier: &str,
192 stored_challenge: &str,
193 method: PkceMethod,
194 policy: &PkcePolicy,
195) -> Result<(), PkceError> {
196 if !policy.permits(method) {
197 return Err(PkceError::MethodNotPermitted { method });
198 }
199 validate_verifier(code_verifier)?;
200 match method {
201 PkceMethod::S256 => {
202 let computed = sha256_base64url(code_verifier);
203 if constant_time_eq(computed.as_bytes(), stored_challenge.as_bytes()) {
204 Ok(())
205 } else {
206 Err(PkceError::Mismatch)
207 }
208 }
209 PkceMethod::Plain => {
210 if constant_time_eq(code_verifier.as_bytes(), stored_challenge.as_bytes()) {
211 Ok(())
212 } else {
213 Err(PkceError::Mismatch)
214 }
215 }
216 }
217}
218
219pub fn verify_with_default_policy(
221 code_verifier: &str,
222 stored_challenge: &str,
223 method: PkceMethod,
224) -> Result<(), PkceError> {
225 verify(
226 code_verifier,
227 stored_challenge,
228 method,
229 &PkcePolicy::default(),
230 )
231}
232
233fn sha256_base64url(input: &str) -> String {
234 let hash = sha2::Sha256::digest(input.as_bytes());
235 URL_SAFE_NO_PAD.encode(hash)
236}
237
238fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
239 if a.len() != b.len() {
240 return false;
241 }
242 a.iter()
243 .zip(b.iter())
244 .fold(0u8, |acc, (x, y)| acc | (x ^ y))
245 == 0
246}
247
248#[cfg(test)]
249#[allow(clippy::unwrap_used, clippy::expect_used)]
251mod tests {
252 use super::*;
253
254 const RFC7636_VERIFIER: &str = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
257 const RFC7636_CHALLENGE: &str = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM";
258
259 #[test]
260 fn rfc7636_appendix_b_vector() {
261 assert!(
262 verify_with_default_policy(RFC7636_VERIFIER, RFC7636_CHALLENGE, PkceMethod::S256)
263 .is_ok()
264 );
265 }
266
267 #[test]
268 fn generate_pkce_pair_deterministic_challenge() {
269 let (v, c) = generate_pkce_pair();
270 let expected = sha256_base64url(&v);
271 assert_eq!(c, expected);
272 validate_verifier(&v).expect("generated verifier is well formed");
274 assert_eq!(v.chars().count(), MIN_VERIFIER_LEN);
275 }
276
277 #[test]
278 fn s256_correct() {
279 let (verifier, challenge) = generate_pkce_pair();
280 assert!(verify_with_default_policy(&verifier, &challenge, PkceMethod::S256).is_ok());
281 }
282
283 #[test]
284 fn s256_wrong_verifier_is_a_mismatch_not_a_client_bug() {
285 let (verifier, challenge) = generate_pkce_pair();
286 let other = format!("{verifier}X");
287 assert_eq!(
288 verify_with_default_policy(&other, &challenge, PkceMethod::S256),
289 Err(PkceError::Mismatch)
290 );
291 }
292
293 #[test]
297 fn plain_is_refused_by_default_even_when_it_would_match() {
298 assert_eq!(
299 verify_with_default_policy(RFC7636_VERIFIER, RFC7636_VERIFIER, PkceMethod::Plain),
300 Err(PkceError::MethodNotPermitted {
301 method: PkceMethod::Plain
302 })
303 );
304 }
305
306 #[test]
307 fn plain_is_available_only_through_an_explicit_policy() {
308 let policy = PkcePolicy::allow_plain();
309 assert!(
310 verify(
311 RFC7636_VERIFIER,
312 RFC7636_VERIFIER,
313 PkceMethod::Plain,
314 &policy
315 )
316 .is_ok(),
317 "a deployment that knowingly accepts plain can still do so"
318 );
319 }
320
321 #[test]
324 fn a_misspelled_method_is_an_error_not_a_mismatch() {
325 for typo in ["s256", "S-256", "S256 ", "sha256", ""] {
326 assert_eq!(
327 PkceMethod::parse(typo),
328 Err(PkceError::UnknownMethod(typo.to_string())),
329 "{typo:?} must not parse as a method"
330 );
331 }
332 assert_eq!(PkceMethod::parse("S256"), Ok(PkceMethod::S256));
333 assert_eq!(PkceMethod::parse("plain"), Ok(PkceMethod::Plain));
334 }
335
336 #[test]
337 fn verifier_syntax_is_enforced_per_rfc7636_4_1() {
338 assert!(validate_verifier(RFC7636_VERIFIER).is_ok());
339 assert!(matches!(
340 validate_verifier("too-short"),
341 Err(PkceError::MalformedVerifier(_))
342 ));
343 assert!(matches!(
344 validate_verifier(&"a".repeat(MAX_VERIFIER_LEN + 1)),
345 Err(PkceError::MalformedVerifier(_))
346 ));
347 assert!(matches!(
348 validate_verifier(&"a".repeat(MAX_VERIFIER_LEN)),
349 Ok(())
350 ));
351 assert!(matches!(
353 validate_verifier(&"a".repeat(42).replace('a', "+")),
354 Err(PkceError::MalformedVerifier(_))
355 ));
356 assert!(matches!(
358 verify_with_default_policy("short", RFC7636_CHALLENGE, PkceMethod::S256),
359 Err(PkceError::MalformedVerifier(_))
360 ));
361 }
362}