Internet Protocols · Identity Protocols
Client Authentication: Secrets, Assertions, and Certificates
What each token endpoint authentication method actually proves, why a shared secret is the weakest of them, and how a signed assertion avoids transmitting one.
Client authentication at the token endpoint answers a narrow question: is this request from the client identified by the client_id? The answer is used for two things, and confusing them is the source of most of the design confusion. It establishes which client’s registered redirect URIs, scope allow-list, and grant types apply. And it makes the token endpoint a place where an intercepted authorization code cannot be redeemed by an unrelated party.
The second purpose is why the method matters more than the first. A method that proves possession of a shared string proves only that the requester has read the string. A method that proves possession of a private key proves that the requester holds a key that never travelled.
Client secret methods and their ordering
RFC 6749 defines two shared-secret methods: HTTP Basic authentication with the client identifier as username and the secret as password, and form parameters client_id and client_secret in the request body.
Basic is preferred, and the reason is not cryptographic. Credentials in an Authorization header are less likely to be captured by a framework’s request-body logging, less likely to be echoed in an error response, and less likely to end up in an analytics pipeline that records form fields. The body-parameter method exists for clients whose HTTP stack cannot set a header, which is a narrow category.
Both methods require the request to be TLS and the secret to be a value the client can protect. That is the definition of a confidential client, and it excludes browser applications and anything shipped as a binary that a user can inspect. A secret in a distributed application is public knowledge; the specification’s term for a client that cannot keep one is public, and public clients must not use these methods.
Secret comparison at the server must be constant-time with respect to the secret, and the stored form should be a hash rather than the secret itself. A token endpoint whose comparison short-circuits on the first differing byte leaks the secret one byte at a time to an attacker who can measure response time, and a token endpoint that stores secrets in plaintext turns a read of its database into a set of working credentials.
Assertions: signing instead of sending
A client assertion is a JWT the client signs with its own private key and presents at the token endpoint. RFC 7523 defines the profile. The assertion’s claims identify the client in iss and sub, name the token endpoint as aud, carry a unique jti and a short exp, and the authorization server verifies the signature against a public key it holds for that client.
The gain over a shared secret is that nothing reusable crosses the wire. An attacker who records the request has a JWT that is valid for its stated audience for a short window and cannot be reused after jti is recorded. An attacker who compromises the client’s storage finds a private key, which is at least a single artifact that can be revoked by replacing the corresponding public key, rather than a string that may also be in a configuration repository, a CI variable, and a developer’s notes.
The jti requirement is what makes replay detection possible, and it costs the authorization server a store of seen values with a retention at least as long as the maximum assertion lifetime. An authorization server that verifies the signature and the time window but does not check jti accepts a replay within that window, which for a one-minute window is a narrow but real exposure.
The audience claim is the second check that is frequently missed. An assertion minted for one authorization server’s token endpoint is a valid signature and a valid time window at another authorization server that happens to trust the same client key. aud must name the exact endpoint, and the server must reject anything else. This is the same class of error as an ID token with the wrong audience, and it has the same consequence: a credential accepted by a party it was not issued to.
Mutual TLS and certificate binding
RFC 8705 defines a method where the client authenticates with a TLS client certificate. The authorization server maps the certificate to a registered client, typically by subject distinguished name, and the TLS handshake itself provides the proof.
The stronger use of the same mechanism is certificate-bound access tokens. The server records the SHA-256 thumbprint of the client certificate in the token’s cnf claim, and a resource server that receives the token requires the presenting connection to use a certificate with the same thumbprint. A stolen token is then useless without the corresponding private key.
The operational cost is the reason this is not universal. Every client needs a certificate, every certificate needs a renewal path, and the certificate authorities involved must be trusted by both the client and the server. The resource server must terminate TLS itself or receive a trustworthy statement about the client certificate from whatever does, which rules out many proxy configurations and is why the mechanism appears more often in machine-to-machine deployments inside an organisation than in third-party integrations.
Proof of possession at the application layer
DPoP, defined in RFC 9449, moves the proof to the HTTP layer so that it does not depend on TLS termination. The client generates a key pair, includes the public key’s thumbprint in a header on the token request, and signs a small JWT over the request method and URL. The token response is bound to that key through a cnf claim, and each subsequent resource request carries a fresh proof signed over that request.
The design goal is a token that cannot be replayed even by someone who has captured it, without requiring client certificates. The cost is a signature computation per request, which for ECDSA is cheap in CPU terms but is a per-request dependency on the client’s key material being available and the clock being roughly correct, since the proof carries a timestamp.
The failure mode specific to DPoP is that the binding must be checked at the resource server, and a resource server that validates the token’s signature but ignores cnf accepts the token as a plain bearer credential. The proof header’s presence proves nothing if nothing verifies it.
Choosing among them
The decision reduces to two questions: can the client keep a secret, and does the deployment need the token itself to be unusable after theft.
A confidential client on a server the operator controls can use a secret, and the secret method’s simplicity is a real advantage: no key distribution, no clock dependency, no assertion store. The trade is that the secret must be rotated on a schedule and that every copy of it is a credential.
A confidential client that wants to avoid transmitting a reusable credential uses assertions. The trade is key management and the jti store.
A deployment where tokens are high-value and travel through environments the operator does not fully control uses certificate binding or DPoP. The trade is per-request work and a much narrower set of client environments that can participate.
A public client has none of these options in the base protocol, which is why PKCE, described in the code challenge binding, is the mechanism that carries the weight instead. PKCE proves that the party redeeming the code is the party that requested it, without identifying the client at all. The two mechanisms are complementary: PKCE protects the code, client authentication protects the endpoint.
What client authentication does not do
It does not authenticate the user. The token endpoint has no user present, and the client’s identity says nothing about whose authority the grant represents. The user’s authentication happened at the authorization endpoint, and its result is recorded in the grant.
It does not constrain what the client can do with the token. A client that authenticates correctly and then leaks its access token has produced exactly the outcome the bearer model predicts; client authentication at issuance does not follow the token afterwards. Only binding the token to a key, through certificate thumbprint or DPoP, carries the client’s identity along with the token.
It also does not survive a compromised client. An attacker running inside a confidential client has the client’s secret, its private key, and its certificate. The method determines what an external observer can steal, not what a compromised host can do. That distinction is why the mitigations for a compromised client are detection and revocation rather than stronger authentication, and why the revocation behaviour of a grant, covered in refresh token rotation, determines how long a compromise lasts.
References
- RFC 6749: The OAuth 2.0 Authorization Framework — IETF
- RFC 7523: JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants — IETF
- RFC 8705: OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens — IETF
- RFC 9449: OAuth 2.0 Demonstrating Proof of Possession (DPoP) — IETF