use crate::authenticator_delegate::{DeviceFlowDelegate, InstalledFlowDelegate};
use crate::device::DeviceFlow;
use crate::error::Error;
use crate::installed::{InstalledFlow, InstalledFlowReturnMethod};
use crate::refresh::RefreshFlow;
use crate::service_account::{ServiceAccountFlow, ServiceAccountFlowOpts, ServiceAccountKey};
use crate::storage::{self, Storage};
use crate::types::{AccessToken, ApplicationSecret, TokenInfo};
use private::AuthFlow;
use futures::lock::Mutex;
use std::borrow::Cow;
use std::fmt;
use std::io;
use std::path::PathBuf;
use std::sync::Arc;
struct InnerAuthenticator<C> {
hyper_client: hyper::Client<C>,
storage: Storage,
auth_flow: AuthFlow,
}
#[derive(Clone)]
pub struct Authenticator<C> {
inner: Arc<InnerAuthenticator<C>>,
}
struct DisplayScopes<'a, T>(&'a [T]);
impl<'a, T> fmt::Display for DisplayScopes<'a, T>
where
T: AsRef<str>,
{
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("[")?;
let mut iter = self.0.iter();
if let Some(first) = iter.next() {
f.write_str(first.as_ref())?;
for scope in iter {
f.write_str(", ")?;
f.write_str(scope.as_ref())?;
}
}
f.write_str("]")
}
}
impl<C> Authenticator<C>
where
C: hyper::client::connect::Connect + Clone + Send + Sync + 'static,
{
pub async fn token<'a, T>(&'a self, scopes: &'a [T]) -> Result<AccessToken, Error>
where
T: AsRef<str>,
{
self.find_token(scopes, false).await
}
pub async fn force_refreshed_token<'a, T>(
&'a self,
scopes: &'a [T],
) -> Result<AccessToken, Error>
where
T: AsRef<str>,
{
self.find_token(scopes, true).await
}
async fn find_token<'a, T>(
&'a self,
scopes: &'a [T],
force_refresh: bool,
) -> Result<AccessToken, Error>
where
T: AsRef<str>,
{
log::debug!(
"access token requested for scopes: {}",
DisplayScopes(scopes)
);
let hashed_scopes = storage::ScopeSet::from(scopes);
match (
self.inner.storage.get(hashed_scopes).await,
self.inner.auth_flow.app_secret(),
) {
(Some(t), _) if !t.is_expired() && !force_refresh => {
log::debug!("found valid token in cache: {:?}", t);
Ok(t.into())
}
(
Some(TokenInfo {
refresh_token: Some(refresh_token),
..
}),
Some(app_secret),
) => {
let token_info = RefreshFlow::refresh_token(
&self.inner.hyper_client,
app_secret,
&refresh_token,
)
.await?;
self.inner
.storage
.set(hashed_scopes, token_info.clone())
.await?;
Ok(token_info.into())
}
_ => {
let token_info = self
.inner
.auth_flow
.token(&self.inner.hyper_client, scopes)
.await?;
self.inner
.storage
.set(hashed_scopes, token_info.clone())
.await?;
Ok(token_info.into())
}
}
}
}
pub struct AuthenticatorBuilder<C, F> {
hyper_client_builder: C,
storage_type: StorageType,
auth_flow: F,
}
pub struct InstalledFlowAuthenticator;
impl InstalledFlowAuthenticator {
pub fn builder(
app_secret: ApplicationSecret,
method: InstalledFlowReturnMethod,
) -> AuthenticatorBuilder<DefaultHyperClient, InstalledFlow> {
AuthenticatorBuilder::<DefaultHyperClient, _>::with_auth_flow(InstalledFlow::new(
app_secret, method,
))
}
}
pub struct DeviceFlowAuthenticator;
impl DeviceFlowAuthenticator {
pub fn builder(
app_secret: ApplicationSecret,
) -> AuthenticatorBuilder<DefaultHyperClient, DeviceFlow> {
AuthenticatorBuilder::<DefaultHyperClient, _>::with_auth_flow(DeviceFlow::new(app_secret))
}
}
pub struct ServiceAccountAuthenticator;
impl ServiceAccountAuthenticator {
pub fn builder(
service_account_key: ServiceAccountKey,
) -> AuthenticatorBuilder<DefaultHyperClient, ServiceAccountFlowOpts> {
AuthenticatorBuilder::<DefaultHyperClient, _>::with_auth_flow(ServiceAccountFlowOpts {
key: service_account_key,
subject: None,
})
}
}
impl<C, F> AuthenticatorBuilder<C, F> {
async fn common_build(
hyper_client_builder: C,
storage_type: StorageType,
auth_flow: AuthFlow,
) -> io::Result<Authenticator<C::Connector>>
where
C: HyperClientBuilder,
{
let hyper_client = hyper_client_builder.build_hyper_client();
let storage = match storage_type {
StorageType::Memory => Storage::Memory {
tokens: Mutex::new(storage::JSONTokens::new()),
},
StorageType::Disk(path) => Storage::Disk(storage::DiskStorage::new(path).await?),
};
Ok(Authenticator {
inner: Arc::new(InnerAuthenticator {
hyper_client,
storage,
auth_flow,
}),
})
}
fn with_auth_flow(auth_flow: F) -> AuthenticatorBuilder<DefaultHyperClient, F> {
AuthenticatorBuilder {
hyper_client_builder: DefaultHyperClient,
storage_type: StorageType::Memory,
auth_flow,
}
}
pub fn hyper_client<NewC>(
self,
hyper_client: hyper::Client<NewC>,
) -> AuthenticatorBuilder<hyper::Client<NewC>, F> {
AuthenticatorBuilder {
hyper_client_builder: hyper_client,
storage_type: self.storage_type,
auth_flow: self.auth_flow,
}
}
pub fn persist_tokens_to_disk<P: Into<PathBuf>>(self, path: P) -> AuthenticatorBuilder<C, F> {
AuthenticatorBuilder {
storage_type: StorageType::Disk(path.into()),
..self
}
}
}
impl<C> AuthenticatorBuilder<C, DeviceFlow> {
pub fn device_code_url(self, url: impl Into<Cow<'static, str>>) -> Self {
AuthenticatorBuilder {
auth_flow: DeviceFlow {
device_code_url: url.into(),
..self.auth_flow
},
..self
}
}
pub fn flow_delegate(self, flow_delegate: Box<dyn DeviceFlowDelegate>) -> Self {
AuthenticatorBuilder {
auth_flow: DeviceFlow {
flow_delegate,
..self.auth_flow
},
..self
}
}
pub fn grant_type(self, grant_type: impl Into<Cow<'static, str>>) -> Self {
AuthenticatorBuilder {
auth_flow: DeviceFlow {
grant_type: grant_type.into(),
..self.auth_flow
},
..self
}
}
pub async fn build(self) -> io::Result<Authenticator<C::Connector>>
where
C: HyperClientBuilder,
{
Self::common_build(
self.hyper_client_builder,
self.storage_type,
AuthFlow::DeviceFlow(self.auth_flow),
)
.await
}
}
impl<C> AuthenticatorBuilder<C, InstalledFlow> {
pub fn flow_delegate(self, flow_delegate: Box<dyn InstalledFlowDelegate>) -> Self {
AuthenticatorBuilder {
auth_flow: InstalledFlow {
flow_delegate,
..self.auth_flow
},
..self
}
}
pub async fn build(self) -> io::Result<Authenticator<C::Connector>>
where
C: HyperClientBuilder,
{
Self::common_build(
self.hyper_client_builder,
self.storage_type,
AuthFlow::InstalledFlow(self.auth_flow),
)
.await
}
}
impl<C> AuthenticatorBuilder<C, ServiceAccountFlowOpts> {
pub fn subject(self, subject: impl Into<String>) -> Self {
AuthenticatorBuilder {
auth_flow: ServiceAccountFlowOpts {
subject: Some(subject.into()),
..self.auth_flow
},
..self
}
}
pub async fn build(self) -> io::Result<Authenticator<C::Connector>>
where
C: HyperClientBuilder,
{
let service_account_auth_flow = ServiceAccountFlow::new(self.auth_flow)?;
Self::common_build(
self.hyper_client_builder,
self.storage_type,
AuthFlow::ServiceAccountFlow(service_account_auth_flow),
)
.await
}
}
mod private {
use crate::device::DeviceFlow;
use crate::error::Error;
use crate::installed::InstalledFlow;
use crate::service_account::ServiceAccountFlow;
use crate::types::{ApplicationSecret, TokenInfo};
pub enum AuthFlow {
DeviceFlow(DeviceFlow),
InstalledFlow(InstalledFlow),
ServiceAccountFlow(ServiceAccountFlow),
}
impl AuthFlow {
pub(crate) fn app_secret(&self) -> Option<&ApplicationSecret> {
match self {
AuthFlow::DeviceFlow(device_flow) => Some(&device_flow.app_secret),
AuthFlow::InstalledFlow(installed_flow) => Some(&installed_flow.app_secret),
AuthFlow::ServiceAccountFlow(_) => None,
}
}
pub(crate) async fn token<'a, C, T>(
&'a self,
hyper_client: &'a hyper::Client<C>,
scopes: &'a [T],
) -> Result<TokenInfo, Error>
where
T: AsRef<str>,
C: hyper::client::connect::Connect + Clone + Send + Sync + 'static,
{
match self {
AuthFlow::DeviceFlow(device_flow) => device_flow.token(hyper_client, scopes).await,
AuthFlow::InstalledFlow(installed_flow) => {
installed_flow.token(hyper_client, scopes).await
}
AuthFlow::ServiceAccountFlow(service_account_flow) => {
service_account_flow.token(hyper_client, scopes).await
}
}
}
}
}
pub trait HyperClientBuilder {
type Connector: hyper::client::connect::Connect + Clone + Send + Sync + 'static;
fn build_hyper_client(self) -> hyper::Client<Self::Connector>;
}
#[cfg(not(feature = "hyper-tls"))]
pub type DefaultAuthenticator =
Authenticator<hyper_rustls::HttpsConnector<hyper::client::HttpConnector>>;
#[cfg(feature = "hyper-tls")]
pub type DefaultAuthenticator =
Authenticator<hyper_tls::HttpsConnector<hyper::client::HttpConnector>>;
pub struct DefaultHyperClient;
impl HyperClientBuilder for DefaultHyperClient {
#[cfg(not(feature = "hyper-tls"))]
type Connector = hyper_rustls::HttpsConnector<hyper::client::connect::HttpConnector>;
#[cfg(feature = "hyper-tls")]
type Connector = hyper_tls::HttpsConnector<hyper::client::connect::HttpConnector>;
fn build_hyper_client(self) -> hyper::Client<Self::Connector> {
#[cfg(not(feature = "hyper-tls"))]
let connector = hyper_rustls::HttpsConnector::with_native_roots();
#[cfg(feature = "hyper-tls")]
let connector = hyper_tls::HttpsConnector::new();
hyper::Client::builder()
.pool_max_idle_per_host(0)
.build::<_, hyper::Body>(connector)
}
}
impl<C> HyperClientBuilder for hyper::Client<C>
where
C: hyper::client::connect::Connect + Clone + Send + Sync + 'static,
{
type Connector = C;
fn build_hyper_client(self) -> hyper::Client<C> {
self
}
}
enum StorageType {
Memory,
Disk(PathBuf),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ensure_send_sync() {
fn is_send_sync<T: Send + Sync>() {}
is_send_sync::<Authenticator<<DefaultHyperClient as HyperClientBuilder>::Connector>>()
}
}