From 1b13ce9816e09caaac9d7644201e73b0101fd011 Mon Sep 17 00:00:00 2001 From: Sean Purcell Date: Fri, 13 Aug 2021 00:18:14 -0400 Subject: [PATCH] feat(NoninteractiveAuthenticator): add a builder for serializable tokens using another authenticator Expose a way to create `NoninteractiveTokens` using another (presumably `InstalledFlow` or `DeviceFlow`) authenticator. It makes a request for tokens, and then stores just the refresh token, assuming that the access token will be expired by the time it's used. There's no clean exposed way to refresh the refresh tokens, but it's easy to do (if somewhat awkwardly) by creating a new builder using a current NoninteractiveFlow authenticator, so this feature doesn't add it. --- src/authenticator.rs | 18 ++++++++--- src/noninteractive.rs | 75 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 4 deletions(-) diff --git a/src/authenticator.rs b/src/authenticator.rs index 3b638c22b..004d706fb 100644 --- a/src/authenticator.rs +++ b/src/authenticator.rs @@ -58,7 +58,10 @@ where where T: AsRef, { - self.find_token(scopes, /* force_refresh = */ false).await + Ok(self + .find_token(scopes, /* force_refresh = */ false) + .await? + .into()) } /// Return a token for the provided scopes, but don't reuse cached tokens. Instead, @@ -70,15 +73,18 @@ where where T: AsRef, { - self.find_token(scopes, /* force_refresh = */ true).await + Ok(self + .find_token(scopes, /* force_refresh = */ true) + .await? + .into()) } /// Return a cached token or fetch a new one from the server. - async fn find_token<'a, T>( + pub(crate) async fn find_token<'a, T>( &'a self, scopes: &'a [T], force_refresh: bool, - ) -> Result + ) -> Result where T: AsRef, { @@ -131,6 +137,10 @@ where } } } + + pub(crate) fn app_secret(&self) -> Option<&ApplicationSecret> { + self.inner.auth_flow.app_secret() + } } /// Configure an Authenticator using the builder pattern. diff --git a/src/noninteractive.rs b/src/noninteractive.rs index deb4dd53f..b09b3be08 100644 --- a/src/noninteractive.rs +++ b/src/noninteractive.rs @@ -1,5 +1,6 @@ //! Module containing functionality for serializing tokens and using them at a later point for //! non-interactive services. +use crate::authenticator::Authenticator; use crate::error::Error; use crate::refresh::RefreshFlow; use crate::types::{ApplicationSecret, TokenInfo}; @@ -13,6 +14,16 @@ struct Entry { } 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, @@ -42,6 +53,70 @@ impl NoninteractiveTokens { .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: hyper::client::connect::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> { + authenticator: &'a Authenticator, + tokens: NoninteractiveTokens, +} + +impl<'a, C> NoninteractiveTokensBuilder<'a, C> +where + C: hyper::client::connect::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(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.