Skip to content
TDACorpIDENTITYSign in

Service client

Get tokens for a service acting as itself, call another service with an exchanged token, and build the DPoP headers for the call.

On this page

The service client is for code that acts as itself, with no person in front of it: a back-end job, an API that calls another API. It is imported from the SDK's service subpath. It authenticates with a signed assertion, takes a token with the client_credentials grant, and can exchange that token for one addressed to another service. A cache sits in front of both steps.

The two-step model, the registration it needs and its limits are in Service to service. This page is the module reference.

Create it#

import { createServiceClient } from '@tdacorp/identity-sdk/service'const service = createServiceClient({  issuer: 'https://identity.tdacorp.in',  clientId,  key: { kid: 'k1', privateKey: privateJwk },  dpop,})
OptionDefaultMeaning
issuerrequiredAs for the relying party.
clientIdrequiredYour service client.
keyrequiredThe registered signing key: { kid, privateKey, lifetimeSeconds? }. Ed25519 or P-256.
dpopnoneA DPoP key pair. A service client's tokens are bound to a key, so in practice you pass one. A service client that proves no key cannot get tokens; see DPoP.
scopenoneA default scope for the client's own token.
skewSeconds30A cached token is reused until it is this close to expiry.
retry3 attemptsBounded retry of an HTTP 429 on the token request, the exchange and the discovery fetch. Same members and limits as the relying party's retry; { maxAttempts: 1 } turns it off. A value outside the limits is refused when the client is created.
fetch, clock, requestTimeoutMs, discoveryTtlMsAs for the relying party.

Methods#

MethodWhat it does
clientCredentials({ scope? })One client_credentials token request. Not cached.
exchangeToken({ subjectToken, audience, scope?, actorToken? })One RFC 8693 exchange. Not cached.
tokenFor(audience, { scope? })The usual call. Returns a cached, ready token for that audience.
headersFor(token, { method, url })The Authorization header, and for a bound token the DPoP proof header, for one request.
dpopProof({ method, url, accessToken, nonce? })A proof on its own.
dpopThumbprint()The key thumbprint, or null.

All token methods return a TokenSet: accessToken, tokenType, expiresAt, scope, refreshToken and idToken. The last two are null for a service.

What tokenFor does#

  • For 'self', or your own client id, it returns the client's own client_credentials token.
  • For any other audience, it takes the client's own token and exchanges it for one addressed to that audience (a client id).
  • Each result is cached by audience and scope. Concurrent calls for the same key share one request, so a burst of calls mints one token.
  • If the exchange is refused with invalid_grant because the cached own-token has gone stale, it discards that token, mints a new one and retries once.

Call a service#

const token = await service.tokenFor('tdaid_c_billing')const url = 'https://billing.example.com/invoices'const headers = await service.headersFor(token, { method: 'GET', url })const response = await fetch(url, { headers })

For a DPoP token, headersFor returns authorization: DPoP <token> and a fresh dpop proof for exactly this method and address. For a Bearer token it returns only authorization: Bearer <token>.

The receiving service verifies the token with resource-server verification. A plain client_credentials token is addressed to the calling service, so another service refuses it. You must exchange first, and tokenFor does.

Limits#

  • headersFor takes an optional server nonce in its second argument, { method, url, nonce? }. It does not fetch or remember one. An API that demands a DPoP nonce, as identity's own endpoints do, answers the first request use_dpop_nonce, and you sign again with the nonce it sends. The decision client does this for you: see pdpCredential in Decision client.
  • A 429 is retried a bounded number of times, waiting for Retry-After. When the attempts run out, or the wait asked for is longer than the cap, it surfaces as an OAuthEndpointError with reason rate_limited and retryAfterSeconds. A 5xx and OAuth errors are never retried: back off yourself.
  • The cache is per process.

Errors#

ConfigurationError for bad options (invalid_options, invalid_key, insecure_url), DiscoveryError for discovery, and OAuthEndpointError for the token endpoint with oauthError set to the OAuth code, such as invalid_client, invalid_grant, unauthorized_client, invalid_scope or invalid_target. See Token and Error codes.

Security notes#

  • Keep both keys secret. The signing key proves who you are; the DPoP key proves each request. Neither belongs in source or logs.
  • Do not pass your own token to other services. Exchange it; the exchanged token names the audience.
  • Do not log TokenSet. It holds credentials.
  • Prefer short scopes. Ask for what the call needs.

Verified against commit ca46b57