diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2d76715..205cd07 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/src/Conn.zig b/src/Conn.zig index 7c834a8..6882653 100644 --- a/src/Conn.zig +++ b/src/Conn.zig @@ -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); + } } } diff --git a/src/integration_tests.zig b/src/integration_tests.zig index ee2ec56..d3bbdac 100644 --- a/src/integration_tests.zig +++ b/src/integration_tests.zig @@ -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; @@ -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, .{ diff --git a/src/protocol.zig b/src/protocol.zig index 269edac..d1cdfcb 100644 --- a/src/protocol.zig +++ b/src/protocol.zig @@ -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, @@ -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, @@ -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,