Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,46 @@ jobs:
kill $SERVER_PID || true


# JWT / Operator Auth
- name: Test JWT Auth
run: |
# 1. Install nsc (Tool to generate NATS JWTs)
curl -L https://github.com/nats-io/nsc/releases/download/v2.10.2/nsc-linux-amd64.zip -o nsc.zip
unzip nsc.zip
sudo mv nsc /usr/local/bin/

# 2. Setup PKI (Operator -> Account)
# Initialize a new Operator and Account locally
nsc init -n CI_OP --dir ./nsc_store
nsc add account -n CI_ACC

# 3. Generate Server Config
# --mem-resolver embeds the Account JWT directly into the config file.
# This allows the server to validate users without needing an external URL/server.
nsc generate config --mem-resolver --config-file ./nsc_store/resolver.conf

# 4. Create a User Dynamically
# We mint a new user signed by the CI_ACC account
nsc add user -n CI_USER
nsc generate creds -a CI_ACC -n CI_USER > user.creds

# 5. Extract Credentials
# The .creds file contains the JWT (starts with 'eyJ') and the Seed (starts with 'S')
# We parse them out to pass as environment variables
JWT_VAL=$(grep -v '\-\-\-' user.creds | grep 'eyJ')
SEED_VAL=$(grep -v '\-\-\-' user.creds | grep '^S')

# 6. Start Server & Run Test
# Start NATS with the generated resolver config (Port 4226)
nats-server -c ./nsc_store/resolver.conf -p 4226 &
SERVER_PID=$!
sleep 2

# Run the test passing the extracted credentials
NATS_JWT="$JWT_VAL" NATS_NKEY_SEED="$SEED_VAL" zig build integration-test --summary all

kill $SERVER_PID || true

# Run Unit Tests without NATS server
# mem leaks removed & ConnError handled
- name: Run Unit Tests without NATS Server
Expand Down
5 changes: 4 additions & 1 deletion src/Conn.zig
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,10 @@ fn _connect(cn: *Conn, allocator: Allocator, co: protocol.ConnectOpts) !void {

// Extract public key from seed (returns base32-encoded with "U" prefix)
// This is sent to server so it knows which key signed the nonce
nkey_pubkey = try nkeys.extractPublicKey(allocator, nkey_seed);
// If JWT is used, NKey public key is not sent
if (co.jwt == null) {
nkey_pubkey = try nkeys.extractPublicKey(allocator, nkey_seed);
}
}
}

Expand Down
25 changes: 25 additions & 0 deletions src/integration_tests.zig
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,30 @@ const protocol = @import("protocol.zig");
const Conn = @import("Conn.zig");
const Core = @import("Core.zig");

test "Auth: JWT Authentication" {
const allocator = std.testing.allocator;

// 1. Read Env Vars
// Get environment variables
var envs = try std.process.getEnvMap(allocator);
defer envs.deinit();

const jwt = envs.get("NATS_JWT") orelse return error.SkipZigTest;
const seed = envs.get("NATS_NKEY_SEED") orelse return error.SkipZigTest;

// 2. Connect using both JWT and Seed
var core = Core{};
try core.CONNECT(allocator, .{
.addr = "127.0.0.1",
.port = 4226,
.jwt = jwt,
.nkey = seed, // The seed is needed to sign the nonce
});
defer core.DISCONNECT();
// 3. Verify connection by pinging the server
try core.PING();
}

test "Auth: Token Authentication" {
const allocator = testing.allocator;

Expand Down Expand Up @@ -69,6 +93,7 @@ test "Auth: NKey Authentication" {

// Only run if NATS_NKEY_SEED env var is set
const seed = envs.get("NATS_NKEY_SEED") orelse return error.SkipZigTest;
if (envs.get("NATS_JWT") != null) return error.SkipZigTest;

var core = Core{};
try core.CONNECT(allocator, .{
Expand Down
7 changes: 6 additions & 1 deletion src/protocol.zig
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ pub const ConnectOpts = struct {
addr: ?[]const u8 = DefaultAddr,
/// Server port.
port: ?u16 = DefaultPort,
jwt: ?[]const u8 = null,
auth_token: ?[]const u8 = null,
user: ?[]const u8 = null,
pass: ?[]const u8 = null,
Expand Down Expand Up @@ -52,6 +53,7 @@ const ConnectMessage = struct {
verbose: bool = false,
pedantic: bool = false,
tls_required: bool = false,
jwt: ?[]const u8 = null,
nkey: ?[]const u8 = null,
sig: ?[]const u8 = null,
auth_token: ?[]const u8 = null,
Expand Down Expand Up @@ -105,7 +107,10 @@ pub fn buildConnectString(

// Build the message structure
const message = ConnectMessage{
.nkey = nkey_pubkey,
.jwt = opts.jwt,
// if JWT is used, NKey public key is not sent
.nkey = if (opts.jwt != null) null else nkey_pubkey,
// nkey_pubkey,
.sig = nkey_sig,
.auth_token = opts.auth_token,
.user = opts.user,
Expand Down