Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a noninteractive authenticator #159

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 64 additions & 4 deletions src/authenticator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -78,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 @@ -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,
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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<DefaultHyperClientBuilder, NoninteractiveFlow> {
Self::with_client(
tokens,
DefaultHyperClientBuilder::default(),
)
}

/// Construct a new Authenticator that uses the installed flow and the provided http client.
pub fn with_client<C>(
tokens: NoninteractiveTokens,
client: C,
) -> AuthenticatorBuilder<C, NoninteractiveFlow> {
AuthenticatorBuilder::new(
NoninteractiveFlow(tokens),
client,
)
}
}

/// ## Methods available when building any Authenticator.
/// ```
/// # async fn foo() {
Expand Down Expand Up @@ -853,6 +888,23 @@ impl<C> AuthenticatorBuilder<C, AccessTokenFlow> {
.await
}
}

/// ## Methods available when building a noninteractive flow Authenticator.
impl<C> AuthenticatorBuilder<C, NoninteractiveFlow> {
/// Create the authenticator.
pub async fn build(self) -> io::Result<Authenticator<C::Connector>>
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;
Expand All @@ -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;
Expand All @@ -878,6 +931,7 @@ mod private {
AuthorizedUserFlow(AuthorizedUserFlow),
ExternalAccountFlow(ExternalAccountFlow),
AccessTokenFlow(AccessTokenFlow),
NoninteractiveFlow(NoninteractiveFlow),
}

impl AuthFlow {
Expand All @@ -892,6 +946,9 @@ mod private {
AuthFlow::AuthorizedUserFlow(_) => None,
AuthFlow::ExternalAccountFlow(_) => None,
AuthFlow::AccessTokenFlow(_) => None,
AuthFlow::NoninteractiveFlow(noninteractive_flow) => {
Some(noninteractive_flow.app_secret())
}
}
}

Expand Down Expand Up @@ -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
}
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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::*;
Expand Down
154 changes: 154 additions & 0 deletions src/noninteractive.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
refresh_token: String,
}

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>,
{
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 {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It may be that you have good reasons to implement this, but it looks similar to storage::JSONTokens. Maybe you can reuse that type, as the storage requirements seem to be very similar (store/retrieve by scope).

app_secret: ApplicationSecret,
refresh_tokens: Vec<Entry>,
}

impl NoninteractiveTokens {
fn entry_for_scopes<T>(&self, scopes: &[T]) -> Option<&Entry>
where
T: AsRef<str>,
{
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<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.
pub struct NoninteractiveFlow(pub(crate) NoninteractiveTokens);

impl NoninteractiveFlow {
pub(crate) fn app_secret(&self) -> &ApplicationSecret {
&self.0.app_secret
}

pub(crate) async fn token<T>(
&self,
hyper_client: &impl SendRequest,
scopes: &[T],
) -> Result<TokenInfo, Error>
where
T: AsRef<str>
{
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::<Vec<_>>()
))),
Some(entry) => Ok(&entry.refresh_token),
})?;

RefreshFlow::refresh_token(hyper_client, self.app_secret(), refresh_token.as_str()).await
}
}
Loading