Skip to content

Instantly share code, notes, and snippets.

@teyfix
Last active December 16, 2025 20:12
Show Gist options
  • Select an option

  • Save teyfix/2b2897557f8c55ed06cb6fbc88f2aaf4 to your computer and use it in GitHub Desktop.

Select an option

Save teyfix/2b2897557f8c55ed06cb6fbc88f2aaf4 to your computer and use it in GitHub Desktop.
NATS WebSocket Client Example with JWT Authentication

NATS WebSocket Client Example with JWT

This gist demonstrates how to use JWT-based authentication with NATS. It includes:

TypeScript example

Checkout example-client.ts for a TypeScript example showing:

  • Generate NATS user credentials (JWT + NKey) entirely in code.
  • Connect a browser or Node.js client to a NATS server over WebSocket.
  • Subscribe to allowed subjects and handle attempts to access prohibited subjects.

Deployment-agnostic guide

Checkout AUTHENTICATION.md for a deployment-agnostic guide showing:

  • Set up NATS operators, accounts, and JWT-based authentication using nsc.
  • Generate resolver configurations and publish accounts to a running NATS server.
  • Inspect keys, account claims, and signing seeds.

Usage

1. Set the issuer account credentials as environment variables

export NATS_ACCOUNT_KEY="..."      # issuer public key
export NATS_ACCOUNT_SECRET="..."   # issuer seed

2. Update the NATS server WebSocket URL in example-client.ts if needed

const servers = "ws://localhost:9222"; // adjust host/port as necessary

3. Run the client example

ts-node example-client.ts

4. Observe the behavior

  • Subscriptions to allowed subjects succeed.
  • Subscriptions to prohibited subjects throw a permissions violation.

This README is designed for educational purposes and assumes the reader may refer to AUTHENTICATION.md for a full walkthrough of NATS JWT authentication setup.

NATS JWT Authentication Setup (NSC)

This document explains how to set up JWT-based authentication for NATS using nsc. It is intentionally deployment-agnostic and focuses purely on concepts, commands, and verification.

All commands are executed using the official natsio/nats-box:0.19.2 Docker image.


Conceptual overview

NATS JWT authentication has three core entities:

  • Operator The root authority. Owns accounts and defines global trust.

  • Account Represents an isolated namespace with its own limits, users, and permissions.

  • User JWTs Short-lived credentials signed by an account that define publish/subscribe permissions.

This guide walks through creating each layer in the correct order.


1. Creating the operator

Create a new operator with a system account and signing key:

nsc add operator --generate-signing-key --sys --name my_operator
[ OK ] generated and stored operator key "OBBMNDXWHOYLA5CMH47QKAKLEIHKGIZBXIH63WSPHPDHDSAQI7SBGITC"
[ OK ] added operator "my_operator"
[ OK ] When running your own nats-server, make sure they run at least version 2.2.0
[ OK ] created operator signing key: OADCCYB6VSDLBRWHFAITYPCCLXYSCVAEJG546B7UBLHZTJXYW3IUWDGJ
[ OK ] created system_account: name:SYS id:ADPWZT7BFKFGQNCLBGPGS5JYEL63E5E2U3R2NXB2Q37VFEDVU7BXB75X
[ OK ] created system account user: name:sys id:UBUKIFZBXVJQDITR4JBWJORJSDAYU32UFTZGUSILNYWHIIPEU6ISVJJ7
[ OK ] system account user creds file stored in `/nsc/nkeys/creds/my_operator/SYS/sys.creds`

Enforce signing-key usage and configure the account JWT resolver URL:

nsc edit operator --require-signing-keys --account-jwt-server-url "nats://0.0.0.0:4222"
[ OK ] strict signing key usage set to: true
[ OK ] set account jwt server url to "nats://0.0.0.0:4222"
[ OK ] edited operator "my_operator"

2. Creating an account

Create a new account under the operator:

nsc add account my_account
[ OK ] generated and stored account key "ABT6QBISNJWXEW5QGCSWV6WWYZWOXEHU6G54XPA44ANTXO7EHXTIH6NI"
[ OK ] added account "my_account"

Generate an account signing key. This key will later be used to sign user JWTs:

nsc edit account my_account --sk generate
[ OK ] added signing key "AAAAKEVGK3YQSLEEYDHHD36ETWKWAZZHWFFDDIZW3FINAVWYCHOEXW6A"
[ OK ] edited account "my_account"

3. Generating the resolver configuration

NATS uses a JWT resolver to fetch account claims. Generate the resolver configuration:

nsc generate config --nats-resolver --sys-account SYS > ./resolver.conf

4. Starting the NATS server

Start NATS with the generated resolver configuration:

nats -c resolver.conf

Alternatively, you can include this config in your nats.conf file:

# NATS Clients Port
port: 4222

# Other configuration options

# NATS JWT Resolver
include ./resolver.conf

Then you can start the server as usual:

nats -c nats.conf

5. Pushing account data to NATS

After starting the NATS server (with resolver.conf included), publish all account JWTs to the running server:

Note

Make sure you are in the same environment where you created the operator and account (see Creating the Operator and Creating the Account).

nsc push -A

This ensures that all accounts and signing keys are available to the server for authentication.


6. Debugging and inspection

Listing operators

nsc list operators
+------------------------------------------------------------------------+
|                               Operators                                |
+-------------+----------------------------------------------------------+
| Name        | Public Key                                               |
+-------------+----------------------------------------------------------+
| my_operator | OBBMNDXWHOYLA5CMH47QKAKLEIHKGIZBXIH63WSPHPDHDSAQI7SBGITC |
+-------------+----------------------------------------------------------+

Listing accounts

nsc list accounts
+-----------------------------------------------------------------------+
| Accounts                                                              |
+------------+----------------------------------------------------------+
| Name       | Public Key                                               |
+------------+----------------------------------------------------------+
| SYS        | ADPWZT7BFKFGQNCLBGPGS5JYEL63E5E2U3R2NXB2Q37VFEDVU7BXB75X |
| my_account | ABT6QBISNJWXEW5QGCSWV6WWYZWOXEHU6G54XPA44ANTXO7EHXTIH6NI |
+------------+----------------------------------------------------------+

Listing keys

nsc list keys
+-----------------------------------------------------------------------------------------------+
|                                             Keys                                              |
+-------------+----------------------------------------------------------+-------------+--------+
| Entity      | Key                                                      | Signing Key | Stored |
+-------------+----------------------------------------------------------+-------------+--------+
| my_operator | OBBMNDXWHOYLA5CMH47QKAKLEIHKGIZBXIH63WSPHPDHDSAQI7SBGITC |             | *      |
| my_operator | OADCCYB6VSDLBRWHFAITYPCCLXYSCVAEJG546B7UBLHZTJXYW3IUWDGJ | *           | *      |
|  my_account | ABT6QBISNJWXEW5QGCSWV6WWYZWOXEHU6G54XPA44ANTXO7EHXTIH6NI |             | *      |
|  my_account | AAAAKEVGK3YQSLEEYDHHD36ETWKWAZZHWFFDDIZW3FINAVWYCHOEXW6A | *           | *      |
+-------------+----------------------------------------------------------+-------------+--------+

Listing seeds (private keys)

Caution

Signing keys are secrets and must be traited with care.

nsc list keys --show-seeds
+----------------------------------------------------------------------------------------+
|                                       Seeds Keys                                       |
+-------------+------------------------------------------------------------+-------------+
| Entity      | Private Key                                                | Signing Key |
+-------------+------------------------------------------------------------+-------------+
| my_operator | SOAE4GW4K7BYO2327I3GE2ZQILOTNGV4QEOC4DYFKN6UG6E5BZIAIIMRNA |             |
| my_operator | SOAPQQCSOBMMSSZS4NFAQ4BLXSTAMMD5E3XOG547VPEHKPTM6YFPX2YM7Y | *           |
|  my_account | SAACZEZRPNTFT2NKAB726KSNVOAUEMY5PFMBPK27GDXC7CCNLRHU3S77UM |             |
|  my_account | SAALMQV2WCX4XF2GZCUCSS2NIF5KD7CFMNLN27N6IWKIEJKOL5Y7NYB57M | *           |
+-------------+------------------------------------------------------------+-------------+

7. Inspecting account details

Describe the account in JSON form:

nsc describe account --name=my_account --json
{
  "iat": 1765913025,
  "iss": "OADCCYB6VSDLBRWHFAITYPCCLXYSCVAEJG546B7UBLHZTJXYW3IUWDGJ",
  "jti": "QIN2SKKOP4FWIRQRESC6DCEWL4BTOPNAHJUSWPMLMAR4ISSM3IAA",
  "name": "my_account",
  "nats": {
    "authorization": {},
    "default_permissions": {
      "pub": {},
      "sub": {}
    },
    "limits": {
      "conn": -1,
      "consumer": -1,
      "data": -1,
      "disk_max_stream_bytes": -1,
      "exports": -1,
      "imports": -1,
      "leaf": -1,
      "max_ack_pending": -1,
      "mem_max_stream_bytes": -1,
      "payload": -1,
      "streams": -1,
      "subs": -1,
      "wildcards": true
    },
    "signing_keys": [
      "AAAAKEVGK3YQSLEEYDHHD36ETWKWAZZHWFFDDIZW3FINAVWYCHOEXW6A"
    ],
    "type": "account",
    "version": 2
  },
  "sub": "ABT6QBISNJWXEW5QGCSWV6WWYZWOXEHU6G54XPA44ANTXO7EHXTIH6NI"
}

8. Keys used for signing user JWTs

Account public key (issuer)

This value is referenced as issuer_account when encoding user JWTs:

nsc describe account --name=my_account --json | jq -r '.sub'
ABT6QBISNJWXEW5QGCSWV6WWYZWOXEHU6G54XPA44ANTXO7EHXTIH6NI

Account signing key (private)

This key signs user JWTs:

nsc describe account --name=my_account --json | jq -r '.nats.signing_keys[0]'
AAAAKEVGK3YQSLEEYDHHD36ETWKWAZZHWFFDDIZW3FINAVWYCHOEXW6A

Extracting the signing seed

Locate the signing seed under /nsc/nkeys/keys:

nsc describe account --name my_account --json | jq -r '.nats.signing_keys[0]' | xargs -I% find /nsc/nkeys/keys -name '%.nk' -exec cat {} \;

Caution

Signing keys are secrets and must be traited with care.

SAALMQV2WCX4XF2GZCUCSS2NIF5KD7CFMNLN27N6IWKIEJKOL5Y7NYB57M

9. Example: signing a user JWT

const accountKey = "..."; // account public key
const accountSeed = "..."; // account signing seed

const accountKeyPair = fromSeed(encodeText(accountSeed));

const jwt = await encodeUser(
  "user-id",
  userKeyPair,
  accountKeyPair,
  {
    issuer_account: accountKey,
    pub: { allow: ["users.id.>"], deny: [] },
    sub: { allow: ["users.id.>"], deny: [] },
  },
  { exp: Math.floor(Date.now() / 1000) + 30 * 60 },
);

This JWT can now be used by clients to authenticate with NATS.

import { connect, credsAuthenticator } from "nats.ws";

const nc = await connect({ authenticator: credsAuthenticator(creds) });

After connecting to NATS, if the client tries to access a resource it should not have access to, it will be denied.

try {
  for await (const msg of nc.subscribe("users.prohibited.subject")) {
  }
} catch (err) {
  if (/Permissions.Violation/i.test(String(err))) {
    console.info("Expected permissions violation error.");
  } else {
    throw err;
  }
}

On the other hand, if the client tries to access a resource it should have access to, it will be allowed.

for await (const msg of nc.subscribe("users.id.subject")) {
  console.info(msg.data); // UInt8Array
}
import { encodeUser } from "@nats-io/jwt";
// import { connect, credsAuthenticator } from "nats";
import { connect, credsAuthenticator } from "nats.ws";
import { createUser, fromSeed } from "nkeys.js";
const encodeText = (text: string) => new TextEncoder().encode(text);
const decodeText = (text: Uint8Array) => new TextDecoder().decode(text);
/**
* NATS server WebSocket URL
*/
// const servers = "ws://localhost:4222";
const servers = "ws://localhost:9222";
/**
* Issuer account credentials.
* Can be obtained with `docker compose up nats-export`.
*
* For additional context, you can checkout outputs from these commands:
*
* Listing accounts:
* ```sh
* nsc list accounts
* ```
*
* Listing account keys:
* ```sh
* nsc list keys
* ```
*
* Listing account seeds:
* ```sh
* nsc list keys --show-seeds
* ```
*
* Describing a NATS account:
* ```sh
* nsc describe account --name $NATS_ACCOUNT --json
* ```
*
* Extracting signing key:
*
* - Describe account
* - Access `nats.signing_keys[0]`
*
* ```sh
* nsc describe account --name $NATS_ACCOUNT --json | jq -r '.nats.signing_keys[0]'
* ```
*
* Extracting signing seed:
*
* - Describe account
* - Access `nats.signing_keys[0]`
* - Find `nk` file inside `/nsc/nkeys/keys`
* - `cat` file contents
*
* ```sh
* nsc describe account --name $NATS_ACCOUNT --json | jq -r '.nats.signing_keys[0]' | xargs -I% find /nsc/nkeys/keys -name '%.nk' -exec cat {} \;
* ```
*/
const accountKey = process.env.NATS_ACCOUNT_KEY;
const accountSeed = process.env.NATS_ACCOUNT_SECRET;
if (!servers || !accountKey || !accountSeed) {
throw new Error("Missing required environment variables");
}
/**
* The user we want to create credentials for.
*/
const userId = "john-doe";
// Create user keypair
const userKP = createUser();
const userSeed = decodeText(userKP.getSeed());
// Load account keypair (issuer)
const accountKP = fromSeed(encodeText(accountSeed));
// UNIX timestamp for expiration
const exp = Math.floor(Date.now() / 1000) + 30 * 60;
// Encode & sign user JWT
const jwt = await encodeUser(
// User `name` for the JWT (only informational)
`user-${userId}`,
userKP,
accountKP,
{
issuer_account: accountKey,
pub: { allow: [`users.${userId}.>`], deny: [] },
sub: { allow: [`users.${userId}.>`], deny: [] },
},
{ exp },
);
/**
* Standard NATS user credentials format
*/
const creds = `
-----BEGIN NATS USER JWT-----
${jwt}
------END NATS USER JWT------
-----BEGIN USER NKEY SEED-----
${userSeed}
------END USER NKEY SEED------
`;
/**
* Connect to NATS
*/
const nc = await connect({
servers,
authenticator: credsAuthenticator(encodeText(creds)),
});
console.info("Connected to NATS at %s", nc.getServer());
console.info();
console.info("Trying to subscribe to prohibited topic");
try {
for await (const message of nc.subscribe("users.")) {
console.log(message);
}
} catch (err) {
if (/Permissions.Violation/i.test(String(err))) {
console.info("Expected permissions violation error.");
} else {
throw err;
}
}
console.info();
console.info("Trying to subscribe to allowed topic");
try {
for await (const message of nc.subscribe(`users.${userId}.notifications`, {
timeout: 1000,
})) {
console.log(message);
}
} catch (err) {
if (/TIMEOUT/i.test(String(err))) {
console.info("Expected timeout error.");
} else {
throw err;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment