Authentication

Aleta authenticates API traffic with signed JWT assertions, not shared secrets. You generate an RSA key pair, register the public half with the platform, and sign a short-lived JWT every time you need a new access token. The private key never leaves your infrastructure; the platform never holds a secret that could be stolen off it.

Why JWT assertions?

Client secrets in wealth infrastructure are a liability. Once issued, they're stored somewhere, and "somewhere" is eventually a log file, an environment dump, or a misconfigured secrets manager. Signed assertions avoid that entire class of problem: the credential that can actually be used against the API is a private key you control, and the thing we see is a public key we can verify against. Rotating is a local operation on your side; you don't need to coordinate a handoff with us to replace it.

One-time setup

1. Generate a key pair

An RSA-2048 key is fine for most deployments; RSA-4096 is accepted if your security posture requires it. OpenSSL, Node's crypto.generateKeyPair, and Python's cryptography all work.

With OpenSSL

openssl genpkey -algorithm RSA -out aleta-private.pem -pkeyopt rsa_keygen_bits:2048
openssl rsa -in aleta-private.pem -pubout -out aleta-public.pem

Store the private key the way you'd store any production secret — a secrets manager, an HSM, encrypted at rest. Our SDKs read the PEM by path or env var; don't commit it to the repository.

2. Register the public key

Upload the public half to your workspace. Each key gets a stable key_id that becomes the kid claim on every assertion you sign with it. You can have multiple active keys at once, which is how graceful rotation works: the new key is added, production traffic shifts to signing with it, the old one is removed after the last caller has rolled.

Register a public key

curl -X POST https://api.aleta.io/v2/auth/keys \
  -H "Authorization: Bearer {admin_token}" \
  -H "Content-Type: application/json" \
  -d @- <<'JSON'
{
  "label": "prod-backend",
  "public_key_pem": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----"
}
JSON

The response carries the key_id — that's the value you'll plug into every assertion's kid header.

Per-request flow

3. Sign an assertion JWT

Every time you want an access token, construct a JWT with the claims below and sign it with your private key. Standard RS256 is expected.

  • Name
    iss
    Type
    string
    Description

    Your workspace identifier — shown in the dashboard next to the key you registered.

  • Name
    sub
    Type
    string
    Description

    Same value as iss for server-to-server integrations. For user-delegated flows, the user's ID.

  • Name
    aud
    Type
    string
    Description

    Always https://api.aleta.io.

  • Name
    exp
    Type
    unix timestamp
    Description

    Assertion expiry. Keep this short — 60 seconds from now is plenty, since assertions are one-shot.

  • Name
    iat
    Type
    unix timestamp
    Description

    Issued-at. Same clock the server uses for exp validation; don't skew more than 30 seconds.

  • Name
    jti
    Type
    string
    Description

    Unique per-assertion identifier (UUID is fine). Prevents replay within the exp window.

The JWT header needs alg: RS256 and kid: {your_key_id}.

Signing in Node.js

import { readFileSync } from "fs";
import { SignJWT, importPKCS8 } from "jose";

const privateKey = await importPKCS8(
  readFileSync(process.env.ALETA_PRIVATE_KEY_PATH!, "utf8"),
  "RS256",
);

const assertion = await new SignJWT({})
  .setProtectedHeader({ alg: "RS256", kid: process.env.ALETA_KEY_ID! })
  .setIssuer(process.env.ALETA_WORKSPACE_ID!)
  .setSubject(process.env.ALETA_WORKSPACE_ID!)
  .setAudience("https://api.aleta.io")
  .setIssuedAt()
  .setExpirationTime("60s")
  .setJti(crypto.randomUUID())
  .sign(privateKey);

4. Exchange for an access token

POST the assertion to the token endpoint. The response is a short-lived bearer token (1 hour by default) that you attach to every subsequent request.

Token exchange

curl -X POST https://api.aleta.io/v2/auth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer" \
  --data-urlencode "assertion=${ASSERTION_JWT}"

Response

{
  "access_token": "aleta_live_9b1c…",
  "token_type": "Bearer",
  "expires_in": 3600
}

Cache the access token in memory until it's about to expire, then re-sign and re-exchange. Don't pre-mint assertions — one per token request.

5. Call the API

Authorised request

curl https://api.aleta.io/v2/clients \
  -H "Authorization: Bearer ${ACCESS_TOKEN}"

All of the above is handled for you by the Aleta SDKs — supply the private key path at construction time and they take care of signing, caching, and refreshing on every call.

Rotation

  1. Register a new public key — you'll get a second key_id.
  2. Deploy the new private key + key_id to production.
  3. Watch metrics for traffic on the old key draining to zero.
  4. Delete the old public key from Aleta. Old assertions signed against it stop verifying immediately.