use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
use serde::{Deserialize, Serialize};
pub const PKCE_METHOD: &str = "S256";
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Pkce {
verifier: String,
challenge: String,
}
impl Default for Pkce {
#[inline]
fn default() -> Self {
Self::new()
}
}
impl Pkce {
pub fn new() -> Self {
Self::from_verifier(random_urlsafe(32))
}
fn from_verifier(verifier: String) -> Self {
let digest = aws_lc_rs::digest::digest(&aws_lc_rs::digest::SHA256, verifier.as_bytes());
let challenge = URL_SAFE_NO_PAD.encode(digest.as_ref());
Self {
verifier,
challenge,
}
}
#[inline]
pub fn verifier(&self) -> &str {
&self.verifier
}
#[inline]
pub fn challenge(&self) -> &str {
&self.challenge
}
}
impl std::fmt::Debug for Pkce {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Pkce")
.field("verifier", &"[redacted]")
.field("challenge", &self.challenge)
.finish()
}
}
pub(crate) fn random_urlsafe(len: usize) -> String {
let mut bytes = vec![0u8; len];
aws_lc_rs::rand::fill(&mut bytes).expect("system RNG failure");
URL_SAFE_NO_PAD.encode(bytes)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_computes_the_rfc7636_reference_challenge() {
let pkce = Pkce::from_verifier("dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk".into());
assert_eq!(
pkce.challenge(),
"E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"
);
}
#[test]
fn it_generates_unique_spec_compliant_verifiers() {
let (a, b) = (Pkce::new(), Pkce::new());
assert_ne!(a.verifier(), b.verifier());
assert_eq!(a.verifier().len(), 43);
assert!(
a.verifier()
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_'))
);
}
#[test]
fn it_redacts_the_verifier_in_debug_output() {
let pkce = Pkce::new();
let debug = format!("{pkce:?}");
assert!(!debug.contains(pkce.verifier()));
assert!(debug.contains(pkce.challenge()));
}
}