Client authentication methods
All domains, identifiers, and credentials in these examples are fictional.
When the printer exchanged its authorization code, the token endpoint needed to know that the request really came from the printer. A stolen code alone should not be enough, and neither should knowledge of the public client ID. The printer proved itself with a header containing its client ID and secret.
That header was one choice among several. OAuth calls the general idea client authentication, and the method a client uses is part of its registration.
Proving which client is asking
Client authentication happens where a confidential client talks directly to the authorization server: the token endpoint in every flow we have followed, and other back-channel endpoints, such as those for revoking or inspecting tokens, which later lessons cover. It does not happen in the browser. An authorization request carries only the client ID, because anything placed in a browser URL can be seen by the person using it and by anything that records it.
The goal is always the same: show that the request comes from the party holding the credential registered for this client ID. The methods differ in what that credential is and how much of it travels with each request.
Methods side by side
Registrations name the method with a standard value. These are the ones you will meet most often:
| Method | What the client sends |
|---|---|
client_secret_basic | Its client ID and shared secret in an HTTP Basic Authorization header. |
client_secret_post | Its client ID and shared secret as fields in the request body. |
client_secret_jwt | A short-lived JWT signed with the shared secret. The secret itself is not sent. |
private_key_jwt | A short-lived JWT signed with the client's private key. The server holds only the public key. |
tls_client_auth and self_signed_tls_client_auth | A client certificate presented while setting up the TLS connection. |
none | Only its client ID. This is how a public client is registered. |
The first two send the secret itself. An authorization server that issues client secrets must support HTTP Basic, which is why the earlier examples used it. One detail catches many developers: before combining the client ID and secret with a colon and encoding them, each is form-encoded, so a secret containing characters such as + or : must be escaped first. A client that skips this step works with simple secrets and then fails mysteriously with one that contains punctuation.
Sending the secret in the body instead looks like this:
POST /token HTTP/1.1
Host: auth.photos.example
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=demo-code-7
&redirect_uri=https%3A%2F%2Fprinter.example%2Foauth%2Fcallback
&code_verifier=btl_training_verifier_for_one_attempt_only_2026
&client_id=photo-printer
&client_secret=demo-only-not-a-real-secret
The protection is the same as with Basic. HTTPS hides the secret in transit, and the server compares it with what it holds. Body fields are more likely to end up in request logs than an Authorization header, so the specification recommends the header form.
The JWT methods take a different approach. The client never sends a reusable secret. It sends a signed statement that it is the client, valid for a few minutes, and the server checks the signature. With private_key_jwt, the server does not hold anything that could be used to create such a statement. The certificate methods move the proof into the TLS connection itself and can also bind the tokens issued to that certificate. The next lesson examines secrets and signed assertions in detail, and Advanced OAuth covers mutual TLS.
One method per registration
The printer's registration says client_secret_basic. A careful authorization server holds it to that. A request from photo-printer that puts the secret in the body instead, or offers a JWT, is rejected even if the credential inside it is correct.
That can seem pedantic, but it closes real gaps. If a client registered for private_key_jwt could also present a shared secret, then whatever secret the server still held for that client would become a second way in, and the stronger method would protect nothing. A request must also use only one method at a time. One that carries both a Basic header and a body secret is malformed, not doubly authenticated.
Public clients are the other side of this rule. A client registered with none cannot authenticate, so the server relies on other protections, such as PKCE and exact redirect URI matching. It should not treat a public client that sends a made-up secret as if it were confidential.
When authentication fails
A wrong secret, an unknown client ID, or the wrong method produces an invalid_client error. When the client tried HTTP Basic, the server answers with status 401 and a header inviting it to authenticate:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Basic realm="auth.photos.example"
Content-Type: application/json
Cache-Control: no-store
{
"error": "invalid_client"
}
The response does not say which part was wrong. Telling a caller "that client exists, but the secret is wrong" would help someone guessing at secrets. For the same reason, servers limit how many failed attempts they accept for a client.
For the printer, invalid_client is rarely something a retry will fix. It usually means the deployed credential does not match the registration: a secret was replaced but not updated in one place, or a new key was not yet registered. That makes the moment a credential changes the riskiest point in a client's life, and the last lesson in this section covers how to change one without breaking anything.