inth_oauth2/client/
mod.rs

1//! Client.
2
3mod error;
4
5pub mod response;
6pub use self::error::ClientError;
7
8use reqwest;
9use reqwest::header::{ACCEPT, CONTENT_TYPE};
10use serde_json::{self, Value};
11use url::form_urlencoded::Serializer;
12use url::Url;
13
14use client::response::FromResponse;
15use error::OAuth2Error;
16use provider::Provider;
17use token::{Lifetime, Refresh, Token};
18
19/// OAuth 2.0 client.
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct Client<P> {
22    /// OAuth provider.
23    pub provider: P,
24
25    /// Client ID.
26    pub client_id: String,
27
28    /// Client secret.
29    pub client_secret: String,
30
31    /// Redirect URI.
32    pub redirect_uri: Option<String>,
33}
34
35impl<P: Provider> Client<P> {
36    /// Creates a client.
37    ///
38    /// # Examples
39    ///
40    /// ```
41    /// use inth_oauth2::Client;
42    /// use inth_oauth2::provider::google::Installed;
43    ///
44    /// let client = Client::new(
45    ///     Installed,
46    ///     String::from("CLIENT_ID"),
47    ///     String::from("CLIENT_SECRET"),
48    ///     Some(String::from("urn:ietf:wg:oauth:2.0:oob")),
49    /// );
50    /// ```
51    pub fn new(
52        provider: P,
53        client_id: String,
54        client_secret: String,
55        redirect_uri: Option<String>,
56    ) -> Self {
57        Client {
58            provider,
59            client_id,
60            client_secret,
61            redirect_uri,
62        }
63    }
64
65    /// Returns an authorization endpoint URI to direct the user to.
66    ///
67    /// See [RFC 6749, section 3.1](http://tools.ietf.org/html/rfc6749#section-3.1).
68    ///
69    /// # Examples
70    ///
71    /// ```
72    /// use inth_oauth2::Client;
73    /// use inth_oauth2::provider::google::Installed;
74    ///
75    /// let client = Client::new(
76    ///     Installed,
77    ///     String::from("CLIENT_ID"),
78    ///     String::from("CLIENT_SECRET"),
79    ///     Some(String::from("urn:ietf:wg:oauth:2.0:oob")),
80    /// );
81    ///
82    /// let auth_uri = client.auth_uri(
83    ///     Some("https://www.googleapis.com/auth/userinfo.email"),
84    ///     None,
85    /// );
86    /// ```
87    pub fn auth_uri(&self, scope: Option<&str>, state: Option<&str>) -> Url
88    {
89        let mut uri = self.provider.auth_uri().clone();
90
91        {
92            let mut query = uri.query_pairs_mut();
93
94            query.append_pair("response_type", "code");
95            query.append_pair("client_id", &self.client_id);
96
97            if let Some(ref redirect_uri) = self.redirect_uri {
98                query.append_pair("redirect_uri", redirect_uri);
99            }
100            if let Some(scope) = scope {
101                query.append_pair("scope", scope);
102            }
103            if let Some(state) = state {
104                query.append_pair("state", state);
105            }
106        }
107
108        uri
109    }
110
111    fn post_token(
112        &self,
113        http_client: &reqwest::Client,
114        mut body: Serializer<String>,
115    ) -> Result<Value, ClientError> {
116        if self.provider.credentials_in_body() {
117            body.append_pair("client_id", &self.client_id);
118            body.append_pair("client_secret", &self.client_secret);
119        }
120
121        let body = body.finish();
122
123        let mut response = http_client
124            .post(self.provider.token_uri().clone())
125            .basic_auth(&self.client_id, Some(&self.client_secret))
126            .header(ACCEPT, "application/json")
127            .header(CONTENT_TYPE, "application/x-www-form-urlencoded")
128            .body(body)
129            .send()?;
130
131        let json = serde_json::from_reader(&mut response)?;
132
133        let error = OAuth2Error::from_response(&json);
134
135        if let Ok(error) = error {
136            Err(ClientError::from(error))
137        } else {
138            Ok(json)
139        }
140    }
141
142    /// Requests an access token using an authorization code.
143    ///
144    /// See [RFC 6749, section 4.1.3](http://tools.ietf.org/html/rfc6749#section-4.1.3).
145    pub fn request_token(
146        &self,
147        http_client: &reqwest::Client,
148        code: &str,
149    ) -> Result<P::Token, ClientError> {
150        let mut body = Serializer::new(String::new());
151        body.append_pair("grant_type", "authorization_code");
152        body.append_pair("code", code);
153
154        if let Some(ref redirect_uri) = self.redirect_uri {
155            body.append_pair("redirect_uri", redirect_uri);
156        }
157
158        let json = self.post_token(http_client, body)?;
159        let token = P::Token::from_response(&json)?;
160        Ok(token)
161    }
162}
163
164impl<P> Client<P> where P: Provider, P::Token: Token<Refresh> {
165    /// Refreshes an access token.
166    ///
167    /// See [RFC 6749, section 6](http://tools.ietf.org/html/rfc6749#section-6).
168    pub fn refresh_token(
169        &self,
170        http_client: &reqwest::Client,
171        token: P::Token,
172        scope: Option<&str>,
173    ) -> Result<P::Token, ClientError> {
174        let mut body = Serializer::new(String::new());
175        body.append_pair("grant_type", "refresh_token");
176        body.append_pair("refresh_token", token.lifetime().refresh_token());
177
178        if let Some(scope) = scope {
179            body.append_pair("scope", scope);
180        }
181
182        let json = self.post_token(http_client, body)?;
183        let token = P::Token::from_response_inherit(&json, &token)?;
184        Ok(token)
185    }
186
187    /// Ensures an access token is valid by refreshing it if necessary.
188    pub fn ensure_token(
189        &self,
190        http_client: &reqwest::Client,
191        token: P::Token,
192    ) -> Result<P::Token, ClientError> {
193        if token.lifetime().expired() {
194            self.refresh_token(http_client, token, None)
195        } else {
196            Ok(token)
197        }
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use url::Url;
204    use token::{Bearer, Static};
205    use provider::Provider;
206    use super::Client;
207
208    struct Test {
209        auth_uri: Url,
210        token_uri: Url
211    }
212    impl Provider for Test {
213        type Lifetime = Static;
214        type Token = Bearer<Static>;
215        fn auth_uri(&self) -> &Url { &self.auth_uri }
216        fn token_uri(&self) -> &Url { &self.token_uri }
217    }
218    impl Test {
219        fn new() -> Self {
220            Test {
221                auth_uri: Url::parse("http://example.com/oauth2/auth").unwrap(),
222                token_uri: Url::parse("http://example.com/oauth2/token").unwrap()
223            }
224        }
225    }
226
227    #[test]
228    fn auth_uri() {
229        let client = Client::new(Test::new(), String::from("foo"), String::from("bar"), None);
230        assert_eq!(
231            "http://example.com/oauth2/auth?response_type=code&client_id=foo",
232            client.auth_uri(None, None).as_str()
233        );
234    }
235
236    #[test]
237    fn auth_uri_with_redirect_uri() {
238        let client = Client::new(
239            Test::new(),
240            String::from("foo"),
241            String::from("bar"),
242            Some(String::from("http://example.com/oauth2/callback")),
243        );
244        assert_eq!(
245            "http://example.com/oauth2/auth?response_type=code&client_id=foo&redirect_uri=http%3A%2F%2Fexample.com%2Foauth2%2Fcallback",
246            client.auth_uri(None, None).as_str()
247        );
248    }
249
250    #[test]
251    fn auth_uri_with_scope() {
252        let client = Client::new(Test::new(), String::from("foo"), String::from("bar"), None);
253        assert_eq!(
254            "http://example.com/oauth2/auth?response_type=code&client_id=foo&scope=baz",
255            client.auth_uri(Some("baz"), None).as_str()
256        );
257    }
258
259    #[test]
260    fn auth_uri_with_state() {
261        let client = Client::new(Test::new(), String::from("foo"), String::from("bar"), None);
262        assert_eq!(
263            "http://example.com/oauth2/auth?response_type=code&client_id=foo&state=baz",
264            client.auth_uri(None, Some("baz")).as_str()
265        );
266    }
267}