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

Enhance and document SASL support #359

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
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
6 changes: 6 additions & 0 deletions docs/clientapi.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ new Irc.Client({
ping_interval: 30,
ping_timeout: 120,
sasl_disconnect_on_fail: false,
sasl_mechanism: "PLAIN",
account: {
account: 'username',
password: 'account_password',
Expand All @@ -40,6 +41,11 @@ new Irc.Client({
});
~~~

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

#### Properties
##### `.connected`
Expand Down
20 changes: 20 additions & 0 deletions docs/ircv3.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,23 @@

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.
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

* <a name="sasl"></a>sasl

Two SASL authentication mechanisms are natively supported.

* EXTERNAL

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.

* PLAIN

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.

* Other simple mechanisms

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.

* Challenge-response mechanisms and more

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.
76 changes: 49 additions & 27 deletions src/commands/handlers/registration.js
Original file line number Diff line number Diff line change
Expand Up @@ -284,25 +284,32 @@ const handlers = {
},

AUTHENTICATE: function(command, handler) {
if (command.params[0] !== '+') {
if (handler.network.cap.negotiating) {
handler.connection.write('CAP END');
handler.network.cap.negotiating = false;
let options = handler.connection.options;
let auth_str = "";
if (options.sasl_function && typeof options.sasl_function === "function")
{
auth_str = options.sasl_function(comand, handler);
} else {
if (command.params[0] !== '+') {
if (handler.network.cap.negotiating) {
handler.connection.write('CAP END');
handler.network.cap.negotiating = false;
}

return;
}

return;
}
// Send blank authenticate for EXTERNAL mechanism
if (options.sasl_mechanism === 'EXTERNAL') {
handler.connection.write('AUTHENTICATE +');
return;
}

// Send blank authenticate for EXTERNAL mechanism
if (handler.connection.options.sasl_mechanism === 'EXTERNAL') {
handler.connection.write('AUTHENTICATE +');
return;
const saslAuth = getSaslAuth(handler);
auth_str = saslAuth.authzid + '\0' +
saslAuth.authcid + '\0' +
saslAuth.secret;
}

const saslAuth = getSaslAuth(handler);
const auth_str = saslAuth.account + '\0' +
saslAuth.account + '\0' +
saslAuth.password;
const b = Buffer.from(auth_str, 'utf8');
const b64 = b.toString('base64');

Expand Down Expand Up @@ -429,25 +436,40 @@ const handlers = {

/**
* Only use the nick+password combo if an account has not been specifically given.
* If an account:{account,password} has been given, use it for SASL auth.
* If account information has been given, use it for SASL auth.
*/
function getSaslAuth(handler) {
const options = handler.connection.options;
if (options.account && options.account.account) {
// An account username has been given, use it for SASL auth
return {
account: options.account.account,
password: options.account.password || '',
};
} else if (options.account) {
// An account object existed but without auth credentials
return null;
if (options.account) {
// Prefer the more general 'secret' over 'password' if present
const secret = options.account.secret ?
options.account.secret :
options.account.password;
if (options.account.authcid && options.account.authzid) {
// authzid and authcid used to log in as one user and act as another, see ircv3.md
return {
authcid: options.account.authcid,
authzid: options.account.authzid,
secret: secret || '',
};
} else if (options.account.account) {
// An account username has been given, use it for SASL auth
return {
authcid: options.account.account,
authzid: options.account.account,
secret: secret || '',
};
} else {
// An account object existed but without auth credentials
return null;
}
} else if (options.password) {
// No account credentials found but we have a server password. Also use it for SASL
// for ease of use
return {
account: options.nick,
password: options.password,
authcid: options.nick,
authzid: options.nick,
secret: options.password,
};
}

Expand Down