mod grant_parameters;
mod grant_token_source;
mod in_memory;
mod token_source;
use std::sync::{Arc, PoisonError, RwLock};
pub use grant_parameters::{
FromFn, GrantParametersSource, NoSource, Reusable, SingleUse, from_fn, reusable, single_use,
};
pub use grant_token_source::{GrantTokenSource, GrantTokenSourceBuilder};
pub use in_memory::{CacheState, InMemoryTokenCache, InMemoryTokenCacheBuilder};
use snafu::Snafu;
pub use token_source::TokenSource;
use crate::{
core::{
Error,
platform::{MaybeSendBoxFuture, MaybeSendSync},
},
token::RefreshToken,
};
pub trait TokenCache: TokenSource {}
impl<T: TokenCache + ?Sized> TokenCache for &T {}
impl<T: TokenCache + ?Sized> TokenCache for Box<T> {}
impl<T: TokenCache + ?Sized> TokenCache for Arc<T> {}
#[derive(Debug, Snafu)]
#[non_exhaustive]
pub enum GetTokenError {
#[snafu(display("token refresh failed and no grant parameters were available: {source}"))]
RefreshFailed {
source: Error,
},
#[snafu(display(
"token refresh failed and exchange also failed: refresh={refresh_source}, exchange={exchange_source}"
))]
BothFailed {
refresh_source: Error,
exchange_source: Error,
},
#[snafu(display("token exchange failed and no refresh token was available: {source}"))]
ExchangeFailed {
source: Error,
},
#[snafu(display(
"no refresh token is stored and the source has no grant parameters — hand an \
interactive flow's token response to `GrantTokenSource::prime()`, or configure \
`grant_parameters` on the source"
))]
NoTokenSource,
#[snafu(display("token source backed off after repeated failures; retry after cooldown"))]
Backoff,
}
#[cfg(test)]
mod tests {
use crate::{
cache::{GrantTokenSource, InMemoryRefreshTokenStore, InMemoryTokenCache},
core::{client_auth::NoAuth, http::HttpClient},
grant::client_credentials::{ClientCredentialsGrant, ClientCredentialsGrantParameters},
};
struct NoHttp;
impl HttpClient for NoHttp {
fn execute(
&self,
_request: http::Request<bytes::Bytes>,
_idempotency: crate::core::http::Idempotency,
) -> crate::core::platform::MaybeSendBoxFuture<
'_,
Result<crate::core::http::HttpResponse, crate::core::Error>,
> {
unreachable!("test only builds the cache, no HTTP expected")
}
}
#[test]
fn test_setup() {
let source = GrantTokenSource::builder()
.grant(
ClientCredentialsGrant::builder()
.client_id("client_id")
.client_auth(NoAuth)
.token_endpoint("https://blah".parse().unwrap())
.http_client(NoHttp)
.build(),
)
.grant_parameters(
ClientCredentialsGrantParameters::builder()
.scope(bon::vec!["read", "write"])
.build(),
)
.refresh_store(InMemoryRefreshTokenStore::default())
.build();
let _cache = InMemoryTokenCache::builder().source(source).build();
}
}
pub trait RefreshTokenStore: MaybeSendSync {
fn get(&self) -> MaybeSendBoxFuture<'_, Result<Option<RefreshToken>, Error>>;
fn set<'a>(&'a self, token: &'a RefreshToken) -> MaybeSendBoxFuture<'a, Result<(), Error>>;
fn clear(&self) -> MaybeSendBoxFuture<'_, Result<(), Error>>;
}
macro_rules! forward_refresh_token_store {
($wrapper:ty) => {
impl<T: RefreshTokenStore + ?Sized> RefreshTokenStore for $wrapper {
fn get(&self) -> MaybeSendBoxFuture<'_, Result<Option<RefreshToken>, Error>> {
(**self).get()
}
fn set<'a>(
&'a self,
token: &'a RefreshToken,
) -> MaybeSendBoxFuture<'a, Result<(), Error>> {
(**self).set(token)
}
fn clear(&self) -> MaybeSendBoxFuture<'_, Result<(), Error>> {
(**self).clear()
}
}
};
}
forward_refresh_token_store!(&T);
forward_refresh_token_store!(Box<T>);
forward_refresh_token_store!(std::sync::Arc<T>);
#[derive(Debug, Default)]
pub struct InMemoryRefreshTokenStore {
refresh_token: RwLock<Option<RefreshToken>>,
}
impl RefreshTokenStore for InMemoryRefreshTokenStore {
fn get(&self) -> MaybeSendBoxFuture<'_, Result<Option<RefreshToken>, Error>> {
Box::pin(async {
Ok(self
.refresh_token
.read()
.unwrap_or_else(PoisonError::into_inner)
.clone())
})
}
fn set<'a>(&'a self, token: &'a RefreshToken) -> MaybeSendBoxFuture<'a, Result<(), Error>> {
Box::pin(async {
*self
.refresh_token
.write()
.unwrap_or_else(PoisonError::into_inner) = Some(token.clone());
Ok(())
})
}
fn clear(&self) -> MaybeSendBoxFuture<'_, Result<(), Error>> {
Box::pin(async {
*self
.refresh_token
.write()
.unwrap_or_else(PoisonError::into_inner) = None;
Ok(())
})
}
}