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}