oauth2_types/
pkce.rs

1// Copyright 2024 New Vector Ltd.
2// Copyright 2021-2024 The Matrix.org Foundation C.I.C.
3//
4// SPDX-License-Identifier: AGPL-3.0-only
5// Please see LICENSE in the repository root for full details.
6
7//! Types for the [Proof Key for Code Exchange].
8//!
9//! [Proof Key for Code Exchange]: https://www.rfc-editor.org/rfc/rfc7636
10
11use std::borrow::Cow;
12
13use base64ct::{Base64UrlUnpadded, Encoding};
14use mas_iana::oauth::PkceCodeChallengeMethod;
15use serde::{Deserialize, Serialize};
16use sha2::{Digest, Sha256};
17use thiserror::Error;
18
19/// Errors that can occur when verifying a code challenge.
20#[derive(Debug, Error, PartialEq, Eq)]
21pub enum CodeChallengeError {
22    /// The code verifier should be at least 43 characters long.
23    #[error("code_verifier should be at least 43 characters long")]
24    TooShort,
25
26    /// The code verifier should be at most 128 characters long.
27    #[error("code_verifier should be at most 128 characters long")]
28    TooLong,
29
30    /// The code verifier contains invalid characters.
31    #[error("code_verifier contains invalid characters")]
32    InvalidCharacters,
33
34    /// The challenge verification failed.
35    #[error("challenge verification failed")]
36    VerificationFailed,
37
38    /// The challenge method is unsupported.
39    #[error("unknown challenge method")]
40    UnknownChallengeMethod,
41}
42
43fn validate_verifier(verifier: &str) -> Result<(), CodeChallengeError> {
44    if verifier.len() < 43 {
45        return Err(CodeChallengeError::TooShort);
46    }
47
48    if verifier.len() > 128 {
49        return Err(CodeChallengeError::TooLong);
50    }
51
52    if !verifier
53        .chars()
54        .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '.' || c == '_' || c == '~')
55    {
56        return Err(CodeChallengeError::InvalidCharacters);
57    }
58
59    Ok(())
60}
61
62/// Helper trait to compute and verify code challenges.
63pub trait CodeChallengeMethodExt {
64    /// Compute the challenge for a given verifier
65    ///
66    /// # Errors
67    ///
68    /// Returns an error if the verifier did not adhere to the rules defined by
69    /// the RFC in terms of length and allowed characters
70    fn compute_challenge<'a>(&self, verifier: &'a str) -> Result<Cow<'a, str>, CodeChallengeError>;
71
72    /// Verify that a given verifier is valid for the given challenge
73    ///
74    /// # Errors
75    ///
76    /// Returns an error if the verifier did not match the challenge, or if the
77    /// verifier did not adhere to the rules defined by the RFC in terms of
78    /// length and allowed characters
79    fn verify(&self, challenge: &str, verifier: &str) -> Result<(), CodeChallengeError>
80    where
81        Self: Sized,
82    {
83        if self.compute_challenge(verifier)? == challenge {
84            Ok(())
85        } else {
86            Err(CodeChallengeError::VerificationFailed)
87        }
88    }
89}
90
91impl CodeChallengeMethodExt for PkceCodeChallengeMethod {
92    fn compute_challenge<'a>(&self, verifier: &'a str) -> Result<Cow<'a, str>, CodeChallengeError> {
93        validate_verifier(verifier)?;
94
95        let challenge = match self {
96            Self::Plain => verifier.into(),
97            Self::S256 => {
98                let mut hasher = Sha256::new();
99                hasher.update(verifier.as_bytes());
100                let hash = hasher.finalize();
101                let verifier = Base64UrlUnpadded::encode_string(&hash);
102                verifier.into()
103            }
104            _ => return Err(CodeChallengeError::UnknownChallengeMethod),
105        };
106
107        Ok(challenge)
108    }
109}
110
111/// The code challenge data added to an authorization request.
112#[derive(Clone, Serialize, Deserialize)]
113pub struct AuthorizationRequest {
114    /// The code challenge method.
115    pub code_challenge_method: PkceCodeChallengeMethod,
116
117    /// The code challenge computed from the verifier and the method.
118    pub code_challenge: String,
119}
120
121/// The code challenge data added to a token request.
122#[derive(Clone, Serialize, Deserialize)]
123pub struct TokenRequest {
124    /// The code challenge verifier.
125    pub code_challenge_verifier: String,
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    #[test]
133    fn test_pkce_verification() {
134        use PkceCodeChallengeMethod::{Plain, S256};
135        // This challenge comes from the RFC7636 appendices
136        let challenge = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM";
137
138        assert!(
139            S256.verify(challenge, "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk")
140                .is_ok()
141        );
142
143        assert!(Plain.verify(challenge, challenge).is_ok());
144
145        assert_eq!(
146            S256.verify(challenge, "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"),
147            Err(CodeChallengeError::VerificationFailed),
148        );
149
150        assert_eq!(
151            S256.verify(challenge, "tooshort"),
152            Err(CodeChallengeError::TooShort),
153        );
154
155        assert_eq!(
156            S256.verify(challenge, "toolongtoolongtoolongtoolongtoolongtoolongtoolongtoolongtoolongtoolongtoolongtoolongtoolongtoolongtoolongtoolongtoolongtoolongtoolong"),
157            Err(CodeChallengeError::TooLong),
158        );
159
160        assert_eq!(
161            S256.verify(
162                challenge,
163                "this is long enough but has invalid characters in it"
164            ),
165            Err(CodeChallengeError::InvalidCharacters),
166        );
167    }
168}