Skip to content

Commit

Permalink
Add initial support for XEP-0484 (FAST) (#1042)
Browse files Browse the repository at this point in the history
---
Co-authored-by: Stephen Paul Weber [email protected]
  • Loading branch information
sonnyp authored Jan 1, 2025
1 parent 16cec58 commit 8ab0b58
Show file tree
Hide file tree
Showing 14 changed files with 490 additions and 774 deletions.
3 changes: 1 addition & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,7 @@ e2e:
cd server && prosodyctl --config prosody.cfg.lua install mod_sasl2 > /dev/null
cd server && prosodyctl --config prosody.cfg.lua install mod_sasl2_bind2 > /dev/null
cd server && prosodyctl --config prosody.cfg.lua install mod_sasl2_sm > /dev/null
# https://github.com/xmppjs/xmpp.js/pull/1006
# cd server && prosodyctl --config prosody.cfg.lua install mod_sasl2_fast > /dev/null
cd server && prosodyctl --config prosody.cfg.lua install mod_sasl2_fast > /dev/null
npm run e2e

clean:
Expand Down
1,018 changes: 279 additions & 739 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion packages/client-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
"dependencies": {
"@xmpp/connection": "^0.14.0",
"@xmpp/jid": "^0.14.0",
"@xmpp/xml": "^0.14.0"
"@xmpp/sasl": "^0.14.0",
"@xmpp/xml": "^0.14.0",
"saslmechanisms": "^0.1.1"
},
"engines": {
"node": ">= 20"
Expand Down
36 changes: 36 additions & 0 deletions packages/client-core/src/fast/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# fast

fast for `@xmpp/client`.

Included and enabled in `@xmpp/client`.

## Usage

Resource is optional and will be chosen by the server if omitted.

### string

```js
import { xmpp } from "@xmpp/client";

const client = xmpp({ resource: "laptop" });
```

### function

Instead, you can provide a function that will be called every time resource binding occurs (every (re)connect).

```js
import { xmpp } from "@xmpp/client";

const client = xmpp({ resource: onBind });

async function onBind(bind) {
const resource = await fetchResource();
return resource;
}
```

## References

[XEP-0484: Fast Authentication Streamlining Tokens](https://xmpp.org/extensions/xep-0484.html)
49 changes: 49 additions & 0 deletions packages/client-core/src/fast/fast.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { getAvailableMechanisms } from "@xmpp/sasl";
import xml from "@xmpp/xml";
import SASLFactory from "saslmechanisms";

const NS = "urn:xmpp:fast:0";

export default function fast({ sasl2 }) {
const saslFactory = new SASLFactory();

const fast = {
token: null,
expiry: null,
saslFactory,
mechanisms: [],
mechanism: null,
available() {
return !!(this.token && this.mechanism);
},
};

sasl2.use(
NS,
async (element) => {
if (!element.is("fast", NS)) return;
fast.mechanisms = getAvailableMechanisms(element, NS, saslFactory);
fast.mechanism = fast.mechanisms[0];

if (!fast.mechanism) return;

if (!fast.token) {
return xml("request-token", {
xmlns: NS,
mechanism: fast.mechanism,
});
}

return xml("fast", { xmlns: NS });
},
async (element) => {
if (element.is("token", NS)) {
const { token, expiry } = element.attrs;
fast.token = token;
fast.expiry = expiry;
}
},
);

return fast;
}
11 changes: 11 additions & 0 deletions packages/client/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@ import _sasl from "@xmpp/sasl";
import _resourceBinding from "@xmpp/resource-binding";
import _streamManagement from "@xmpp/stream-management";
import _bind2 from "@xmpp/client-core/src/bind2/bind2.js";
import _fast from "@xmpp/client-core/src/fast/fast.js";

import SASLFactory from "saslmechanisms";
import scramsha1 from "@xmpp/sasl-scram-sha-1";
import plain from "@xmpp/sasl-plain";
import anonymous from "@xmpp/sasl-anonymous";
import htsha256none from "@xmpp/sasl-ht-sha-256-none";

function client(options = {}) {
const { resource, credentials, username, password, userAgent, ...params } =
Expand Down Expand Up @@ -63,9 +65,17 @@ function client(options = {}) {
createOnAuthenticate(credentials ?? { username, password }, userAgent),
);

const fast = setupIfAvailable(_fast, {
sasl2,
});
fast && sasl2.setup({ fast });

// SASL2 inline features
const bind2 = _bind2({ sasl2, entity }, resource);

// FAST mechanisms - order matters and define priority
fast && setupIfAvailable(htsha256none, fast.saslFactory);

// Stream features - order matters and define priority
const sasl = _sasl(
{ streamFeatures, saslFactory },
Expand Down Expand Up @@ -102,6 +112,7 @@ function client(options = {}) {
streamManagement,
mechanisms,
bind2,
fast,
});
}

Expand Down
8 changes: 6 additions & 2 deletions packages/client/lib/createOnAuthenticate.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
const ANONYMOUS = "ANONYMOUS";

export default function createOnAuthenticate(credentials, userAgent) {
return async function onAuthenticate(authenticate, mechanisms) {
return async function onAuthenticate(authenticate, mechanisms, fast) {
if (typeof credentials === "function") {
await credentials(authenticate, mechanisms);
await credentials(authenticate, mechanisms, fast);
return;
}

Expand All @@ -16,6 +16,10 @@ export default function createOnAuthenticate(credentials, userAgent) {
return;
}

if (fast?.token) {
credentials.password = fast.token;
}

await authenticate(credentials, mechanisms[0], userAgent);
};
}
6 changes: 4 additions & 2 deletions packages/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@
"@xmpp/resource-binding": "^0.14.0",
"@xmpp/sasl": "^0.14.0",
"@xmpp/sasl-anonymous": "^0.14.0",
"@xmpp/sasl-ht-sha-256-none": "^0.14.0",
"@xmpp/sasl-plain": "^0.14.0",
"@xmpp/sasl-scram-sha-1": "^0.14.0",
"@xmpp/sasl2": "^0.14.0",
"@xmpp/session-establishment": "^0.14.0",
"@xmpp/starttls": "^0.14.0",
"@xmpp/stream-features": "^0.14.0",
"@xmpp/stream-management": "^0.14.0",
Expand All @@ -32,7 +32,9 @@
"@xmpp/tcp": false,
"@xmpp/tls": false,
"@xmpp/starttls": false,
"@xmpp/sasl-scram-sha-1": false
"@xmpp/sasl-scram-sha-1": false,
"@xmpp/sasl-ht-sha-256-none": false,
"@xmpp/fast": false
},
"engines": {
"node": ">= 20"
Expand Down
27 changes: 27 additions & 0 deletions packages/sasl-ht-sha-256-none/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// https://datatracker.ietf.org/doc/draft-schmaus-kitten-sasl-ht/
import createHmac from "create-hmac";

export function Mechanism() {}

Mechanism.prototype.Mechanism = Mechanism;
Mechanism.prototype.name = "HT-SHA-256-NONE";
Mechanism.prototype.clientFirst = true;

Mechanism.prototype.response = function response(cred) {
this.password = cred.password;
const hmac = createHmac("sha256", this.password);
hmac.update("Initiator");
return cred.username + "\0" + hmac.digest("latin1");
};

Mechanism.prototype.final = function final(data) {
const hmac = createHmac("sha256", this.password);
hmac.update("Responder");
if (hmac.digest("latin1") !== data) {
throw new Error("Responder message from server was wrong");
}
};

export default function saslHashedToken(sasl) {
sasl.use(Mechanism);
}
24 changes: 24 additions & 0 deletions packages/sasl-ht-sha-256-none/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "@xmpp/sasl-ht-sha-256-none",
"description": "XMPP SASL HT-SHA-256-NONE for JavaScript",
"repository": "github:xmppjs/xmpp.js",
"homepage": "https://github.com/xmppjs/xmpp.js/tree/main/packages/sasl-ht-sha-256-none",
"bugs": "http://github.com/xmppjs/xmpp.js/issues",
"version": "0.14.0",
"license": "ISC",
"type": "module",
"main": "index.js",
"keywords": [
"XMPP",
"sasl"
],
"dependencies": {
"create-hmac": "^1.1.7"
},
"engines": {
"node": ">= 20"
},
"publishConfig": {
"access": "public"
}
}
17 changes: 9 additions & 8 deletions packages/sasl/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@ import { procedure } from "@xmpp/events";

const NS = "urn:ietf:params:xml:ns:xmpp-sasl";

function getMechanismNames(element) {
return element.getChildElements().map((el) => el.text());
export function getAvailableMechanisms(element, NS, saslFactory) {
const offered = new Set(
element.getChildren("mechanism", NS).map((m) => m.text()),
);
const supported = saslFactory._mechs.map(({ name }) => name);
return supported.filter((mech) => offered.has(mech));
}

async function authenticate({ saslFactory, entity, mechanism, credentials }) {
Expand Down Expand Up @@ -66,11 +70,8 @@ async function authenticate({ saslFactory, entity, mechanism, credentials }) {

export default function sasl({ streamFeatures, saslFactory }, onAuthenticate) {
streamFeatures.use("mechanisms", NS, async ({ entity }, _next, element) => {
const offered = getMechanismNames(element);
const supported = saslFactory._mechs.map(({ name }) => name);
const intersection = supported.filter((mech) => offered.includes(mech));

if (intersection.length === 0) {
const mechanisms = getAvailableMechanisms(element, NS, saslFactory);
if (mechanisms.length === 0) {
throw new SASLError("SASL: No compatible mechanism available.");
}

Expand All @@ -83,7 +84,7 @@ export default function sasl({ streamFeatures, saslFactory }, onAuthenticate) {
});
}

await onAuthenticate(done, intersection);
await onAuthenticate(done, mechanisms);

await entity.restart();
});
Expand Down
57 changes: 39 additions & 18 deletions packages/sasl2/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,12 @@ import { encode, decode } from "@xmpp/base64";
import SASLError from "@xmpp/sasl/lib/SASLError.js";
import xml from "@xmpp/xml";
import { procedure } from "@xmpp/events";
import { getAvailableMechanisms } from "@xmpp/sasl";

// https://xmpp.org/extensions/xep-0388.html

const NS = "urn:xmpp:sasl:2";

function getMechanismNames(stanza) {
return stanza.getChildren("mechanism", NS).map((m) => m.text());
}

async function authenticate({
saslFactory,
entity,
Expand Down Expand Up @@ -95,45 +92,69 @@ async function authenticate({

export default function sasl2({ streamFeatures, saslFactory }, onAuthenticate) {
const features = new Map();
let fast;

streamFeatures.use(
"authentication",
NS,
async ({ entity }, _next, element) => {
const offered = getMechanismNames(element);
const supported = saslFactory._mechs.map(({ name }) => name);
const intersection = supported.filter((mech) => offered.includes(mech));

if (intersection.length === 0) {
throw new SASLError("SASL: No compatible mechanism available.");
}

const streamFeatures = await getStreamFeatures({ element, features });

const fastStreamFeature = [...streamFeatures].find((el) =>
el?.is("fast", "urn:xmpp:fast:0"),
);
const is_fast = fastStreamFeature && fast;

async function done(credentials, mechanism, userAgent) {
await authenticate({
saslFactory,
const params = {
entity,
mechanism,
credentials,
userAgent,
streamFeatures,
features,
};

if (is_fast) {
try {
await authenticate({
saslFactory: fast.saslFactory,
mechanism: fast.mechanisms[0],
...params,
});
return;
} catch {
// If fast authentication fails, continue and try with sasl
streamFeatures.delete(fastStreamFeature);
}
}

await authenticate({
saslFactory,
mechanism,
...params,
});
}

await onAuthenticate(done, intersection);
const mechanisms = getAvailableMechanisms(element, NS, saslFactory);
if (mechanisms.length === 0) {
throw new SASLError("SASL: No compatible mechanism available.");
}

await onAuthenticate(done, mechanisms, is_fast && fast);
},
);

return {
use(ns, req, res) {
features.set(ns, [req, res]);
},
setup({ fast: _fast }) {
fast = _fast;
},
};
}

function getStreamFeatures({ element, features }) {
async function getStreamFeatures({ element, features }) {
const promises = [];

const inline = element.getChild("inline");
Expand All @@ -146,5 +167,5 @@ function getStreamFeatures({ element, features }) {
promises.push(feature[0](element));
}

return Promise.all(promises);
return new Set(await Promise.all(promises));
}
1 change: 1 addition & 0 deletions packages/xmpp.js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"@xmpp/resource-binding": "^0.14.0",
"@xmpp/sasl": "^0.14.0",
"@xmpp/sasl-anonymous": "^0.14.0",
"@xmpp/sasl-ht-sha-256-none": "^0.14.0",
"@xmpp/sasl-plain": "^0.14.0",
"@xmpp/sasl-scram-sha-1": "^0.14.0",
"@xmpp/sasl2": "^0.14.0",
Expand Down
Loading

0 comments on commit 8ab0b58

Please sign in to comment.