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,})| Option | Default | Meaning |
|---|---|---|
issuer | required | As for the relying party. |
clientId | required | Your service client. |
key | required | The registered signing key: { kid, privateKey, lifetimeSeconds? }. Ed25519 or P-256. |
dpop | none | A 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. |
scope | none | A default scope for the client's own token. |
skewSeconds | 30 | A cached token is reused until it is this close to expiry. |
retry | 3 attempts | Bounded 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, discoveryTtlMs | As for the relying party. |
Methods#
| Method | What 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 ownclient_credentialstoken. - 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_grantbecause 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#
headersFortakes 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 requestuse_dpop_nonce, and you sign again with the nonce it sends. The decision client does this for you: seepdpCredentialin Decision client.- A
429is retried a bounded number of times, waiting forRetry-After. When the attempts run out, or the wait asked for is longer than the cap, it surfaces as anOAuthEndpointErrorwithreasonrate_limitedandretryAfterSeconds. A5xxand 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.