mishraJi

Certificate vs Client Secret in the Client Credentials Flow

Certificate vs Client Secret in the Client Credentials Flow

Introduction

Most OAuth flows are about a user proving who they are. The client credentials flow is the odd one out — there is no user. It is machine-to-machine. A backend service, a daemon, a scheduled job — something with no human sitting in front of it — needs to call an API and prove it is who it claims to be.

The app needs to authenticate to get back an access token and for that it needs a credential. Typically two choices are available:

  • A client secret — a shared string that both your app and the authorization server hold.
  • A certificate — your app holds a private key, authorization server holds only the matching public key.

This post compares the two keeping in mind the various aspecets, most notably scurity.

Note: I am framing this around OAuth and Entra ID because that is where I keep running into it, but nothing here is specific to OAuth. This is the old symmetric secret vs. asymmetric keypair question.

Before diving into the details, let’s first understand how authentication works in each case?

Client secret: When the app needs a token, it makes a request to the token endpoint and includes its client (application) ID along with the client secret directly in the request body. Entra looks up the app by its client ID, compares the secret it received against the copy it has stored for that app, and if they match, issues an access token. The secret itself is the proof — it travels over the wire (inside TLS) on every authentication, and both parties must hold the same value for the comparison to work.

Certificate: The app builds a small JSON Web Token (a client_assertion) whose claims identify it — the client ID goes in the iss and sub fields — and signs that assertion with its private key. It sends the signed assertion to the token endpoint instead of a secret; the assertion’s header carries an x5t thumbprint so Entra knows which uploaded certificate to check. Entra takes the public key from that certificate, verifies the signature, and — if it’s valid and the assertion hasn’t expired — issues the access token. The private key never leaves the app.

Comparison

The Authorization Server Gets Hacked

Start with the worst case, because it is the most revealing. Assume Entra itself is breached and an attacker dumps its credential store.

  • Client secret: Entra keeps a copy of your secret so it can compare against what your app sends. Dump that store and the attacker now has a credential they can replay to authenticate as your app. Your app did nothing wrong. Your vault is intact. You are compromised anyway, because a shared secret is only as safe as the more breachable of the two parties holding it — and you don’t control one of them.

  • Certificate: Entra holds only your public key. An attacker who dumps it gets something they could have had anyway. They cannot forge your signature with it and cannot derive your private key from it. Your private key never entered Entra’s boundary, so the breach never touches it.

This is the cleanest argument of the lot. A symmetric secret makes you depend on the security of every party that holds it. An asymmetric keypair means only one party holds the thing that can impersonate you — and that party is you.

Travelling Over the Wire

  • Client secret: To authenticate, the app sends the secret in the request to the token endpoint. TLS protects it in transit, but the value itself still leaves your process and travels every single time you authenticate.

  • Certificate: The private key never travels over the wire. The app signs a short-lived JWT assertion (client_assertion) with the private key and sends that. The credential (private key) and the thing-you-transmit (signed assertion) are different objects. Intercept the assertion and you have gained little — it is short-lived, scoped, and cannot be reused to derive the key or become the app.

Storage in Vaults

A fair objection: both can go in a vault. At rest, a secret in Azure Key Vault and a private key in Key Vault are protected equally. But there is a difference that survives even then:

A secret has to leave the vault to be used. A private key doesn’t.

Client secret Certificate
Storable in a vault Yes Yes
Must leave the vault to be used Yes No — the vault signs for you
Materialises in app memory / on the wire per-auth Yes No

When you authenticate with a secret, it is fetched out of the vault into your app’s memory, dropped into the request, and sent. It materialises in cleartext in at least three places on every auth. When you authenticate with a certificate, Key Vault can perform the signing operation itself — you hand it the data, it signs with a key that never leaves the HSM boundary, and hands back the signature. The private key stays sealed inside the vault its entire life.

Cross-cloud note: Azure folds both capabilities into one service — Key Vault has a Secrets plane (fetch-to-use) and a Keys plane (sign-in-vault). AWS splits them: Secrets Manager is the fetch-to-use store, KMS is the sign-without-the-key-leaving service.

Rotation

Both credentials expire — there is no free lunch there. The difference is what rotation feels like day to day.

  • Client secret: In Entra a secret has a forced expiry (max 24 months, and the portal nudges you shorter). When it expires, auth breaks — token requests start failing with AADSTS7000215 and stay broken until someone rotates it. The nasty part is the failure profile: nothing breaks for 23 months, then everything breaks at once, on a date nobody wrote down.

  • Certificate: Also expires, but slots into automated, zero-downtime rotation far more naturally. You can register multiple certs on the same app registration at once, so you upload the new one before the old expires, both are valid during the overlap, you cut over, then retire the old one. Key Vault can own that lifecycle end to end — auto-rotate the cert and even auto-renew from an integrated CA — and an app that reads “current cert from vault” just picks up the new one.

One honest caveat: a file-based certificate you rotate by hand — CSRs, CA issuance, juggling .pfx files — can be worse ops than a secret. The rotation advantage only really shows up when the vault owns the lifecycle. Manual certs are not automatically the easier path.

Conclusion

Use a certificate, and let a vault or HSM hold and use the key so it never touches a disk or your app’s memory.

The reasoning stacks cleanly:

  • If the authorization server is breached, a secret leaks and is replayable; a public key is worthless to an attacker.
  • Over the wire, a secret is the credential being transmitted; a signed assertion is not reusable.
  • In a vault, a secret must be extracted to be used; a private key can sign without ever leaving.
  • On rotation, both expire, but certs fit overlapping, vault-owned, zero-downtime rotation naturally.

A client secret is fine for a throwaway lab or a quick proof of concept where convenience wins and you will delete the app tomorrow.