Skip to content

Commit

Permalink
feat(NoninteractiveAuthenticator): add a builder for serializable tok…
Browse files Browse the repository at this point in the history
…ens 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.
  • Loading branch information
sean-purcell committed Jan 8, 2025
1 parent a006f6c commit 63ee4dc
Show file tree
Hide file tree
Showing 2 changed files with 86 additions and 4 deletions.
13 changes: 9 additions & 4 deletions src/authenticator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,10 @@ where
where
T: AsRef<str>,
{
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,
Expand Down Expand Up @@ -109,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,
Expand Down Expand Up @@ -175,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.
Expand Down
77 changes: 77 additions & 0 deletions src/noninteractive.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
//! 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)]
Expand All @@ -14,6 +16,16 @@ struct Entry {
}

impl Entry {
fn create<T>(scopes: &[T], refresh_token: String) -> Self
where
T: AsRef<str>,
{
Entry {
scopes: (scopes.iter().map(|x| x.as_ref().to_string()).collect()),
refresh_token,
}
}

fn is_subset<T>(&self, scopes: &[T]) -> bool
where
T: AsRef<str>,
Expand Down Expand Up @@ -43,6 +55,71 @@ 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<C>,
) -> Result<NoninteractiveTokensBuilder<'a, C>, 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<C>,
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<T>(
mut self,
scopes: &[T],
force_refresh: bool,
) -> Result<NoninteractiveTokensBuilder<'a, C>, Error>
where
T: AsRef<str>,
{
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.
Expand Down

0 comments on commit 63ee4dc

Please sign in to comment.