From 9fcc0c08072bcd6d37b3944036c0aec8e77c0c61 Mon Sep 17 00:00:00 2001 From: Kufat Date: Sat, 27 May 2023 22:14:12 -0400 Subject: [PATCH 1/2] Add atheme SASL AUTHCOOKIE support --- src/commands/handlers/registration.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/commands/handlers/registration.js b/src/commands/handlers/registration.js index 8b793a6c..5255a04d 100644 --- a/src/commands/handlers/registration.js +++ b/src/commands/handlers/registration.js @@ -302,7 +302,7 @@ const handlers = { const saslAuth = getSaslAuth(handler); const auth_str = saslAuth.account + '\0' + saslAuth.account + '\0' + - saslAuth.password; + saslAuth.secret; const b = Buffer.from(auth_str, 'utf8'); const b64 = b.toString('base64'); @@ -437,7 +437,9 @@ function getSaslAuth(handler) { // An account username has been given, use it for SASL auth return { account: options.account.account, - password: options.account.password || '', + secret: (options.sasl_mechanism.toUpperCase() === 'AUTHCOOKIE' ? + options.account.authcookie : + options.account.password) || '', }; } else if (options.account) { // An account object existed but without auth credentials @@ -447,7 +449,7 @@ function getSaslAuth(handler) { // for ease of use return { account: options.nick, - password: options.password, + secret: options.password, }; } From b3886282bcb45a3a061f05432850ec420554e6f5 Mon Sep 17 00:00:00 2001 From: Kufat Date: Sun, 28 May 2023 18:40:12 -0400 Subject: [PATCH 2/2] Add callback and authc/zid support, document --- docs/clientapi.md | 6 +++ docs/ircv3.md | 20 +++++++ src/commands/handlers/registration.js | 76 +++++++++++++++++---------- 3 files changed, 74 insertions(+), 28 deletions(-) diff --git a/docs/clientapi.md b/docs/clientapi.md index 5bd37a3f..115c2445 100644 --- a/docs/clientapi.md +++ b/docs/clientapi.md @@ -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', @@ -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` diff --git a/docs/ircv3.md b/docs/ircv3.md index ebcb2121..f89c0c66 100644 --- a/docs/ircv3.md +++ b/docs/ircv3.md @@ -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 + +* 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. \ No newline at end of file diff --git a/src/commands/handlers/registration.js b/src/commands/handlers/registration.js index 5255a04d..bd393f5b 100644 --- a/src/commands/handlers/registration.js +++ b/src/commands/handlers/registration.js @@ -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.secret; const b = Buffer.from(auth_str, 'utf8'); const b64 = b.toString('base64'); @@ -429,26 +436,39 @@ 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, - secret: (options.sasl_mechanism.toUpperCase() === 'AUTHCOOKIE' ? - options.account.authcookie : - 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, + authcid: options.nick, + authzid: options.nick, secret: options.password, }; }