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}