#[non_exhaustive]pub enum ClientAuthFailure {
UnknownClient,
SecretMismatch,
RateLimited,
SecretExpired,
NotConfidential,
NoDynamicRegistration,
NoCertificatePresented,
CertificateMismatch,
AssertionInvalid {
reason: AssertionFailure,
},
}Expand description
Why a client failed to authenticate (RFC 6749 section 5.2 invalid_client).
The WIRE collapses all of these into one invalid_client, deliberately, so an attacker cannot
probe which client ids exist. The AUDIT channel separates them just as deliberately: the host
is not the attacker, and “a thousand unknown client ids” and “a thousand wrong secrets for one
real client” are different incidents with different responses.
Shared by BOTH planes: it is the reason carried by
Event::ClientAuthenticationFailed (the token plane) and by
Event::ClientRegistrationAuthenticationFailed (the RFC 7592 management plane). One
vocabulary rather than two, because a host counting credential guesses wants to count the same
shapes wherever they happen; WHICH plane an attempt arrived on is the event variant, not this
enum, so a sink can separate them without having to learn a second set of names.
Variants (Non-exhaustive)§
This enum is marked as non-exhaustive
UnknownClient
No registration exists for the presented client_id.
SecretMismatch
The registration exists and the presented credential did not verify (or none was presented for a confidential client, or one was presented for a public one).
RateLimited
The host’s own RateLimiter refused the attempt before it was evaluated.
SecretExpired
The registration is dynamic (RFC 7591) and its client_secret_expires_at has passed, so the
secret is no longer a credential however correct it is.
Separated from ClientAuthFailure::SecretMismatch because the response differs and the
urgency differs. This is not an attack: it is a client that missed a rotation window this
server announced when it registered them, and the fix is to re-register rather than to
investigate. A run of these after a rotation deadline is expected; a run of them before one
means the deployment’s clock or its issued lifetimes are wrong.
NotConfidential
The registration is PUBLIC and the endpoint the caller reached admits confidential clients only: RFC 7662 section 2.1 introspection, and the RFC 6749 section 4.4 client credentials grant.
Not a wrong credential — no credential was ever in play. A public registration has no secret, so “authenticated as a public client” is a sentence true of every caller on the internet, and naming a client id is not authentication.
IT EXISTS SO THAT THE WIRE DOES NOT HAVE TO SAY IT. Through 0.9.1 both endpoints answered
this case with an invalid_client CARRYING A DESCRIPTION (“introspection requires a
confidential client”), while an unknown client id and a confidential client with the wrong
secret both got a BARE invalid_client — so the description sorted “this id is registered,
and it is public” from everything else, which is the enumeration
ClientAuthFailure::UnknownClient and ClientAuthFailure::SecretMismatch are collapsed
on the wire to prevent. The description is gone; the fact is here instead, in the channel
where the reader is not the attacker. It is also the sentence an operator actually needs,
because the usual cause is a resource server registered with the wrong
token_endpoint_auth_method rather than an attack.
NoDynamicRegistration
MANAGEMENT PLANE ONLY. The client_id names a client the HOST provisioned itself, which
carries no RFC 7591 registration record and therefore no registration access token that
could ever verify.
Separated from ClientAuthFailure::UnknownClient because it says something that one does
not: the client id was REAL. A run of these is somebody walking a deployment’s static client
ids looking for one that happens to be dynamically registered and therefore rewritable
through RFC 7592 section 2.2; a run of UnknownClient is somebody who has not found a live
id yet. The wire tells the caller neither (both are the same 401).
NoCertificatePresented
mtls only.The registration authenticates with RFC 8705 mutual TLS and NO certificate reached this crate. Worth separating from a mismatch: in practice it usually means the TLS terminator is not configured to request, verify or forward a client certificate, which is an operational fault affecting every mutual-TLS client at once rather than an attack on one of them.
CertificateMismatch
mtls only.A certificate was presented and did not match the registration (RFC 8705 section 2.1 subject values, or section 2.2 thumbprints). This one IS the attack shape: a caller holding some valid certificate trying to be a client it is not.
AssertionInvalid
client-assertion only.The registration exists and an RFC 7523 client assertion was presented that did not verify:
a bad signature, an alg the registration does not use, an audience naming another server,
an expired assertion, or a jti that had already been spent.
Separated from ClientAuthFailure::SecretMismatch because the responses differ. A run of
wrong secrets is credential stuffing; a run of REPLAYED assertions is somebody who has
captured a client’s traffic, which is a different incident and a much worse one.
CARRIES THE REASON, because collapsing the nine into one told the operator nothing they
could act on. AssertionFailure documents itself as existing “for the host’s audit channel,
where the reader is not the attacker”, and until 0.9.1 the server discarded it here, so a
burst of these was indistinguishable between clock skew on the client
(Expired/NotYetValid, fix NTP), a key rotation the registration did not follow
(BadSignature, fix the registration), and assertions captured at another authorization
server and replayed here (WrongAudience, an incident). The mutual-TLS arm beside it
already forwarded its failure verbatim, which is how the omission was found.
Fields
reason: AssertionFailureWhich of the RFC 7523 section 3 checks the assertion failed. Never reaches the wire:
every one of them is the same invalid_client there.
Trait Implementations§
Source§impl Clone for ClientAuthFailure
impl Clone for ClientAuthFailure
Source§fn clone(&self) -> ClientAuthFailure
fn clone(&self) -> ClientAuthFailure
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read moreimpl Copy for ClientAuthFailure
Source§impl Debug for ClientAuthFailure
impl Debug for ClientAuthFailure
Source§impl Display for ClientAuthFailure
The OPERATOR’s sentence, never the client’s. Everything here is what the wire deliberately
refuses to distinguish (see the type’s docs), so these strings must not reach a response body.
impl Display for ClientAuthFailure
The OPERATOR’s sentence, never the client’s. Everything here is what the wire deliberately refuses to distinguish (see the type’s docs), so these strings must not reach a response body.
impl Eq for ClientAuthFailure
Source§impl Error for ClientAuthFailure
It is the Err payload of crate::mtls::authenticate_via_mtls, so a host handling that with
? or collecting it into a Box<dyn Error> needs this, exactly as DpopFailure and
AssertionFailure do for theirs. (Plain text rather than intra-doc links: those two types are
behind features this one is not, so a link would dangle in a default build.)
impl Error for ClientAuthFailure
It is the Err payload of crate::mtls::authenticate_via_mtls, so a host handling that with
? or collecting it into a Box<dyn Error> needs this, exactly as DpopFailure and
AssertionFailure do for theirs. (Plain text rather than intra-doc links: those two types are
behind features this one is not, so a link would dangle in a default build.)
1.30.0 · Source§fn source(&self) -> Option<&(dyn Error + 'static)>
fn source(&self) -> Option<&(dyn Error + 'static)>
1.0.0 · Source§fn description(&self) -> &str
fn description(&self) -> &str
use the Display impl or to_string()