oauth1_header/
lib.rs

1//! Generate OAuth 1.0 Authorization headers.
2//!
3//! # Example
4//!
5//! ```
6//! use oauth1_header::http::Method;
7//! use oauth1_header::Credentials;
8//! use std::collections::HashMap;
9//!
10//! let mut params = HashMap::new();
11//! params.insert("foo", "bar");
12//! let credentials = Credentials::new(
13//!     "some-consumer-key",
14//!     "some-consumer-secret",
15//!     "some-token",
16//!     "some-token-secret",
17//! );
18//! let header_value = credentials.auth(&Method::GET, "https://example.com", &params);
19//! ```
20//!
21//! Where `header_value` will contain the [OAuth Protocol Parameters][oauth_pp]
22//! ready to be sent in the HTTP Authorization header.
23//!
24//! [oauth_pp]: https://oauth.net/core/1.0a/#auth_header_authorization
25
26#![warn(missing_docs)]
27
28use hmac::{Hmac, Mac};
29pub use http;
30use http::Method;
31use percent_encoding::{utf8_percent_encode, AsciiSet, NON_ALPHANUMERIC};
32use rand::distributions::Alphanumeric;
33use rand::{thread_rng, Rng};
34use sha1::Sha1;
35use std::collections::HashMap;
36use std::fmt;
37use std::hash::BuildHasher;
38use std::time::{SystemTime, UNIX_EPOCH};
39
40/// The consumer key, consumer secret, token and token secret used in OAuth 1.0.
41#[derive(Debug)]
42pub struct Credentials<'a> {
43    consumer_key: &'a str,
44    consumer_secret: &'a str,
45    token: &'a str,
46    token_secret: &'a str,
47}
48
49impl<'a> Credentials<'a> {
50    /// Creates a new `Credentials` struct.
51    pub fn new(
52        consumer_key: &'a str,
53        consumer_secret: &'a str,
54        token: &'a str,
55        token_secret: &'a str,
56    ) -> Credentials<'a> {
57        Credentials {
58            consumer_key,
59            consumer_secret,
60            token,
61            token_secret,
62        }
63    }
64
65    /// Returns the OAuth Protocol Parameters which will be used as the value
66    /// of the HTTP Authorization header.
67    pub fn auth<S: BuildHasher>(
68        &self,
69        method: &Method,
70        base_url: &str,
71        params: &HashMap<&str, &str, S>,
72    ) -> String {
73        let nonce_string: String = thread_rng().sample_iter(Alphanumeric).take(32).collect();
74        let oauth_header = OAuthHeader {
75            credentials: self,
76            nonce: nonce_string.as_str(),
77            signature_method: "HMAC-SHA1",
78            timestamp: current_timestamp(),
79            version: "1.0",
80            method,
81            base_url,
82            params,
83        };
84        oauth_header.to_string()
85    }
86}
87
88struct OAuthHeader<'a, S: BuildHasher> {
89    credentials: &'a Credentials<'a>,
90    nonce: &'a str,
91    signature_method: &'a str,
92    timestamp: u64,
93    version: &'a str,
94    method: &'a Method,
95    base_url: &'a str,
96    params: &'a HashMap<&'a str, &'a str, S>,
97}
98
99impl<'a, S: BuildHasher> OAuthHeader<'a, S> {
100    fn sign(&self) -> String {
101        let signature_base = format!(
102            "{}&{}&{}",
103            self.method,
104            percent_encode(self.base_url),
105            percent_encode(&self.params_string())
106        );
107        let key = format!(
108            "{}&{}",
109            self.credentials.consumer_secret, self.credentials.token_secret
110        );
111        let mut hmac =
112            Hmac::<Sha1>::new_varkey(key.as_bytes()).expect("HMAC can take keys of any size");
113        hmac.input(signature_base.as_bytes());
114        let signature = hmac.result();
115        percent_encode(&base64::encode(signature.code()))
116    }
117
118    fn params_string(&self) -> String {
119        let timestamp_string = self.timestamp.to_string();
120        let mut oauth_params = HashMap::new();
121        oauth_params.insert("oauth_consumer_key", self.credentials.consumer_key);
122        oauth_params.insert("oauth_nonce", self.nonce);
123        oauth_params.insert("oauth_signature_method", "HMAC-SHA1");
124        oauth_params.insert("oauth_timestamp", timestamp_string.as_str());
125        oauth_params.insert("oauth_token", self.credentials.token);
126        oauth_params.insert("oauth_version", "1.0");
127        let mut params: Vec<String> = oauth_params
128            .iter()
129            .chain(self.params.iter())
130            .map({ |(&k, &v)| format!("{}={}", percent_encode(k), percent_encode(v)) })
131            .collect();
132        params.sort();
133        params.join("&")
134    }
135}
136
137impl<'a, S: BuildHasher> fmt::Display for OAuthHeader<'a, S> {
138    fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
139        write!(
140            formatter,
141            "OAuth oauth_consumer_key=\"{}\", oauth_nonce=\"{}\", oauth_signature=\"{}\", \
142                oauth_signature_method=\"{}\", oauth_timestamp=\"{}\", oauth_token=\"{}\", \
143                oauth_version=\"{}\"",
144            self.credentials.consumer_key,
145            self.nonce,
146            self.sign(),
147            self.signature_method,
148            self.timestamp,
149            self.credentials.token,
150            self.version
151        )
152    }
153}
154
155fn current_timestamp() -> u64 {
156    match SystemTime::now().duration_since(UNIX_EPOCH) {
157        Ok(duration) => duration.as_secs(),
158        Err(_) => 1_234_567_890,
159    }
160}
161
162// See:
163// https://github.com/servo/rust-url/blob/v2.1.0/percent_encoding/lib.rs#L120-L156
164// https://github.com/mehcode/oauth1-rs/blob/2b6ae40/src/lib.rs#L73-L84
165const ENCODE_SET: &AsciiSet = &NON_ALPHANUMERIC
166    .remove(b'-')
167    .remove(b'.')
168    .remove(b'_')
169    .remove(b'~');
170
171fn percent_encode(string: &str) -> String {
172    utf8_percent_encode(string, ENCODE_SET).to_string()
173}