oauth2_types/
pkce.rs

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