Skip to content

Commit

Permalink
fast: Delete token when it is invalid
Browse files Browse the repository at this point in the history
  • Loading branch information
sonnyp committed Jan 16, 2025
1 parent aed84b2 commit 2534ed4
Show file tree
Hide file tree
Showing 12 changed files with 163 additions and 18 deletions.
17 changes: 15 additions & 2 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,6 @@ export default [
// node
// https://github.com/eslint-community/eslint-plugin-n/
"n/no-unpublished-require": "off", // doesn't play nice with monorepo
"n/no-extraneous-require": ["error", { allowModules: ["@xmpp/test"] }],
"n/no-extraneous-import": ["error", { allowModules: ["@xmpp/test"] }],
"n/hashbang": "off",

// promise
Expand Down Expand Up @@ -109,6 +107,21 @@ export default [
// },
// ],
"promise/no-callback-in-promise": "off",
// "n/no-extraneous-require": ["error", { allowModules: ["@xmpp/test"] }],
"n/no-extraneous-import": [
"error",
{
allowModules: [
"@xmpp/test",
"@xmpp/time",
"@xmpp/xml",
"@xmpp/connection",
"@xmpp/websocket",
"selfsigned",
"@xmpp/events",
],
},
],
},
},
];
9 changes: 4 additions & 5 deletions packages/client-core/src/fast/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down
18 changes: 18 additions & 0 deletions packages/client-core/src/fast/fast.js
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -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);
Expand All @@ -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,
Expand Down Expand Up @@ -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;
}
Expand Down
1 change: 0 additions & 1 deletion packages/client-core/src/fast/isTokenValid.test.js
Original file line number Diff line number Diff line change
@@ -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();
Expand Down
118 changes: 118 additions & 0 deletions packages/client-core/test/fast.js
Original file line number Diff line number Diff line change
@@ -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(
<features xmlns="http://etherx.jabber.org/streams">
<authentication xmlns="urn:xmpp:sasl:2">
<mechanism>PLAIN</mechanism>
<inline>
<fast xmlns="urn:xmpp:fast:0">
<mechanism>{mechanism}</mechanism>
</fast>
</inline>
</authentication>
</features>,
);

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(
<success xmlns="urn:xmpp:sasl:2">
<token expiry={expiry} xmlns="urn:xmpp:fast:0" token={token} />
<authorization-identifier>
username@localhost/rOYwkWIywtnF
</authorization-identifier>
</success>,
);

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(
<features xmlns="http://etherx.jabber.org/streams">
<authentication xmlns="urn:xmpp:sasl:2">
<mechanism>PLAIN</mechanism>
<inline>
<fast xmlns="urn:xmpp:fast:0">
<mechanism>{mechanism}</mechanism>
</fast>
</inline>
</authentication>
</features>,
);

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(
<failure xmlns="urn:xmpp:sasl:2">
<not-authorized xmlns="urn:ietf:params:xml:ns:xmpp-sasl" />
</failure>,
);
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(
<failure xmlns="urn:xmpp:sasl:2">
<credentials-expired xmlns="urn:ietf:params:xml:ns:xmpp-sasl" />
</failure>,
);
await tick();
expect(spy_deleteToken).toHaveBeenCalled();
});
1 change: 0 additions & 1 deletion packages/error/test.js
Original file line number Diff line number Diff line change
@@ -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", () => {
Expand Down
7 changes: 7 additions & 0 deletions packages/events/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -19,4 +25,5 @@ export {
procedure,
listeners,
onoff,
tick,
};
1 change: 0 additions & 1 deletion packages/reconnect/test.js
Original file line number Diff line number Diff line change
@@ -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", () => {
Expand Down
1 change: 0 additions & 1 deletion packages/starttls/starttls.test.js
Original file line number Diff line number Diff line change
@@ -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", () => {
Expand Down
6 changes: 1 addition & 5 deletions packages/stream-management/stream-features.test.js
Original file line number Diff line number Diff line change
@@ -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();
Expand Down
1 change: 0 additions & 1 deletion packages/tls/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()", () => {
Expand Down
1 change: 0 additions & 1 deletion test/client.test.js
Original file line number Diff line number Diff line change
@@ -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";
Expand Down

0 comments on commit 2534ed4

Please sign in to comment.