Token command-line tool
Keep a fresh service access token in a file with the tdacorp-identity-token command, and the same behaviour as library functions.
On this page
Some processes need a service token but cannot mint one: a sidecar, a script, a tool that reads a credential from a file. The SDK ships a command, tdacorp-identity-token, that does the minting for them. It runs a service client, writes the access token to a file, and keeps rewriting it before it expires.
The command is installed with the SDK, the package @tdacorp/identity-sdk from the public npm registry. It needs Node 22.12 or later.
What it does#
- Builds a service client from your issuer, client id and registered key.
- Gets a token for the audience you name: its own
client_credentialstoken by default, or an exchanged token for another service. - Writes the access token to the file you name, atomically.
- Unless
--onceis set, repeats every 15 seconds. The client reuses a cached token until it is within 30 seconds of expiry and then mints a new one, so the file holds an unexpired token and new tokens are minted only when needed.
The write is atomic: the token goes to a temporary file created with permission 0600, is synced to disk and is renamed over the target. A reader never sees a half-written token. If a refresh fails, the command prints the error to standard error and tries again at the next interval; the file keeps the previous token until it expires.
Usage#
tdacorp-identity-token \ --issuer https://identity.tdacorp.in \ --client-id tdaid_c_billing_job \ --key-id k1 \ --private-key-file ./client-key.jwk \ --dpop-key-file ./dpop-key.jwk \ --out /run/secrets/identity-token| Option | Required | Meaning |
|---|---|---|
--issuer <url> | Yes | The issuer. |
--client-id <id> | Yes | Your service client. |
--key-id <kid> | Yes | The kid of the registered signing key. |
--out <path> | Yes | The file to write the access token to. |
--private-key-file <path> | One of this or the environment | A private JWK file, Ed25519 or P-256, for client authentication. |
--dpop-key-file <path> | No | A private JWK file for DPoP. The token is bound to this key. Omit it for a client that proves no key. |
--audience <client id> | No | The service the token is for. Default self, the client's own token. Any other value is exchanged. |
--scope <scopes> | No | Space-separated scopes. |
--once | No | Write the token once and exit. |
--help | No | Print usage. |
The private key can instead be supplied in the environment as JSON. The name of the variable is printed by --help. Give exactly one of the file and the environment, not both. A key is never accepted as a command-line argument, because arguments are visible to other users of the machine.
Without --once the command runs until it receives SIGINT or SIGTERM, then finishes any write in progress and exits.
Exit status#
| Status | Meaning |
|---|---|
0 | Success, or a clean stop. |
1 | A runtime failure, for example an unreadable key or a refused token request. |
2 | A usage error: a missing option, an unknown option, both key sources, or no key. |
In code#
The same behaviour is available as functions from the SDK's service subpath.
import { createServiceClient, startTokenFileWriter } from '@tdacorp/identity-sdk/service'const service = createServiceClient({ issuer, clientId, key, dpop })const writer = startTokenFileWriter({ path: '/run/secrets/identity-token', getToken: async () => (await service.tokenFor('self')).accessToken, refreshIntervalMs: 15_000, onError: (error) => log.error(error),})// laterawait writer.stop()| Function | Behaviour |
|---|---|
startTokenFileWriter({ path, getToken, refreshIntervalMs?, onError? }) | Writes now, then at each interval. onError defaults to a line on standard error. Returns { stop() }. |
writeTokenFileAtomic(path, token) | The atomic write on its own. |
Worked example#
A sidecar keeps a token for a job that calls the billing service.
tdacorp-identity-token --issuer https://identity.tdacorp.in --client-id tdaid_c_job \ --key-id k1 --private-key-file ./key.jwk --dpop-key-file ./dpop.jwk \ --audience tdaid_c_billing --out ./billing-token &The job reads ./billing-token for each call. Because the token is bound to the DPoP key, the job must also hold dpop.jwk to sign a proof for each request; the file alone is not enough to call the service.
Errors#
A refused token request prints the OAuth code, for example invalid_client or invalid_target. See Token and Error codes.
Security notes#
- Protect the output file. It is created
0600. Do not widen it, and put it on a path only the consuming process can read. - Protect both key files the same way. Anyone who reads them can act as the service.
- Prefer a bound token. Pass
--dpop-key-fileso a copy of the output file is not enough on its own. - Use a short interval and let the file rotate. Do not copy the token elsewhere.