-
Notifications
You must be signed in to change notification settings - Fork 376
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement SASL2 (with optional BIND2 and FAST)
- Loading branch information
1 parent
ce88dbf
commit 95e8f61
Showing
6 changed files
with
327 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
# SASL | ||
|
||
SASL2 Negotiation for `@xmpp/client` (including optional BIND2 and FAST). | ||
|
||
Note that if you set clientId then BIND2 will be used so you will not get offline messages (and are expected to do a MAM sync instead if you want that). | ||
|
||
Included and enabled in `@xmpp/client`. | ||
|
||
## Usage | ||
|
||
### object | ||
|
||
```js | ||
const {xmpp} = require('@xmpp/client') | ||
const client = xmpp({credentials: { | ||
username: 'foo', | ||
password: 'bar', | ||
clientId: "Some UUID for this client/server pair (optional)", | ||
software: "Name of this software (optional)", | ||
device: "Description of this device (optional)", | ||
}) | ||
``` | ||
### function | ||
Instead, you can provide a function that will be called every time authentication occurs (every (re)connect). | ||
Uses cases: | ||
- Have the user enter the password every time | ||
- Do not ask for password before connection is made | ||
- Debug authentication | ||
- Using a SASL mechanism with specific requirements (such as FAST) | ||
- Perform an asynchronous operation to get credentials | ||
```js | ||
const { xmpp } = require("@xmpp/client"); | ||
const client = xmpp({ | ||
credentials: authenticate, | ||
clientId: "Some UUID for this client/server pair (optional)", | ||
software: "Name of this software (optional)", | ||
device: "Description of this device (optional)", | ||
}); | ||
|
||
async function authenticate(callback, mechanisms) { | ||
const fast = mechanisms.find((mech) => mech.canFast)?.name; | ||
const mech = mechanisms.find((mech) => mech.canOther)?.name; | ||
|
||
if (fast) { | ||
const [token, count] = await db.lookupFast(clientId); | ||
if (token) { | ||
await db.incrementFastCount(clientId); | ||
return callback(fast, { | ||
username: await prompt("enter username"), | ||
password: token, | ||
fastCount: count, | ||
}); | ||
} | ||
} | ||
|
||
return callback(mech, { | ||
username: await prompt("enter username"), | ||
password: await prompt("enter password"), | ||
requestToken: fast, | ||
}); | ||
} | ||
``` | ||
## References | ||
[SASL2](https://xmpp.org/extensions/xep-0388.html) | ||
[BIND2](https://xmpp.org/extensions/xep-0386.html) | ||
[FAST](https://xmpp.org/extensions/inbox/xep-fast.html) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,182 @@ | ||
"use strict"; | ||
|
||
const { encode, decode } = require("@xmpp/base64"); | ||
const SASLError = require("./lib/SASLError"); | ||
var jid = require("@xmpp/jid"); | ||
const xml = require("@xmpp/xml"); | ||
const SASLFactory = require("saslmechanisms"); | ||
|
||
// https://xmpp.org/rfcs/rfc6120.html#sasl | ||
|
||
const NS = "urn:xmpp:sasl:2"; | ||
const BIND2_NS = "urn:xmpp:bind:0"; | ||
const FAST_NS = "urn:xmpp:fast:0"; | ||
|
||
async function authenticate( | ||
SASL, | ||
entity, | ||
mechname, | ||
credentials, | ||
userAgent, | ||
features, | ||
) { | ||
const mech = SASL.create([mechname]); | ||
if (!mech) { | ||
throw new Error("No compatible mechanism"); | ||
} | ||
|
||
const { domain } = entity.options; | ||
const creds = { | ||
username: null, | ||
password: null, | ||
server: domain, | ||
host: domain, | ||
realm: domain, | ||
serviceType: "xmpp", | ||
serviceName: domain, | ||
...credentials, | ||
}; | ||
|
||
return new Promise((resolve, reject) => { | ||
const handler = (element) => { | ||
if (element.attrs.xmlns !== NS) { | ||
return; | ||
} | ||
|
||
if (element.name === "challenge") { | ||
mech.challenge(decode(element.text())); | ||
const resp = mech.response(creds); | ||
entity.send( | ||
xml( | ||
"response", | ||
{ xmlns: NS, mechanism: mech.name }, | ||
typeof resp === "string" ? encode(resp) : "", | ||
), | ||
); | ||
return; | ||
} | ||
|
||
switch (element.name) { | ||
case "failure": | ||
reject(SASLError.fromElement(element)); | ||
break; | ||
case "continue": | ||
// No tasks supported yet | ||
reject(); | ||
break; | ||
case "success": { | ||
const additionalData = element.getChild("additional-data")?.text(); | ||
if (additionalData && mech.final) { | ||
mech.final(decode(additionalData)); | ||
} | ||
// This jid will be bare unless we do inline bind2 then it will be the bound full jid | ||
const aid = element.getChild("authorization-identifier")?.text(); | ||
if (aid) { | ||
if (!entity.jid?.resource) { | ||
// No jid or bare jid, so update it | ||
entity._jid(aid); | ||
} else if (jid(aid).resource) { | ||
// We have a full jid so use it | ||
entity._jid(aid); | ||
} | ||
} | ||
const token = element.getChild("token", FAST_NS); | ||
if (token) { | ||
entity.emit("fast-token", token); | ||
} | ||
resolve(); | ||
break; | ||
} | ||
} | ||
|
||
entity.removeListener("nonza", handler); | ||
}; | ||
|
||
entity.on("nonza", handler); | ||
|
||
const bind2 = features | ||
.getChild("authentication", NS) | ||
.getChild("inline") | ||
?.getChild("bind", BIND2_NS); | ||
|
||
entity.send( | ||
xml("authenticate", { xmlns: NS, mechanism: mech.name }, [ | ||
mech.clientFirst && | ||
xml("initial-response", {}, encode(mech.response(creds))), | ||
(userAgent?.clientId || userAgent?.software || userAgent?.device) && | ||
xml( | ||
"user-agent", | ||
userAgent.clientId ? { id: userAgent.clientId } : {}, | ||
[ | ||
userAgent.software && xml("software", {}, userAgent.software), | ||
userAgent.device && xml("device", {}, userAgent.device), | ||
], | ||
), | ||
bind2 != null && | ||
userAgent?.clientId && | ||
xml("bind", { xmlns: BIND2_NS }, [ | ||
userAgent?.software && xml("tag", {}, userAgent.software), | ||
]), | ||
credentials.requestToken && | ||
xml( | ||
"request-token", | ||
{ xmlns: FAST_NS, mechanism: credentials.requestToken }, | ||
[], | ||
), | ||
credentials.fastCount && | ||
xml("fast", { xmlns: FAST_NS, count: credentials.fastCount }, []), | ||
]), | ||
); | ||
}); | ||
} | ||
|
||
module.exports = function sasl({ streamFeatures }, credentials, userAgent) { | ||
const SASL = new SASLFactory(); | ||
|
||
streamFeatures.use("authentication", NS, async ({ stanza, entity }) => { | ||
const offered = new Set( | ||
stanza | ||
.getChild("authentication", NS) | ||
.getChildren("mechanism", NS) | ||
.map((m) => m.text()), | ||
); | ||
const fast = new Set( | ||
stanza | ||
.getChild("authentication", NS) | ||
.getChild("inline") | ||
?.getChild("fast", FAST_NS) | ||
?.getChildren("mechanism", FAST_NS) | ||
?.map((m) => m.text()) || [], | ||
); | ||
const supported = SASL._mechs.map(({ name }) => name); | ||
// eslint-disable-next-line unicorn/prefer-array-find | ||
const intersection = supported | ||
.map((mech) => ({ | ||
name: mech, | ||
canFast: fast.has(mech), | ||
canOther: offered.has(mech), | ||
})) | ||
.filter((mech) => mech.canFast || mech.canOther); | ||
|
||
if (typeof credentials === "function") { | ||
await credentials( | ||
(mech, creds) => | ||
authenticate(SASL, entity, mech, creds, userAgent, stanza), | ||
intersection, | ||
); | ||
} else { | ||
let mech = intersection[0]?.name; | ||
if (!credentials.username && !credentials.password) { | ||
mech = "ANONYMOUS"; | ||
} | ||
|
||
await authenticate(SASL, entity, mech, credentials, userAgent, stanza); | ||
} | ||
}); | ||
|
||
return { | ||
use(...args) { | ||
return SASL.use(...args); | ||
}, | ||
}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
"use strict"; | ||
|
||
const XMPPError = require("@xmpp/error"); | ||
|
||
// https://xmpp.org/rfcs/rfc6120.html#sasl-errors | ||
|
||
class SASLError extends XMPPError { | ||
constructor(...args) { | ||
super(...args); | ||
this.name = "SASLError"; | ||
} | ||
} | ||
|
||
module.exports = SASLError; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
{ | ||
"name": "@xmpp/sasl2", | ||
"description": "XMPP SASL2 for JavaScript", | ||
"repository": "github:xmppjs/xmpp.js", | ||
"homepage": "https://github.com/xmppjs/xmpp.js/tree/main/packages/sasl2", | ||
"bugs": "http://github.com/xmppjs/xmpp.js/issues", | ||
"version": "0.13.0", | ||
"license": "ISC", | ||
"keywords": [ | ||
"XMPP", | ||
"sasl" | ||
], | ||
"dependencies": { | ||
"@xmpp/base64": "^0.13.0", | ||
"@xmpp/error": "^0.13.0", | ||
"@xmpp/jid": "^0.13.0", | ||
"@xmpp/xml": "^0.13.0", | ||
"saslmechanisms": "^0.1.1" | ||
}, | ||
"engines": { | ||
"node": ">= 14" | ||
}, | ||
"publishConfig": { | ||
"access": "public" | ||
} | ||
} |