Skip to main content

light_openid/
code_challenge.rs

1use crate::utils::crypt_utils::sha256;
2use base64::Engine as _;
3use base64::engine::general_purpose::URL_SAFE_NO_PAD as BASE64_URL_SAFE_NO_PAD;
4use rand::distr::{Alphanumeric, SampleString};
5
6/// Wrapper around the string type that holds a code challenge verifier
7#[derive(Debug, Clone, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
8pub struct CodeChallengeVerifier(pub String);
9
10/// Code challenge, as specified in <https://datatracker.ietf.org/doc/rfc7636/>
11///
12/// See some implementation help in <https://docs.hidglobal.com/activid-as-v8.5/api/openid/leverage-pkce-auth-code-grant-flow.htm>
13#[derive(Debug, Clone, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
14pub struct CodeChallenge {
15    pub code_challenge: String,
16    pub code_challenge_method: String,
17}
18
19impl CodeChallenge {
20    pub fn new_s256() -> (Self, CodeChallengeVerifier) {
21        let code_verifier = Alphanumeric.sample_string(&mut rand::rng(), 70);
22        (
23            Self {
24                code_challenge: BASE64_URL_SAFE_NO_PAD.encode(sha256(code_verifier.as_bytes())),
25                code_challenge_method: "S256".to_string(),
26            },
27            CodeChallengeVerifier(code_verifier),
28        )
29    }
30
31    pub fn verify_code(&self, verifier: &CodeChallengeVerifier) -> bool {
32        // RFC 7636 - Section 4.1 - Client Creates a Code Verifier
33        if verifier.0.len() < 43 || verifier.0.len() > 128 {
34            tracing::error!("Invalid code verifier len ({})!", verifier.0.len());
35            return false;
36        }
37
38        match self.code_challenge_method.as_str() {
39            "plain" => verifier.0.eq(&self.code_challenge),
40            "S256" => {
41                let encoded = BASE64_URL_SAFE_NO_PAD.encode(sha256(verifier.0.as_bytes()));
42
43                encoded.eq(&self.code_challenge)
44            }
45            s => {
46                tracing::error!("Unknown code challenge method: {s}");
47                false
48            }
49        }
50    }
51}
52
53#[cfg(test)]
54mod test {
55    use crate::code_challenge::{CodeChallenge, CodeChallengeVerifier};
56
57    #[test]
58    fn test_plain() {
59        let chal = CodeChallenge {
60            code_challenge_method: "plain".to_string(),
61            code_challenge: "cho9ohv9fuG9ongei9eegaicoungoh0tuna0pohr8thekaing4".to_string(),
62        };
63
64        assert!(chal.verify_code(&CodeChallengeVerifier(
65            "cho9ohv9fuG9ongei9eegaicoungoh0tuna0pohr8thekaing4".into()
66        )));
67        assert!(!chal.verify_code(&CodeChallengeVerifier(
68            "niz8pha4gu0faibahtahghuofoom1yaagh5ipaiWicei2Roo6u".into()
69        )));
70    }
71
72    #[test]
73    fn test_s256() {
74        let chal = CodeChallenge {
75            code_challenge_method: "S256".to_string(),
76            code_challenge: "uSOvC48D8TMh6RgW-36XppMlMgys-6KAE_wEIev9W2g".to_string(),
77        };
78
79        assert!(chal.verify_code(&CodeChallengeVerifier("HIwht3lCHfnsruA+7Sq8NP2mPj5cBZe0Ewf23eK9UQhK4TdCIt3SK7Fr/giCdnfjxYQILOPG2D562emggAa2lA==".into())));
80        assert!(!chal.verify_code(&CodeChallengeVerifier("text1".into())));
81    }
82
83    #[test]
84    fn test_s256_2() {
85        let chal = CodeChallenge {
86            code_challenge_method: "S256".to_string(),
87            code_challenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM".to_string(),
88        };
89
90        assert!(chal.verify_code(&CodeChallengeVerifier(
91            "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk".into()
92        )));
93        assert!(!chal.verify_code(&CodeChallengeVerifier("text1".into())));
94    }
95}