# HG changeset patch
# Node ID ed53b4727887
# Parent  2626e9390dcf
diff --git a/ibis-cli/ibiscli.c b/ibis-cli/ibiscli.c
--- a/ibis-cli/ibiscli.c
+++ b/ibis-cli/ibiscli.c
@@ -28,6 +28,8 @@
 #include <glib-unix.h>
 #endif /* _WIN32 */
 
+#include <hasl.h>
+
 #include <ibis.h>
 
 /******************************************************************************
@@ -37,6 +39,9 @@
 static char *server = NULL;
 static int port = 6667;
 static char *password = NULL;
+static gboolean sasl_allow_clear_text = FALSE;
+static char *sasl_password = NULL;
+static char *sasl_username = NULL;
 static gboolean tls = FALSE;
 
 static GOptionEntry cli_options[] = {
@@ -68,6 +73,21 @@
 		.arg_data = &password,
 		.description = N_("The server password to use."),
 	}, {
+		.long_name = "sasl-allow-clear-text",
+		.arg = G_OPTION_ARG_NONE,
+		.arg_data = &sasl_allow_clear_text,
+		.description = N_("Allow clear text mechanisms."),
+	}, {
+		.long_name = "sasl-password",
+		.arg = G_OPTION_ARG_STRING,
+		.arg_data = &sasl_password,
+		.description = N_("A password to use for SASL."),
+	}, {
+		.long_name = "sasl-username",
+		.arg = G_OPTION_ARG_STRING,
+		.arg_data = &sasl_username,
+		.description = N_("A username to use for SASL."),
+	}, {
 		.long_name = "tls",
 		.short_name = 't',
 		.arg = G_OPTION_ARG_NONE,
@@ -88,6 +108,23 @@
 	g_application_hold(application);
 
 	ibis_client_set_nick(client, nick);
+
+	if(!ibis_str_is_empty(sasl_username) &&
+	   !ibis_str_is_empty(sasl_password))
+	{
+		HaslContext *hasl_context = NULL;
+
+		hasl_context = hasl_context_new();
+		hasl_context_set_username(hasl_context, sasl_username);
+		hasl_context_set_password(hasl_context, sasl_password);
+		hasl_context_set_allow_clear_text(hasl_context, sasl_allow_clear_text);
+		hasl_context_set_tls(hasl_context, tls);
+
+		ibis_client_set_hasl_context(client, hasl_context);
+
+		g_clear_object(&hasl_context);
+	}
+
 	ibis_client_connect(client, server, (guint16)port, password, tls, NULL);
 }
 
diff --git a/ibis/ibiscapabilities.h b/ibis/ibiscapabilities.h
--- a/ibis/ibiscapabilities.h
+++ b/ibis/ibiscapabilities.h
@@ -36,40 +36,6 @@
 G_BEGIN_DECLS
 
 /**
- * IBIS_CAPABILITY_CAP_LS_VERSION: (skip)
- *
- * The version to pass to the `CAP LS` command.
- *
- * See https://ircv3.net/specs/extensions/capability-negotiation for more
- * information.
- *
- * Since: 0.1
- */
-#define IBIS_CAPABILITY_CAP_LS_VERSION "302"
-
-/**
- * IBIS_CAPABILITY_SASL: (skip)
- *
- * The constant string to use when negotiating the SASL capability.
- *
- * See https://ircv3.net/specs/extensions/sasl-3.2 for more information.
- *
- * Since: 0.1
- */
-#define IBIS_CAPABILITY_SASL "sasl"
-
-/**
- * IBIS_CAPABILITY_SERVER_TIME: (skip)
- *
- * The constant string to use when negotiating the server-time capability.
- *
- * See https://ircv3.net/specs/extensions/server-time for more information.
- *
- * Since: 0.1
- */
-#define IBIS_CAPABILITY_SERVER_TIME "server-time"
-
-/**
  * IbisCapabilities:
  *
  * A object that handles capability negotiation.
diff --git a/ibis/ibiscapabilities.c b/ibis/ibiscapabilities.c
--- a/ibis/ibiscapabilities.c
+++ b/ibis/ibiscapabilities.c
@@ -20,9 +20,13 @@
  * along with this library; if not, see <https://www.gnu.org/licenses/>.
  */
 
+#include <hasl.h>
+
 #include "ibiscapabilities.h"
 
+#include "ibisconstants.h"
 #include "ibiscore.h"
+#include "ibissasl.h"
 #include "ibisstring.h"
 
 /* Windows is does something weird with signal handling that includes defining
@@ -264,6 +268,14 @@
  *****************************************************************************/
 static void
 ibis_capabilities_default_ready_cb(IbisCapabilities *capabilities) {
+	HaslContext *hasl_context = NULL;
+
+	/* If we have a hasl context, we need to request SASL. */
+	hasl_context = ibis_client_get_hasl_context(capabilities->client);
+	if(HASL_IS_CONTEXT(hasl_context)) {
+		ibis_sasl_request(capabilities);
+	}
+
 	/* cap-notify is implied when we use CAP LS 302, so this is really just to
 	 * make sure it's requested.
 	 */
diff --git a/ibis/ibisclient.h b/ibis/ibisclient.h
--- a/ibis/ibisclient.h
+++ b/ibis/ibisclient.h
@@ -32,6 +32,8 @@
 
 #include <gio/gio.h>
 
+#include <hasl.h>
+
 #include "ibismessage.h"
 #include "ibisversion.h"
 
@@ -176,6 +178,35 @@
 GError *ibis_client_get_error(IbisClient *client);
 
 /**
+ * ibis_client_get_hasl_context:
+ * @client: The instance.
+ *
+ * Gets the [class@Hasl.Context] from @client.
+ *
+ * Returns: (nullable) (transfer none): The Hasl context or %NULL.
+ *
+ * Since: 0.1
+ */
+IBIS_AVAILABLE_IN_0_1
+HaslContext *ibis_client_get_hasl_context(IbisClient *client);
+
+/**
+ * ibis_client_set_hasl_context:
+ * @client: The instance.
+ * @hasl_context: (nullable) (transfer none): The new context to set.
+ *
+ * Sets the [class@Hasl.Context] for @client to use.
+ *
+ * You should avoid switching this out during registration. If you want to
+ * remove this from memory you should connect to the `notify::connected` signal
+ * and set it to %NULL from that handler.
+ *
+ * Since: 0.1
+ */
+IBIS_AVAILABLE_IN_0_1
+void ibis_client_set_hasl_context(IbisClient *client, HaslContext *hasl_context);
+
+/**
  * ibis_client_get_nick:
  * @client: The instance.
  *
diff --git a/ibis/ibisclient.c b/ibis/ibisclient.c
--- a/ibis/ibisclient.c
+++ b/ibis/ibisclient.c
@@ -39,6 +39,7 @@
 	PROP_NICK,
 	PROP_REALNAME,
 	PROP_USERNAME,
+	PROP_HASL_CONTEXT,
 	N_PROPERTIES,
 };
 static GParamSpec *properties[N_PROPERTIES] = {NULL, };
@@ -65,6 +66,8 @@
 	char *alt_nick;
 	char *realname;
 	char *username;
+
+	HaslContext *hasl_context;
 };
 
 /******************************************************************************
@@ -342,6 +345,9 @@
 	case PROP_USERNAME:
 		g_value_set_string(value, ibis_client_get_username(client));
 		break;
+	case PROP_HASL_CONTEXT:
+		g_value_set_object(value, ibis_client_get_hasl_context(client));
+		break;
 	default:
 		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
 		break;
@@ -367,6 +373,9 @@
 	case PROP_USERNAME:
 		ibis_client_set_username(client, g_value_get_string(value));
 		break;
+	case PROP_HASL_CONTEXT:
+		ibis_client_set_hasl_context(client, g_value_get_object(value));
+		break;
 	default:
 		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
 		break;
@@ -489,6 +498,20 @@
 		NULL,
 		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
 
+	/**
+	 * IbisClient:hasl-context:
+	 *
+	 * The [class@Hasl.Context] to use during SASL negotiation.
+	 *
+	 * If this is %NULL, SASL negotiation will not be attempted.
+	 *
+	 * Since: 0.1
+	 */
+	properties[PROP_HASL_CONTEXT] = g_param_spec_object(
+		"hasl-context", NULL, NULL,
+		HASL_TYPE_CONTEXT,
+		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
 	g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
 
 	/**
@@ -651,6 +674,23 @@
 	return client->error;
 }
 
+HaslContext *
+ibis_client_get_hasl_context(IbisClient *client) {
+	g_return_val_if_fail(IBIS_IS_CLIENT(client), NULL);
+
+	return client->hasl_context;
+}
+
+void
+ibis_client_set_hasl_context(IbisClient *client, HaslContext *hasl_context) {
+	g_return_if_fail(IBIS_IS_CLIENT(client));
+
+	if(g_set_object(&client->hasl_context, hasl_context)) {
+		g_object_notify_by_pspec(G_OBJECT(client),
+		                         properties[PROP_HASL_CONTEXT]);
+	}
+}
+
 const char *
 ibis_client_get_nick(IbisClient *client) {
 	g_return_val_if_fail(IBIS_IS_CLIENT(client), NULL);
diff --git a/ibis/ibisconstants.h b/ibis/ibisconstants.h
--- a/ibis/ibisconstants.h
+++ b/ibis/ibisconstants.h
@@ -28,6 +28,40 @@
 #define IBIS_CONSTANTS_H
 
 /**
+ * IBIS_CAPABILITY_CAP_LS_VERSION: (skip)
+ *
+ * The version to pass to the `CAP LS` command.
+ *
+ * See https://ircv3.net/specs/extensions/capability-negotiation for more
+ * information.
+ *
+ * Since: 0.1
+ */
+#define IBIS_CAPABILITY_CAP_LS_VERSION "302"
+
+/**
+ * IBIS_CAPABILITY_SASL: (skip)
+ *
+ * The constant string to use when negotiating the SASL capability.
+ *
+ * See https://ircv3.net/specs/extensions/sasl-3.2 for more information.
+ *
+ * Since: 0.1
+ */
+#define IBIS_CAPABILITY_SASL "sasl"
+
+/**
+ * IBIS_CAPABILITY_SERVER_TIME: (skip)
+ *
+ * The constant string to use when negotiating the server-time capability.
+ *
+ * See https://ircv3.net/specs/extensions/server-time for more information.
+ *
+ * Since: 0.1
+ */
+#define IBIS_CAPABILITY_SERVER_TIME "server-time"
+
+/**
  * IBIS_CTCP_ACTION:
  *
  * A constant representing the CTCP ACTION command.
diff --git a/ibis/ibissasl.h b/ibis/ibissasl.h
new file mode 100644
--- /dev/null
+++ b/ibis/ibissasl.h
@@ -0,0 +1,46 @@
+/*
+ * Ibis - IRCv3 Library
+ * Copyright (C) 2022-2024 Ibis Developers
+ *
+ * Ibis is the legal property of its developers, whose names are too numerous
+ * to list here.  Please refer to the COPYRIGHT file distributed with this
+ * source distribution.
+ *
+ * This library is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this library; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#if !defined(IBIS_GLOBAL_HEADER_INSIDE) && !defined(IBIS_COMPILATION)
+# error "only <ibis.h> may be included directly"
+#endif
+
+#ifndef IBIS_SASL_H
+#define IBIS_SASL_H
+
+#include <glib.h>
+
+#include "ibiscapabilities.h"
+
+G_BEGIN_DECLS
+
+/**
+ * ibis_sasl_request: (skip)
+ * @capabilities: The capabilities instance.
+ *
+ * Called from [class@Client] during connection if SASL should be attempted.
+ */
+G_GNUC_INTERNAL void ibis_sasl_request(IbisCapabilities *capabilities);
+
+G_END_DECLS
+
+#endif /* IBIS_SASL_H */
diff --git a/ibis/ibissasl.c b/ibis/ibissasl.c
new file mode 100644
--- /dev/null
+++ b/ibis/ibissasl.c
@@ -0,0 +1,449 @@
+/*
+ * Ibis - IRCv3 Library
+ * Copyright (C) 2022-2024 Ibis Developers
+ *
+ * Ibis is the legal property of its developers, whose names are too numerous
+ * to list here.  Please refer to the COPYRIGHT file distributed with this
+ * source distribution.
+ *
+ * This library is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this library; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <glib/gi18n-lib.h>
+
+#include <hasl.h>
+
+#include "ibissasl.h"
+
+#include "ibisconstants.h"
+#include "ibiscore.h"
+
+#define IBIS_SASL_DATA_KEY ("sasl-data")
+
+typedef struct {
+	GString *server_in_buffer;
+} IbisSASLData;
+
+/******************************************************************************
+ * SASL Helpers
+ *****************************************************************************/
+static void
+ibis_sasl_data_free(IbisSASLData *data) {
+	g_string_free(data->server_in_buffer, TRUE);
+
+	g_free(data);
+}
+
+static void
+ibis_sasl_data_add(IbisClient *client) {
+	IbisSASLData *data = NULL;
+
+	data = g_new0(IbisSASLData, 1);
+	g_object_set_data_full(G_OBJECT(client), IBIS_SASL_DATA_KEY,
+	                       data, (GDestroyNotify)ibis_sasl_data_free);
+
+	/* We truncate the server_in_buffer when we need to so that we can minimize
+	 * allocations and simplify the logic involved with it.
+	 */
+	data->server_in_buffer = g_string_new("");
+}
+
+static void
+ibis_sasl_attempt(IbisClient *client) {
+	IbisMessage *message = NULL;
+	HaslContext *hasl_context = NULL;
+	const char *next_mechanism = NULL;
+
+	hasl_context = ibis_client_get_hasl_context(client);
+
+	next_mechanism = hasl_context_next(hasl_context);
+	if(next_mechanism == NULL) {
+		GError *error = g_error_new(IBIS_DOMAIN, 0,
+		                            _("No valid SASL mechanisms found"));
+
+		ibis_client_disconnect(client, error, NULL);
+
+		return;
+	}
+
+	g_message("trying SASL '%s' mechanism", next_mechanism);
+
+	message = ibis_message_new(IBIS_MSG_AUTHENTICATE);
+	ibis_message_set_params(message, next_mechanism, NULL);
+	ibis_client_write(client, message);
+}
+
+/******************************************************************************
+ * Callbacks
+ *****************************************************************************/
+static gboolean
+ibis_sasl_logged_in(G_GNUC_UNUSED IbisClient *client,
+                    G_GNUC_UNUSED const char *command,
+                    G_GNUC_UNUSED IbisMessage *message,
+                    G_GNUC_UNUSED gpointer data)
+{
+	return TRUE;
+}
+
+static gboolean
+ibis_sasl_logged_out(G_GNUC_UNUSED IbisClient *client,
+                     G_GNUC_UNUSED const char *command,
+                     G_GNUC_UNUSED IbisMessage *message,
+                     G_GNUC_UNUSED gpointer data)
+{
+	/* Not sure how to trigger this or what we should do in this case to be
+	 * honest, so just note it for now.
+	 * -- GK 2023-01-12
+	 */
+	g_warning("Server sent SASL logged out");
+
+	return TRUE;
+}
+
+static gboolean
+ibis_sasl_nick_locked(IbisClient *client,
+                      G_GNUC_UNUSED const char *command,
+                      IbisMessage *ibis_message,
+                      G_GNUC_UNUSED gpointer data)
+{
+	GError *error = NULL;
+	GStrv params = NULL;
+	char *message = NULL;
+
+	params = ibis_message_get_params(ibis_message);
+	message = g_strjoinv(" ", params);
+
+	error = g_error_new(IBIS_DOMAIN, 0, _("Nick name is locked: %s"), message);
+	g_free(message);
+
+	ibis_client_disconnect(client, error, NULL);
+
+	return TRUE;
+}
+
+
+static gboolean
+ibis_sasl_success(IbisClient *client,
+                  G_GNUC_UNUSED const char *command,
+                  G_GNUC_UNUSED IbisMessage *message,
+                  G_GNUC_UNUSED gpointer user_data)
+{
+	IbisCapabilities *capabilities = NULL;
+	HaslContext *hasl_context = NULL;
+
+	capabilities = ibis_client_get_capabilities(client);
+	hasl_context = ibis_client_get_hasl_context(client);
+
+	ibis_capabilities_remove_wait(capabilities);
+
+	g_message("successfully authenticated with SASL '%s' mechanism.",
+	          hasl_context_get_current_mechanism(hasl_context));
+
+	return TRUE;
+}
+
+static gboolean
+ibis_sasl_failed(IbisClient *client,
+                 G_GNUC_UNUSED const char *command,
+                 G_GNUC_UNUSED IbisMessage *message,
+                 G_GNUC_UNUSED gpointer data)
+{
+	HaslContext *hasl_context = NULL;
+	const char *current = NULL;
+
+	hasl_context = ibis_client_get_hasl_context(client);
+	current = hasl_context_get_current_mechanism(hasl_context);
+	if(current != NULL) {
+		g_message("SASL '%s' mechanism failed", current);
+	}
+
+	ibis_sasl_attempt(client);
+
+	return TRUE;
+}
+
+static gboolean
+ibis_sasl_message_too_long(G_GNUC_UNUSED IbisClient *client,
+                           G_GNUC_UNUSED const char *command,
+                           G_GNUC_UNUSED IbisMessage *message,
+                           G_GNUC_UNUSED gpointer data)
+{
+	g_warning("received SASL_MESSAGE_TOO_LONG");
+
+	return TRUE;
+}
+
+static gboolean
+ibis_sasl_aborted(G_GNUC_UNUSED IbisClient *client,
+                  G_GNUC_UNUSED const char *command,
+                  G_GNUC_UNUSED IbisMessage *message,
+                  G_GNUC_UNUSED gpointer data)
+{
+	/* This is supposed to get sent, when the client sends `AUTHENTICATE *`,
+	 * but we don't do this, so I guess we'll note it for now...?
+	 * --GK 2023-01-12
+	 */
+	g_warning("The server claims we aborted SASL authentication.");
+
+	return TRUE;
+}
+
+static gboolean
+ibis_sasl_already_authed(G_GNUC_UNUSED IbisClient *client,
+                         G_GNUC_UNUSED const char *command,
+                         G_GNUC_UNUSED IbisMessage *message,
+                         G_GNUC_UNUSED gpointer data)
+{
+	/* Similar to aborted above, we don't allow this, so just note that it
+	 * happened.
+	 * -- GK 2023-01-12
+	 */
+	g_warning("Server claims we tried to SASL authenticate again.");
+
+	return TRUE;
+}
+
+static gboolean
+ibis_sasl_mechanisms(G_GNUC_UNUSED IbisClient *client,
+                     G_GNUC_UNUSED const char *command,
+                     IbisMessage *message,
+                     G_GNUC_UNUSED gpointer data)
+{
+	GStrv params = NULL;
+	guint n_params = 0;
+
+	params = ibis_message_get_params(message);
+	if(params != NULL) {
+		n_params = g_strv_length(params);
+	}
+
+	/* We need to find a server that sends this message. The specification says
+	 * it _may_ be sent when the client sends AUTHENTICATE with an unknown
+	 * mechanism, but ergo doesn't.
+	 *
+	 * We can't just blindly accept the new list either, as depending on how
+	 * the server implements it, we'll need to remove mechanisms we've already
+	 * tried in the event the server just dumps the entire list. As we're not
+	 * currently tracking which mechanisms we've tried, this will have to be
+	 * addressed as well.
+	 */
+	if(n_params > 0) {
+		char *message = g_strjoinv(" ", params);
+
+		g_message("Server sent the following SASL mechanisms: %s", message);
+
+		g_free(message);
+	} else {
+		g_message("Server sent an empty list of SASL mechanisms");
+	}
+
+	return TRUE;
+}
+
+static gboolean
+ibis_sasl_authenticate(IbisClient *client,
+                       G_GNUC_UNUSED const char *command,
+                       IbisMessage *message,
+                       G_GNUC_UNUSED gpointer user_data)
+{
+	IbisSASLData *data = NULL;
+	GStrv params = NULL;
+	char *payload = NULL;
+	gboolean done = FALSE;
+	guint n_params = 0;
+
+	params = ibis_message_get_params(message);
+	if(params != NULL) {
+		n_params = g_strv_length(params);
+	}
+
+	if(n_params != 1) {
+		g_warning("ignoring AUTHENTICATE with %d parameters", n_params);
+
+		return TRUE;
+	}
+
+	payload = params[0];
+	data = g_object_get_data(G_OBJECT(client), IBIS_SASL_DATA_KEY);
+	if(data == NULL) {
+		g_warning("AUTHENTICATE received with no SASL data present");
+
+		return TRUE;
+	}
+
+	/* If the server sent us a payload, combine the chunks. */
+	if(payload[0] != '+') {
+		g_string_append(data->server_in_buffer, payload);
+
+		if(strlen(payload) < 400) {
+			done = TRUE;
+		}
+	} else {
+		/* The server sent a + which is an empty message or the final message
+		 * ended on a 400 byte barrier. */
+		done = TRUE;
+	}
+
+	if(done) {
+		IbisMessage *response = NULL;
+		HaslContext *hasl_context = NULL;
+		HaslMechanismResult res = 0;
+		GError *error = NULL;
+		guint8 *server_in = NULL;
+		guint8 *client_out = NULL;
+		gsize server_in_length = 0;
+		size_t client_out_length = 0;
+
+		/* If we have a buffer, base64 decode it, and then truncate it. */
+		if(data->server_in_buffer->len > 0) {
+			server_in = g_base64_decode(data->server_in_buffer->str,
+			                            &server_in_length);
+			g_string_truncate(data->server_in_buffer, 0);
+		}
+
+		/* Try to move to the next step of the sasl client. */
+		hasl_context = ibis_client_get_hasl_context(client);
+		res = hasl_context_step(hasl_context, server_in, server_in_length,
+		                        &client_out, &client_out_length, &error);
+
+		/* We should be done with server_in, so free it.*/
+		g_clear_pointer(&server_in, g_free);
+
+		if(res == HASL_MECHANISM_RESULT_ERROR) {
+			const char *error_message = "Unknown error";
+
+			if(error != NULL) {
+				error_message = error->message;
+			}
+
+			g_warning("hasl mechanism failed: %s", error_message);
+
+			g_clear_error(&error);
+
+			return TRUE;
+		}
+
+		if(error != NULL) {
+			g_warning("hasl_context_step returned an error without an error "
+			          "status: %s", error->message);
+
+			g_clear_error(&error);
+		}
+
+		/* If we got an output for the client, write it out. */
+		response = ibis_message_new(IBIS_MSG_AUTHENTICATE);
+
+		if(client_out_length > 0) {
+			char *encoded = NULL;
+
+			encoded = g_base64_encode(client_out, client_out_length);
+			ibis_message_set_params(response, encoded, NULL);
+
+			g_clear_pointer(&client_out, g_free);
+			g_free(encoded);
+		} else {
+			ibis_message_set_params(response, "+", NULL);
+		}
+
+		ibis_client_write(client, response);
+	}
+
+	return TRUE;
+}
+
+static void
+ibis_sasl_ack_cb(IbisCapabilities *capabilities,
+                 IbisClient *client,
+                 G_GNUC_UNUSED const char *capability,
+                 G_GNUC_UNUSED gpointer data)
+{
+	HaslContext *hasl_context = NULL;
+	const char *mechanisms = NULL;
+
+	/* Add the SASL mechanisms that the server advertised. */
+	hasl_context = ibis_client_get_hasl_context(client);
+
+	mechanisms = ibis_capabilities_lookup(capabilities, IBIS_CAPABILITY_SASL,
+	                                      NULL);
+	hasl_context_set_allowed_mechanisms(hasl_context, mechanisms);
+
+	/* There isn't a good way to know if our handlers are here or not, so we
+	 * just clear them and reconnect them.
+	 */
+	g_signal_handlers_disconnect_by_func(client,
+	                                     G_CALLBACK(ibis_sasl_logged_in),
+	                                     NULL);
+	g_signal_handlers_disconnect_by_func(client,
+	                                     G_CALLBACK(ibis_sasl_logged_out),
+	                                     NULL);
+	g_signal_handlers_disconnect_by_func(client,
+	                                     G_CALLBACK(ibis_sasl_nick_locked),
+	                                     NULL);
+	g_signal_handlers_disconnect_by_func(client,
+	                                     G_CALLBACK(ibis_sasl_success), NULL);
+	g_signal_handlers_disconnect_by_func(client,
+	                                     G_CALLBACK(ibis_sasl_failed), NULL);
+	g_signal_handlers_disconnect_by_func(client,
+	                                     G_CALLBACK(ibis_sasl_message_too_long),
+	                                     NULL);
+	g_signal_handlers_disconnect_by_func(client,
+	                                     G_CALLBACK(ibis_sasl_aborted), NULL);
+	g_signal_handlers_disconnect_by_func(client,
+	                                     G_CALLBACK(ibis_sasl_already_authed),
+	                                     NULL);
+	g_signal_handlers_disconnect_by_func(client,
+	                                     G_CALLBACK(ibis_sasl_mechanisms),
+	                                     NULL);
+	g_signal_handlers_disconnect_by_func(client,
+	                                     G_CALLBACK(ibis_sasl_authenticate),
+	                                     NULL);
+
+	g_signal_connect(client, "message::" IBIS_RPL_LOGGEDIN,
+	                 G_CALLBACK(ibis_sasl_logged_in), NULL);
+	g_signal_connect(client, "message::" IBIS_RPL_LOGGEDOUT,
+	                 G_CALLBACK(ibis_sasl_logged_out), NULL);
+	g_signal_connect(client, "message::" IBIS_ERR_NICKLOCKED,
+	                 G_CALLBACK(ibis_sasl_nick_locked), NULL);
+	g_signal_connect(client, "message::" IBIS_RPL_SASLSUCCESS,
+	                 G_CALLBACK(ibis_sasl_success), NULL);
+	g_signal_connect(client, "message::" IBIS_ERR_SASLFAIL,
+	                 G_CALLBACK(ibis_sasl_failed), NULL);
+	g_signal_connect(client, "message::" IBIS_ERR_SASLTOOLONG,
+	                 G_CALLBACK(ibis_sasl_message_too_long), NULL);
+	g_signal_connect(client, "message::" IBIS_ERR_SASLABORTED,
+	                 G_CALLBACK(ibis_sasl_aborted), NULL);
+	g_signal_connect(client, "message::" IBIS_ERR_SASLALREADY,
+	                 G_CALLBACK(ibis_sasl_already_authed), NULL);
+	g_signal_connect(client, "message::" IBIS_RPL_SASLMECHS,
+	                 G_CALLBACK(ibis_sasl_mechanisms), NULL);
+	g_signal_connect(client, "message::" IBIS_MSG_AUTHENTICATE,
+	                 G_CALLBACK(ibis_sasl_authenticate), NULL);
+
+	ibis_sasl_data_add(client);
+
+	ibis_capabilities_add_wait(capabilities);
+
+	ibis_sasl_attempt(client);
+}
+
+/******************************************************************************
+ * Internal API
+ *****************************************************************************/
+void
+ibis_sasl_request(IbisCapabilities *capabilities) {
+	ibis_capabilities_request(capabilities, IBIS_CAPABILITY_SASL);
+
+	g_signal_connect(capabilities, "ack::" IBIS_CAPABILITY_SASL,
+	                 G_CALLBACK(ibis_sasl_ack_cb), NULL);
+}
diff --git a/ibis/meson.build b/ibis/meson.build
--- a/ibis/meson.build
+++ b/ibis/meson.build
@@ -3,6 +3,7 @@
   'ibisclient.c',
   'ibisformatting.c',
   'ibismessage.c',
+  'ibissasl.c',
   'ibissource.c',
   'ibistags.c',
   'ibisversion.c',
@@ -15,6 +16,7 @@
   'ibiscore.h',
   'ibisformatting.h',
   'ibismessage.h',
+  'ibissasl.h',
   'ibissource.h',
   'ibisstring.h',
   'ibistags.h',
@@ -81,7 +83,7 @@
   IBIS_SOURCES + IBIS_HEADERS + IBIS_BUILT_SOURCES + IBIS_BUILT_HEADERS,
   c_args : ['-DIBIS_COMPILATION', '-DG_LOG_USE_STRUCTURED', '-DG_LOG_DOMAIN="Ibis"'],
   gnu_symbol_visibility : 'hidden',
-  dependencies : [birb_dep, gio_dep, glib_dep, gobject_dep],
+  dependencies : [birb_dep, gio_dep, glib_dep, gobject_dep, hasl_dep],
   include_directories : [toplevel_inc, ibis_inc],
   install : true)
 
@@ -95,7 +97,7 @@
   description : 'Ibis IRCv3 Parsing Library',
   filebase : 'ibis',
   subdirs : 'ibis-1.0',
-  libraries : [birb_dep, gio_dep, glib_dep, gobject_dep],
+  libraries : [birb_dep, gio_dep, glib_dep, gobject_dep, hasl_dep],
 )
 
 ###############################################################################
@@ -104,14 +106,15 @@
 if get_option('introspection')
   ibis_gir = gnome.generate_gir(ibis_lib,
     sources : IBIS_SOURCES + IBIS_HEADERS,
-    includes : ['Gio-2.0', 'GObject-2.0'],
+    includes : ['Gio-2.0', 'GObject-2.0', 'Hasl-1.0'],
     header : 'ibis.h',
     namespace : 'Ibis',
     symbol_prefix : 'ibis',
     nsversion : '1.0',
     install : true,
     export_packages : ['ibis'],
-    extra_args : ['--quiet', '-DIBIS_COMPILATION'])
+    extra_args : ['--quiet', '-DIBIS_COMPILATION'],
+    dependencies : [birb_dep, hasl_dep])
   IBIS_GENERATED_TARGETS += ibis_gir
 endif
 
@@ -119,7 +122,7 @@
 # Library Dependency Object
 ###############################################################################
 ibis_dep = declare_dependency(
-  dependencies : [gio_dep, glib_dep, gobject_dep],
+  dependencies : [birb_dep, gio_dep, glib_dep, gobject_dep, hasl_dep],
   include_directories : [toplevel_inc, ibis_inc],
   link_with : ibis_lib,
   sources : IBIS_BUILT_HEADERS + IBIS_GENERATED_TARGETS)
diff --git a/ibis/reference/ibis.toml.in b/ibis/reference/ibis.toml.in
--- a/ibis/reference/ibis.toml.in
+++ b/ibis/reference/ibis.toml.in
@@ -11,6 +11,11 @@
 devhelp = true
 search_index = true
 
+  [dependencies."Birb-1.0"]
+  name = "Birb"
+  description = "A library of GLib utilities."
+  docs_url = "https://docs.imfreedom.org/birb/"
+
   [dependencies."Gio-2.0"]
   name = "Gio"
   description = "Stream based io library."
@@ -26,6 +31,11 @@
   description = "The base type system library."
   docs_url = "https://docs.gtk.org/gobject/"
 
+  [dependencies."Hasl-1.0"]
+  name = "Hasl"
+  description = "Hassle-free Authentication and Security Layer client library."
+  docs_url = "https://docs.imfreedom.org/hasl/"
+
 [theme]
 name = "basic"
 show_index_summary = true
diff --git a/ibis/reference/meson.build b/ibis/reference/meson.build
--- a/ibis/reference/meson.build
+++ b/ibis/reference/meson.build
@@ -25,6 +25,8 @@
     '--output-dir=@OUTPUT@',
     '--no-namespace-dir',
     '--content-dir=@0@'.format(meson.current_source_dir()),
+    '--add-include-path=@0@'.format(meson.global_build_root() / 'subprojects/birb/birb'),
+    '--add-include-path=@0@'.format(meson.global_build_root() / 'subprojects/hasl/hasl'),
     '@INPUT1@'
   ],
   depend_files : ibis_doc_content_files,
@@ -38,6 +40,8 @@
   args: [
     'check',
     '--config', ibis_gidocgen_toml,
+    '--add-include-path=@0@'.format(meson.global_build_root() / 'subprojects/birb/birb'),
+    '--add-include-path=@0@'.format(meson.global_build_root() / 'subprojects/hasl/hasl'),
     ibis_gir[0],
   ],
   depends: ibis_gir[0],
diff --git a/meson.build b/meson.build
--- a/meson.build
+++ b/meson.build
@@ -39,6 +39,7 @@
 gio_dep = dependency('gio-2.0')
 gobject_dep = dependency('gobject-2.0')
 birb_dep = dependency('birb')
+hasl_dep = dependency('hasl')
 
 add_project_arguments(
   '-DGLIB_VERSION_MIN_REQUIRED=GLIB_VERSION_2_76',
diff --git a/subprojects/hasl.wrap b/subprojects/hasl.wrap
new file mode 100644
--- /dev/null
+++ b/subprojects/hasl.wrap
@@ -0,0 +1,4 @@
+[wrap-hg]
+directory = hasl
+url = https://keep.imfreedom.org/hasl/hasl/
+revision = 24a2c23b96f6
