diff --git a/src/authenticator.rs b/src/authenticator.rs index 4e153b4c..d611a88a 100644 --- a/src/authenticator.rs +++ b/src/authenticator.rs @@ -11,6 +11,7 @@ use crate::device::DeviceFlow; use crate::error::Error; use crate::external_account::{ExternalAccountFlow, ExternalAccountSecret}; use crate::installed::{InstalledFlow, InstalledFlowReturnMethod}; +use crate::noninteractive::{NoninteractiveFlow, NoninteractiveTokens}; use crate::refresh::RefreshFlow; use crate::service_account_impersonator::ServiceAccountImpersonationFlow; @@ -78,9 +79,10 @@ where where T: AsRef, { - self.find_token_info(scopes, /* force_refresh = */ false) - .await - .map(|info| info.into()) + Ok(self + .find_token_info(scopes, /* force_refresh = */ false) + .await? + .into()) } /// Return a token for the provided scopes, but don't reuse cached tokens. Instead, @@ -108,7 +110,7 @@ where } /// Return a cached token or fetch a new one from the server. - async fn find_token_info<'a, T>( + pub(crate) async fn find_token_info<'a, T>( &'a self, scopes: &'a [T], force_refresh: bool, @@ -174,6 +176,10 @@ where } } } + + pub(crate) fn app_secret(&self) -> Option<&ApplicationSecret> { + self.inner.auth_flow.app_secret() + } } /// Configure an Authenticator using the builder pattern. @@ -523,6 +529,35 @@ impl ServiceAccountImpersonationAuthenticator { } } +/// Create an authenticator that uses a pre-constructed `NoninteractiveTokens` to authenticate. +/// This authenticator is guaranteed to be non-interactive, the tokens can be created in advance. +pub struct NoninteractiveAuthenticator; +impl NoninteractiveAuthenticator { + /// Use the builder pattern to create an Authenticator that uses previously constructed + /// `NoninteractiveTokens`. + #[cfg(any(feature = "hyper-rustls", feature = "hyper-tls"))] + #[cfg_attr(docsrs, doc(cfg(any(feature = "hyper-rustls", feature = "hyper-tls"))))] + pub fn builder( + tokens: NoninteractiveTokens, + ) -> AuthenticatorBuilder { + Self::with_client( + tokens, + DefaultHyperClientBuilder::default(), + ) + } + + /// Construct a new Authenticator that uses the installed flow and the provided http client. + pub fn with_client( + tokens: NoninteractiveTokens, + client: C, + ) -> AuthenticatorBuilder { + AuthenticatorBuilder::new( + NoninteractiveFlow(tokens), + client, + ) + } +} + /// ## Methods available when building any Authenticator. /// ``` /// # async fn foo() { @@ -853,6 +888,23 @@ impl AuthenticatorBuilder { .await } } + +/// ## Methods available when building a noninteractive flow Authenticator. +impl AuthenticatorBuilder { + /// Create the authenticator. + pub async fn build(self) -> io::Result> + where + C: HyperClientBuilder, + { + Self::common_build( + self.hyper_client_builder, + self.storage_type, + AuthFlow::NoninteractiveFlow(self.auth_flow), + ) + .await + } +} + mod private { use crate::access_token::AccessTokenFlow; use crate::application_default_credentials::ApplicationDefaultCredentialsFlow; @@ -862,6 +914,7 @@ mod private { use crate::error::Error; use crate::external_account::ExternalAccountFlow; use crate::installed::InstalledFlow; + use crate::noninteractive::NoninteractiveFlow; #[cfg(feature = "service-account")] use crate::service_account::ServiceAccountFlow; use crate::service_account_impersonator::ServiceAccountImpersonationFlow; @@ -878,6 +931,7 @@ mod private { AuthorizedUserFlow(AuthorizedUserFlow), ExternalAccountFlow(ExternalAccountFlow), AccessTokenFlow(AccessTokenFlow), + NoninteractiveFlow(NoninteractiveFlow), } impl AuthFlow { @@ -892,6 +946,9 @@ mod private { AuthFlow::AuthorizedUserFlow(_) => None, AuthFlow::ExternalAccountFlow(_) => None, AuthFlow::AccessTokenFlow(_) => None, + AuthFlow::NoninteractiveFlow(noninteractive_flow) => { + Some(noninteractive_flow.app_secret()) + } } } @@ -929,6 +986,9 @@ mod private { AuthFlow::AccessTokenFlow(access_token_flow) => { access_token_flow.token(hyper_client, scopes).await } + AuthFlow::NoninteractiveFlow(noninteractive_flow) => { + noninteractive_flow.token(hyper_client, scopes).await + } } } } diff --git a/src/lib.rs b/src/lib.rs index 46b940b4..e8cb08f0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -83,6 +83,7 @@ pub mod error; pub mod external_account; mod helper; mod installed; +pub mod noninteractive; mod refresh; pub mod service_account_impersonator; @@ -115,7 +116,7 @@ pub use crate::client::{CustomHyperClientBuilder, HttpClient, HyperClientBuilder pub use crate::authenticator::{ ApplicationDefaultCredentialsAuthenticator, AuthorizedUserAuthenticator, DeviceFlowAuthenticator, ExternalAccountAuthenticator, InstalledFlowAuthenticator, - ServiceAccountImpersonationAuthenticator, + NoninteractiveAuthenticator, ServiceAccountImpersonationAuthenticator, }; pub use crate::helper::*; diff --git a/src/noninteractive.rs b/src/noninteractive.rs new file mode 100644 index 00000000..0c656d58 --- /dev/null +++ b/src/noninteractive.rs @@ -0,0 +1,154 @@ +//! Module containing functionality for serializing tokens and using them at a later point for +//! non-interactive services. +use crate::authenticator::Authenticator; +use crate::client::SendRequest; +use crate::error::Error; +use crate::refresh::RefreshFlow; +use crate::types::{ApplicationSecret, TokenInfo}; + +use hyper_util::client::legacy::connect::Connect; +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize, Serialize, Clone, Default, Debug)] +struct Entry { + scopes: Vec, + refresh_token: String, +} + +impl Entry { + fn create(scopes: &[T], refresh_token: String) -> Self + where + T: AsRef, + { + Entry { + scopes: (scopes.iter().map(|x| x.as_ref().to_string()).collect()), + refresh_token, + } + } + + fn is_subset(&self, scopes: &[T]) -> bool + where + T: AsRef, + { + scopes + .iter() + .all(|scope| self.scopes.iter().any(|s| s.as_str() == scope.as_ref())) + } +} + +/// These tokens are meant to be constructed interactively using another flow, and then can be +/// serialized to be deserialized and used non-interactively later on. Since access tokens are +/// typically short-lived, this authenticator assumes it will be expired and only stores the +/// refresh token. +#[derive(Deserialize, Serialize, Clone, Default, Debug)] +pub struct NoninteractiveTokens { + app_secret: ApplicationSecret, + refresh_tokens: Vec, +} + +impl NoninteractiveTokens { + fn entry_for_scopes(&self, scopes: &[T]) -> Option<&Entry> + where + T: AsRef, + { + self.refresh_tokens + .iter() + .find(|entry| entry.is_subset(scopes)) + } + + /// Create a builder using an existing authenticator to get tokens interactively, which can be + /// saved and used later non-interactively.. + pub fn builder<'a, C>( + authenticator: &'a Authenticator, + ) -> Result, Error> + where + C: Connect + Clone + Send + Sync + 'static, + { + let app_secret = (match authenticator.app_secret() { + Some(secret) => Ok(secret.clone()), + None => Err(Error::UserError( + "No application secret present in authenticator".into(), + )), + })?; + + Ok(NoninteractiveTokensBuilder { + authenticator, + tokens: NoninteractiveTokens { + app_secret, + refresh_tokens: vec![], + }, + }) + } +} + +/// A builder to construct `NoninteractiveTokens` using an existing authenticator. +#[derive(Clone)] +pub struct NoninteractiveTokensBuilder<'a, C> +where C: Connect + Clone + Send + Sync + 'static { + authenticator: &'a Authenticator, + tokens: NoninteractiveTokens, +} + +impl<'a, C> NoninteractiveTokensBuilder<'a, C> +where + C: Connect + Clone + Send + Sync + 'static, +{ + /// Finalize the `NoninteractiveTokens`. + pub fn build(self) -> NoninteractiveTokens { + self.tokens + } + + /// Add a cached refresh token for a given set of scopes. + pub async fn add_token_for( + mut self, + scopes: &[T], + force_refresh: bool, + ) -> Result, Error> + where + T: AsRef, + { + let info = self.authenticator.find_token_info(scopes, force_refresh).await?; + match info.refresh_token { + Some(token) => { + self.tokens + .refresh_tokens + .push(Entry::create(scopes, token.clone())); + Ok(self) + } + None => Err(Error::UserError( + "Returned token doesn't contain a refresh token".into(), + )), + } + } +} + +/// A flow that uses a `NoninteractiveTokens` instance to provide access tokens. +pub struct NoninteractiveFlow(pub(crate) NoninteractiveTokens); + +impl NoninteractiveFlow { + pub(crate) fn app_secret(&self) -> &ApplicationSecret { + &self.0.app_secret + } + + pub(crate) async fn token( + &self, + hyper_client: &impl SendRequest, + scopes: &[T], + ) -> Result + where + T: AsRef + { + let refresh_token = (match self.0.entry_for_scopes(scopes) { + None => Err(Error::UserError(format!( + "No matching token found for scopes {:?}", + scopes + .iter() + .map(|x| x.as_ref().to_string()) + .collect::>() + ))), + Some(entry) => Ok(&entry.refresh_token), + })?; + + RefreshFlow::refresh_token(hyper_client, self.app_secret(), refresh_token.as_str()).await + } +}