use base64::Engine;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use rand::Rng;
use sha2::Digest;
pub const MIN_VERIFIER_LEN: usize = 43;
pub const MAX_VERIFIER_LEN: usize = 128;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PkceMethod {
S256,
Plain,
}
impl PkceMethod {
pub fn as_str(self) -> &'static str {
match self {
Self::S256 => "S256",
Self::Plain => "plain",
}
}
pub fn parse(value: &str) -> Result<Self, PkceError> {
match value {
"S256" => Ok(Self::S256),
"plain" => Ok(Self::Plain),
other => Err(PkceError::UnknownMethod(other.to_string())),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PkceError {
UnknownMethod(String),
MethodNotPermitted {
method: PkceMethod,
},
MalformedVerifier(String),
Mismatch,
}
impl std::fmt::Display for PkceError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::UnknownMethod(m) => {
write!(
f,
"unknown code_challenge_method {m:?}; RFC 7636 defines plain and S256"
)
}
Self::MethodNotPermitted { method } => write!(
f,
"code_challenge_method {:?} is refused; OAuth 2.1 section 7.5.2 forbids it",
method.as_str()
),
Self::MalformedVerifier(why) => {
write!(f, "code_verifier rejected: {why}")
}
Self::Mismatch => write!(f, "code_verifier does not match the stored challenge"),
}
}
}
impl std::error::Error for PkceError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PkcePolicy {
pub plain: bool,
}
impl Default for PkcePolicy {
fn default() -> Self {
Self { plain: false }
}
}
impl PkcePolicy {
pub fn allow_plain() -> Self {
Self { plain: true }
}
pub fn permits(&self, method: PkceMethod) -> bool {
match method {
PkceMethod::S256 => true,
PkceMethod::Plain => self.plain,
}
}
}
pub fn generate_pkce_pair() -> (String, String) {
let mut verifier_bytes = [0u8; 32];
rand::rng().fill(&mut verifier_bytes[..]);
let verifier = URL_SAFE_NO_PAD.encode(verifier_bytes);
let challenge = sha256_base64url(&verifier);
(verifier, challenge)
}
pub fn validate_verifier(code_verifier: &str) -> Result<(), PkceError> {
let len = code_verifier.chars().count();
if !(MIN_VERIFIER_LEN..=MAX_VERIFIER_LEN).contains(&len) {
return Err(PkceError::MalformedVerifier(format!(
"{len} characters, RFC 7636 section 4.1 requires {MIN_VERIFIER_LEN}-{MAX_VERIFIER_LEN}"
)));
}
if let Some(bad) = code_verifier
.chars()
.find(|c| !(c.is_ascii_alphanumeric() || matches!(c, '-' | '.' | '_' | '~')))
{
return Err(PkceError::MalformedVerifier(format!(
"{bad:?} is not in the unreserved set [A-Za-z0-9-._~]"
)));
}
Ok(())
}
pub fn verify(
code_verifier: &str,
stored_challenge: &str,
method: PkceMethod,
policy: &PkcePolicy,
) -> Result<(), PkceError> {
if !policy.permits(method) {
return Err(PkceError::MethodNotPermitted { method });
}
validate_verifier(code_verifier)?;
match method {
PkceMethod::S256 => {
let computed = sha256_base64url(code_verifier);
if constant_time_eq(computed.as_bytes(), stored_challenge.as_bytes()) {
Ok(())
} else {
Err(PkceError::Mismatch)
}
}
PkceMethod::Plain => {
if constant_time_eq(code_verifier.as_bytes(), stored_challenge.as_bytes()) {
Ok(())
} else {
Err(PkceError::Mismatch)
}
}
}
}
pub fn verify_with_default_policy(
code_verifier: &str,
stored_challenge: &str,
method: PkceMethod,
) -> Result<(), PkceError> {
verify(
code_verifier,
stored_challenge,
method,
&PkcePolicy::default(),
)
}
fn sha256_base64url(input: &str) -> String {
let hash = sha2::Sha256::digest(input.as_bytes());
URL_SAFE_NO_PAD.encode(hash)
}
fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
if a.len() != b.len() {
return false;
}
a.iter()
.zip(b.iter())
.fold(0u8, |acc, (x, y)| acc | (x ^ y))
== 0
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
const RFC7636_VERIFIER: &str = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
const RFC7636_CHALLENGE: &str = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM";
#[test]
fn rfc7636_appendix_b_vector() {
assert!(
verify_with_default_policy(RFC7636_VERIFIER, RFC7636_CHALLENGE, PkceMethod::S256)
.is_ok()
);
}
#[test]
fn generate_pkce_pair_deterministic_challenge() {
let (v, c) = generate_pkce_pair();
let expected = sha256_base64url(&v);
assert_eq!(c, expected);
validate_verifier(&v).expect("generated verifier is well formed");
assert_eq!(v.chars().count(), MIN_VERIFIER_LEN);
}
#[test]
fn s256_correct() {
let (verifier, challenge) = generate_pkce_pair();
assert!(verify_with_default_policy(&verifier, &challenge, PkceMethod::S256).is_ok());
}
#[test]
fn s256_wrong_verifier_is_a_mismatch_not_a_client_bug() {
let (verifier, challenge) = generate_pkce_pair();
let other = format!("{verifier}X");
assert_eq!(
verify_with_default_policy(&other, &challenge, PkceMethod::S256),
Err(PkceError::Mismatch)
);
}
#[test]
fn plain_is_refused_by_default_even_when_it_would_match() {
assert_eq!(
verify_with_default_policy(RFC7636_VERIFIER, RFC7636_VERIFIER, PkceMethod::Plain),
Err(PkceError::MethodNotPermitted {
method: PkceMethod::Plain
})
);
}
#[test]
fn plain_is_available_only_through_an_explicit_policy() {
let policy = PkcePolicy::allow_plain();
assert!(
verify(
RFC7636_VERIFIER,
RFC7636_VERIFIER,
PkceMethod::Plain,
&policy
)
.is_ok(),
"a deployment that knowingly accepts plain can still do so"
);
}
#[test]
fn a_misspelled_method_is_an_error_not_a_mismatch() {
for typo in ["s256", "S-256", "S256 ", "sha256", ""] {
assert_eq!(
PkceMethod::parse(typo),
Err(PkceError::UnknownMethod(typo.to_string())),
"{typo:?} must not parse as a method"
);
}
assert_eq!(PkceMethod::parse("S256"), Ok(PkceMethod::S256));
assert_eq!(PkceMethod::parse("plain"), Ok(PkceMethod::Plain));
}
#[test]
fn verifier_syntax_is_enforced_per_rfc7636_4_1() {
assert!(validate_verifier(RFC7636_VERIFIER).is_ok());
assert!(matches!(
validate_verifier("too-short"),
Err(PkceError::MalformedVerifier(_))
));
assert!(matches!(
validate_verifier(&"a".repeat(MAX_VERIFIER_LEN + 1)),
Err(PkceError::MalformedVerifier(_))
));
assert!(matches!(
validate_verifier(&"a".repeat(MAX_VERIFIER_LEN)),
Ok(())
));
assert!(matches!(
validate_verifier(&"a".repeat(42).replace('a', "+")),
Err(PkceError::MalformedVerifier(_))
));
assert!(matches!(
verify_with_default_policy("short", RFC7636_CHALLENGE, PkceMethod::S256),
Err(PkceError::MalformedVerifier(_))
));
}
}