Skip to main content

oauth2_reqwest/
lib.rs

1#![warn(missing_docs)]
2//! A [`reqwest`] client for [`oauth2`].
3//!
4//! # Motivation
5//!
6//! The `reqwest` client [bundled](https://docs.rs/oauth2/latest/oauth2/#http-clients) with `oauth2`
7//! supports `reqwest` version 0.12. This separate crate supports `reqwest` version 0.13 and is
8//! intended to support future versions of `reqwest` without needing a new SemVer major version
9//! number for `oauth2` (which would otherwise be required due to breaking changes to that crate's
10//! public API).
11//!
12//! # Usage
13//!
14//! To get started, add the following dependencies to your crate's `Cargo.toml`:
15//! ```toml
16//! # Disables oauth2's default reqwest 0.12 client.
17//! oauth2 = { version = "5", default-features = false }
18//!
19//! # Imports reqwest without any feature flags enabled.
20#![doc = concat!("oauth2-reqwest = \"", env!("CARGO_PKG_VERSION"), "\"")]
21//!
22//! # Enables reqwest's default features.
23//! reqwest = "0.13"
24//! # Alternatively, specify the desired set of features:
25//! # reqwest = { version = "0.13", default-features = false, features = ["native-tls"] }
26//! ```
27//!
28//! For flexibility, this crate disables all of `reqwest`'s Cargo feature flags by default. To
29//! enable specific `reqwest` features (including its default `rustls` feature), separately import
30//! `reqwest` in your crate's `Cargo.toml` and specify the
31//! [desired features](https://docs.rs/crate/reqwest/latest/features). This approach leverages Cargo
32//! [feature unification](https://doc.rust-lang.org/cargo/reference/features.html#feature-unification).
33//! While this approach requires a separate import, it provides maximum flexibility and reduces the
34//! need for future breaking changes to this crate.
35//!
36//! ## Asynchronous Client
37//!
38//! To use the async `reqwest` client, simply wrap the `reqwest` [`Client`](reqwest::Client) with
39//! this crate's [`ReqwestClient`] and pass the `ReqwestClient` to the desired `request_async`
40//! method:
41//!
42//! ```rust,no_run
43//! use oauth2_reqwest::ReqwestClient;
44//!
45//! # async fn err_wrapper() -> Result<(), anyhow::Error> {
46//! # let client = oauth2::basic::BasicClient::new(oauth2::ClientId::new("client_id".to_string()))
47//! #     .set_token_uri(oauth2::TokenUrl::new("http://token".to_string())?);
48//! let reqwest_client = reqwest::ClientBuilder::new()
49//!     // Following redirects opens the client up to SSRF vulnerabilities.
50//!     .redirect(reqwest::redirect::Policy::none())
51//!     .build()
52//!     .expect("Client should build");
53//! let http_client = ReqwestClient::from(reqwest_client);
54//!
55//! # let code = oauth2::AuthorizationCode::new("code".to_string());
56//! // This code assumes `client` is a previously constructed `oauth2::Client` and `code` is an
57//! // `oauth2::AuthorizationCode`.
58//! let token_result = client
59//!     .exchange_code(code)
60//!     .request_async(&http_client)
61//!     .await?;
62//!
63//! # Ok(())
64//! # }
65//! ```
66//!
67//! ## Synchronous Client
68//!
69//! To use the blocking `reqwest` client, first enable the `blocking` feature in `Cargo.toml`:
70//! ```toml
71#![doc = concat!("oauth2-reqwest = { version = \"", env!("CARGO_PKG_VERSION"), "\", features = [\"blocking\"] }")]
72//! ```
73//!
74//! Then, simply wrap the `reqwest` blocking [`Client`](reqwest::blocking::Client) with
75//! this crate's [`ReqwestBlockingClient`] and pass the `ReqwestBlockingClient` to the desired
76//! `request` method:
77//!
78//! ```rust,no_run
79//! # #[cfg(feature = "blocking")]
80//! use oauth2_reqwest::ReqwestBlockingClient;
81//!
82//! # #[cfg(feature = "blocking")]
83//! # fn err_wrapper() -> Result<(), anyhow::Error> {
84//! # let client = oauth2::basic::BasicClient::new(oauth2::ClientId::new("client_id".to_string()))
85//! #     .set_token_uri(oauth2::TokenUrl::new("http://token".to_string())?);
86//! let reqwest_client = reqwest::blocking::ClientBuilder::new()
87//!     // Following redirects opens the client up to SSRF vulnerabilities.
88//!     .redirect(reqwest::redirect::Policy::none())
89//!     .build()
90//!     .expect("Client should build");
91//! let http_client = ReqwestBlockingClient::from(reqwest_client);
92//!
93//! # let code = oauth2::AuthorizationCode::new("code".to_string());
94//! // This code assumes `client` is a previously constructed `oauth2::Client` and `code` is an
95//! // `oauth2::AuthorizationCode`.
96//! let token_result = client
97//!     .exchange_code(code)
98//!     .request(&http_client)?;
99//!
100//! # Ok(())
101//! # }
102//! ```
103use oauth2::{http, AsyncHttpClient, HttpClientError, HttpRequest, HttpResponse};
104use std::future::Future;
105use std::pin::Pin;
106
107/// Asynchronous `reqwest` [`Client`](reqwest::Client) wrapper.
108///
109/// See the [crate-level documentation](crate) for usage instructions.
110#[derive(Clone, Debug, Default)]
111pub struct ReqwestClient(reqwest::Client);
112
113impl From<reqwest::Client> for ReqwestClient {
114    fn from(inner: reqwest::Client) -> Self {
115        Self(inner)
116    }
117}
118
119impl<'c> AsyncHttpClient<'c> for ReqwestClient {
120    type Error = HttpClientError<reqwest::Error>;
121
122    #[cfg(target_arch = "wasm32")]
123    type Future = Pin<Box<dyn Future<Output = Result<HttpResponse, Self::Error>> + 'c>>;
124    #[cfg(not(target_arch = "wasm32"))]
125    type Future =
126        Pin<Box<dyn Future<Output = Result<HttpResponse, Self::Error>> + Send + Sync + 'c>>;
127
128    fn call(&'c self, request: HttpRequest) -> Self::Future {
129        Box::pin(async move {
130            let response = self
131                .0
132                .execute(request.try_into().map_err(Box::new)?)
133                .await
134                .map_err(Box::new)?;
135
136            let mut builder = http::Response::builder().status(response.status());
137
138            #[cfg(not(target_arch = "wasm32"))]
139            {
140                builder = builder.version(response.version());
141            }
142
143            for (name, value) in response.headers().iter() {
144                builder = builder.header(name, value);
145            }
146
147            builder
148                .body(response.bytes().await.map_err(Box::new)?.to_vec())
149                .map_err(HttpClientError::Http)
150        })
151    }
152}
153
154#[cfg(all(feature = "blocking", not(target_arch = "wasm32")))]
155pub use blocking::ReqwestBlockingClient;
156
157#[cfg(all(feature = "blocking", not(target_arch = "wasm32")))]
158mod blocking {
159    use oauth2::{http, HttpClientError, HttpRequest, HttpResponse};
160
161    /// Synchronous `reqwest` blocking [`Client`](reqwest::blocking::Client) wrapper.
162    ///
163    /// See the [crate-level documentation](crate) for usage instructions.
164    #[derive(Clone, Debug, Default)]
165    pub struct ReqwestBlockingClient(reqwest::blocking::Client);
166
167    impl oauth2::SyncHttpClient for ReqwestBlockingClient {
168        type Error = HttpClientError<reqwest::Error>;
169
170        fn call(&self, request: HttpRequest) -> Result<HttpResponse, Self::Error> {
171            let mut response = self
172                .0
173                .execute(request.try_into().map_err(Box::new)?)
174                .map_err(Box::new)?;
175
176            let mut builder = http::Response::builder()
177                .status(response.status())
178                .version(response.version());
179
180            for (name, value) in response.headers().iter() {
181                builder = builder.header(name, value);
182            }
183
184            let mut body = Vec::new();
185            <reqwest::blocking::Response as std::io::Read>::read_to_end(&mut response, &mut body)?;
186
187            builder.body(body).map_err(HttpClientError::Http)
188        }
189    }
190
191    impl From<reqwest::blocking::Client> for ReqwestBlockingClient {
192        fn from(inner: reqwest::blocking::Client) -> Self {
193            Self(inner)
194        }
195    }
196}