use std::sync::{
Arc, Mutex, PoisonError,
atomic::{AtomicBool, Ordering},
};
use bon::Builder;
use crate::{
cache::{GetTokenError, GrantParametersSource, RefreshTokenStore, TokenSource},
core::{
Error, ErrorKind,
dpop::ResourceServerDPoP,
platform::{Duration, MaybeSendBoxFuture, SystemTime},
},
grant::{
core::{OAuth2ExchangeGrant, TokenResponse},
refresh::RefreshGrantParameters,
},
};
mod breaker;
use breaker::Breaker;
#[derive(Builder)]
pub struct GrantTokenSource<G: OAuth2ExchangeGrant, S: RefreshTokenStore> {
pub(crate) grant: G,
#[builder(with = |source: impl GrantParametersSource<G::Parameters> + 'static|
Box::new(source) as Box<dyn GrantParametersSource<G::Parameters>>)]
grant_parameters: Box<dyn GrantParametersSource<G::Parameters>>,
#[builder(skip)]
params_spent: AtomicBool,
refresh_store: S,
#[builder(skip = grant.dpop().to_resource_server_dpop())]
resource_server_dpop: Arc<dyn ResourceServerDPoP>,
#[builder(skip)]
pending: Mutex<Option<TokenResponse>>,
#[builder(skip)]
credential_view: Mutex<Option<CredentialView>>,
#[builder(default = 3)]
breaker_threshold: u32,
#[builder(default = Duration::from_mins(1))]
breaker_cooldown: Duration,
#[builder(skip)]
breaker: Breaker,
}
impl<G: OAuth2ExchangeGrant, S: RefreshTokenStore> core::fmt::Debug for GrantTokenSource<G, S> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("GrantTokenSource").finish_non_exhaustive()
}
}
impl<G: OAuth2ExchangeGrant, S: RefreshTokenStore> TokenSource for GrantTokenSource<G, S> {
fn token(&self) -> MaybeSendBoxFuture<'_, Result<Arc<TokenResponse>, Error>> {
Box::pin(async move { self.token_inner().await.map(Arc::new) })
}
fn resource_server_dpop(&self) -> &dyn ResourceServerDPoP {
self.resource_server_dpop.as_ref()
}
fn has_pending_token(&self) -> bool {
self.pending
.lock()
.unwrap_or_else(PoisonError::into_inner)
.is_some()
}
fn clear(&self) -> MaybeSendBoxFuture<'_, Result<(), Error>> {
Box::pin(async move {
*self.pending.lock().unwrap_or_else(PoisonError::into_inner) = None;
self.refresh_store.clear().await?;
self.record_credential(false);
self.breaker.reset();
Ok(())
})
}
}
impl<G: OAuth2ExchangeGrant, S: RefreshTokenStore> GrantTokenSource<G, S> {
pub async fn prime(&self, token: TokenResponse) -> Result<(), Error> {
if let Some(refresh_token) = token.refresh_token() {
self.refresh_store.set(refresh_token).await?;
self.record_credential(true);
}
*self.pending.lock().unwrap_or_else(PoisonError::into_inner) = Some(token);
self.breaker.reset();
Ok(())
}
async fn token_inner(&self) -> Result<TokenResponse, Error> {
if let Some(token) = self
.pending
.lock()
.unwrap_or_else(PoisonError::into_inner)
.take()
{
self.breaker.reset();
return Ok(token);
}
let refresh_error = match self.try_refresh().await {
Ok(token) => {
self.breaker.reset();
return Ok(token);
}
Err(e) => e,
};
if !self.breaker.try_acquire(self.breaker_threshold) {
return Err(match refresh_error {
Some(source) if source.is_retryable() => {
Error::new(source.kind(), GetTokenError::RefreshFailed { source })
}
_ => Error::new(ErrorKind::Backoff, GetTokenError::Backoff),
});
}
let retryable = self.grant_parameters.retryable();
let params = if self.params_spent.load(Ordering::Relaxed) {
None
} else {
match self.grant_parameters.acquire().await {
Ok(params) => params,
Err(exchange_source) => {
if Self::counts_toward_breaker(&exchange_source, false) {
self.breaker
.record_failure(self.breaker_threshold, self.breaker_cooldown);
}
return Err(Self::combine_exchange_error(
refresh_error,
exchange_source,
retryable,
));
}
}
};
let Some(params) = params else {
return Err(match refresh_error {
Some(source) if source.is_retryable() => {
Error::new(source.kind(), GetTokenError::RefreshFailed { source })
}
Some(source) => Error::new(
ErrorKind::ReauthRequired,
GetTokenError::RefreshFailed { source },
),
None => Error::new(ErrorKind::ReauthRequired, GetTokenError::NoTokenSource),
});
};
match self.grant.exchange(params).await {
Ok(token_response) => {
self.persist_refresh_token(&token_response).await;
self.breaker.reset();
Ok(token_response)
}
Err(exchange_source) => {
let credential_dead = exchange_source.kind() == ErrorKind::InvalidGrant;
let spent = credential_dead && self.grant_parameters.discard_after_rejection();
if spent {
self.params_spent.store(true, Ordering::Relaxed);
}
if Self::counts_toward_breaker(&exchange_source, spent) {
self.breaker
.record_failure(self.breaker_threshold, self.breaker_cooldown);
}
Err(Self::combine_exchange_error(
refresh_error,
exchange_source,
retryable && !spent,
))
}
}
}
fn combine_exchange_error(
refresh_error: Option<Error>,
exchange_source: Error,
source_retryable: bool,
) -> Error {
let Some(refresh_source) = refresh_error else {
let kind = if source_retryable {
exchange_source.kind()
} else {
ErrorKind::ReauthRequired
};
return Error::new(
kind,
GetTokenError::ExchangeFailed {
source: exchange_source,
},
);
};
let exchange_recoverable =
exchange_source.is_retryable() || exchange_source.kind() == ErrorKind::RequestRejected;
let kind = if source_retryable && exchange_recoverable {
exchange_source.kind()
} else if refresh_source.is_retryable() {
refresh_source.kind()
} else {
ErrorKind::ReauthRequired
};
Error::new(
kind,
GetTokenError::BothFailed {
refresh_source,
exchange_source,
},
)
}
fn counts_toward_breaker(err: &Error, spent: bool) -> bool {
!spent && !err.is_retryable() && err.kind() != ErrorKind::RequestRejected
}
pub fn grant(&self) -> &G {
&self.grant
}
pub fn has_grant_parameters(&self) -> bool {
!self.params_spent.load(Ordering::Relaxed) && self.grant_parameters.available()
}
pub async fn has_refresh_token(&self) -> Result<bool, Error> {
let has = self.refresh_store.get().await?.is_some();
self.record_credential(has);
Ok(has)
}
pub async fn can_restore(&self, max_staleness: Option<Duration>) -> Result<bool, Error> {
if self.has_pending_token() || self.has_grant_parameters() {
return Ok(true);
}
match self.fresh_credential_view(max_staleness) {
Some(has) => Ok(has),
None => self.has_refresh_token().await,
}
}
fn record_credential(&self, has_refresh_token: bool) {
*self
.credential_view
.lock()
.unwrap_or_else(PoisonError::into_inner) = Some(CredentialView {
has_refresh_token,
reconciled_at: SystemTime::now(),
});
}
fn fresh_credential_view(&self, max_staleness: Option<Duration>) -> Option<bool> {
let view = (*self
.credential_view
.lock()
.unwrap_or_else(PoisonError::into_inner))?;
match max_staleness {
None => Some(view.has_refresh_token),
Some(max) => {
let age = SystemTime::now()
.duration_since(view.reconciled_at)
.unwrap_or(Duration::MAX);
(age <= max).then_some(view.has_refresh_token)
}
}
}
async fn try_refresh(&self) -> Result<TokenResponse, Option<Error>> {
const MAX_ATTEMPTS: u32 = 3;
for attempt in 1..=MAX_ATTEMPTS {
let refresh_token = self.refresh_store.get().await.map_err(Some)?.ok_or(None)?;
let err = match self
.grant
.to_refresh_grant()
.exchange(
RefreshGrantParameters::builder()
.refresh_token(refresh_token.clone())
.build(),
)
.await
{
Ok(token_response) => {
self.persist_refresh_token(&token_response).await;
return Ok(token_response);
}
Err(err) => err,
};
if err.kind() != ErrorKind::InvalidGrant {
return Err(Some(err));
}
match self.refresh_store.get().await {
Ok(Some(current)) if attempt < MAX_ATTEMPTS && current != refresh_token => {
continue;
}
Ok(Some(current)) if current == refresh_token => {
let _ = self.refresh_store.clear().await;
self.record_credential(false);
}
_ => {}
}
return Err(Some(err));
}
unreachable!("the final attempt never continues, so the loop always returns");
}
async fn persist_refresh_token(&self, token: &TokenResponse) {
if let Some(refresh_token) = token.refresh_token().as_ref() {
if self.refresh_store.set(refresh_token).await.is_ok() {
self.record_credential(true);
}
}
}
}
#[derive(Clone, Copy)]
struct CredentialView {
has_refresh_token: bool,
reconciled_at: SystemTime,
}
#[cfg(test)]
mod tests;