What is a JWT (and how it’s generated)?
A JWT (JSON Web Token) is just a signed JSON blob. It has 3 parts:
- Header – algorithm + key id (e.g.,
{ "alg": "RS256", "kid": "key-2025-10" }) - Payload (claims) – who the user is, which org they belong to, expiry, etc.
- Signature – cryptographic signature over header+payload to prevent tampering.
Minimal payload you’ll want
sub: user idorgId: the tenant/org idroles: authorization rolesiat,exp: issued-at and expiryiss,aud: issuer and audiencejti: unique token id (for revocation/uniqueness)
Two common signing models
- HS256 (shared secret): same symmetric secret to sign/verify.
- RS256/ES256 (public/private keys): private key signs, public key verifies. Easier for rotation & zero-trust between services.
How orgId fits in (and “unique” JWTs)
- orgId goes into the token payload as a claim. This does not by itself make the token unique;
jtidoes. - Token uniqueness: set a fresh, random
jti(UUID) each time you issue an access token. -
Tenant isolation:
- Put
orgIdin the claims. - The API reads
orgIdfrom the JWT, never from the request body. - Optionally: use per-org signing keys or per-org secrets so a token issued for Org A can’t be verified for Org B.
- Put
Practical code (Node/TypeScript)
RS256 (recommended for services)
import jwt from "jsonwebtoken";
import { randomUUID } from "crypto";
const PRIVATE_KEY = process.env.AUTH_PRIVATE_KEY!; // PEM
const PUBLIC_KEY = process.env.AUTH_PUBLIC_KEY!; // PEM
type Claims = {
sub: string; // userId
orgId: string; // tenant
roles: string[]; // ["admin","trader"]
jti: string; // unique token id
iss: string; // issuer
aud: string; // audience
};
export function issueAccessToken({
userId,
orgId,
roles,
ttlSeconds = 900, // 15 min
kid = "key-2025-10",
}: {
userId: string;
orgId: string;
roles: string[];
ttlSeconds?: number;
kid?: string;
}) {
const now = Math.floor(Date.now() / 1000);
const claims: Claims = {
sub: userId,
orgId,
roles,
jti: randomUUID(),
iss: "https://auth.quub.exchange",
aud: "quub.exchange.api",
};
return jwt.sign(claims, PRIVATE_KEY, {
algorithm: "RS256",
keyid: kid,
expiresIn: ttlSeconds,
notBefore: 0,
// iat is auto-filled unless you set it; you can also pass { iat: now }
});
}
export function verifyAccessToken(token: string) {
// You’ll normally resolve PUBLIC_KEY by `kid` via JWKS.
return jwt.verify(token, PUBLIC_KEY, {
algorithms: ["RS256"],
audience: "quub.exchange.api",
issuer: "https://auth.quub.exchange",
}) as jwt.JwtPayload; // contains orgId, sub, roles, jti, iat, exp...
}
HS256 (simple, single secret)
import jwt from "jsonwebtoken";
import { randomUUID } from "crypto";
const SECRET = process.env.AUTH_SECRET!;
export function issueAccessTokenHS({
userId,
orgId,
roles,
}: {
userId: string;
orgId: string;
roles: string[];
}) {
return jwt.sign(
{
sub: userId,
orgId,
roles,
jti: randomUUID(),
iss: "https://auth.quub.exchange",
aud: "quub.exchange.api",
},
SECRET,
{ algorithm: "HS256", expiresIn: "15m" }
);
}
Making JWTs “per-org unique” (multi-tenant options)
You have three good patterns—pick one:
-
Single issuer & key +
orgIdclaim- One keypair (or secret) for all orgs.
orgIdis always in claims and enforced at the API.- Uniqueness via
jti. - Pros: simplest ops. Cons: revocation & key rotation are global.
-
Per-org keys (RS256)
- Each org has its own keypair (kid ties token to org key).
- API uses JWKS to pick the right public key based on
kid. - If a key leaks, only that org is affected.
- Pros: strong isolation; cleaner revocation per org. Cons: more ops complexity.
-
Per-org shared secret (HS256)
- Each org has its own secret (via secret manager).
- API selects verifier secret by org context (from token’s
iss/kidor routing). - Pros: simple. Cons: secret distribution risk, rotate carefully.
In all options,
jtimakes every token instance unique, andorgIdis the tenant scope that your API trusts only from the token, never from request bodies.
Validation & enforcement checklist (what the API must do)
- Verify signature (RS256/ES256 via JWKS; HS256 via secret).
- Check
exp/nbf(expiry/not-before). - Check
iss&aud(right issuer & audience). - Read
orgIdfrom token (claim), never from request payload. - (Optional) Match URL org:
:orgIdin path must equal JWTorgId. - Enforce RLS in DB using the JWT
orgId. - Use
jtito support revocation lists/allow-lists when needed.
Quick answers to your implied questions
-
How is JWT generated? By signing JSON claims (with
orgId,sub, etc.) using either a private key (RS256/ES256) or a shared secret (HS256). -
Can
orgIdmake the token unique? Not by itself. Usejtifor uniqueness; useorgIdfor tenant scoping. -
How to ensure tokens can’t be reused across orgs? Include
orgIdin claims and enforce it in the API; optionally use per-org keys/secrets so a token for Org A can’t even verify under Org B.