diff --git a/eslint.config.js b/eslint.config.js index 73236769..a750d61f 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -109,6 +109,7 @@ export default [ // }, // ], "promise/no-callback-in-promise": "off", + "n/no-extraneous-import": ["error", { resolvePaths: ["packages"] }], }, }, ]; diff --git a/packages/client-core/src/fast/README.md b/packages/client-core/src/fast/README.md index 8940b9a3..48c23d5c 100644 --- a/packages/client-core/src/fast/README.md +++ b/packages/client-core/src/fast/README.md @@ -6,7 +6,7 @@ By default `@xmpp/fast` stores the token in memory and as such fast authenticati You can supply your own functions to store and retrieve the token from a persistent database. -If fast authentication fails, regular authentication with `credentials` will happen. +If fast authentication fails, regular authentication will happen. ## Usage @@ -26,10 +26,9 @@ client.fast.saveToken = async (token) => { await secureStorage.set("token", JSON.stringify(token)); } -// Debugging only -client.fast.on("error", (error) => { - console.log("fast error", error); -}) +client.fast.removeToken = async () => { + await secureStorage.del("token"); +} ``` ## References diff --git a/packages/client-core/src/fast/fast.js b/packages/client-core/src/fast/fast.js index 6fb629d1..f48c9364 100644 --- a/packages/client-core/src/fast/fast.js +++ b/packages/client-core/src/fast/fast.js @@ -1,5 +1,6 @@ import { EventEmitter } from "@xmpp/events"; import { getAvailableMechanisms } from "@xmpp/sasl"; +import SASLError from "@xmpp/sasl/lib/SASLError.js"; import xml from "@xmpp/xml"; import SASLFactory from "saslmechanisms"; @@ -20,6 +21,9 @@ export default function fast({ sasl2, entity }) { async fetchToken() { return token; }, + async deleteToken() { + token = null; + }, async save(token) { try { await this.saveToken(token); @@ -34,6 +38,13 @@ export default function fast({ sasl2, entity }) { entity.emit("error", err); } }, + async delete() { + try { + await this.deleteToken(); + } catch (err) { + entity.emit("error", err); + } + }, saslFactory, async auth({ authenticate, @@ -68,6 +79,13 @@ export default function fast({ sasl2, entity }) { }); return true; } catch (err) { + if ( + err instanceof SASLError && + ["not-authorized", "credentials-expired"].includes(err.condition) + ) { + this.delete(); + return false; + } entity.emit("error", err); return false; } diff --git a/packages/client-core/src/fast/isTokenValid.test.js b/packages/client-core/src/fast/isTokenValid.test.js index dc20d811..61220040 100644 --- a/packages/client-core/src/fast/isTokenValid.test.js +++ b/packages/client-core/src/fast/isTokenValid.test.js @@ -1,5 +1,4 @@ import { isTokenValid } from "./fast.js"; -// eslint-disable-next-line n/no-extraneous-import import { datetime } from "@xmpp/time"; const tomorrow = new Date(); diff --git a/packages/client-core/test/fast.js b/packages/client-core/test/fast.js new file mode 100644 index 00000000..7ad4cc5b --- /dev/null +++ b/packages/client-core/test/fast.js @@ -0,0 +1,118 @@ +import { tick } from "@xmpp/events"; +import { mockClient } from "@xmpp/test"; +import { datetime } from "@xmpp/time"; +import { Element } from "@xmpp/xml"; + +const mechanism = "HT-SHA-256-NONE"; + +test("requests and saves token if server advertises fast", async () => { + const { entity, fast } = mockClient(); + + const spy_saveToken = jest.spyOn(fast, "saveToken"); + + entity.mockInput( + + + PLAIN + + + {mechanism} + + + + , + ); + + const authenticate = await entity.catchOutgoing(); + expect(authenticate.is("authenticate", "urn:xmpp:sasl:2")).toBe(true); + const request_token = authenticate.getChild( + "request-token", + "urn:xmpp:fast:0", + ); + expect(request_token.attrs.mechanism).toBe(mechanism); + + const token = "secret-token:fast-HZzFpFwHTy4nc3C8Y1NVNZqYef_7Q3YjMLu2"; + const expiry = "2025-02-06T09:58:40.774329Z"; + + expect(spy_saveToken).not.toHaveBeenCalled(); + + entity.mockInput( + + + + username@localhost/rOYwkWIywtnF + + , + ); + + expect(spy_saveToken).toHaveBeenCalledWith({ token, expiry, mechanism }); +}); + +async function setupFast() { + const { entity, fast } = mockClient(); + + const d = new Date(); + d.setFullYear(d.getFullYear() + 1); + const expiry = datetime(d); + + fast.fetchToken = async () => { + return { + mechanism, + expiry, + token: "foobar", + }; + }; + + entity.mockInput( + + + PLAIN + + + {mechanism} + + + + , + ); + + expect(fast.mechanism).toBe(mechanism); + + const authenticate = await entity.catchOutgoing(); + expect(authenticate.is("authenticate", "urn:xmpp:sasl:2")); + expect(authenticate.attrs.mechanism).toBe(mechanism); + expect(authenticate.getChild("fast", "urn:xmpp:fast:0")).toBeInstanceOf( + Element, + ); + + return entity; +} + +test("deletes the token if server replies with not-authorized", async () => { + const entity = await setupFast(); + const spy_deleteToken = jest.spyOn(entity.fast, "deleteToken"); + + expect(spy_deleteToken).not.toHaveBeenCalled(); + entity.mockInput( + + + , + ); + await tick(); + expect(spy_deleteToken).toHaveBeenCalled(); +}); + +test("deletes the token if server replies with credentials-expired", async () => { + const entity = await setupFast(); + const spy_deleteToken = jest.spyOn(entity.fast, "deleteToken"); + + // credentials-expired + expect(spy_deleteToken).not.toHaveBeenCalled(); + entity.mockInput( + + + , + ); + await tick(); + expect(spy_deleteToken).toHaveBeenCalled(); +}); diff --git a/packages/error/test.js b/packages/error/test.js index 5905200d..af5c26a3 100644 --- a/packages/error/test.js +++ b/packages/error/test.js @@ -1,5 +1,4 @@ import XMPPError from "./index.js"; -// eslint-disable-next-line n/no-extraneous-import import parse from "@xmpp/xml/lib/parse.js"; test("fromElement", () => { diff --git a/packages/events/index.js b/packages/events/index.js index cbe287f7..56c64636 100644 --- a/packages/events/index.js +++ b/packages/events/index.js @@ -9,6 +9,12 @@ import procedure from "./lib/procedure.js"; import listeners from "./lib/listeners.js"; import onoff from "./lib/onoff.js"; +function tick() { + return new Promise((resolve) => { + process.nextTick(resolve); + }); +} + export { EventEmitter, timeout, @@ -19,4 +25,5 @@ export { procedure, listeners, onoff, + tick, }; diff --git a/packages/reconnect/test.js b/packages/reconnect/test.js index 6ace155d..76933e14 100644 --- a/packages/reconnect/test.js +++ b/packages/reconnect/test.js @@ -1,5 +1,4 @@ import _reconnect from "./index.js"; -// eslint-disable-next-line n/no-extraneous-import import Connection from "@xmpp/connection"; test("schedules a reconnect when disconnect is emitted", () => { diff --git a/packages/starttls/starttls.test.js b/packages/starttls/starttls.test.js index 3cad7750..c2f19d1a 100644 --- a/packages/starttls/starttls.test.js +++ b/packages/starttls/starttls.test.js @@ -1,7 +1,6 @@ import tls from "tls"; import { canUpgrade } from "./starttls.js"; import net from "net"; -// eslint-disable-next-line n/no-extraneous-import import WebSocket from "@xmpp/websocket/lib/Socket.js"; test("canUpgrade", () => { diff --git a/packages/stream-management/stream-features.test.js b/packages/stream-management/stream-features.test.js index 4728a1a1..69f2fa64 100644 --- a/packages/stream-management/stream-features.test.js +++ b/packages/stream-management/stream-features.test.js @@ -1,10 +1,6 @@ import { mockClient } from "@xmpp/test"; -function tick() { - return new Promise((resolve) => { - process.nextTick(resolve); - }); -} +import { tick } from "@xmpp/events"; test("enable - enabled", async () => { const { entity } = mockClient(); diff --git a/packages/tls/test.js b/packages/tls/test.js index 82097918..bbfbba14 100644 --- a/packages/tls/test.js +++ b/packages/tls/test.js @@ -2,7 +2,6 @@ import ConnectionTLS from "./lib/Connection.js"; import tls from "tls"; import { promise } from "@xmpp/test"; -// eslint-disable-next-line n/no-extraneous-import import selfsigned from "selfsigned"; test("socketParameters()", () => { diff --git a/test/client.test.js b/test/client.test.js index 457f180f..6437b653 100644 --- a/test/client.test.js +++ b/test/client.test.js @@ -1,4 +1,3 @@ -// eslint-disable-next-line n/no-extraneous-import import { promise } from "@xmpp/events"; import { client, xml, jid } from "../packages/client/index.js"; import debug from "../packages/debug/index.js";