/* * Copyright 2018-2023 The NATS Authors * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ const test = require("ava"); const { connect, ErrorCode, jwtAuthenticator, nkeyAuthenticator, credsAuthenticator, StringCodec, deferred, } = require( "./index", ); const { nkeys, Events } = require("../lib/nats-base-client/internal_mod"); const { NatsServer, wsConfig } = require("./helpers/launcher"); const { createOperator, createAccount, encodeAccount, encodeOperator, encodeUser, createUser, } = require("nats-jwt"); const conf = Object.assign({ authorization: { PERM: { subscribe: "bar", publish: "foo", }, users: [{ user: "derek", password: "foobar", permission: "$PERM", }], }, }, wsConfig()); test("auth - none", async (t) => { t.plan(1); const ns = await NatsServer.start(conf); try { const nc = await connect({ servers: `ws://127.0.0.1:${ns.websocket}` }); await nc.close(); t.fail("shouldnt have been able to connect"); } catch (ex) { t.is(ex.code, ErrorCode.AuthorizationViolation); } await ns.stop(); }); test("auth - bad", async (t) => { t.plan(1); const ns = await NatsServer.start(conf); try { const nc = await connect( { servers: `ws://127.0.0.1:${ns.websocket}`, user: "me", pass: "hello" }, ); await nc.close(); t.fail("shouldnt have been able to connect"); } catch (ex) { t.is(ex.code, ErrorCode.AuthorizationViolation); } await ns.stop(); }); test("auth - un/pw", async (t) => { t.plan(1); const ns = await NatsServer.start(conf); const nc = await connect( { servers: `ws://127.0.0.1:${ns.websocket}`, user: "derek", pass: "foobar", }, ); await nc.flush(); await nc.close(); await ns.stop(); t.pass(); }); test("auth - sub permissions", async (t) => { t.plan(4); const ns = await NatsServer.start(conf); const nc = await connect( { servers: `ws://127.0.0.1:${ns.websocket}`, user: "derek", pass: "foobar", }, ); const errStatus = deferred(); const _ = (async () => { for await (const s of nc.status()) { errStatus.resolve(s); } })(); const iterErr = deferred(); const sub = nc.subscribe("foo"); (async () => { for await (const m of sub) { } })().catch((err) => { iterErr.resolve(err); }); const v = await Promise.all([errStatus, iterErr, sub.closed]); t.is(v[0].data, ErrorCode.PermissionsViolation); t.is(v[1].message, "'Permissions Violation for Subscription to \"foo\"'"); t.true(sub.isClosed()); t.false(nc.isClosed()); await nc.close(); await ns.stop(); }); test("auth - weird characters", async (t) => { const pass = "§12§12§12"; const conf = Object.assign({ authorization: { username: "admin", password: pass, }, }, wsConfig()); const ns = await NatsServer.start(conf); const nc = await connect({ servers: [`ws://127.0.0.1:${ns.websocket}`], user: "admin", pass: pass, }); await nc.flush; await nc.close(); await ns.stop(); t.pass(); }); test("auth - pub perm", async (t) => { t.plan(2); const ns = await NatsServer.start(conf); const nc = await connect( { servers: `ws://127.0.0.1:${ns.websocket}`, user: "derek", pass: "foobar", }, ); const errStatus = deferred(); const _ = (async () => { for await (const s of nc.status()) { errStatus.resolve(s); } })(); nc.publish("bar"); const v = await errStatus; t.is(v.data, ErrorCode.PermissionsViolation); t.false(nc.isClosed()); await nc.close(); await ns.stop(); }); test("auth - token", async (t) => { const ns = await NatsServer.start( Object.assign({ authorization: { token: "foo" } }, wsConfig()), ); const nc = await connect( { servers: `ws://127.0.0.1:${ns.websocket}`, token: "foo" }, ); await nc.flush(); await nc.close(); await ns.stop(); t.pass(); }); test("auth - nkey", async (t) => { t.plan(1); const kp = nkeys.createUser(); const pk = kp.getPublicKey(); const seed = kp.getSeed(); const conf = Object.assign({ authorization: { users: [ { nkey: pk }, ], }, }, wsConfig()); const ns = await NatsServer.start(conf); const nc = await connect( { servers: `ws://127.0.0.1:${ns.websocket}`, authenticator: nkeyAuthenticator(seed), }, ); await nc.flush(); await nc.close(); await ns.stop(); t.pass(); }); test("auth - creds", async (t) => { const creds = `-----BEGIN NATS USER JWT----- eyJ0eXAiOiJqd3QiLCJhbGciOiJlZDI1NTE5In0.eyJqdGkiOiJFU1VQS1NSNFhGR0pLN0FHUk5ZRjc0STVQNTZHMkFGWERYQ01CUUdHSklKUEVNUVhMSDJBIiwiaWF0IjoxNTQ0MjE3NzU3LCJpc3MiOiJBQ1pTV0JKNFNZSUxLN1FWREVMTzY0VlgzRUZXQjZDWENQTUVCVUtBMzZNSkpRUlBYR0VFUTJXSiIsInN1YiI6IlVBSDQyVUc2UFY1NTJQNVNXTFdUQlAzSDNTNUJIQVZDTzJJRUtFWFVBTkpYUjc1SjYzUlE1V002IiwidHlwZSI6InVzZXIiLCJuYXRzIjp7InB1YiI6e30sInN1YiI6e319fQ.kCR9Erm9zzux4G6M-V2bp7wKMKgnSNqMBACX05nwePRWQa37aO_yObbhcJWFGYjo1Ix-oepOkoyVLxOJeuD8Bw ------END NATS USER JWT------ ************************* IMPORTANT ************************* NKEY Seed printed below can be used sign and prove identity. NKEYs are sensitive and should be treated as secrets. -----BEGIN USER NKEY SEED----- SUAIBDPBAUTWCWBKIO6XHQNINK5FWJW4OHLXC3HQ2KFE4PEJUA44CNHTC4 ------END USER NKEY SEED------ `; const conf = Object.assign({ operator: "eyJ0eXAiOiJqd3QiLCJhbGciOiJlZDI1NTE5In0.eyJhdWQiOiJURVNUUyIsImV4cCI6MTg1OTEyMTI3NSwianRpIjoiWE5MWjZYWVBIVE1ESlFSTlFPSFVPSlFHV0NVN01JNVc1SlhDWk5YQllVS0VRVzY3STI1USIsImlhdCI6MTU0Mzc2MTI3NSwiaXNzIjoiT0NBVDMzTVRWVTJWVU9JTUdOR1VOWEo2NkFIMlJMU0RBRjNNVUJDWUFZNVFNSUw2NU5RTTZYUUciLCJuYW1lIjoiU3luYWRpYSBDb21tdW5pY2F0aW9ucyBJbmMuIiwibmJmIjoxNTQzNzYxMjc1LCJzdWIiOiJPQ0FUMzNNVFZVMlZVT0lNR05HVU5YSjY2QUgyUkxTREFGM01VQkNZQVk1UU1JTDY1TlFNNlhRRyIsInR5cGUiOiJvcGVyYXRvciIsIm5hdHMiOnsic2lnbmluZ19rZXlzIjpbIk9EU0tSN01ZRlFaNU1NQUo2RlBNRUVUQ1RFM1JJSE9GTFRZUEpSTUFWVk40T0xWMllZQU1IQ0FDIiwiT0RTS0FDU1JCV1A1MzdEWkRSVko2NTdKT0lHT1BPUTZLRzdUNEhONk9LNEY2SUVDR1hEQUhOUDIiLCJPRFNLSTM2TFpCNDRPWTVJVkNSNlA1MkZaSlpZTVlXWlZXTlVEVExFWjVUSzJQTjNPRU1SVEFCUiJdfX0.hyfz6E39BMUh0GLzovFfk3wT4OfualftjdJ_eYkLfPvu5tZubYQ_Pn9oFYGCV_6yKy3KMGhWGUCyCdHaPhalBw", resolver: "MEMORY", resolver_preload: { ACZSWBJ4SYILK7QVDELO64VX3EFWB6CXCPMEBUKA36MJJQRPXGEEQ2WJ: "eyJ0eXAiOiJqd3QiLCJhbGciOiJlZDI1NTE5In0.eyJqdGkiOiJXVFdYVDNCT1JWSFNLQkc2T0pIVVdFQ01QRVdBNldZVEhNRzVEWkJBUUo1TUtGU1dHM1FRIiwiaWF0IjoxNTQ0MjE3NzU3LCJpc3MiOiJPQ0FUMzNNVFZVMlZVT0lNR05HVU5YSjY2QUgyUkxTREFGM01VQkNZQVk1UU1JTDY1TlFNNlhRRyIsInN1YiI6IkFDWlNXQko0U1lJTEs3UVZERUxPNjRWWDNFRldCNkNYQ1BNRUJVS0EzNk1KSlFSUFhHRUVRMldKIiwidHlwZSI6ImFjY291bnQiLCJuYXRzIjp7ImxpbWl0cyI6eyJzdWJzIjotMSwiY29ubiI6LTEsImltcG9ydHMiOi0xLCJleHBvcnRzIjotMSwiZGF0YSI6LTEsInBheWxvYWQiOi0xLCJ3aWxkY2FyZHMiOnRydWV9fX0.q-E7bBGTU0uoTmM9Vn7WaEHDzCUrqvPDb9mPMQbry_PNzVAjf0RG9vd15lGxW5lu7CuGVqpj4CYKhNDHluIJAg", }, }, wsConfig()); const ns = await NatsServer.start(conf); const nc = await connect( { servers: `ws://127.0.0.1:${ns.websocket}`, authenticator: credsAuthenticator(new TextEncoder().encode(creds)), }, ); await nc.flush(); await nc.close(); await ns.stop(); t.pass(); }); test("auth - custom", async (t) => { const jwt = "eyJ0eXAiOiJqd3QiLCJhbGciOiJlZDI1NTE5In0.eyJqdGkiOiJFU1VQS1NSNFhGR0pLN0FHUk5ZRjc0STVQNTZHMkFGWERYQ01CUUdHSklKUEVNUVhMSDJBIiwiaWF0IjoxNTQ0MjE3NzU3LCJpc3MiOiJBQ1pTV0JKNFNZSUxLN1FWREVMTzY0VlgzRUZXQjZDWENQTUVCVUtBMzZNSkpRUlBYR0VFUTJXSiIsInN1YiI6IlVBSDQyVUc2UFY1NTJQNVNXTFdUQlAzSDNTNUJIQVZDTzJJRUtFWFVBTkpYUjc1SjYzUlE1V002IiwidHlwZSI6InVzZXIiLCJuYXRzIjp7InB1YiI6e30sInN1YiI6e319fQ.kCR9Erm9zzux4G6M-V2bp7wKMKgnSNqMBACX05nwePRWQa37aO_yObbhcJWFGYjo1Ix-oepOkoyVLxOJeuD8Bw"; const useed = "SUAIBDPBAUTWCWBKIO6XHQNINK5FWJW4OHLXC3HQ2KFE4PEJUA44CNHTC4"; const conf = Object.assign({ operator: "eyJ0eXAiOiJqd3QiLCJhbGciOiJlZDI1NTE5In0.eyJhdWQiOiJURVNUUyIsImV4cCI6MTg1OTEyMTI3NSwianRpIjoiWE5MWjZYWVBIVE1ESlFSTlFPSFVPSlFHV0NVN01JNVc1SlhDWk5YQllVS0VRVzY3STI1USIsImlhdCI6MTU0Mzc2MTI3NSwiaXNzIjoiT0NBVDMzTVRWVTJWVU9JTUdOR1VOWEo2NkFIMlJMU0RBRjNNVUJDWUFZNVFNSUw2NU5RTTZYUUciLCJuYW1lIjoiU3luYWRpYSBDb21tdW5pY2F0aW9ucyBJbmMuIiwibmJmIjoxNTQzNzYxMjc1LCJzdWIiOiJPQ0FUMzNNVFZVMlZVT0lNR05HVU5YSjY2QUgyUkxTREFGM01VQkNZQVk1UU1JTDY1TlFNNlhRRyIsInR5cGUiOiJvcGVyYXRvciIsIm5hdHMiOnsic2lnbmluZ19rZXlzIjpbIk9EU0tSN01ZRlFaNU1NQUo2RlBNRUVUQ1RFM1JJSE9GTFRZUEpSTUFWVk40T0xWMllZQU1IQ0FDIiwiT0RTS0FDU1JCV1A1MzdEWkRSVko2NTdKT0lHT1BPUTZLRzdUNEhONk9LNEY2SUVDR1hEQUhOUDIiLCJPRFNLSTM2TFpCNDRPWTVJVkNSNlA1MkZaSlpZTVlXWlZXTlVEVExFWjVUSzJQTjNPRU1SVEFCUiJdfX0.hyfz6E39BMUh0GLzovFfk3wT4OfualftjdJ_eYkLfPvu5tZubYQ_Pn9oFYGCV_6yKy3KMGhWGUCyCdHaPhalBw", resolver: "MEMORY", resolver_preload: { ACZSWBJ4SYILK7QVDELO64VX3EFWB6CXCPMEBUKA36MJJQRPXGEEQ2WJ: "eyJ0eXAiOiJqd3QiLCJhbGciOiJlZDI1NTE5In0.eyJqdGkiOiJXVFdYVDNCT1JWSFNLQkc2T0pIVVdFQ01QRVdBNldZVEhNRzVEWkJBUUo1TUtGU1dHM1FRIiwiaWF0IjoxNTQ0MjE3NzU3LCJpc3MiOiJPQ0FUMzNNVFZVMlZVT0lNR05HVU5YSjY2QUgyUkxTREFGM01VQkNZQVk1UU1JTDY1TlFNNlhRRyIsInN1YiI6IkFDWlNXQko0U1lJTEs3UVZERUxPNjRWWDNFRldCNkNYQ1BNRUJVS0EzNk1KSlFSUFhHRUVRMldKIiwidHlwZSI6ImFjY291bnQiLCJuYXRzIjp7ImxpbWl0cyI6eyJzdWJzIjotMSwiY29ubiI6LTEsImltcG9ydHMiOi0xLCJleHBvcnRzIjotMSwiZGF0YSI6LTEsInBheWxvYWQiOi0xLCJ3aWxkY2FyZHMiOnRydWV9fX0.q-E7bBGTU0uoTmM9Vn7WaEHDzCUrqvPDb9mPMQbry_PNzVAjf0RG9vd15lGxW5lu7CuGVqpj4CYKhNDHluIJAg", }, }, wsConfig()); const ns = await NatsServer.start(conf); const authenticator = (nonce) => { const seed = nkeys.fromSeed(new TextEncoder().encode(useed)); const nkey = seed.getPublicKey(); const hash = seed.sign(new TextEncoder().encode(nonce)); const sig = nkeys.encode(hash); return { nkey, sig, jwt }; }; const nc = await connect( { servers: `ws://127.0.0.1:${ns.websocket}`, authenticator: authenticator, }, ); await nc.flush(); await nc.close(); await ns.stop(); t.pass(); }); test("auth - jwt", async (t) => { const jwt = "eyJ0eXAiOiJqd3QiLCJhbGciOiJlZDI1NTE5In0.eyJqdGkiOiJFU1VQS1NSNFhGR0pLN0FHUk5ZRjc0STVQNTZHMkFGWERYQ01CUUdHSklKUEVNUVhMSDJBIiwiaWF0IjoxNTQ0MjE3NzU3LCJpc3MiOiJBQ1pTV0JKNFNZSUxLN1FWREVMTzY0VlgzRUZXQjZDWENQTUVCVUtBMzZNSkpRUlBYR0VFUTJXSiIsInN1YiI6IlVBSDQyVUc2UFY1NTJQNVNXTFdUQlAzSDNTNUJIQVZDTzJJRUtFWFVBTkpYUjc1SjYzUlE1V002IiwidHlwZSI6InVzZXIiLCJuYXRzIjp7InB1YiI6e30sInN1YiI6e319fQ.kCR9Erm9zzux4G6M-V2bp7wKMKgnSNqMBACX05nwePRWQa37aO_yObbhcJWFGYjo1Ix-oepOkoyVLxOJeuD8Bw"; const useed = "SUAIBDPBAUTWCWBKIO6XHQNINK5FWJW4OHLXC3HQ2KFE4PEJUA44CNHTC4"; const conf = Object.assign({ operator: "eyJ0eXAiOiJqd3QiLCJhbGciOiJlZDI1NTE5In0.eyJhdWQiOiJURVNUUyIsImV4cCI6MTg1OTEyMTI3NSwianRpIjoiWE5MWjZYWVBIVE1ESlFSTlFPSFVPSlFHV0NVN01JNVc1SlhDWk5YQllVS0VRVzY3STI1USIsImlhdCI6MTU0Mzc2MTI3NSwiaXNzIjoiT0NBVDMzTVRWVTJWVU9JTUdOR1VOWEo2NkFIMlJMU0RBRjNNVUJDWUFZNVFNSUw2NU5RTTZYUUciLCJuYW1lIjoiU3luYWRpYSBDb21tdW5pY2F0aW9ucyBJbmMuIiwibmJmIjoxNTQzNzYxMjc1LCJzdWIiOiJPQ0FUMzNNVFZVMlZVT0lNR05HVU5YSjY2QUgyUkxTREFGM01VQkNZQVk1UU1JTDY1TlFNNlhRRyIsInR5cGUiOiJvcGVyYXRvciIsIm5hdHMiOnsic2lnbmluZ19rZXlzIjpbIk9EU0tSN01ZRlFaNU1NQUo2RlBNRUVUQ1RFM1JJSE9GTFRZUEpSTUFWVk40T0xWMllZQU1IQ0FDIiwiT0RTS0FDU1JCV1A1MzdEWkRSVko2NTdKT0lHT1BPUTZLRzdUNEhONk9LNEY2SUVDR1hEQUhOUDIiLCJPRFNLSTM2TFpCNDRPWTVJVkNSNlA1MkZaSlpZTVlXWlZXTlVEVExFWjVUSzJQTjNPRU1SVEFCUiJdfX0.hyfz6E39BMUh0GLzovFfk3wT4OfualftjdJ_eYkLfPvu5tZubYQ_Pn9oFYGCV_6yKy3KMGhWGUCyCdHaPhalBw", resolver: "MEMORY", resolver_preload: { ACZSWBJ4SYILK7QVDELO64VX3EFWB6CXCPMEBUKA36MJJQRPXGEEQ2WJ: "eyJ0eXAiOiJqd3QiLCJhbGciOiJlZDI1NTE5In0.eyJqdGkiOiJXVFdYVDNCT1JWSFNLQkc2T0pIVVdFQ01QRVdBNldZVEhNRzVEWkJBUUo1TUtGU1dHM1FRIiwiaWF0IjoxNTQ0MjE3NzU3LCJpc3MiOiJPQ0FUMzNNVFZVMlZVT0lNR05HVU5YSjY2QUgyUkxTREFGM01VQkNZQVk1UU1JTDY1TlFNNlhRRyIsInN1YiI6IkFDWlNXQko0U1lJTEs3UVZERUxPNjRWWDNFRldCNkNYQ1BNRUJVS0EzNk1KSlFSUFhHRUVRMldKIiwidHlwZSI6ImFjY291bnQiLCJuYXRzIjp7ImxpbWl0cyI6eyJzdWJzIjotMSwiY29ubiI6LTEsImltcG9ydHMiOi0xLCJleHBvcnRzIjotMSwiZGF0YSI6LTEsInBheWxvYWQiOi0xLCJ3aWxkY2FyZHMiOnRydWV9fX0.q-E7bBGTU0uoTmM9Vn7WaEHDzCUrqvPDb9mPMQbry_PNzVAjf0RG9vd15lGxW5lu7CuGVqpj4CYKhNDHluIJAg", }, }, wsConfig()); const ns = await NatsServer.start(conf); const nc = await connect( { servers: `ws://127.0.0.1:${ns.websocket}`, authenticator: jwtAuthenticator(jwt, new TextEncoder().encode(useed)), }, ); await nc.flush(); await nc.close(); await ns.stop(); t.pass(); }); test("auth - custom error", async (t) => { t.plan(1); const ns = await NatsServer.start(conf); const authenticator = () => { throw new Error("user code exploded"); }; await connect( { servers: `ws://127.0.0.1:${ns.websocket}`, maxReconnectAttempts: 1, authenticator: authenticator, }, ).then(() => { t.fail("shouldn't have connected"); }).catch((err) => { t.is(err.message, "user code exploded"); }); await ns.stop(); }); test("auth - ngs", async (t) => { t.plan(1); const token = process.env.WS_NGS_CI_USER || ""; if (token.length === 0) { t.log("test skipped - no WS_NGS_CI_USER defined in the environment"); t.pass(); return; } else { t.log("token.len", token.length); } const sc = StringCodec(); const authenticator = jwtAuthenticator(token); const nc1 = await connect({ servers: "wss://connect.ngs.global", authenticator: authenticator, }); const nc2 = await connect({ servers: "wss://connect.ngs.global", authenticator: authenticator, }); nc1.subscribe("hello.ngs", { callback: (err, msg) => { msg.respond(sc.encode("hi!")); }, max: 1, }); await nc1.flush(); const m = await nc2.request("hello.ngs"); t.is(sc.decode(m.data), "hi!"); await nc1.close(); await nc2.close(); }); test("auth - expiration notified", async (t) => { const O = createOperator(); const A = createAccount(); const resolver = {}; resolver[A.getPublicKey()] = await encodeAccount("A", A, { limits: { conn: -1, subs: -1, }, }, { signer: O }); const conf = Object.assign({ operator: await encodeOperator("O", O), resolver: "MEMORY", "resolver_preload": resolver, }, wsConfig()); const ns = await NatsServer.start(conf); const U = createUser(); const ujwt = await encodeUser("U", U, A, { bearer_token: true }, { exp: Math.round(Date.now() / 1000) + 5, }); const nc = await connect({ servers: `ws://localhost:${ns.websocket}`, maxReconnectAttempts: -1, authenticator: jwtAuthenticator(ujwt), }); let authErrors = 0; (async () => { for await (const s of nc.status()) { if ( s.type === Events.Error && s.data === ErrorCode.AuthenticationExpired ) { authErrors++; } } })().then(); const err = await nc.closed(); t.true(authErrors >= 1); await ns.stop(); });