Safeguard
Application Security•

ASVS 5.0 V10.4.16: Use private_key_jwt or mTLS for Client Auth

At Level 3, shared client secrets are out. Use public-key client authentication that resists replay, such as private_key_jwt or mutual TLS.

Safeguard Security Team
4 min read

ASVS 5.0 V10.4.16 (Level 3) requires the client to be confidential and the authorization server to demand strong client authentication: methods based on public-key cryptography that resist replay. The standard names mutual TLS (tls_client_auth and self_signed_tls_client_auth) and private_key_jwt.

A shared secret sent on every request is the thing this requirement moves away from. If anyone copies it, they can impersonate the client until it is rotated.

Why it matters

With a private key, the secret never leaves the client. The server stores only the public key, so a database leak at the authorization server does not expose usable credentials. Assertions are signed per request and short-lived, so a captured one cannot be replayed for long.

How to implement it

With private_key_jwt, the client signs a JWT assertion for each token request.

import time, uuid, jwt  # PyJWT

def client_assertion(client_id, token_url, private_key, kid):
    now = int(time.time())
    return jwt.encode(
        {"iss": client_id, "sub": client_id, "aud": token_url,
         "iat": now, "exp": now + 60, "jti": str(uuid.uuid4())},
        private_key, algorithm="ES256", headers={"kid": kid})

The request carries client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer and the client_assertion.

The server must verify the signature with the registered public key, check iss, sub and aud, enforce a short exp, and reject a jti it has already seen. Accept only the algorithms you intend, never none.

For mutual TLS, register the certificate or its subject and verify it during the handshake.

Key management for the client

The client's private key needs the same care as any signing key. Generate it in a hardware module or a cloud key management service when you can, so it never exists as a file. Give each environment its own key. Publish the public part as a JWKS endpoint or register it directly, and rotate on a schedule by adding the new key before retiring the old one. Use a kid header so the server knows which to check.

Server side validation checklist

When an assertion arrives, the server should check each of these in order: the algorithm is on the allow list, the key matches the registered one for that client, the signature verifies, iss and sub equal the client ID, aud equals the token endpoint URL or issuer, exp is soon and iat is recent, and the jti has not been seen. Only then does it proceed with the grant. Failing any one returns invalid_client.

Mutual TLS in practice

With mutual TLS, certificate handling becomes the main task. Decide whether you trust a certificate authority (tls_client_auth) or pin self-signed certificates (self_signed_tls_client_auth). Make sure any proxy or load balancer in front of the server passes the verified client certificate through a channel the outside world cannot spoof.

How to test it

Send a token request with a client secret instead of an assertion and expect rejection. Send an expired assertion, one with the wrong audience, one signed with another key, and one reusing a jti. All must fail. A correct assertion should succeed.

Migration path

Start by listing every confidential client and its current authentication method. Move internal services first, since you control both sides. Run both methods side by side per client during the change, then disable the shared secret for each client once its traffic shows only assertions. Finally, remove the secret method from the server's supported list.

Common mistakes

  • Leaving client_secret_basic enabled as a fallback.
  • Not checking the audience, which allows assertions meant for another server.
  • Skipping jti replay tracking.

Quick check

What does the authorization server store with private_key_jwt?

Only the client's public key, so a leak of the server's data does not expose a usable credential.

Sources

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.