Skip to content

Commit b388628

Browse files
committed
Add callback and authc/zid support, document
1 parent 9fcc0c0 commit b388628

File tree

3 files changed

+74
-28
lines changed

3 files changed

+74
-28
lines changed

docs/clientapi.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ new Irc.Client({
1818
ping_interval: 30,
1919
ping_timeout: 120,
2020
sasl_disconnect_on_fail: false,
21+
sasl_mechanism: "PLAIN",
2122
account: {
2223
account: 'username',
2324
password: 'account_password',
@@ -40,6 +41,11 @@ new Irc.Client({
4041
});
4142
~~~
4243

44+
##### SASL support
45+
The `sasl_mechanism`, `sasl_function`, and `account` fields are used for SASL
46+
authentication. The above example represents `PLAIN` authentication; for
47+
`EXTERNAL` authentication, no `account` should be specified. See
48+
[ircv3.md](ircv3.md#sasl) for more details on advanced SASL functionality.
4349

4450
#### Properties
4551
##### `.connected`

docs/ircv3.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,23 @@
2929

3030
Only enabled if the client `enable_echomessage` option is `true`. Clients may not be expecting their own messages being echoed back by default so it must be enabled manually.
3131
Until IRCv3 labelled replies are available, sent message confirmations will not be available. More information on the echo-message limitations can be found here https://github.com/ircv3/ircv3-specifications/pull/284/files
32+
33+
* <a name="sasl"></a>sasl
34+
35+
Two SASL authentication mechanisms are natively supported.
36+
37+
* EXTERNAL
38+
39+
The EXTERNAL mechanism is used with a pre-arranged out-of-band identification and authentication system. This means that no identity or authentication information is exchanged over the IRC connection during login. In theory this out-of-band system could involve virtually anything; in practice, SASL EXTERNAL authentication on IRC is generally used with client TLS certificates and a list of user hashes.
40+
41+
* PLAIN
42+
43+
The PLAIN mechanism is essentially the usual username-and-password authentication, with the exception that two different usernames may be specified (see [authzid and authcid](#authzid-and-authcid) below.) The use of this functionality is uncommon, and the desired username may be specified simply as `account.account` if a user will be logging in and acting under the same account.
44+
45+
* Other simple mechanisms
46+
47+
Any mechanism for SASL authentication that doesn't require a challenge-and-response and which follows the general format of PLAIN may be used. This includes e.g. authentication with transient cookies. To make use of this, configure the Client instance API constructor as normal, with `sasl_mechanism` set to the name of the mechanism and `account.secret` set to the secret used by the mechanism. Either `account` or the combination of `authzid` and `authcid` may be set within the `account` object.
48+
49+
* Challenge-response mechanisms and more
50+
51+
Arbitrary mechanisms may be supported by providing a helper function in the `options.sasl_function` field. When called, this function will receive the `command` and `handler` objects as parameters, and should return a UTF-8 string. (Do _not_ encode the string to Base64, as the framework will handle that.) Note that the function is responsible for maintianing state between calls if that is required by the desired mechanism.

src/commands/handlers/registration.js

Lines changed: 48 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -284,25 +284,32 @@ const handlers = {
284284
},
285285

286286
AUTHENTICATE: function(command, handler) {
287-
if (command.params[0] !== '+') {
288-
if (handler.network.cap.negotiating) {
289-
handler.connection.write('CAP END');
290-
handler.network.cap.negotiating = false;
287+
let options = handler.connection.options;
288+
let auth_str = "";
289+
if (options.sasl_function && typeof options.sasl_function === "function")
290+
{
291+
auth_str = options.sasl_function(comand, handler);
292+
} else {
293+
if (command.params[0] !== '+') {
294+
if (handler.network.cap.negotiating) {
295+
handler.connection.write('CAP END');
296+
handler.network.cap.negotiating = false;
297+
}
298+
299+
return;
291300
}
292301

293-
return;
294-
}
302+
// Send blank authenticate for EXTERNAL mechanism
303+
if (options.sasl_mechanism === 'EXTERNAL') {
304+
handler.connection.write('AUTHENTICATE +');
305+
return;
306+
}
295307

296-
// Send blank authenticate for EXTERNAL mechanism
297-
if (handler.connection.options.sasl_mechanism === 'EXTERNAL') {
298-
handler.connection.write('AUTHENTICATE +');
299-
return;
308+
const saslAuth = getSaslAuth(handler);
309+
auth_str = saslAuth.authzid + '\0' +
310+
saslAuth.authcid + '\0' +
311+
saslAuth.secret;
300312
}
301-
302-
const saslAuth = getSaslAuth(handler);
303-
const auth_str = saslAuth.account + '\0' +
304-
saslAuth.account + '\0' +
305-
saslAuth.secret;
306313
const b = Buffer.from(auth_str, 'utf8');
307314
const b64 = b.toString('base64');
308315

@@ -429,26 +436,39 @@ const handlers = {
429436

430437
/**
431438
* Only use the nick+password combo if an account has not been specifically given.
432-
* If an account:{account,password} has been given, use it for SASL auth.
439+
* If account information has been given, use it for SASL auth.
433440
*/
434441
function getSaslAuth(handler) {
435442
const options = handler.connection.options;
436-
if (options.account && options.account.account) {
437-
// An account username has been given, use it for SASL auth
438-
return {
439-
account: options.account.account,
440-
secret: (options.sasl_mechanism.toUpperCase() === 'AUTHCOOKIE' ?
441-
options.account.authcookie :
442-
options.account.password) || '',
443-
};
444-
} else if (options.account) {
445-
// An account object existed but without auth credentials
446-
return null;
443+
if (options.account) {
444+
// Prefer the more general 'secret' over 'password' if present
445+
const secret = options.account.secret ?
446+
options.account.secret :
447+
options.account.password;
448+
if (options.account.authcid && options.account.authzid) {
449+
// authzid and authcid used to log in as one user and act as another, see ircv3.md
450+
return {
451+
authcid: options.account.authcid,
452+
authzid: options.account.authzid,
453+
secret: secret || '',
454+
};
455+
} else if (options.account.account) {
456+
// An account username has been given, use it for SASL auth
457+
return {
458+
authcid: options.account.account,
459+
authzid: options.account.account,
460+
secret: secret || '',
461+
};
462+
} else {
463+
// An account object existed but without auth credentials
464+
return null;
465+
}
447466
} else if (options.password) {
448467
// No account credentials found but we have a server password. Also use it for SASL
449468
// for ease of use
450469
return {
451-
account: options.nick,
470+
authcid: options.nick,
471+
authzid: options.nick,
452472
secret: options.password,
453473
};
454474
}

0 commit comments

Comments
 (0)