Skip to main content

yapi_ai/auth/
pkce.rs

1//! Proof Key for Code Exchange (PKCE, RFC 7636) verifier and S256 challenge.
2//! Port of `oauth/pkce.ts`.
3
4use aws_lc_rs::digest::{SHA256, digest};
5use base64::Engine as _;
6use base64::engine::general_purpose::URL_SAFE_NO_PAD;
7
8use super::AuthError;
9
10/// A code verifier and its challenge.
11#[derive(Clone, Debug, PartialEq, Eq)]
12pub struct Pkce {
13    /// Sent with the token exchange.
14    pub verifier: String,
15    /// Sent with the authorization request.
16    pub challenge: String,
17}
18
19/// 32 random bytes, base64url without padding.
20pub fn random_base64url() -> Result<String, AuthError> {
21    yapi_types::time::try_random_bytes::<32>()
22        .map(|bytes| URL_SAFE_NO_PAD.encode(bytes))
23        .map_err(AuthError::random)
24}
25
26/// A fresh verifier from 32 random bytes and its SHA-256 challenge.
27pub fn generate() -> Result<Pkce, AuthError> {
28    let verifier = random_base64url()?;
29    let challenge = challenge(&verifier);
30    Ok(Pkce {
31        verifier,
32        challenge,
33    })
34}
35
36/// The S256 challenge of a verifier.
37pub fn challenge(verifier: &str) -> String {
38    URL_SAFE_NO_PAD.encode(digest(&SHA256, verifier.as_bytes()))
39}
40
41#[cfg(test)]
42mod tests {
43    use super::*;
44
45    #[test]
46    fn matches_rfc_7636_example() {
47        assert_eq!(
48            challenge("dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"),
49            "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"
50        );
51        let pkce = generate().unwrap();
52        assert_eq!(pkce.verifier.len(), 43);
53        assert_eq!(pkce.challenge, challenge(&pkce.verifier));
54    }
55}