Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prepare sasl for sasl2 #1033

Merged
merged 2 commits into from
Dec 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export default [
],
"prefer-arrow-callback": ["error", { allowNamedFunctions: true }],
"no-redeclare": ["error", { builtinGlobals: false }],
"no-unused-vars": ["error", { argsIgnorePattern: "^_" }],

// node
// https://github.com/eslint-community/eslint-plugin-n/
Expand Down
8 changes: 5 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 15 additions & 5 deletions packages/client/browser.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { xml, jid, Client } from "@xmpp/client-core";
import getDomain from "./lib/getDomain.js";
import createOnAuthenticate from "./lib/createOnAuthenticate.js";

import _reconnect from "@xmpp/reconnect";
import _websocket from "@xmpp/websocket";
Expand All @@ -14,6 +15,7 @@ import _resourceBinding from "@xmpp/resource-binding";
import _sessionEstablishment from "@xmpp/session-establishment";
import _streamManagement from "@xmpp/stream-management";

import SASLFactory from "saslmechanisms";
import plain from "@xmpp/sasl-plain";
import anonymous from "@xmpp/sasl-anonymous";

Expand All @@ -35,8 +37,19 @@ function client(options = {}) {
const iqCaller = _iqCaller({ middleware, entity });
const iqCallee = _iqCallee({ middleware, entity });
const resolve = _resolve({ entity });

// SASL mechanisms - order matters and define priority
const saslFactory = new SASLFactory();
const mechanisms = Object.entries({
plain,
anonymous,
}).map(([k, v]) => ({ [k]: v(saslFactory) }));

// Stream features - order matters and define priority
const sasl = _sasl({ streamFeatures }, credentials || { username, password });
const sasl = _sasl(
{ streamFeatures, saslFactory },
createOnAuthenticate(credentials ?? { username, password }),
);
const streamManagement = _streamManagement({
streamFeatures,
entity,
Expand All @@ -50,10 +63,6 @@ function client(options = {}) {
iqCaller,
streamFeatures,
});
// SASL mechanisms - order matters and define priority
const mechanisms = Object.entries({ plain, anonymous }).map(([k, v]) => ({
[k]: v(sasl),
}));

return Object.assign(entity, {
entity,
Expand All @@ -69,6 +78,7 @@ function client(options = {}) {
sessionEstablishment,
streamManagement,
mechanisms,
saslFactory,
});
}

Expand Down
25 changes: 17 additions & 8 deletions packages/client/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { xml, jid, Client } from "@xmpp/client-core";
import getDomain from "./lib/getDomain.js";
import createOnAuthenticate from "./lib/createOnAuthenticate.js";

import _reconnect from "@xmpp/reconnect";
import _websocket from "@xmpp/websocket";
Expand All @@ -11,12 +12,13 @@ import _iqCaller from "@xmpp/iq/caller.js";
import _iqCallee from "@xmpp/iq/callee.js";
import _resolve from "@xmpp/resolve";

import _starttls from "@xmpp/starttls/client.js";
import _starttls from "@xmpp/starttls";
import _sasl from "@xmpp/sasl";
import _resourceBinding from "@xmpp/resource-binding";
import _sessionEstablishment from "@xmpp/session-establishment";
import _streamManagement from "@xmpp/stream-management";

import SASLFactory from "saslmechanisms";
import scramsha1 from "@xmpp/sasl-scram-sha-1";
import plain from "@xmpp/sasl-plain";
import anonymous from "@xmpp/sasl-anonymous";
Expand All @@ -41,9 +43,21 @@ function client(options = {}) {
const iqCaller = _iqCaller({ middleware, entity });
const iqCallee = _iqCallee({ middleware, entity });
const resolve = _resolve({ entity });

// SASL mechanisms - order matters and define priority
const saslFactory = new SASLFactory();
const mechanisms = Object.entries({
scramsha1,
plain,
anonymous,
}).map(([k, v]) => ({ [k]: v(saslFactory) }));

// Stream features - order matters and define priority
const starttls = _starttls({ streamFeatures });
const sasl = _sasl({ streamFeatures }, credentials || { username, password });
const sasl = _sasl(
{ streamFeatures, saslFactory },
createOnAuthenticate(credentials ?? { username, password }),
);
const streamManagement = _streamManagement({
streamFeatures,
entity,
Expand All @@ -57,12 +71,6 @@ function client(options = {}) {
iqCaller,
streamFeatures,
});
// SASL mechanisms - order matters and define priority
const mechanisms = Object.entries({
scramsha1,
plain,
anonymous,
}).map(([k, v]) => ({ [k]: v(sasl) }));

return Object.assign(entity, {
entity,
Expand All @@ -81,6 +89,7 @@ function client(options = {}) {
sessionEstablishment,
streamManagement,
mechanisms,
saslFactory,
});
}

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

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

if (
!credentials?.username &&
!credentials?.password &&
mechanisms.includes(ANONYMOUS)
) {
await authenticate(credentials, ANONYMOUS);
return;
}

await authenticate(credentials, mechanisms[0]);
};
}
7 changes: 4 additions & 3 deletions packages/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,11 @@
"@xmpp/stream-management": "^0.13.2",
"@xmpp/tcp": "^0.13.2",
"@xmpp/tls": "^0.13.2",
"@xmpp/websocket": "^0.13.2"
"@xmpp/websocket": "^0.13.2",
"saslmechanisms": "^0.1.1"
},
"browser": "./browser.js",
"react-native": "./react-native.js",
"browser": "browser.js",
"react-native": "browser.js",
"engines": {
"node": ">= 20"
},
Expand Down
75 changes: 0 additions & 75 deletions packages/client/react-native.js

This file was deleted.

10 changes: 5 additions & 5 deletions packages/sasl/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,22 +29,22 @@ Uses cases:
- Do not ask for password before connection is made
- Debug authentication
- Using a SASL mechanism with specific requirements
- Perform an asynchronous operation to get credentials
- Fetch credentials from a secure database

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

const client = xmpp({ credentials: authenticate });
const client = xmpp({ credentials: onAuthenticate });

async function authenticate(auth, mechanism) {
console.debug("authenticate", mechanism);
async function onAuthenticate(authenticate, mechanisms) {
console.debug("authenticate", mechanisms);
const credentials = {
username: await prompt("enter username"),
password: await prompt("enter password"),
};
console.debug("authenticating");
try {
await auth(credentials);
await authenticate(credentials, mechanisms[0]);
console.debug("authenticated");
} catch (err) {
console.error(err);
Expand Down
49 changes: 19 additions & 30 deletions packages/sasl/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { encode, decode } from "@xmpp/base64";
import SASLError from "./lib/SASLError.js";
import xml from "@xmpp/xml";
import SASLFactory from "saslmechanisms";

// https://xmpp.org/rfcs/rfc6120.html#sasl

Expand All @@ -14,10 +13,10 @@ function getMechanismNames(features) {
.map((el) => el.text());
}

async function authenticate(SASL, entity, mechname, credentials) {
const mech = SASL.create([mechname]);
async function authenticate({ saslFactory, entity, mechanism, credentials }) {
const mech = saslFactory.create([mechanism]);
if (!mech) {
throw new Error("No compatible mechanism");
throw new Error(`SASL: Mechanism ${mechanism} not found.`);
}

const { domain } = entity.options;
Expand Down Expand Up @@ -74,37 +73,27 @@ async function authenticate(SASL, entity, mechname, credentials) {
});
}

export default function sasl({ streamFeatures }, credentials) {
const SASL = new SASLFactory();

export default function sasl({ streamFeatures, saslFactory }, onAuthenticate) {
streamFeatures.use("mechanisms", NS, async ({ stanza, entity }) => {
const offered = getMechanismNames(stanza);
const supported = SASL._mechs.map(({ name }) => name);
// eslint-disable-next-line unicorn/prefer-array-find
const intersection = supported.filter((mech) => {
return offered.includes(mech);
});
let mech = intersection[0];

if (typeof credentials === "function") {
await credentials(
(creds) => authenticate(SASL, entity, mech, creds, stanza),
mech,
);
} else {
if (!credentials.username && !credentials.password) {
mech = "ANONYMOUS";
}
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.");
}

await authenticate(SASL, entity, mech, credentials, stanza);
async function done(credentials, mechanism) {
await authenticate({
saslFactory,
entity,
mechanism,
credentials,
});
}

await onAuthenticate(done, intersection);

await entity.restart();
});

return {
use(...args) {
return SASL.use(...args);
},
};
}
3 changes: 1 addition & 2 deletions packages/sasl/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@
"dependencies": {
"@xmpp/base64": "^0.13.2",
"@xmpp/error": "^0.13.2",
"@xmpp/xml": "^0.13.2",
"saslmechanisms": "^0.1.1"
"@xmpp/xml": "^0.13.2"
},
"engines": {
"node": ">= 20"
Expand Down
Loading
Loading