Secrets and signed assertions
All domains, identifiers, keys, and credentials in these examples are fictional.
The printer has used a client secret since its first token request. It works, and it is simple. It also means the most valuable thing the printer owns is a string that has to be copied into every server that talks to the photo service, and sent across the network with every token request.
This lesson compares that arrangement with one where the printer keeps a private key and never sends it anywhere. The Secrets and key pairs lesson introduced the difference between a shared secret and a key pair. Here we follow both through a token request.
Living with a client secret
A client secret is a shared secret: the printer has it, and the photo service can check it. The authorization server should generate it, not the developer, so that it is long and random rather than a memorable word. Many services show it once, when it is created, and never again.
On the server side, a secret used with client_secret_basic or client_secret_post can be stored the way passwords are, as a hash. The server compares a hash of the presented secret with the stored hash, so a copy of its database does not reveal working secrets. A secret used with client_secret_jwt cannot be stored that way, because the server needs the secret itself to check each signature.
On the client side, the secret belongs in a secrets store or protected configuration, loaded by the backend at run time. It does not belong in source code, in a file committed to a repository, or in a shared document that developers pass around. Each extra copy is another place it can leak from, and a secret looks the same whether the printer or someone else presents it.
The weakness is built in. Every token request sends the secret to the server. Anything that records requests in full, such as a misconfigured proxy or a debugging log, can capture it, and a captured secret works until someone notices and replaces it.
Signing a client assertion
With private_key_jwt, the printer generates a key pair. It keeps the private key on its backend, ideally in a key management service or hardware module that can sign without ever revealing the key. It registers only the public key with the photo service, either as a value in the registration or as the address of a published key set that the server can fetch.
For each token request, the printer creates a short JWT about itself, called a client assertion. Its header names the key that signed it:
Header:
{
"alg": "ES256",
"kid": "printer-2026-10",
"typ": "JWT"
}
Claims:
{
"iss": "photo-printer",
"sub": "photo-printer",
"aud": "https://auth.photos.example",
"iat": 1790845200,
"exp": 1790845260,
"jti": "demo-client-assertion-118"
}
Both iss and sub are the printer's client ID, because the printer is both making the statement and the subject of it. aud names the authorization server the assertion is meant for. Some servers ask for their token endpoint's URL here and others for their issuer identifier, so the client uses the value its server documents. The assertion lasts one minute, and jti gives it a unique identifier.
The printer signs the assertion and sends it in place of a secret:
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_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer
&client_assertion=eyJhbGciOiJFUzI1NiIsImtpZCI6InByaW50ZXItMjAyNi0xMCIsInR5cCI6IkpXVCJ9...
The assertion is shortened here for display. Compare this with the JWT and SAML assertion grants lesson. There, the JWT was about a designer and took the place of the authorization code. Here, the JWT is about the client and takes the place of the client secret, while the authorization code is still present. The parameters are different too: client_assertion for authenticating the client, and assertion for a grant.
What the server checks
The authorization server works through the assertion before it considers the rest of the request:
- The
kididentifies a public key registered for this client, and the signature verifies with it using an algorithm the registration allows. issandsubboth equal the client ID the request is about.audnames this authorization server.- The assertion has not expired, and its lifetime is short.
- Its
jtihas not been used before, so the same assertion cannot be presented twice.
Only then does the server go on to check the code, the redirect URI, and the PKCE verifier, exactly as before.
Suppose a debugging log captured this whole request. The assertion inside it has a one-minute lifetime and an identifier the server has already seen, so it cannot be replayed. Nothing in the log lets anyone create a new assertion, because the private key was never in the request. The same log with a client secret in it would be an emergency.
Choosing between them
Client secrets remain common because they are easy to set up and every server supports them. For a small backend with one deployment and a good secrets store, a long random secret sent with HTTP Basic is a reasonable choice.
Signed assertions cost more effort: generating keys, publishing or registering the public key, and building a JWT for each request. In return, the credential never travels, the server stores nothing that could be used to impersonate the client, and the private key can live in hardware that will sign but never export it. Current security guidance prefers asymmetric methods such as private_key_jwt and mutual TLS where they are practical, and high-assurance profiles require them.
client_secret_jwt sits between the two. The secret no longer travels, but both sides still hold the same secret, so a leak from either side lets an attacker sign assertions. It is less common than either neighbor.
Whichever method a client uses, its credential will one day need replacing. The last lesson in this section follows that change, which is much easier to get right with key pairs than with secrets.