diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7bae34d34..f4c0d2cf4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,7 +4,7 @@ name: Actions CI jobs: build_and_test: - name: yup-oauth2 + name: Run tests runs-on: ubuntu-latest strategy: fail-fast: false @@ -21,7 +21,7 @@ jobs: - run: cargo test --no-default-features --features ${{ matrix.features }} doc: - name: yup-oauth2 + name: Create docs runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -30,10 +30,10 @@ jobs: toolchain: nightly - run: cargo doc --no-deps --all-features env: - RUSTDOCFLAGS: --cfg yup_oauth2_docsrs + RUSTDOCFLAGS: --cfg docsrs -D warnings fmt: - name: yup-oauth2 + name: Check formatting runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -41,3 +41,14 @@ jobs: with: toolchain: stable - run: cargo fmt -- --check + + clippy: + name: Run clippy lints + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable + components: clippy + - run: cargo clippy --all-features --all-targets -- -D warnings diff --git a/Cargo.toml b/Cargo.toml index 0998f0ca4..6347800a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -71,4 +71,4 @@ members = ["examples/test-installed/", "examples/test-svc-acct/", "examples/test [package.metadata.docs.rs] all-features = true -rustdoc-args = ["--cfg", "yup_oauth2_docsrs"] +rustdoc-args = ["--cfg", "docsrs"] diff --git a/examples/custom_client.rs b/examples/custom_client.rs index 7b7b89444..441ff1a50 100644 --- a/examples/custom_client.rs +++ b/examples/custom_client.rs @@ -5,7 +5,10 @@ //! //! It is also a better use of resources (memory, sockets, etc.) +use std::time::Duration; + use hyper_util::client::legacy::connect::Connect; +use yup_oauth2::HyperClientBuilder; async fn r#use( client: hyper_util::client::legacy::Client, @@ -43,11 +46,14 @@ async fn main() { .enable_http2() .build(), ); - let authenticator = - yup_oauth2::ServiceAccountAuthenticator::with_client(secret, client.clone()) - .build() - .await - .expect("could not create an authenticator"); + let authenticator = yup_oauth2::ServiceAccountAuthenticator::with_client( + secret, + yup_oauth2::CustomHyperClientBuilder::from(client.clone()) + .with_timeout(Duration::from_secs(10)), + ) + .build() + .await + .expect("could not create an authenticator"); r#use(client, authenticator) .await .expect("use is successful!"); diff --git a/src/access_token.rs b/src/access_token.rs index 418f6c65c..9332b4f92 100644 --- a/src/access_token.rs +++ b/src/access_token.rs @@ -5,9 +5,9 @@ //! The intention behind this is that if two services using the //! same refresh token then each service will invalitate the //! access token of the other service by generating a new token. +use crate::client::SendRequest; use crate::error::Error; use crate::types::TokenInfo; -use hyper_util::client::legacy::connect::Connect; /// the flow for the access token authenticator pub struct AccessTokenFlow { @@ -16,14 +16,13 @@ pub struct AccessTokenFlow { impl AccessTokenFlow { /// just return the access token - pub(crate) async fn token( + pub(crate) async fn token( &self, - _hyper_client: &hyper_util::client::legacy::Client, + _hyper_client: &impl SendRequest, _scopes: &[T], ) -> Result where T: AsRef, - C: Connect + Clone + Send + Sync + 'static, { Ok(TokenInfo { access_token: Some(self.access_token.clone()), diff --git a/src/application_default_credentials.rs b/src/application_default_credentials.rs index 41924c615..b280305c1 100644 --- a/src/application_default_credentials.rs +++ b/src/application_default_credentials.rs @@ -1,7 +1,7 @@ +use crate::client::SendRequest; use crate::error::Error; use crate::types::TokenInfo; use http_body_util::BodyExt; -use hyper_util::client::legacy::connect::Connect; /// Provide options for the Application Default Credential Flow, mostly used for testing #[derive(Default, Clone, Debug)] @@ -20,14 +20,13 @@ impl ApplicationDefaultCredentialsFlow { ApplicationDefaultCredentialsFlow { metadata_url } } - pub(crate) async fn token( + pub(crate) async fn token( &self, - hyper_client: &hyper_util::client::legacy::Client, + hyper_client: &impl SendRequest, scopes: &[T], ) -> Result where T: AsRef, - C: Connect + Clone + Send + Sync + 'static, { let scope = crate::helper::join(scopes, ","); let token_uri = format!("{}?scopes={}", self.metadata_url, scope); diff --git a/src/authenticator.rs b/src/authenticator.rs index 7edf2ebd3..4e153b4c0 100644 --- a/src/authenticator.rs +++ b/src/authenticator.rs @@ -4,6 +4,9 @@ use crate::application_default_credentials::{ }; use crate::authenticator_delegate::{DeviceFlowDelegate, InstalledFlowDelegate}; use crate::authorized_user::{AuthorizedUserFlow, AuthorizedUserSecret}; +#[cfg(any(feature = "hyper-rustls", feature = "hyper-tls"))] +use crate::client::DefaultHyperClientBuilder; +use crate::client::{HttpClient, HyperClientBuilder}; use crate::device::DeviceFlow; use crate::error::Error; use crate::external_account::{ExternalAccountFlow, ExternalAccountSecret}; @@ -16,10 +19,6 @@ use crate::service_account::{self, ServiceAccountFlow, ServiceAccountFlowOpts, S use crate::storage::{self, Storage, TokenStorage}; use crate::types::{AccessToken, ApplicationSecret, TokenInfo}; use private::AuthFlow; -#[cfg(all(feature = "aws-lc-rs", feature = "hyper-rustls", not(feature = "ring")))] -use rustls::crypto::aws_lc_rs::default_provider as default_crypto_provider; -#[cfg(all(feature = "ring", feature = "hyper-rustls"))] -use rustls::crypto::ring::default_provider as default_crypto_provider; use crate::access_token::AccessTokenFlow; @@ -30,9 +29,13 @@ use std::fmt; use std::io; use std::path::PathBuf; use std::sync::Arc; +use std::time::Duration; -struct InnerAuthenticator { - hyper_client: hyper_util::client::legacy::Client, +struct InnerAuthenticator +where + C: Connect + Clone + Send + Sync + 'static, +{ + hyper_client: HttpClient, storage: Storage, auth_flow: AuthFlow, } @@ -40,7 +43,10 @@ struct InnerAuthenticator { /// Authenticator is responsible for fetching tokens, handling refreshing tokens, /// and optionally persisting tokens to disk. #[derive(Clone)] -pub struct Authenticator { +pub struct Authenticator +where + C: Connect + Clone + Send + Sync + 'static, +{ inner: Arc>, } @@ -197,15 +203,12 @@ pub struct InstalledFlowAuthenticator; impl InstalledFlowAuthenticator { /// Use the builder pattern to create an Authenticator that uses the installed flow. #[cfg(any(feature = "hyper-rustls", feature = "hyper-tls"))] - #[cfg_attr( - yup_oauth2_docsrs, - doc(cfg(any(feature = "hyper-rustls", feature = "hyper-tls"))) - )] + #[cfg_attr(docsrs, doc(cfg(any(feature = "hyper-rustls", feature = "hyper-tls"))))] pub fn builder( app_secret: ApplicationSecret, method: InstalledFlowReturnMethod, - ) -> AuthenticatorBuilder { - Self::with_client(app_secret, method, DefaultHyperClient) + ) -> AuthenticatorBuilder { + Self::with_client(app_secret, method, DefaultHyperClientBuilder::default()) } /// Construct a new Authenticator that uses the installed flow and the provided http client. @@ -233,14 +236,11 @@ pub struct DeviceFlowAuthenticator; impl DeviceFlowAuthenticator { /// Use the builder pattern to create an Authenticator that uses the device flow. #[cfg(any(feature = "hyper-rustls", feature = "hyper-tls"))] - #[cfg_attr( - yup_oauth2_docsrs, - doc(cfg(any(feature = "hyper-rustls", feature = "hyper-tls"))) - )] + #[cfg_attr(docsrs, doc(cfg(any(feature = "hyper-rustls", feature = "hyper-tls"))))] pub fn builder( app_secret: ApplicationSecret, - ) -> AuthenticatorBuilder { - Self::with_client(app_secret, DefaultHyperClient) + ) -> AuthenticatorBuilder { + Self::with_client(app_secret, DefaultHyperClientBuilder::default()) } /// Construct a new Authenticator that uses the installed flow and the provided http client. @@ -270,14 +270,11 @@ pub struct ServiceAccountAuthenticator; impl ServiceAccountAuthenticator { /// Use the builder pattern to create an Authenticator that uses a service account. #[cfg(any(feature = "hyper-rustls", feature = "hyper-tls"))] - #[cfg_attr( - yup_oauth2_docsrs, - doc(cfg(any(feature = "hyper-rustls", feature = "hyper-tls"))) - )] + #[cfg_attr(docsrs, doc(cfg(any(feature = "hyper-rustls", feature = "hyper-tls"))))] pub fn builder( service_account_key: ServiceAccountKey, - ) -> AuthenticatorBuilder { - Self::with_client(service_account_key, DefaultHyperClient) + ) -> AuthenticatorBuilder { + Self::with_client(service_account_key, DefaultHyperClientBuilder::default()) } /// Construct a new Authenticator that uses the installed flow and the provided http client. @@ -333,14 +330,11 @@ impl ApplicationDefaultCredentialsAuthenticator { /// Service account one or GCE instance metadata kind #[cfg(feature = "service-account")] #[cfg(any(feature = "hyper-rustls", feature = "hyper-tls"))] - #[cfg_attr( - yup_oauth2_docsrs, - doc(cfg(any(feature = "hyper-rustls", feature = "hyper-tls"))) - )] + #[cfg_attr(docsrs, doc(cfg(any(feature = "hyper-rustls", feature = "hyper-tls"))))] pub async fn builder( opts: ApplicationDefaultCredentialsFlowOpts, - ) -> ApplicationDefaultCredentialsTypes { - Self::with_client(opts, DefaultHyperClient).await + ) -> ApplicationDefaultCredentialsTypes { + Self::with_client(opts, DefaultHyperClientBuilder::default()).await } /// Use the builder pattern to deduce which model of authenticator should be used and allow providing a hyper client @@ -392,14 +386,11 @@ pub struct AuthorizedUserAuthenticator; impl AuthorizedUserAuthenticator { /// Use the builder pattern to create an Authenticator that uses an authorized user. #[cfg(any(feature = "hyper-rustls", feature = "hyper-tls"))] - #[cfg_attr( - yup_oauth2_docsrs, - doc(cfg(any(feature = "hyper-rustls", feature = "hyper-tls"))) - )] + #[cfg_attr(docsrs, doc(cfg(any(feature = "hyper-rustls", feature = "hyper-tls"))))] pub fn builder( authorized_user_secret: AuthorizedUserSecret, - ) -> AuthenticatorBuilder { - Self::with_client(authorized_user_secret, DefaultHyperClient) + ) -> AuthenticatorBuilder { + Self::with_client(authorized_user_secret, DefaultHyperClientBuilder::default()) } /// Construct a new Authenticator that uses the installed flow and the provided http client. @@ -432,14 +423,14 @@ pub struct ExternalAccountAuthenticator; impl ExternalAccountAuthenticator { /// Use the builder pattern to create an Authenticator that uses an external account. #[cfg(any(feature = "hyper-rustls", feature = "hyper-tls"))] - #[cfg_attr( - yup_oauth2_docsrs, - doc(cfg(any(feature = "hyper-rustls", feature = "hyper-tls"))) - )] + #[cfg_attr(docsrs, doc(cfg(any(feature = "hyper-rustls", feature = "hyper-tls"))))] pub fn builder( external_account_secret: ExternalAccountSecret, - ) -> AuthenticatorBuilder { - Self::with_client(external_account_secret, DefaultHyperClient) + ) -> AuthenticatorBuilder { + Self::with_client( + external_account_secret, + DefaultHyperClientBuilder::default(), + ) } /// Construct a new Authenticator that uses the installed flow and the provided http client. @@ -475,8 +466,8 @@ impl AccessTokenAuthenticator { /// the builder pattern for the authenticator pub fn builder( access_token: String, - ) -> AuthenticatorBuilder { - Self::with_client(access_token, DefaultHyperClient) + ) -> AuthenticatorBuilder { + Self::with_client(access_token, DefaultHyperClientBuilder::default()) } /// Construct a new Authenticator that uses the installed flow and the provided http client. /// the client itself is not used @@ -507,18 +498,15 @@ pub struct ServiceAccountImpersonationAuthenticator; impl ServiceAccountImpersonationAuthenticator { /// Use the builder pattern to create an Authenticator that uses the device flow. #[cfg(any(feature = "hyper-rustls", feature = "hyper-tls"))] - #[cfg_attr( - yup_oauth2_docsrs, - doc(cfg(any(feature = "hyper-rustls", feature = "hyper-tls"))) - )] + #[cfg_attr(docsrs, doc(cfg(any(feature = "hyper-rustls", feature = "hyper-tls"))))] pub fn builder( authorized_user_secret: AuthorizedUserSecret, service_account_email: &str, - ) -> AuthenticatorBuilder { + ) -> AuthenticatorBuilder { Self::with_client( authorized_user_secret, service_account_email, - DefaultHyperClient, + DefaultHyperClientBuilder::default(), ) } @@ -537,12 +525,10 @@ impl ServiceAccountImpersonationAuthenticator { /// ## Methods available when building any Authenticator. /// ``` -/// # #[cfg(any(feature = "hyper-rustls", feature = "hyper-tls"))] /// # async fn foo() { -/// # let custom_hyper_client = hyper_util::client::legacy::Client::builder(hyper_util::rt::TokioExecutor::new()).build_http::(); +/// # let client = hyper_util::client::legacy::Client::builder(hyper_util::rt::TokioExecutor::new()).build_http::(); /// # let app_secret = yup_oauth2::read_application_secret("/tmp/foo").await.unwrap(); -/// let authenticator = yup_oauth2::DeviceFlowAuthenticator::builder(app_secret) -/// .hyper_client(custom_hyper_client) +/// let authenticator = yup_oauth2::DeviceFlowAuthenticator::with_client(app_secret, yup_oauth2::CustomHyperClientBuilder::from(client)) /// .persist_tokens_to_disk("/tmp/tokenfile.json") /// .build() /// .await @@ -598,22 +584,23 @@ impl AuthenticatorBuilder { } } - /// Use the provided hyper client. - pub fn hyper_client( - self, - hyper_client: hyper_util::client::legacy::Client, - ) -> AuthenticatorBuilder, F> { + /// Persist tokens to disk in the provided filename. + pub fn persist_tokens_to_disk>(self, path: P) -> AuthenticatorBuilder { AuthenticatorBuilder { - hyper_client_builder: hyper_client, - storage_type: self.storage_type, - auth_flow: self.auth_flow, + storage_type: StorageType::Disk(path.into()), + ..self } } +} - /// Persist tokens to disk in the provided filename. - pub fn persist_tokens_to_disk>(self, path: P) -> AuthenticatorBuilder { +impl AuthenticatorBuilder +where + C: HyperClientBuilder, +{ + /// Sets the duration after which a HTTP request times out + pub fn with_timeout(self, timeout: Duration) -> Self { AuthenticatorBuilder { - storage_type: StorageType::Disk(path.into()), + hyper_client_builder: self.hyper_client_builder.with_timeout(timeout), ..self } } @@ -869,8 +856,8 @@ impl AuthenticatorBuilder { mod private { use crate::access_token::AccessTokenFlow; use crate::application_default_credentials::ApplicationDefaultCredentialsFlow; - use crate::authenticator::Connect; use crate::authorized_user::AuthorizedUserFlow; + use crate::client::SendRequest; use crate::device::DeviceFlow; use crate::error::Error; use crate::external_account::ExternalAccountFlow; @@ -908,14 +895,13 @@ mod private { } } - pub(crate) async fn token<'a, C, T>( + pub(crate) async fn token<'a, T>( &'a self, - hyper_client: &'a hyper_util::client::legacy::Client, + hyper_client: &'a impl SendRequest, scopes: &'a [T], ) -> Result where T: AsRef, - C: Connect + Clone + Send + Sync + 'static, { match self { AuthFlow::DeviceFlow(device_flow) => device_flow.token(hyper_client, scopes).await, @@ -948,117 +934,18 @@ mod private { } } -/// A trait implemented for any hyper_util::client::legacy::Client as well as the DefaultHyperClient. -pub trait HyperClientBuilder { - /// The hyper connector that the resulting hyper client will use. - type Connector: Connect + Clone + Send + Sync + 'static; - - /// Create a hyper::Client - fn build_hyper_client( - self, - ) -> Result, Error>; - - /// Create a `hyper_util::client::legacy::Client` for tests (HTTPS not required) - #[doc(hidden)] - fn build_test_hyper_client(self) - -> hyper_util::client::legacy::Client; -} - #[cfg(feature = "hyper-rustls")] -#[cfg_attr( - yup_oauth2_docsrs, - doc(cfg(any(feature = "hyper-rustls", feature = "hyper-tls"))) -)] +#[cfg_attr(docsrs, doc(cfg(any(feature = "hyper-rustls", feature = "hyper-tls"))))] /// Default authenticator type pub type DefaultAuthenticator = Authenticator>; #[cfg(all(not(feature = "hyper-rustls"), feature = "hyper-tls"))] -#[cfg_attr( - yup_oauth2_docsrs, - doc(cfg(any(feature = "hyper-rustls", feature = "hyper-tls"))) -)] +#[cfg_attr(docsrs, doc(cfg(any(feature = "hyper-rustls", feature = "hyper-tls"))))] /// Default authenticator type pub type DefaultAuthenticator = Authenticator>; -/// The builder value used when the default hyper client should be used. -#[cfg(any(feature = "hyper-rustls", feature = "hyper-tls"))] -#[cfg_attr( - yup_oauth2_docsrs, - doc(cfg(any(feature = "hyper-rustls", feature = "hyper-tls"))) -)] -pub struct DefaultHyperClient; - -#[cfg(any(feature = "hyper-rustls", feature = "hyper-tls"))] -#[cfg_attr( - yup_oauth2_docsrs, - doc(cfg(any(feature = "hyper-rustls", feature = "hyper-tls"))) -)] -impl HyperClientBuilder for DefaultHyperClient { - #[cfg(feature = "hyper-rustls")] - type Connector = - hyper_rustls::HttpsConnector; - #[cfg(all(not(feature = "hyper-rustls"), feature = "hyper-tls"))] - type Connector = hyper_tls::HttpsConnector; - - fn build_hyper_client( - self, - ) -> Result, Error> { - #[cfg(feature = "hyper-rustls")] - let connector = hyper_rustls::HttpsConnectorBuilder::new() - .with_provider_and_native_roots(default_crypto_provider())? - .https_or_http() - .enable_http1() - .enable_http2() - .build(); - #[cfg(all(not(feature = "hyper-rustls"), feature = "hyper-tls"))] - let connector = hyper_tls::HttpsConnector::new(); - - Ok( - hyper_util::client::legacy::Client::builder(hyper_util::rt::TokioExecutor::new()) - .pool_max_idle_per_host(0) - .build::<_, String>(connector), - ) - } - - fn build_test_hyper_client( - self, - ) -> hyper_util::client::legacy::Client { - #[cfg(feature = "hyper-rustls")] - let connector = hyper_rustls::HttpsConnectorBuilder::new() - .with_provider_and_native_roots(default_crypto_provider()) - .unwrap() - .https_or_http() - .enable_http1() - .enable_http2() - .build(); - #[cfg(all(not(feature = "hyper-rustls"), feature = "hyper-tls"))] - let connector = hyper_tls::HttpsConnector::new(); - - hyper_util::client::legacy::Client::builder(hyper_util::rt::TokioExecutor::new()) - .pool_max_idle_per_host(0) - .build::<_, String>(connector) - } -} - -impl HyperClientBuilder for hyper_util::client::legacy::Client -where - C: Connect + Clone + Send + Sync + 'static, -{ - type Connector = C; - - fn build_hyper_client(self) -> Result, Error> { - Ok(self) - } - - fn build_test_hyper_client( - self, - ) -> hyper_util::client::legacy::Client { - self - } -} - /// How should the acquired tokens be stored? enum StorageType { /// Store tokens in memory (and always log in again to acquire a new token on startup) @@ -1076,6 +963,7 @@ mod tests { fn ensure_send_sync() { use super::*; fn is_send_sync() {} - is_send_sync::::Connector>>() + is_send_sync::::Connector>>( + ) } } diff --git a/src/authorized_user.rs b/src/authorized_user.rs index 9f8476d8e..bfda9eab1 100644 --- a/src/authorized_user.rs +++ b/src/authorized_user.rs @@ -4,11 +4,11 @@ //! Resources: //! - [gcloud auth application-default login](https://cloud.google.com/sdk/gcloud/reference/auth/application-default/login) //! +use crate::client::SendRequest; use crate::error::Error; use crate::types::TokenInfo; use http::header; use http_body_util::BodyExt; -use hyper_util::client::legacy::connect::Connect; use serde::{Deserialize, Serialize}; use url::form_urlencoded; @@ -40,14 +40,13 @@ pub struct AuthorizedUserFlow { impl AuthorizedUserFlow { /// Send a request for a new Bearer token to the OAuth provider. - pub(crate) async fn token( + pub(crate) async fn token( &self, - hyper_client: &hyper_util::client::legacy::Client, + hyper_client: &impl SendRequest, _scopes: &[T], ) -> Result where T: AsRef, - C: Connect + Clone + Send + Sync + 'static, { let req = form_urlencoded::Serializer::new(String::new()) .extend_pairs(&[ diff --git a/src/client.rs b/src/client.rs new file mode 100644 index 000000000..5f76544cf --- /dev/null +++ b/src/client.rs @@ -0,0 +1,199 @@ +//! Module containing the HTTP client used for sending requests +use std::time::Duration; + +use futures::TryFutureExt; +use http::Uri; +use hyper_util::client::legacy::{connect::Connect, Error as LegacyHyperError}; +#[cfg(all(feature = "aws-lc-rs", feature = "hyper-rustls", not(feature = "ring")))] +use rustls::crypto::aws_lc_rs::default_provider as default_crypto_provider; +#[cfg(all(feature = "ring", feature = "hyper-rustls"))] +use rustls::crypto::ring::default_provider as default_crypto_provider; +use thiserror::Error as ThisError; + +use crate::Error; + +type HyperResponse = http::Response; +pub(crate) type LegacyClient = hyper_util::client::legacy::Client; + +#[derive(Debug, ThisError)] +/// Errors that can happen when a request is sent +pub enum SendError { + /// Request could not complete before timeout elapsed + #[error("Request timed out")] + Timeout, + /// Wrapper for hyper errors + #[error("Hyper error: {0}")] + Hyper(#[source] LegacyHyperError), +} + +/// A trait implemented for any hyper_util::client::legacy::Client as well as the DefaultHyperClient. +pub trait HyperClientBuilder { + /// The hyper connector that the resulting hyper client will use. + type Connector: Connect + Clone + Send + Sync + 'static; + + /// Sets duration after which a request times out + fn with_timeout(self, timeout: Duration) -> Self; + + /// Create a hyper::Client + fn build_hyper_client(self) -> Result, Error>; +} + +/// Client that can be configured that a request will timeout after a specified +/// duration. +#[derive(Clone)] +pub struct HttpClient +where + C: Connect + Clone + Send + Sync + 'static, +{ + client: LegacyClient, + timeout: Option, +} + +impl HttpClient +where + C: Connect + Clone + Send + Sync + 'static, +{ + pub(crate) fn new(hyper_client: LegacyClient, timeout: Option) -> Self { + Self { + client: hyper_client, + timeout, + } + } + + pub(crate) fn set_timeout(&mut self, timeout: Duration) { + self.timeout = Some(timeout); + } + + /// Execute a get request with the underlying hyper client + #[doc(hidden)] + pub async fn get(&self, uri: Uri) -> Result { + self.client.get(uri).await + } +} + +impl HyperClientBuilder for HttpClient +where + C: Connect + Clone + Send + Sync + 'static, +{ + type Connector = C; + + fn with_timeout(mut self, timeout: Duration) -> Self { + self.set_timeout(timeout); + self + } + + fn build_hyper_client(self) -> Result, Error> { + Ok(self) + } +} + +impl SendRequest for HttpClient +where + C: Connect + Clone + Send + Sync + 'static, +{ + async fn request(&self, payload: http::Request) -> Result { + let future = self.client.request(payload); + match self.timeout { + Some(duration) => { + tokio::time::timeout(duration, future) + .map_err(|_| SendError::Timeout) + .await? + } + None => future.await, + } + .map_err(SendError::Hyper) + } +} + +pub(crate) trait SendRequest { + async fn request(&self, payload: http::Request) -> Result; +} + +/// The builder value used when the default hyper client should be used. +#[cfg(any(feature = "hyper-rustls", feature = "hyper-tls"))] +#[cfg_attr(docsrs, doc(cfg(any(feature = "hyper-rustls", feature = "hyper-tls"))))] +#[derive(Default)] +pub struct DefaultHyperClientBuilder { + timeout: Option, +} + +#[cfg(any(feature = "hyper-rustls", feature = "hyper-tls"))] +impl DefaultHyperClientBuilder { + /// Set the duration after which a request times out + pub fn with_timeout(mut self, timeout: Duration) -> Self { + self.timeout = Some(timeout); + self + } +} + +#[cfg(any(feature = "hyper-rustls", feature = "hyper-tls"))] +#[cfg_attr(docsrs, doc(cfg(any(feature = "hyper-rustls", feature = "hyper-tls"))))] +impl HyperClientBuilder for DefaultHyperClientBuilder { + #[cfg(feature = "hyper-rustls")] + type Connector = + hyper_rustls::HttpsConnector; + #[cfg(all(not(feature = "hyper-rustls"), feature = "hyper-tls"))] + type Connector = hyper_tls::HttpsConnector; + + fn with_timeout(mut self, timeout: Duration) -> Self { + self.timeout = Some(timeout); + self + } + + fn build_hyper_client(self) -> Result, Error> { + #[cfg(feature = "hyper-rustls")] + let connector = hyper_rustls::HttpsConnectorBuilder::new() + .with_provider_and_native_roots(default_crypto_provider())? + .https_or_http() + .enable_http1() + .enable_http2() + .build(); + #[cfg(all(not(feature = "hyper-rustls"), feature = "hyper-tls"))] + let connector = hyper_tls::HttpsConnector::new(); + + Ok(HttpClient::new( + hyper_util::client::legacy::Client::builder(hyper_util::rt::TokioExecutor::new()) + .pool_max_idle_per_host(0) + .build::<_, String>(connector), + self.timeout, + )) + } +} + +/// Intended for using an existing hyper client with `yup-oauth2`. Instantiate +/// with [`CustomHyperClientBuilder::from`] +pub struct CustomHyperClientBuilder +where + C: Connect + Clone + Send + Sync + 'static, +{ + client: HttpClient, + timeout: Option, +} + +impl From> for CustomHyperClientBuilder +where + C: Connect + Clone + Send + Sync + 'static, +{ + fn from(client: LegacyClient) -> Self { + Self { + client: HttpClient::new(client, None), + timeout: None, + } + } +} + +impl HyperClientBuilder for CustomHyperClientBuilder +where + C: Connect + Clone + Send + Sync + 'static, +{ + type Connector = C; + + fn with_timeout(mut self, timeout: Duration) -> Self { + self.timeout = Some(timeout); + self + } + + fn build_hyper_client(self) -> Result, Error> { + Ok(self.client) + } +} diff --git a/src/device.rs b/src/device.rs index ea2c489c0..f7affe826 100644 --- a/src/device.rs +++ b/src/device.rs @@ -1,6 +1,7 @@ use crate::authenticator_delegate::{ DefaultDeviceFlowDelegate, DeviceAuthResponse, DeviceFlowDelegate, }; +use crate::client::SendRequest; use crate::error::{AuthError, Error}; use crate::types::{ApplicationSecret, TokenInfo}; @@ -9,7 +10,6 @@ use std::time::Duration; use http::header; use http_body_util::BodyExt; -use hyper_util::client::legacy::connect::Connect; use url::form_urlencoded; pub const GOOGLE_DEVICE_CODE_URL: &str = "https://accounts.google.com/o/oauth2/device/code"; @@ -40,14 +40,13 @@ impl DeviceFlow { } } - pub(crate) async fn token( + pub(crate) async fn token( &self, - hyper_client: &hyper_util::client::legacy::Client, + hyper_client: &impl SendRequest, scopes: &[T], ) -> Result where T: AsRef, - C: Connect + Clone + Send + Sync + 'static, { let device_auth_resp = Self::request_code( &self.app_secret, @@ -69,16 +68,13 @@ impl DeviceFlow { .await } - async fn wait_for_device_token( + async fn wait_for_device_token( &self, - hyper_client: &hyper_util::client::legacy::Client, + hyper_client: &impl SendRequest, app_secret: &ApplicationSecret, device_auth_resp: &DeviceAuthResponse, grant_type: &str, - ) -> Result - where - C: Connect + Clone + Send + Sync + 'static, - { + ) -> Result { let mut interval = device_auth_resp.interval; log::debug!("Polling every {:?} for device token", interval); loop { @@ -126,15 +122,14 @@ impl DeviceFlow { /// * If called after a successful result was returned at least once. /// # Examples /// See test-cases in source code for a more complete example. - async fn request_code( + async fn request_code( application_secret: &ApplicationSecret, - client: &hyper_util::client::legacy::Client, + client: &impl SendRequest, device_code_url: &str, scopes: &[T], ) -> Result where T: AsRef, - C: Connect + Clone + Send + Sync + 'static, { let req = form_urlencoded::Serializer::new(String::new()) .extend_pairs(&[ @@ -174,15 +169,13 @@ impl DeviceFlow { /// /// # Examples /// See test-cases in source code for a more complete example. - async fn poll_token<'a, C>( + async fn poll_token<'a>( application_secret: &ApplicationSecret, - client: &hyper_util::client::legacy::Client, + client: &impl SendRequest, device_code: &str, grant_type: &str, ) -> Result - where - C: Connect + Clone + Send + Sync + 'static, - { +where { // We should be ready for a new request let req = form_urlencoded::Serializer::new(String::new()) .extend_pairs(&[ diff --git a/src/error.rs b/src/error.rs index 0144511bd..f3961c97b 100644 --- a/src/error.rs +++ b/src/error.rs @@ -6,10 +6,10 @@ use std::fmt; use std::io; use hyper::Error as HyperError; -use hyper_util::client::legacy::Error as LegacyHyperError; use serde::Deserialize; use thiserror::Error as ThisError; +pub use crate::client::SendError; pub use crate::external_account::CredentialSourceError; pub use crate::storage::TokenStorageError; @@ -155,7 +155,7 @@ pub enum Error { HttpError(#[from] HyperError), /// Indicates connection failure #[error("Connection failure: {0}")] - HttpClientError(#[from] LegacyHyperError), + HttpClientError(#[from] SendError), /// The server returned an error. #[error("Server error: {0}")] AuthError(#[from] AuthError), diff --git a/src/external_account.rs b/src/external_account.rs index 033098559..10e0cfe05 100644 --- a/src/external_account.rs +++ b/src/external_account.rs @@ -5,11 +5,11 @@ //! - [Workload identity federation](https://cloud.google.com/iam/docs/workload-identity-federation) //! - [External Account Credentials (Workload Identity Federation)](https://google.aip.dev/auth/4117) //! +use crate::client::SendRequest; use crate::error::Error; use crate::types::TokenInfo; use http::header; use http_body_util::BodyExt; -use hyper_util::client::legacy::connect::Connect; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use thiserror::Error; @@ -47,8 +47,7 @@ pub enum CredentialSource { file: String, }, - //// [Microsoft Azure and URL-sourced - ///credentials](https://google.aip.dev/auth/4117#determining-the-subject-token-in-microsoft-azure-and-url-sourced-credentials) + /// [Microsoft Azure and URL-sourced credentials](https://google.aip.dev/auth/4117#determining-the-subject-token-in-microsoft-azure-and-url-sourced-credentials) Url { /// This defines the local metadata server to retrieve the external credentials from. For /// Azure, this should be the Azure Instance Metadata Service (IMDS) URL used to retrieve @@ -102,14 +101,13 @@ pub struct ExternalAccountFlow { impl ExternalAccountFlow { /// Send a request for a new Bearer token to the OAuth provider. - pub(crate) async fn token( + pub(crate) async fn token( &self, - hyper_client: &hyper_util::client::legacy::Client, + hyper_client: &impl SendRequest, scopes: &[T], ) -> Result where T: AsRef, - C: Connect + Clone + Send + Sync + 'static, { let subject_token = match &self.secret.credential_source { CredentialSource::File { file } => tokio::fs::read_to_string(file).await?, diff --git a/src/installed.rs b/src/installed.rs index cfa864e80..4afab09b3 100644 --- a/src/installed.rs +++ b/src/installed.rs @@ -3,6 +3,7 @@ // Refer to the project root for licensing information. // use crate::authenticator_delegate::{DefaultInstalledFlowDelegate, InstalledFlowDelegate}; +use crate::client::SendRequest; use crate::error::Error; use crate::types::{ApplicationSecret, TokenInfo}; @@ -13,7 +14,6 @@ use std::net::SocketAddr; use std::sync::Arc; use http::header; -use hyper_util::client::legacy::connect::Connect; use percent_encoding::{percent_encode, AsciiSet, CONTROLS}; use tokio::sync::oneshot; use url::form_urlencoded; @@ -119,14 +119,13 @@ impl InstalledFlow { /// . Return that token /// /// It's recommended not to use the DefaultInstalledFlowDelegate, but a specialized one. - pub(crate) async fn token( + pub(crate) async fn token( &self, - hyper_client: &hyper_util::client::legacy::Client, + hyper_client: &impl SendRequest, scopes: &[T], ) -> Result where T: AsRef, - C: Connect + Clone + Send + Sync + 'static, { match self.method { InstalledFlowReturnMethod::HTTPRedirect => { @@ -144,15 +143,14 @@ impl InstalledFlow { } } - async fn ask_auth_code_interactively( + async fn ask_auth_code_interactively( &self, - hyper_client: &hyper_util::client::legacy::Client, + hyper_client: &impl SendRequest, app_secret: &ApplicationSecret, scopes: &[T], ) -> Result where T: AsRef, - C: Connect + Clone + Send + Sync + 'static, { let url = build_authentication_request_url( &app_secret.auth_uri, @@ -172,16 +170,15 @@ impl InstalledFlow { .await } - async fn ask_auth_code_via_http( + async fn ask_auth_code_via_http( &self, - hyper_client: &hyper_util::client::legacy::Client, + hyper_client: &impl SendRequest, port: Option, app_secret: &ApplicationSecret, scopes: &[T], ) -> Result where T: AsRef, - C: Connect + Clone + Send + Sync + 'static, { use std::borrow::Cow; let server = InstalledFlowServer::run(port)?; @@ -211,16 +208,13 @@ impl InstalledFlow { .await } - async fn exchange_auth_code( + async fn exchange_auth_code( &self, authcode: &str, - hyper_client: &hyper_util::client::legacy::Client, + hyper_client: &impl SendRequest, app_secret: &ApplicationSecret, server_addr: Option, - ) -> Result - where - C: Connect + Clone + Send + Sync + 'static, - { + ) -> Result { let redirect_uri = self.flow_delegate.redirect_uri(); let request = Self::request_token(app_secret, authcode, redirect_uri, server_addr); log::debug!("Sending request: {:?}", request); @@ -419,6 +413,8 @@ mod installed_flow_server { #[cfg(test)] mod tests { + use crate::client::LegacyClient; + use super::*; use http::Uri; @@ -479,11 +475,9 @@ mod tests { #[tokio::test] async fn test_server() { - let client: hyper_util::client::legacy::Client< - hyper_util::client::legacy::connect::HttpConnector, - String, - > = hyper_util::client::legacy::Client::builder(hyper_util::rt::TokioExecutor::new()) - .build_http(); + let client: LegacyClient = + hyper_util::client::legacy::Client::builder(hyper_util::rt::TokioExecutor::new()) + .build_http(); let server = InstalledFlowServer::run(None).unwrap(); let response = client diff --git a/src/lib.rs b/src/lib.rs index 082aa1ad7..46b940b4e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -70,13 +70,14 @@ //! ``` //! #![deny(missing_docs)] -#![cfg_attr(yup_oauth2_docsrs, feature(doc_cfg))] +#![cfg_attr(docsrs, feature(doc_cfg))] pub mod access_token; mod application_default_credentials; pub mod authenticator; pub mod authenticator_delegate; pub mod authorized_user; +pub mod client; mod device; pub mod error; pub mod external_account; @@ -106,6 +107,10 @@ pub use crate::authenticator::ServiceAccountAuthenticator; #[cfg(any(feature = "hyper-rustls", feature = "hyper-tls"))] pub use crate::authenticator::AccessTokenAuthenticator; +#[cfg(any(feature = "hyper-rustls", feature = "hyper-tls"))] +pub use crate::client::DefaultHyperClientBuilder; +pub use crate::client::{CustomHyperClientBuilder, HttpClient, HyperClientBuilder}; + #[doc(inline)] pub use crate::authenticator::{ ApplicationDefaultCredentialsAuthenticator, AuthorizedUserAuthenticator, diff --git a/src/refresh.rs b/src/refresh.rs index 0c8b14a8c..fef563092 100644 --- a/src/refresh.rs +++ b/src/refresh.rs @@ -1,9 +1,9 @@ +use crate::client::SendRequest; use crate::error::Error; use crate::types::{ApplicationSecret, TokenInfo}; use http::header; use http_body_util::BodyExt; -use hyper_util::client::legacy::connect::Connect; use url::form_urlencoded; /// Implements the [OAuth2 Refresh Token Flow](https://developers.google.com/youtube/v3/guides/authentication#devices). @@ -28,14 +28,12 @@ impl RefreshFlow { /// /// # Examples /// Please see the crate landing page for an example. - pub(crate) async fn refresh_token( - client: &hyper_util::client::legacy::Client, + pub(crate) async fn refresh_token( + client: &impl SendRequest, client_secret: &ApplicationSecret, refresh_token: &str, ) -> Result - where - C: Connect + Clone + Send + Sync + 'static, - { +where { log::debug!( "refreshing access token with refresh token: {}", refresh_token diff --git a/src/service_account.rs b/src/service_account.rs index 3b66f4ff4..02b047df1 100644 --- a/src/service_account.rs +++ b/src/service_account.rs @@ -1,5 +1,3 @@ -#![cfg(feature = "service-account")] - //! This module provides a flow that obtains tokens for service accounts. //! //! Service accounts are usually used by software (i.e., non-human actors) to get access to @@ -8,11 +6,12 @@ //! //! Resources: //! - [Using OAuth 2.0 for Server to Server -//! Applications](https://developers.google.com/identity/protocols/OAuth2ServiceAccount) +//! Applications](https://developers.google.com/identity/protocols/OAuth2ServiceAccount) //! - [JSON Web Tokens](https://jwt.io/) //! //! Copyright (c) 2016 Google Inc (lewinb@google.com). +use crate::client::SendRequest; use crate::error::Error; use crate::types::TokenInfo; @@ -22,7 +21,6 @@ use base64::Engine as _; use http::header; use http_body_util::BodyExt; -use hyper_util::client::legacy::connect::Connect; #[cfg(all(feature = "aws-lc-rs", not(feature = "ring")))] use rustls::crypto::aws_lc_rs as crypto_provider; #[cfg(feature = "ring")] @@ -194,14 +192,13 @@ impl ServiceAccountFlow { } /// Send a request for a new Bearer token to the OAuth provider. - pub(crate) async fn token( + pub(crate) async fn token( &self, - hyper_client: &hyper_util::client::legacy::Client, + hyper_client: &impl SendRequest, scopes: &[T], ) -> Result where T: AsRef, - C: Connect + Clone + Send + Sync + 'static, { let claims = Claims::new(&self.key, scopes, self.subject.as_deref()); let signed = self.signer.sign_claims(&claims).map_err(|_| { @@ -244,7 +241,7 @@ mod tests { }) .await .unwrap(); - let client = + let client = crate::client::HttpClient::new( hyper_util::client::legacy::Client::builder(hyper_util::rt::TokioExecutor::new()) .build( hyper_rustls::HttpsConnectorBuilder::new() @@ -254,7 +251,9 @@ mod tests { .enable_http1() .enable_http2() .build(), - ); + ), + None, + ); println!( "{:?}", acc.token(&client, &["https://www.googleapis.com/auth/pubsub"]) diff --git a/src/service_account_impersonator.rs b/src/service_account_impersonator.rs index 0273dd346..e3902dd56 100644 --- a/src/service_account_impersonator.rs +++ b/src/service_account_impersonator.rs @@ -6,11 +6,11 @@ use http::header; use http_body_util::BodyExt; -use hyper_util::client::legacy::connect::Connect; use serde::Serialize; use crate::{ authorized_user::{AuthorizedUserFlow, AuthorizedUserSecret}, + client::SendRequest, storage::TokenInfo, Error, }; @@ -119,14 +119,13 @@ impl ServiceAccountImpersonationFlow { } } - pub(crate) async fn token( + pub(crate) async fn token( &self, - hyper_client: &hyper_util::client::legacy::Client, + hyper_client: &impl SendRequest, scopes: &[T], ) -> Result where T: AsRef, - C: Connect + Clone + Send + Sync + 'static, { let inner_token = self .inner_flow @@ -188,8 +187,8 @@ fn id_request( .unwrap()) } -pub(crate) async fn token_impl( - hyper_client: &hyper_util::client::legacy::Client, +pub(crate) async fn token_impl( + hyper_client: &impl SendRequest, uri: &str, access_token: bool, inner_token: &str, @@ -197,7 +196,6 @@ pub(crate) async fn token_impl( ) -> Result where T: AsRef, - C: Connect + Clone + Send + Sync + 'static, { let scopes: Vec<_> = scopes.iter().map(|s| s.as_ref()).collect(); let request = if access_token { diff --git a/src/storage.rs b/src/storage.rs index 010d169ce..9a2af3026 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -462,10 +462,8 @@ mod tests { let scope_set = ScopeSet::from(&["myscope"]); let tempdir = tempfile::Builder::new() - .prefix("yup-oauth2-tests_") - .rand_bytes(15) .tempdir() - .unwrap(); + .expect("Tempdir to be created"); let filename = tempdir.path().join("tokenstorage.json"); @@ -491,10 +489,7 @@ mod tests { tokio::time::timeout(Duration::from_secs(1), find_file(&filename)) .await - .expect(&format!( - "File not created at {}", - filename.to_string_lossy() - )); + .unwrap_or_else(|_| panic!("File not created at {}", filename.to_string_lossy())); { // Create a new DiskStorage instance and verify the tokens were read from disk correctly. diff --git a/tests/tests.rs b/tests/tests.rs index 845cd2a80..7766dff60 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -1,18 +1,24 @@ use yup_oauth2::{ - authenticator::{DefaultAuthenticator, DefaultHyperClient, HyperClientBuilder}, + authenticator::DefaultAuthenticator, authenticator_delegate::{DeviceAuthResponse, DeviceFlowDelegate, InstalledFlowDelegate}, + client::{DefaultHyperClientBuilder, HttpClient, HyperClientBuilder}, + error::SendError, AccessTokenAuthenticator, ApplicationDefaultCredentialsAuthenticator, ApplicationDefaultCredentialsFlowOpts, ApplicationSecret, DeviceFlowAuthenticator, InstalledFlowAuthenticator, InstalledFlowReturnMethod, ServiceAccountAuthenticator, ServiceAccountKey, }; -use std::future::Future; use std::path::PathBuf; use std::pin::Pin; +use std::{future::Future, time::Duration}; use http::Uri; -use httptest::{matchers::*, responders::json_encoded, Expectation, Server}; +use httptest::{ + matchers::*, + responders::{delay_and_then, json_encoded}, + Expectation, Server, +}; use url::form_urlencoded; /// Utility function for parsing json. Useful in unit tests. Simply wrap the @@ -24,6 +30,13 @@ macro_rules! parse_json { } async fn create_device_flow_auth(server: &Server) -> DefaultAuthenticator { + create_device_flow_auth_with_timeout(server, None).await +} + +async fn create_device_flow_auth_with_timeout( + server: &Server, + timeout: Option, +) -> DefaultAuthenticator { let app_secret: ApplicationSecret = parse_json!({ "client_id": "902216714886-k2v9uei3p1dk6h686jbsn9mo96tnbvto.apps.googleusercontent.com", "project_id": "yup-test-243420", @@ -44,12 +57,22 @@ async fn create_device_flow_auth(server: &Server) -> DefaultAuthenticator { } } - DeviceFlowAuthenticator::with_client(app_secret, DefaultHyperClient.build_test_hyper_client()) - .flow_delegate(Box::new(FD)) - .device_code_url(server.url_str("/code")) - .build() - .await - .unwrap() + let mut client = DefaultHyperClientBuilder::default(); + if let Some(duration) = timeout { + client = client.with_timeout(duration); + } + + DeviceFlowAuthenticator::with_client( + app_secret, + client + .build_hyper_client() + .expect("Hyper client to be built"), + ) + .flow_delegate(Box::new(FD)) + .device_code_url(server.url_str("/code")) + .build() + .await + .unwrap() } #[tokio::test] @@ -88,7 +111,7 @@ async fn test_device_success() { }))), ); - let auth = create_device_flow_auth(&server).await; + let auth = create_device_flow_auth_with_timeout(&server, Some(Duration::from_secs(1))).await; let token = auth .token(&["https://www.googleapis.com/scope/1"]) .await @@ -99,6 +122,41 @@ async fn test_device_success() { ); } +#[tokio::test] +async fn test_device_delay() { + let _ = env_logger::try_init(); + let server = Server::run(); + let delay = Duration::from_micros(500); + + server.expect( + Expectation::matching(all_of![ + request::method_path("POST", "/code"), + request::body(url_decoded(contains(( + "client_id", + matches("902216714886") + )))), + ]) + .respond_with(delay_and_then(delay, || { + json_encoded(serde_json::json!({ + "device_code": "devicecode", + "user_code": "usercode", + "verification_url": "https://example.com/verify", + "expires_in": 1234567, + "interval": 1 + })) + })), + ); + + let auth = + create_device_flow_auth_with_timeout(&server, Some(delay - Duration::from_micros(1))).await; + let result = auth.token(&["https://www.googleapis.com/scope/1"]).await; + + assert!(matches!( + result, + Err(yup_oauth2::Error::HttpClientError(SendError::Timeout)) + )) +} + #[tokio::test] async fn test_device_no_code() { let _ = env_logger::try_init(); @@ -175,12 +233,7 @@ async fn create_installed_flow_auth( "client_secret": "iuMPN6Ne1PD7cos29Tk9rlqH", "redirect_uris": ["urn:ietf:wg:oauth:2.0:oob","http://localhost"], }); - struct FD( - hyper_util::client::legacy::Client< - ::Connector, - String, - >, - ); + struct FD(HttpClient<::Connector>); impl InstalledFlowDelegate for FD { /// Depending on need_code, return the pre-set code or send the code to the server at /// the redirect_uri given in the url. @@ -223,7 +276,9 @@ async fn create_installed_flow_auth( } } - let client = DefaultHyperClient.build_test_hyper_client(); + let client = DefaultHyperClientBuilder::default() + .build_hyper_client() + .expect("Hyper client to be built"); let mut builder = InstalledFlowAuthenticator::with_client(app_secret, method, client.clone()) .flow_delegate(Box::new(FD(client))); @@ -341,10 +396,15 @@ async fn create_service_account_auth(server: &Server) -> DefaultAuthenticator { "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/yup-test-sa-1%40yup-test-243420.iam.gserviceaccount.com" }); - ServiceAccountAuthenticator::with_client(key, DefaultHyperClient.build_test_hyper_client()) - .build() - .await - .unwrap() + ServiceAccountAuthenticator::with_client( + key, + DefaultHyperClientBuilder::default() + .build_hyper_client() + .expect("Hyper client to be built"), + ) + .build() + .await + .unwrap() } #[tokio::test] @@ -673,7 +733,9 @@ async fn test_default_application_credentials_from_metadata_server() { }; let authenticator = match ApplicationDefaultCredentialsAuthenticator::with_client( opts, - DefaultHyperClient.build_test_hyper_client(), + DefaultHyperClientBuilder::default() + .build_hyper_client() + .expect("Hyper client to be built"), ) .await { @@ -692,11 +754,13 @@ async fn test_default_application_credentials_from_metadata_server() { #[tokio::test] async fn test_token() { - let authenticator = - AccessTokenAuthenticator::with_client("0815".to_string(), DefaultHyperClient) - .build() - .await - .unwrap(); + let authenticator = AccessTokenAuthenticator::with_client( + "0815".to_string(), + DefaultHyperClientBuilder::default(), + ) + .build() + .await + .unwrap(); let access_token = authenticator .token(&["https://googleapis.com/some/scope"]) .await