egg_mode/auth/raw.rs
1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4
5//! Internal mechanisms for the `auth` module.
6
7use std::borrow::Cow;
8use std::collections::BTreeMap;
9use std::fmt;
10use std::time::{SystemTime, UNIX_EPOCH};
11
12use base64;
13use hmac::{Hmac, Mac, NewMac};
14use hyper::header::{AUTHORIZATION, CONTENT_TYPE};
15use hyper::{Body, Method, Request};
16use rand::{self, Rng};
17use sha1::Sha1;
18
19use crate::common::*;
20
21use super::{KeyPair, Token};
22
23// n.b. this type is exported in `raw::auth` - these docs are public!
24/// Builder struct to assemble and sign an API request.
25///
26/// For more information about how to use this type and about building requests manually, see [the
27/// module docs](index.html).
28pub struct RequestBuilder<'a> {
29 base_uri: &'a str,
30 method: Method,
31 params: Option<ParamList>,
32 query: Option<String>,
33 body: Option<(Body, &'static str)>,
34 addon: OAuthAddOn,
35}
36
37impl<'a> RequestBuilder<'a> {
38 /// Creates a new `RequestBuilder` with the given HTTP method and base URL.
39 pub fn new(method: Method, base_uri: &'a str) -> Self {
40 RequestBuilder {
41 base_uri,
42 method,
43 params: None,
44 query: None,
45 body: None,
46 addon: OAuthAddOn::None,
47 }
48 }
49
50 /// Adds the given parameters as a query string. Parameters given this way will be included in
51 /// the OAuth signature.
52 ///
53 /// Note that functions that take a `ParamList` accumulate parameters as part of the OAuth
54 /// signature. If you call both `with_query_params` and `with_body_params`, both sets of
55 /// parameters will be used as part of the OAuth signature.
56 ///
57 /// On the other hand, the query string is not cumulative. If you call `with_query_params`
58 /// multiple times, only the last set of parameters will actually be considered part of the
59 /// query string.
60 pub fn with_query_params(self, params: &ParamList) -> Self {
61 let total_params = if let Some(mut my_params) = self.params {
62 my_params.combine(params.clone());
63 my_params
64 } else {
65 params.clone()
66 };
67 RequestBuilder {
68 query: Some(params.to_urlencoded()),
69 params: Some(total_params),
70 ..self
71 }
72 }
73
74 /// Adds the given params as a request body, formatted as `application/x-www-form-urlencoded`.
75 /// Parameters given this way will be included in the OAuth signature.
76 ///
77 /// Note that functions that take a `ParamList` accumulate parameters as part of the OAuth
78 /// signature. If you call both `with_query_params` and `with_body_params`, both sets of
79 /// parameters will be used as part of the OAuth signature.
80 ///
81 /// Note that the functions that specify a request body each overwrite the body. For example,
82 /// if you specify `with_body_params` and also `with_body_json`, only the one you call last
83 /// will be sent with the request.
84 pub fn with_body_params(self, params: &ParamList) -> Self {
85 let total_params = if let Some(mut my_params) = self.params {
86 my_params.combine(params.clone());
87 my_params
88 } else {
89 params.clone()
90 };
91 RequestBuilder {
92 body: Some((
93 Body::from(params.to_urlencoded()),
94 "application/x-www-form-urlencoded",
95 )),
96 params: Some(total_params),
97 ..self
98 }
99 }
100
101 /// Includes the given data as the request body, formatted as JSON. Data given this way will
102 /// *not* be included in the OAuth signature.
103 ///
104 /// Note that the functions that specify a request body each overwrite the body. For example,
105 /// if you specify `with_body_params` and also `with_body_json`, only the one you call last
106 /// will be sent with the request.
107 pub fn with_body_json(self, body: impl serde::Serialize) -> Self {
108 self.with_body(
109 serde_json::to_string(&body).unwrap(),
110 "application/json; charset=UTF-8",
111 )
112 }
113
114 /// Includes the given data as the request body, with the given content type. Data given this
115 /// way will *not* be included in the OAuth signature.
116 ///
117 /// Note that the functions that specify a request body each overwrite the body. For example,
118 /// if you specify `with_body_params` and also `with_body`, only the one you call last will be
119 /// sent with the request.
120 pub fn with_body(self, body: impl Into<Body>, content: &'static str) -> Self {
121 RequestBuilder {
122 body: Some((body.into(), content)),
123 ..self
124 }
125 }
126
127 /// Includes the given OAuth Callback into the OAuth parameters.
128 ///
129 /// Note that `oauth_callback` and `oauth_verifier` are mutually exclusive. If you specify both
130 /// on the same request, only the last one will be sent.
131 pub fn oauth_callback(self, callback: impl Into<String>) -> Self {
132 RequestBuilder {
133 addon: OAuthAddOn::Callback(callback.into()),
134 ..self
135 }
136 }
137
138 /// Includes the given OAuth Verifier into the OAuth parameters.
139 ///
140 /// Note that `oauth_callback` and `oauth_verifier` are mutually exclusive. If you specify both
141 /// on the same request, only the last one will be sent.
142 pub fn oauth_verifier(self, verifier: impl Into<String>) -> Self {
143 RequestBuilder {
144 addon: OAuthAddOn::Verifier(verifier.into()),
145 ..self
146 }
147 }
148
149 /// Formats this `RequestBuilder` into a complete `Request`, signing it with the given keys.
150 ///
151 /// While the `token` parameter is an Option here, it should only be `None` when generating a
152 /// request token; all other calls must have two sets of keys (or be authenticated in a
153 /// different way, i.e. a Bearer token).
154 pub fn request_keys(self, consumer_key: &KeyPair, token: Option<&KeyPair>) -> Request<Body> {
155 let oauth = OAuthParams::from_keys(consumer_key.clone(), token.cloned())
156 .with_addon(self.addon.clone())
157 .sign_request(self.method.clone(), self.base_uri, self.params.as_ref());
158 self.request_authorization(oauth.to_string())
159 }
160
161 /// Formats this `RequestBuilder` into a complete `Request`, signing it with the given token.
162 ///
163 /// If the given `Token` is an Access token, the request will be signed using OAuth 1.0a, using
164 /// the given URI, HTTP method, and parameters to create a signature.
165 ///
166 /// If the given `Token` is a Bearer token, the request will be authenticated using OAuth 2.0,
167 /// specifying the given Bearer token as authorization.
168 pub fn request_token(self, token: &Token) -> Request<Body> {
169 match token {
170 Token::Access { consumer, access } => self.request_keys(consumer, Some(access)),
171 Token::Bearer(bearer) => self.request_authorization(format!("Bearer {}", bearer)),
172 }
173 }
174
175 /// Formats this `RequestBuilder` into a complete `Request`, with an Authorization header
176 /// formatted using HTTP Basic authentication using the given consumer key, as expected by the
177 /// `POST oauth2/token` endpoint.
178 ///
179 /// This Authorization should only be used when requesting a Bearer token; other requests need
180 /// to be signed with multiple keys (as with `request_keys` or giving an Access token to
181 /// `request_token`) or with a proper Bearer token given to `request_token`.
182 ///
183 /// This authorization can also be used to access Enterprise API endpoints that require Basic
184 /// authentication, using a `KeyPair` with the email address and password that would ordinarily
185 /// be used to access the Enterprise API Console.
186 pub fn request_consumer_bearer(self, consumer_key: &KeyPair) -> Request<Body> {
187 self.request_authorization(bearer_request(consumer_key))
188 }
189
190 /// Assembles the final `Request` with the given Authorization header. This is private to
191 /// require that a well-formed header is constructed given, as constructed from the other
192 /// `request_*` methods.
193 fn request_authorization(self, authorization: String) -> Request<Body> {
194 let full_url = if let Some(query) = self.query {
195 format!("{}?{}", self.base_uri, query)
196 } else {
197 self.base_uri.to_string()
198 };
199 let request = Request::builder()
200 .method(self.method)
201 .uri(full_url)
202 .header(AUTHORIZATION, authorization);
203
204 if let Some((body, content)) = self.body {
205 request.header(CONTENT_TYPE, content).body(body).unwrap()
206 } else {
207 request.body(Body::empty()).unwrap()
208 }
209 }
210}
211
212/// OAuth header set used to create an OAuth signature.
213#[derive(Clone, Debug)]
214struct OAuthParams {
215 /// The consumer key that represents the app making the API request.
216 consumer_key: KeyPair,
217 /// The token that represents the user authorizing the request (or the access request
218 /// representing a user authorizing the app).
219 token: Option<KeyPair>,
220 /// A random token representing the request itself. Used to de-duplicate requests on Twitter's
221 /// end.
222 nonce: String,
223 /// A Unix timestamp for when the request was created.
224 timestamp: u64,
225 /// A callback or verifier parameter, if necessary.
226 addon: OAuthAddOn,
227}
228
229impl OAuthParams {
230 /// Creates an empty `OAuthParams` header with a new `timestamp` and `nonce`.
231 ///
232 /// **Note**: This should only be used as part of another constructor that populates the tokens!
233 /// Attempting to sign a request with an empty consumer and access token will result in an
234 /// invalid request.
235 fn empty() -> OAuthParams {
236 let timestamp = match SystemTime::now().duration_since(UNIX_EPOCH) {
237 Ok(dur) => dur,
238 Err(err) => err.duration(),
239 }
240 .as_secs();
241 let mut rng = rand::thread_rng();
242 let nonce = ::std::iter::repeat(())
243 .map(|()| rng.sample(rand::distributions::Alphanumeric))
244 .map(char::from)
245 .take(32)
246 .collect::<String>();
247 OAuthParams {
248 consumer_key: KeyPair::empty(),
249 token: None,
250 nonce,
251 timestamp,
252 addon: OAuthAddOn::None,
253 }
254 }
255
256 /// Creates a new `OAuthParams` header with the given keys. The `token` is optional
257 /// specifically for when you're generating a request token; otherwise it should be the request
258 /// token (for when you're generating an access token) or an access token (for when you're
259 /// requesting a regular API function).
260 fn from_keys(consumer_key: KeyPair, token: Option<KeyPair>) -> OAuthParams {
261 OAuthParams {
262 consumer_key,
263 token,
264 ..OAuthParams::empty()
265 }
266 }
267
268 /// Adds the given callback or verifier to this `OAuthParams` header.
269 fn with_addon(self, addon: OAuthAddOn) -> OAuthParams {
270 OAuthParams { addon, ..self }
271 }
272
273 /// Uses the parameters in this `OAuthParams` instance to generate a signature for the given
274 /// request, returning it as a `SignedHeader`.
275 fn sign_request(self, method: Method, uri: &str, params: Option<&ParamList>) -> SignedHeader {
276 let query_string = {
277 let sig_params = params
278 .cloned()
279 .unwrap_or_default()
280 .add_param("oauth_consumer_key", self.consumer_key.key.clone())
281 .add_param("oauth_nonce", self.nonce.clone())
282 .add_param("oauth_signature_method", "HMAC-SHA1")
283 .add_param("oauth_timestamp", format!("{}", self.timestamp.clone()))
284 .add_param("oauth_version", "1.0")
285 .add_opt_param("oauth_token", self.token.clone().map(|k| k.key))
286 .add_opt_param(
287 "oauth_callback",
288 self.addon.as_callback().map(|s| s.to_string()),
289 )
290 .add_opt_param(
291 "oauth_verifier",
292 self.addon.as_verifier().map(|s| s.to_string()),
293 );
294
295 let mut query = sig_params
296 .iter()
297 .map(|(k, v)| format!("{}={}", percent_encode(k), percent_encode(v)))
298 .collect::<Vec<_>>();
299 query.sort();
300
301 query.join("&")
302 };
303
304 let base_str = format!(
305 "{}&{}&{}",
306 percent_encode(method.as_str()),
307 percent_encode(uri),
308 percent_encode(&query_string)
309 );
310 let key = format!(
311 "{}&{}",
312 percent_encode(&self.consumer_key.secret),
313 percent_encode(&self.token.as_ref().unwrap_or(&KeyPair::new("", "")).secret)
314 );
315
316 // TODO check if key is correct length? Can this fail?
317 let mut digest = Hmac::<Sha1>::new_from_slice(key.as_bytes()).expect("Wrong key length");
318 digest.update(base_str.as_bytes());
319
320 let mut params: BTreeMap<&'static str, Cow<'static, str>> = BTreeMap::new();
321 params.insert("oauth_signature_method", "HMAC-SHA1".into());
322 params.insert("oauth_version", "1.0".into());
323
324 params.insert("oauth_consumer_key", self.consumer_key.key);
325 if let Some(token) = self.token {
326 params.insert("oauth_token", token.key);
327 }
328
329 params.insert("oauth_nonce", self.nonce.into());
330 params.insert("oauth_timestamp", self.timestamp.to_string().into());
331
332 match self.addon {
333 OAuthAddOn::Callback(c) => {
334 params.insert("oauth_callback", c.into());
335 }
336 OAuthAddOn::Verifier(v) => {
337 params.insert("oauth_verifier", v.into());
338 }
339 OAuthAddOn::None => (),
340 }
341
342 params.insert(
343 "oauth_signature",
344 base64::encode(&digest.finalize().into_bytes()).into(),
345 );
346
347 SignedHeader { params }
348 }
349}
350
351/// Represents an "addon" to an OAuth header.
352#[derive(Clone, Debug)]
353enum OAuthAddOn {
354 /// An `oauth_callback` parameter, used when generating a request token.
355 Callback(String),
356 /// An `oauth_verifier` parameter, used when generating an access token.
357 Verifier(String),
358 /// Neither an `oauth_callback` nor an `oauth_verifier` parameter are present in this header.
359 /// This is the default used when signing a regular API request.
360 None,
361}
362
363impl OAuthAddOn {
364 /// Returns the `oauth_callback` parameter, if present.
365 fn as_callback(&self) -> Option<&str> {
366 match self {
367 OAuthAddOn::Callback(c) => Some(c),
368 _ => None,
369 }
370 }
371
372 /// Returns the `oauth_verifier` parameter, if present.
373 fn as_verifier(&self) -> Option<&str> {
374 match self {
375 OAuthAddOn::Verifier(v) => Some(v),
376 _ => None,
377 }
378 }
379}
380
381/// A set of `OAuthParams` parameters combined with a request signature, ready to be attached to a
382/// request.
383struct SignedHeader {
384 /// The OAuth parameters used to create the signature.
385 params: BTreeMap<&'static str, Cow<'static, str>>,
386}
387
388/// The `Display` impl for `SignedHeader` formats it as an `Authorization` header for an HTTP
389/// request.
390impl fmt::Display for SignedHeader {
391 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
392 // authorization scheme
393 write!(f, "OAuth ")?;
394
395 // authorization data
396
397 let mut first = true;
398 for (k, v) in &self.params {
399 if first {
400 first = false;
401 } else {
402 write!(f, ", ")?;
403 }
404
405 write!(f, "{}=\"{}\"", k, percent_encode(v))?;
406 }
407
408 Ok(())
409 }
410}
411
412/// Creates a basic `Authorization` header based on the given consumer token.
413///
414/// The authorization created by this function can only be used with requests to generate or
415/// invalidate a bearer token. Using this authorization with any other endpoint will result in an
416/// invalid request.
417fn bearer_request(con_token: &KeyPair) -> String {
418 let text = format!("{}:{}", con_token.key, con_token.secret);
419 format!("Basic {}", base64::encode(&text))
420}
421
422// n.b. this function is re-exported in the `raw` module - these docs are public!
423/// Assemble a signed GET request to the given URL with the given parameters.
424///
425/// The given parameters, if present, will be appended to the given `uri` as a percent-encoded
426/// query string. If the given `token` is not a Bearer token, the parameters will also be used to
427/// create the OAuth signature.
428pub fn get(uri: &str, token: &Token, params: Option<&ParamList>) -> Request<Body> {
429 let mut request = RequestBuilder::new(Method::GET, uri);
430 if let Some(params) = params {
431 request = request.with_query_params(params);
432 }
433 request.request_token(token)
434}
435
436// n.b. this function is re-exported in the `raw` module - these docs are public!
437/// Assemble a signed DELETE request to the given URL with the given parameters.
438///
439/// The given parameters, if present, will be appended to the given `uri` as a percent-encoded
440/// query string. If the given `token` is not a Bearer token, the parameters will also be used to
441/// create the OAuth signature.
442pub fn delete(uri: &str, token: &Token, params: Option<&ParamList>) -> Request<Body> {
443 let mut request = RequestBuilder::new(Method::DELETE, uri);
444 if let Some(params) = params {
445 request = request.with_query_params(params);
446 }
447 request.request_token(token)
448}
449
450// n.b. this function is re-exported in the `raw` module - these docs are public!
451/// Assemble a signed POST request to the given URL with the given parameters.
452///
453/// The given parameters, if present, will be percent-encoded and included in the POST body
454/// formatted with a content-type of `application/x-www-form-urlencoded`. If the given `token` is
455/// not a Bearer token, the parameters will also be used to create the OAuth signature.
456pub fn post(uri: &str, token: &Token, params: Option<&ParamList>) -> Request<Body> {
457 let mut request = RequestBuilder::new(Method::POST, uri);
458 if let Some(params) = params {
459 request = request.with_body_params(params);
460 }
461 request.request_token(token)
462}
463
464// n.b. this function is re-exported in the `raw` module - these docs are public!
465/// Assemble a signed POST request to the given URL with the given JSON body.
466///
467/// This method of building requests allows you to use endpoints that require a request body of
468/// plain text or JSON, like `POST media/metadata/create`. Note that this function does not encode
469/// its parameters into the OAuth signature, so take care if the endpoint you're using lists
470/// parameters as part of its specification.
471pub fn post_json<B: serde::Serialize>(uri: &str, token: &Token, body: B) -> Request<Body> {
472 RequestBuilder::new(Method::POST, uri)
473 .with_body_json(body)
474 .request_token(token)
475}
476
477#[cfg(test)]
478mod tests {
479 use super::bearer_request;
480
481 #[test]
482 fn bearer_header() {
483 let con_key = "xvz1evFS4wEEPTGEFPHBog";
484 let con_secret = "L8qq9PZyRg6ieKGEKhZolGC0vJWLw8iEJ88DRdyOg";
485 let con_token = super::KeyPair::new(con_key, con_secret);
486
487 let output = bearer_request(&con_token);
488
489 assert_eq!(output, "Basic eHZ6MWV2RlM0d0VFUFRHRUZQSEJvZzpMOHFxOVBaeVJnNmllS0dFS2hab2xHQzB2SldMdzhpRUo4OERSZHlPZw==");
490 }
491}