use std::fmt;
use std::sync::Arc;
use std::time::Instant;
use asupersync::Cx;
use asupersync::sync::OwnedMutexGuard;
use asupersync::types::Time;
use fastmcp_core::McpRequestCancellation;
use super::super::{
ManagedOAuthSession, OAuthSessionError, PendingPermit, SessionGuard, deadline_after,
};
use crate::http_auth::oauth::{OAuthClient, OAuthCredentials, OAuthError};
#[derive(Debug)]
pub enum OAuthAccessRotationError {
Context(OAuthError),
Session(OAuthSessionError),
NotComplete,
GenerationMismatch,
InMemoryRefreshOwnership,
ScopeExpansion,
}
impl fmt::Display for OAuthAccessRotationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Context(error) => fmt::Display::fmt(error, f),
Self::Session(error) => fmt::Display::fmt(error, f),
Self::NotComplete => {
f.write_str("persistent renewal has no completed access grant to install")
}
Self::GenerationMismatch => {
f.write_str("managed OAuth generation changed before access installation")
}
Self::InMemoryRefreshOwnership => f.write_str(
"persistent access installation cannot replace in-memory refresh ownership",
),
Self::ScopeExpansion => f.write_str(
"persistent access installation would expand the current session scopes",
),
}
}
}
impl std::error::Error for OAuthAccessRotationError {}
impl From<OAuthSessionError> for OAuthAccessRotationError {
fn from(error: OAuthSessionError) -> Self {
Self::Session(error)
}
}
pub(crate) struct AccessRotation<'a> {
session: &'a ManagedOAuthSession,
guard: SessionGuard<'a>,
_permit: PendingPermit<'a>,
cx: &'a Cx,
cancellation: &'a McpRequestCancellation,
deadline: Time,
expected_generation: u64,
}
impl ManagedOAuthSession {
pub(crate) async fn reserve_access_rotation<'a>(
&'a self,
cx: &'a Cx,
cancellation: &'a McpRequestCancellation,
expected_generation: u64,
candidate: &OAuthCredentials,
) -> Result<AccessRotation<'a>, OAuthAccessRotationError> {
self.check(cx, cancellation)?;
admit_candidate(self, candidate)?;
let deadline = deadline_after(cx, self.inner.policy.acquisition_timeout)?;
let permit = PendingPermit::acquire(
&self.inner.pending,
self.inner.policy.max_pending_acquisitions,
)?;
let guard = self
.await_active(cx, cancellation, deadline, None, async {
let guard = OwnedMutexGuard::lock(Arc::clone(&self.inner.state), cx)
.await
.map_err(|_| OAuthSessionError::StateUnavailable)?;
Ok(SessionGuard {
guard,
closed: &self.inner.closed,
logout_handoff: &self.inner.logout_handoff,
})
})
.await?;
let reserved = AccessRotation {
session: self,
guard,
_permit: permit,
cx,
cancellation,
deadline,
expected_generation,
};
reserved.admit(candidate)?;
Ok(reserved)
}
}
impl AccessRotation<'_> {
fn admit(&self, candidate: &OAuthCredentials) -> Result<u64, OAuthAccessRotationError> {
self.session.check(self.cx, self.cancellation)?;
if self.cx.now() >= self.deadline {
return Err(OAuthSessionError::TimedOut.into());
}
admit_candidate(self.session, candidate)?;
let state = self.guard.as_ref().ok_or(OAuthSessionError::Closed)?;
if state.generation != self.expected_generation {
return Err(OAuthAccessRotationError::GenerationMismatch);
}
if state.credentials.has_refresh_token() {
return Err(OAuthAccessRotationError::InMemoryRefreshOwnership);
}
if state.renewal_failed || state.credentials.bearer_credential().is_revoked() {
return Err(OAuthSessionError::LoginRequired.into());
}
if candidate
.scopes()
.iter()
.any(|scope| !state.credentials.scopes().contains(scope))
{
return Err(OAuthAccessRotationError::ScopeExpansion);
}
state
.generation
.checked_add(1)
.ok_or(OAuthSessionError::GenerationExhausted.into())
}
pub(crate) fn commit(
mut self,
candidate: OAuthCredentials,
) -> Result<u64, (OAuthAccessRotationError, Box<OAuthCredentials>)> {
let generation = match self.admit(&candidate) {
Ok(generation) => generation,
Err(error) => return Err((error, Box::new(candidate))),
};
let Some(state) = self.guard.as_mut() else {
return Err((OAuthSessionError::Closed.into(), Box::new(candidate)));
};
state.renew_after = candidate.expires_at();
state.credentials = candidate;
state.generation = generation;
Ok(generation)
}
}
fn admit_candidate(
session: &ManagedOAuthSession,
candidate: &OAuthCredentials,
) -> Result<(), OAuthAccessRotationError> {
if !session.inner.client.accepts_credentials(candidate) {
return Err(OAuthSessionError::OAuth(OAuthError::CredentialBindingMismatch).into());
}
if candidate.has_refresh_token() {
return Err(OAuthAccessRotationError::InMemoryRefreshOwnership);
}
if candidate.bearer_credential().is_revoked() || Instant::now() >= candidate.expires_at() {
return Err(OAuthSessionError::LoginRequired.into());
}
Ok(())
}
#[derive(Debug)]
pub enum OAuthRefreshTransferError {
Session(OAuthSessionError),
GenerationMismatch,
}
impl fmt::Display for OAuthRefreshTransferError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Session(error) => fmt::Display::fmt(error, f),
Self::GenerationMismatch => {
f.write_str("managed OAuth generation changed before refresh transfer")
}
}
}
}
impl std::error::Error for OAuthRefreshTransferError {}
impl From<OAuthSessionError> for OAuthRefreshTransferError {
fn from(error: OAuthSessionError) -> Self {
Self::Session(error)
}
}
pub(crate) struct RefreshTransfer<'a> {
session: &'a ManagedOAuthSession,
guard: SessionGuard<'a>,
_permit: PendingPermit<'a>,
cx: &'a Cx,
cancellation: &'a McpRequestCancellation,
client: &'a OAuthClient,
deadline: Time,
expected_generation: u64,
}
impl ManagedOAuthSession {
pub(crate) async fn reserve_refresh_transfer<'a>(
&'a self,
cx: &'a Cx,
cancellation: &'a McpRequestCancellation,
expected_generation: u64,
client: &'a OAuthClient,
) -> Result<RefreshTransfer<'a>, OAuthRefreshTransferError> {
self.check(cx, cancellation)?;
let deadline = deadline_after(cx, self.inner.policy.acquisition_timeout)?;
let permit = PendingPermit::acquire(
&self.inner.pending,
self.inner.policy.max_pending_acquisitions,
)?;
let guard = self
.await_active(cx, cancellation, deadline, None, async {
let guard = OwnedMutexGuard::lock(Arc::clone(&self.inner.state), cx)
.await
.map_err(|_| OAuthSessionError::StateUnavailable)?;
Ok(SessionGuard {
guard,
closed: &self.inner.closed,
logout_handoff: &self.inner.logout_handoff,
})
})
.await?;
let reserved = RefreshTransfer {
session: self,
guard,
_permit: permit,
cx,
cancellation,
client,
deadline,
expected_generation,
};
reserved.admit()?;
Ok(reserved)
}
}
impl RefreshTransfer<'_> {
fn admit(&self) -> Result<(), OAuthRefreshTransferError> {
self.session.check(self.cx, self.cancellation)?;
if self.cx.now() >= self.deadline {
return Err(OAuthSessionError::TimedOut.into());
}
let state = self.guard.as_ref().ok_or(OAuthSessionError::Closed)?;
if state.generation != self.expected_generation {
return Err(OAuthRefreshTransferError::GenerationMismatch);
}
if !self.client.accepts_credentials(&state.credentials) {
return Err(OAuthSessionError::OAuth(OAuthError::CredentialBindingMismatch).into());
}
if state.renewal_failed || state.credentials.bearer_credential().is_revoked() {
return Err(OAuthSessionError::LoginRequired.into());
}
if !state.credentials.has_refresh_token() {
return Err(OAuthSessionError::OAuth(OAuthError::RefreshUnavailable).into());
}
Ok(())
}
pub(crate) fn commit(mut self) -> Result<OAuthCredentials, OAuthRefreshTransferError> {
self.admit()?;
let state = self.guard.as_mut().ok_or(OAuthSessionError::Closed)?;
let credentials = state
.credentials
.take_persistence_credentials(&self.session.inner.closed)
.map_err(OAuthSessionError::OAuth)?;
state.renew_after = state.credentials.expires_at();
Ok(credentials)
}
}