inth_oauth2_async/client/
mod.rs

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