Skip to content
TDACorpIDENTITYSign in

Client authentication

How a client proves who it is to identity with a signed assertion, every claim the assertion needs, and what makes one fail.

On this page

Every call your server makes to the pushed authorization, token, revocation and introspection endpoints must say which client it is and prove it. Identity has no client secrets. A client proves itself by signing a short JWT, called a client assertion, with a private key that only it holds. Identity checks the signature against the public key registered for the client.

This removes the shared secret from the picture: nothing identity stores can be used to impersonate you, and nothing you send can be replayed.

Methods#

The discovery document lists them in token_endpoint_auth_methods_supported.

MethodForHow
private_key_jwtweb and service clientsA signed assertion in the request body. The default, and the one this page describes.
nonePublic clients (spa, native)No credential. The client must prove a DPoP key instead, and identity requires it. Status: In development, see Choosing an integration.
tls_client_authClients that hold a certificateIn development.

A service client may not use none. A client registered with none and without DPoP is refused at registration, because it would present no authentication of any kind.

The assertion#

An assertion is a compact JWT. Send it as two form fields next to client_id:

client_id=<client id>&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer&client_assertion=<the JWT>

Each of client_assertion_type and client_assertion must appear exactly once. The assertion must be at most 8192 characters.

PartValue
Header algEdDSA or ES256, matching your key.
Header kidThe key id you registered.
issYour client id.
subYour client id, the same value.
audThe issuer, as one string: https://identity.tdacorp.in. Not the endpoint address, and not an array.
expAt most 300 seconds from now (a deployment can lower that ceiling, to no less than 60). The SDK uses 60 by default.
jtiA fresh unique value, at most 255 characters.
iatNow.

Identity looks up the key by kid among the live keys of that client. A key that is revoked, expired or unknown is refused. It then verifies the signature with the algorithm in the header, iss, sub, aud and exp.

One assertion, one request#

Identity records every jti it accepts and refuses a second use with invalid_client. That has a consequence people trip over: when a request is answered with use_dpop_nonce and you retry, the retry needs a new assertion with a new jti, because the first one was already spent. The SDK rebuilds the whole request on that retry. If you build requests yourself, generate the assertion as the last step before sending, every time.

Lifetime and clocks#

An assertion whose exp is already past is refused, and so is one whose exp is more than 300 seconds ahead of identity's clock. A server whose clock is a few minutes wrong therefore fails here first, with invalid_client and no other clue. See Clock skew.

What fails, and how it looks#

All failures are invalid_client and are meant to be indistinguishable from outside beyond a short description. At the token, revocation and introspection endpoints the status is 401. At the pushed authorization endpoint it is 400. Read error, not the status.

CauseDescription (abridged)
Missing, repeated or wrong-typed assertion fieldsclient_assertion and client_assertion_type are each required exactly once or a type message
Not a JWT, or an algorithm identity does not usethe client assertion is not a decodable JWS or the client assertion alg is not one this server supports
No kid in the headerthe client assertion header carries no kid
Unknown, revoked or expired keyno live registered key matches the assertion's kid
Bad signature, wrong iss, sub or aud, expiredthe client assertion does not verify
aud is an arraythe client assertion aud must be the issuer as a single string
exp too far aheada client assertion may not live longer than 300 seconds
jti or exp missingthe client assertion does not verify
jti empty or too longthe client assertion carries no usable jti
jti seen beforethis client assertion has already been presented
Client suspendedthis client is suspended

Client authentication is checked before the DPoP proof. A request with a bad assertion never reaches the proof check.

Keys#

  • Ed25519 or P-256. Anything else is refused at registration.
  • Client metadata documents. When the oauth_cimd flag is on (it defaults OFF), a client whose client_id is an https URL publishes its public keys in that document, and may use only private_key_jwt or none. No client secret is accepted there.
  • Public half only. You send identity the public JWK and a kid. The private key never leaves your infrastructure.
  • Rotation. A client may hold several keys. Register the new key, switch your application to sign with it, then ask for the old key to be revoked. See Get a client registered.
  • Compromise. Ask for the key to be revoked. A revoked key stops working at once, and since it was only ever a signing key, nothing else needs to be rotated on identity's side.

With the SDK#

Pass key: { kid, privateKey } to createRelyingParty or createServiceClient. The key is a private JWK or a CryptoKey. lifetimeSeconds sets the assertion lifetime, a whole number from 1 to 300. The SDK builds a new assertion for every request.

Without the SDK#

Signing an assertion needs nothing but your language's crypto library. The Raw HTTP walkthrough includes a small script that builds assertions with only Node's built-in crypto module, and shows where each one goes in a request.

Verified against commit 936c0b4