Skip to main content

DpopVerifier

Struct DpopVerifier 

Source
pub struct DpopVerifier {
    skew: Duration,
    nonce_ttl: Duration,
    require_nonce: bool,
    started: OffsetDateTime,
    replay: Mutex<SpentKeys>,
    nonces: Mutex<NonceState>,
}
Expand description

Proof checking, nonce issuance, and the replay cache, held together because the replay window and the clock-skew window are the same number – see DpopVerifier::new.

One instance covers one nonce space. This deployment’s authorization server and resource server are one origin and hold one verifier between them, which the atproto profile allows: “The Resource Server and Authorization Server may share nonces (especially if they are the same server)”.

Fields§

§skew: Duration

How far from this verifier’s own clock a proof’s iat may sit, either direction, and also how long a jti is retained in the replay cache: past that point the proof itself would fail the clock check on a second presentation, so retaining it longer catches nothing a resend would not already be refused for.

§nonce_ttl: Duration

How long a nonce is handed out before a fresh one replaces it. The replaced nonce is accepted for as long again.

§require_nonce: bool

Whether DpopVerifier::verify requires a nonce, and so whether this verifier hands any out.

§started: OffsetDateTime

When this verifier began accepting proofs, to the whole second.

The replay window is in memory, so a restart empties it and a proof captured just before the restart would verify a second time. For the first skew after this instant a proof stamped earlier than it is refused, which is exactly the window the emptied cache could have covered. Past that, no proof old enough to matter still passes the clock check.

Whole seconds because iat is whole seconds: a proof minted inside the second this verifier started reads as minted at the start, and is accepted.

§replay: Mutex<SpentKeys>

jti values already accepted, and when each one falls out of the window in which it could still be replayed usefully.

An entry is spent by DpopVerifier::accept, which a caller reaches only once the credential the proof arrived with has checked out, so filling REPLAY_CAPACITY takes that many authenticated requests inside one clock-skew window.

§nonces: Mutex<NonceState>

Implementations§

Source§

impl DpopVerifier

Source

pub fn new(skew: Duration, nonce_ttl: Duration, require_nonce: bool) -> Self

Builds a verifier with the given clock-skew/replay window, nonce lifetime, and whether a nonce is required at all. OAuthState::default supplies this deployment’s numbers.

Source

fn started_at( skew: Duration, nonce_ttl: Duration, require_nonce: bool, started: OffsetDateTime, ) -> Self

DpopVerifier::new with the instant it began accepting proofs given rather than read off the clock, so a test can put a proof on either side of it.

Source

pub fn current_nonce(&self) -> String

The nonce to hand a client in DPoP-Nonce: the one this verifier hands out now, replaced with a fresh one once it has been handed out for nonce_ttl.

Source

fn current_nonce_at(&self, now: OffsetDateTime) -> String

Source

pub fn verify( &self, proof_jwt: &str, method: &Method, htu: &str, access_token: Option<&str>, expected_thumbprint: Option<&str>, ) -> Result<VerifiedProof, DpopError>

Verifies a DPoP proof JWT against a request.

method and htu describe the request this proof must be bound to; htu is a caller-built canonical absolute URL (scheme, host, and path – no query, no fragment) because assembling one correctly behind a reverse proxy is a deployment concern this module has no way to get right on its own; see axum::extract::OriginalUri and the Host header for how a caller typically builds it.

access_token, if given, is checked against the proof’s ath claim (RFC 9449 §4.3’s binding of a proof to a specific bearer of a specific access token). expected_thumbprint, if given, is checked against the RFC 7638 thumbprint of the proof’s own key – the check that makes a copied access token useless to whoever copied it, because their proof is signed by a different key with a different thumbprint.

Source

fn verify_at( &self, proof_jwt: &str, method: &Method, htu: &str, access_token: Option<&str>, expected_thumbprint: Option<&str>, now: OffsetDateTime, ) -> Result<VerifiedProof, DpopError>

Source

pub fn accept(&self, proof: &VerifiedProof) -> Result<(), DpopError>

Records a verified proof’s jti, so that presenting the same proof again is DpopError::Replayed.

Called once the credential the proof arrived with has checked out — the access token at a repo write, the authorization code or the refresh token at POST /oauth/token. Before that, nothing about the caller is known: POST /oauth/token’s refresh grant is reachable with any freshly generated key, so an entry spent at DpopVerifier::verify is an entry an unauthenticated caller spends, and the window would fill with proofs naming no credential at all.

DpopError::ReplayWindowFull rather than an eviction, for REPLAY_CAPACITY’s own reason: dropping an entry that still names a live proof is how a full cache becomes a way to get a replay accepted.

Source

fn accept_at( &self, proof: &VerifiedProof, now: OffsetDateTime, ) -> Result<(), DpopError>

Source

fn check_nonce_at( &self, presented: Option<&str>, now: OffsetDateTime, ) -> Result<(), DpopError>

Accepts a nonce this verifier handed out and still honours. A refusal names the nonce every client is being handed, and replaces nothing.

Trait Implementations§

Source§

impl DpopVerifier for DpopVerifier

Source§

fn verify( &self, proof: &str, http_method: &str, http_uri: &str, access_token: Option<&str>, ) -> Result<VerifiedProof, DpopError>

Verifies proof (the DPoP request header) against the request it was presented on, returning the key it proves possession of. Read more
Source§

fn accept(&self, proof: &VerifiedProof) -> Result<(), DpopError>

Records a verified proof so that the same proof presented again is refused. Read more
Source§

fn nonce(&self) -> Option<String>

The nonce a response to a DPoP request hands the client in DPoP-Nonce, or None for a verifier that checks none. Default: none.

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
§

impl<'a, T, E> AsTaggedExplicit<'a, E> for T
where T: 'a,

§

fn explicit(self, class: Class, tag: u32) -> TaggedParser<'a, Explicit, Self, E>

§

impl<'a, T, E> AsTaggedImplicit<'a, E> for T
where T: 'a,

§

fn implicit( self, class: Class, constructed: bool, tag: u32, ) -> TaggedParser<'a, Implicit, Self, E>

Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

§

impl<T> Instrument for T

§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided [Span], returning an Instrumented wrapper. Read more
§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self>

Converts self into a Left variant of Either<Self, Self> if into_left is true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
where F: FnOnce(&Self) -> bool,

Converts self into a Left variant of Either<Self, Self> if into_left(&self) returns true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
§

impl<T> PolicyExt for T
where T: ?Sized,

§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns [Action::Follow] only if self and other return Action::Follow. Read more
§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns [Action::Follow] if either self or other returns Action::Follow. Read more
Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

§

fn vzip(self) -> V

§

impl<T> WithSubscriber for T

§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a [WithDispatch] wrapper. Read more
§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a [WithDispatch] wrapper. Read more
§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,