use std::future::Future;
use std::time::Duration;
use plakous_http::{Client as HttpClient, ExchangeControl, Request as HttpRequest};
use crate::{
ClaimedHttpError, ClaimedHttpResponse, CredentialFence, CredentialGeneration, CredentialRecord,
PrivateCredentialStore, PrivateStoreError, RefreshClaim,
};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum CredentialRefreshPhase {
Began,
Completed,
OutcomeUnknown,
Quarantined,
Restored,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum CredentialRefreshFailureClass {
Transport,
Timeout,
HttpStatus(HttpStatusClass),
Validation,
Store,
Cancelled,
NotReady,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum HttpStatusClass {
ClientError,
ServerError,
Other,
}
impl HttpStatusClass {
#[must_use]
pub const fn from_status(status: u16) -> Self {
match status {
400..=499 => Self::ClientError,
500..=599 => Self::ServerError,
_ => Self::Other,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum CredentialRefreshFailureReason {
TokenShape,
ResponseTooLarge,
ExpiresIn,
AccessTokenExpired,
MissingRefreshToken,
IdTokenHeader,
IdTokenUnknownKid,
IdTokenSigningKey,
IdTokenSignature,
IdTokenIss,
IdTokenAud,
IdTokenExp,
IdTokenIat,
IdTokenNbf,
IdTokenAzp,
IdTokenSub,
IdTokenClaims,
SubjectMismatch,
SelectionChanged,
RetiredFormat,
StoredCredential,
ProviderRejected,
}
impl CredentialRefreshFailureReason {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::TokenShape => "token_shape",
Self::ResponseTooLarge => "response_too_large",
Self::ExpiresIn => "expires_in",
Self::AccessTokenExpired => "access_token_expired",
Self::MissingRefreshToken => "missing_refresh_token",
Self::IdTokenHeader => "id_token_header",
Self::IdTokenUnknownKid => "id_token_unknown_kid",
Self::IdTokenSigningKey => "id_token_signing_key",
Self::IdTokenSignature => "id_token_signature",
Self::IdTokenIss => "id_token_iss",
Self::IdTokenAud => "id_token_aud",
Self::IdTokenExp => "id_token_exp",
Self::IdTokenIat => "id_token_iat",
Self::IdTokenNbf => "id_token_nbf",
Self::IdTokenAzp => "id_token_azp",
Self::IdTokenSub => "id_token_sub",
Self::IdTokenClaims => "id_token_claims",
Self::SubjectMismatch => "subject_mismatch",
Self::SelectionChanged => "selection_changed",
Self::RetiredFormat => "retired_format",
Self::StoredCredential => "stored_credential",
Self::ProviderRejected => "provider_rejected",
}
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct CredentialRefreshDetail {
pub reason: Option<CredentialRefreshFailureReason>,
pub expires_in_secs: Option<u64>,
pub id_token_iat_age_secs: Option<i64>,
pub id_token_exp_in_secs: Option<i64>,
}
impl CredentialRefreshDetail {
#[must_use]
pub fn from_response(
now_unix: u64,
expires_in_secs: Option<u64>,
id_token: Option<&str>,
max_bytes: usize,
) -> Self {
let delta = |a: u64, b: u64| i64::try_from(i128::from(a) - i128::from(b)).ok();
let (iat, exp) = id_token
.and_then(|token| crate::oauth_workflow::unverified_jwt_timing(token, max_bytes))
.unwrap_or((None, None));
Self {
reason: None,
expires_in_secs,
id_token_iat_age_secs: iat.and_then(|iat| delta(now_unix, iat)),
id_token_exp_in_secs: exp.and_then(|exp| delta(exp, now_unix)),
}
}
#[must_use]
pub const fn with_reason(mut self, reason: Option<CredentialRefreshFailureReason>) -> Self {
self.reason = reason;
self
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct CredentialRefreshEvent {
pub phase: CredentialRefreshPhase,
pub failure_class: Option<CredentialRefreshFailureClass>,
pub transport_phase: Option<plakous_http::Phase>,
pub http_status: Option<u16>,
pub elapsed: Duration,
pub generation: u64,
pub detail: CredentialRefreshDetail,
}
pub trait CredentialRefreshObserver: Send + Sync {
fn observe(&self, event: CredentialRefreshEvent);
}
#[derive(Debug)]
#[non_exhaustive]
pub enum BuildRequestFailure<E> {
LeaveQuarantined(E),
Restore(E),
}
pub trait ClaimedRefreshHooks {
type Error;
type Prepared: Send;
type Output;
fn check_profile(&mut self) -> impl Future<Output = Result<(), Self::Error>> + Send;
fn fence_needing_refresh(
&mut self,
) -> impl Future<Output = Result<Option<CredentialFence>, Self::Error>> + Send;
fn fresh_output(&mut self) -> Result<Self::Output, Self::Error>;
fn build_refresh_request(
&mut self,
claim: &RefreshClaim,
) -> Result<(Self::Prepared, HttpRequest), BuildRequestFailure<Self::Error>>;
fn authorize_dispatch(&mut self) -> impl Future<Output = Result<(), Self::Error>> + Send {
async { Ok(()) }
}
fn exchange_control(&mut self) -> ExchangeControl;
fn execute_claimed(
&mut self,
store: &PrivateCredentialStore,
client: &HttpClient,
claim: RefreshClaim,
request: HttpRequest,
control: ExchangeControl,
) -> impl Future<Output = Result<ClaimedHttpResponse, ClaimedHttpError>> + Send {
async move {
store
.execute_claimed_http(claim, client, request, control)
.await
}
}
fn validate_tokens(
&mut self,
prepared: Self::Prepared,
response: &mut ClaimedHttpResponse,
) -> impl Future<Output = Result<CredentialRecord, Self::Error>> + Send;
fn recheck_profile_before_complete(
&mut self,
) -> impl Future<Output = Result<(), Self::Error>> + Send {
async { Ok(()) }
}
fn map_claimed_http_error(
&mut self,
store: &PrivateCredentialStore,
error: ClaimedHttpError,
) -> impl Future<Output = Self::Error> + Send;
fn map_store_error(&mut self, error: PrivateStoreError) -> Self::Error;
fn output_from_completed(
&mut self,
fence: CredentialFence,
) -> Result<Self::Output, Self::Error>;
fn refresh_observer(&self) -> Option<&dyn CredentialRefreshObserver> {
None
}
fn failure_detail(&mut self, error: &Self::Error) -> CredentialRefreshDetail {
let _ = error;
CredentialRefreshDetail::default()
}
fn reclaim_unknown_refresh(&self) -> bool {
false
}
fn after_post_start_failure(&mut self, store: &PrivateCredentialStore, error: &Self::Error) {
let _ = (store, error);
}
}
pub trait ClaimedRevokeHooks {
type Error;
type Output;
fn check_profile(&mut self) -> impl Future<Output = Result<(), Self::Error>> + Send;
fn fence_to_revoke(
&mut self,
) -> impl Future<Output = Result<CredentialFence, Self::Error>> + Send;
fn build_revoke_request(
&mut self,
claim: &RefreshClaim,
) -> Result<HttpRequest, BuildRequestFailure<Self::Error>>;
fn authorize_dispatch(&mut self) -> impl Future<Output = Result<(), Self::Error>> + Send {
async { Ok(()) }
}
fn exchange_control(&mut self) -> ExchangeControl;
fn execute_claimed(
&mut self,
store: &PrivateCredentialStore,
client: &HttpClient,
claim: RefreshClaim,
request: HttpRequest,
control: ExchangeControl,
) -> impl Future<Output = Result<ClaimedHttpResponse, ClaimedHttpError>> + Send {
async move {
store
.execute_claimed_http(claim, client, request, control)
.await
}
}
fn accept_revoke_response(
&mut self,
response: &mut ClaimedHttpResponse,
) -> impl Future<Output = Result<(), Self::Error>> + Send;
fn recheck_profile_before_complete(
&mut self,
) -> impl Future<Output = Result<(), Self::Error>> + Send {
async { Ok(()) }
}
fn map_claimed_http_error(
&mut self,
store: &PrivateCredentialStore,
error: ClaimedHttpError,
) -> impl Future<Output = Self::Error> + Send;
fn map_store_error(&mut self, error: PrivateStoreError) -> Self::Error;
fn output_from_revoked(
&mut self,
generation: CredentialGeneration,
) -> Result<Self::Output, Self::Error>;
fn observer(&self) -> Option<&dyn CredentialRefreshObserver> {
None
}
}