oxide_auth/code_grant/extensions/
pkce.rs

1use std::borrow::Cow;
2
3use crate::primitives::grant::{GrantExtension, Value};
4
5use base64::{self, engine::general_purpose::URL_SAFE_NO_PAD, Engine};
6use sha2::{Digest, Sha256};
7use subtle::ConstantTimeEq;
8
9/// Proof Key for Code Exchange by OAuth Public Clients
10///
11/// > Auth 2.0 public clients utilizing the Authorization Code Grant are
12/// susceptible to the authorization code interception attack.  This
13/// specification describes the attack as well as a technique to mitigate
14/// against the threat through the use of Proof Key for Code Exchange
15/// (PKCE, pronounced "pixy").
16///
17/// (from the respective [RFC 7636])
18///
19/// In short, public clients share a verifier for a secret token when requesting their initial
20/// authorization code. When they then make a second request to the autorization server, trading
21/// this code for an access token, they can credible assure the server of their identity by
22/// presenting the secret token.
23///
24/// The simple `plain` method only prevents attackers unable to snoop on the connection from
25/// impersonating the client, while the `S256` method, which uses one-way hash functions, makes
26/// any attack short of reading the victim client's memory infeasible.
27///
28/// Support for the `plain` method is OPTIONAL and must be turned on explicitely.
29///
30/// [RFC 7636]: https://tools.ietf.org/html/rfc7636
31pub struct Pkce {
32    required: bool,
33    allow_plain: bool,
34}
35
36enum Method {
37    Plain(String),
38    Sha256(String),
39}
40
41impl Pkce {
42    /// A pkce extensions which requires clients to use it.
43    pub fn required() -> Pkce {
44        Pkce {
45            required: true,
46            allow_plain: false,
47        }
48    }
49
50    /// Pkce extension which will check verifiers if present but not require them.
51    pub fn optional() -> Pkce {
52        Pkce {
53            required: false,
54            allow_plain: false,
55        }
56    }
57
58    /// Allow usage of the less secure `plain` verification method. This method is NOT secure
59    /// an eavesdropping attacker such as rogue processes capturing a devices requests.
60    pub fn allow_plain(&mut self) {
61        self.allow_plain = true;
62    }
63
64    /// Create the encoded method for proposed method and challenge.
65    ///
66    /// The method defaults to `plain` when none is given, effectively offering increased
67    /// compatibility but less security. Support for `plain` is optional and needs to be enabled
68    /// explicitely through `Pkce::allow_plain`. This extension may also require clients to use it,
69    /// in which case giving no challenge also leads to an error.
70    ///
71    /// The resulting string MUST NOT be publicly available to the client. Otherwise, it would be
72    /// trivial for a third party to impersonate the client in the access token request phase. For
73    /// a SHA256 methods the results would not be quite as severe but still bad practice.
74    pub fn challenge(
75        &self, method: Option<Cow<str>>, challenge: Option<Cow<str>>,
76    ) -> Result<Option<Value>, ()> {
77        let method = method.unwrap_or(Cow::Borrowed("plain"));
78
79        let challenge = match challenge {
80            None if self.required => return Err(()),
81            None => return Ok(None),
82            Some(challenge) => challenge,
83        };
84
85        let method = Method::from_parameter(method, challenge)?;
86        let method = method.assert_supported_method(self.allow_plain)?;
87
88        Ok(Some(Value::private(Some(method.encode()))))
89    }
90
91    /// Verify against the encoded challenge.
92    ///
93    /// When the challenge is required, ensure again that a challenge was made and a corresponding
94    /// method data is present as an extension. This is not strictly necessary since clients should
95    /// not be able to delete private extension data but this check does not cost a lot.
96    ///
97    /// When a challenge was agreed upon but no verifier is present, this method will return an
98    /// error.
99    pub fn verify(&self, method: Option<Value>, verifier: Option<Cow<str>>) -> Result<(), ()> {
100        let (method, verifier) = match (method, verifier) {
101            (None, _) if self.required => return Err(()),
102            (None, _) => return Ok(()),
103            // An internal saved method but no verifier
104            (Some(_), None) => return Err(()),
105            (Some(method), Some(verifier)) => (method, verifier),
106        };
107
108        let method = match method.into_private_value() {
109            Ok(Some(method)) => method,
110            _ => return Err(()),
111        };
112
113        let method = Method::from_encoded(Cow::Owned(method))?;
114
115        method.verify(&verifier)
116    }
117}
118
119impl GrantExtension for Pkce {
120    fn identifier(&self) -> &'static str {
121        "pkce"
122    }
123}
124
125/// Base 64 encoding without padding
126fn b64encode(data: &[u8]) -> String {
127    URL_SAFE_NO_PAD.encode(data)
128}
129
130impl Method {
131    fn from_parameter(method: Cow<str>, challenge: Cow<str>) -> Result<Self, ()> {
132        match method.as_ref() {
133            "plain" => Ok(Method::Plain(challenge.into_owned())),
134            "S256" => Ok(Method::Sha256(challenge.into_owned())),
135            _ => Err(()),
136        }
137    }
138
139    fn assert_supported_method(self, allow_plain: bool) -> Result<Self, ()> {
140        match (self, allow_plain) {
141            (this, true) => Ok(this),
142            (Method::Sha256(content), false) => Ok(Method::Sha256(content)),
143            (Method::Plain(_), false) => Err(()),
144        }
145    }
146
147    fn encode(self) -> String {
148        match self {
149            Method::Plain(challenge) => challenge + "p",
150            Method::Sha256(challenge) => challenge + "S",
151        }
152    }
153
154    fn from_encoded(encoded: Cow<str>) -> Result<Method, ()> {
155        // TODO: avoid allocation in case of borrow and invalid.
156        let mut encoded = encoded.into_owned();
157        match encoded.pop() {
158            None => Err(()),
159            Some('p') => Ok(Method::Plain(encoded)),
160            Some('S') => Ok(Method::Sha256(encoded)),
161            _ => Err(()),
162        }
163    }
164
165    fn verify(&self, verifier: &str) -> Result<(), ()> {
166        match self {
167            Method::Plain(encoded) => {
168                if encoded.as_bytes().ct_eq(verifier.as_bytes()).into() {
169                    Ok(())
170                } else {
171                    Err(())
172                }
173            }
174            Method::Sha256(encoded) => {
175                let mut hasher = Sha256::new();
176                hasher.update(verifier.as_bytes());
177                let b64digest = b64encode(&hasher.finalize());
178                if encoded.as_bytes().ct_eq(b64digest.as_bytes()).into() {
179                    Ok(())
180                } else {
181                    Err(())
182                }
183            }
184        }
185    }
186}