Beta

Create a tenant

A new tenant starts with its own users, OAuth settings, audit history and logs. You are its first Tenant Admin.

BTL Admin

Secrets and signed assertions

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.

The alternative is a private key that the printer keeps and never sends anywhere. Secrets and key pairs 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 and says what kind of JWT it is:

Header:
{
  "alg": "ES256",
  "kid": "printer-2026-10",
  "typ": "client-authentication+jwt"
}

Claims:
{
  "iss": "photo-printer",
  "sub": "photo-printer",
  "aud": "https://auth.photos.example",
  "iat": 1790845200,
  "exp": 1790845260,
  "jti": "demo-client-assertion-118"
}

All domains, identifiers, keys, and credentials in these examples are fictional.

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, using its issuer identifier as the only value. The assertion lasts one minute, and jti gives it a unique identifier.

That aud rule used to be looser. Servers accepted their token endpoint's URL, their issuer identifier, or sometimes a list of values. In 2025, researchers showed how a malicious authorization server could abuse that flexibility against a client that uses the same key with several servers. A client often learns a server's endpoint addresses from configuration the server publishes about itself, so a malicious server could list a genuine server's token endpoint as its own, get the client to sign assertions naming that address, and present them to the genuine server as the client. Current guidance for client assertions, from the bodies that maintain these specifications, closes the gap: the client uses the authorization server's issuer identifier as the single aud value, and the server rejects assertions with anything else. Another server cannot pass off that identifier as its own, because the client checks it against the address it used to find the server's configuration.

The same guidance asks clients to label the assertion with the typ value client-authentication+jwt, as the printer's header does. The label tells the server that the client follows the tighter aud rule. It is a signal, not the protection itself, so servers are advised not to reject assertions without it: many clients already follow the audience rule but have not added the label yet.

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=eyJhbGciOiJFUzI1NiIsImtpZCI6InByaW50ZXItMjAyNi0xMCIsInR5cCI6ImNsaWVudC1hdXRoZW50aWNhdGlvbitqd3QifQ...

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 kid identifies a public key registered for this client, and the signature verifies with it using an algorithm the registration allows.
  • iss and sub both equal the client ID the request is about.
  • aud is exactly this authorization server's issuer identifier.
  • The assertion has not expired, and its lifetime is short.
  • If the server records jti values to stop replays, as many do, this one has not been used before.

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 expires within a minute. A server that records jti values has already seen this one and refuses it, and even without that check, the window for replaying it is short. 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. Rotating client credentials follows that change, which is much easier to get right with key pairs than with secrets.

Try it in the Lab

PUT IT INTO PRACTICE

Check your understanding

Try these questions before moving on. If an answer isn't right, use the feedback and try again.

0 of 2 answered correctly

Enable JavaScript to answer these questions and save progress in this browser.

QUESTION 1 OF 2A debugging log captured a complete token request that used private_key_jwt. What can someone with that log do?

QUESTION 2 OF 2In a client assertion, what do iss and sub contain?

We value your privacy

We use cookies and similar technologies to enhance your browsing experience, and analytics to understand our traffic. By clicking "Allow All", you consent to optional analytics. Cookie Policy

Learn identity