/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
 *
 * Copyright 2009-2012  Red Hat, Inc.
 *
 * This program 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 3 of the License, or
 * (at your option) any later version.
 *
 * This program 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 program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 *
 * Written by: Matthias Clasen <mclasen@redhat.com>
 *             Stef Walter <stefw@gnome.org>
 */

#include "config.h"

#include "um-realm-manager.h"

#include <krb5/krb5.h>

#include <glib.h>
#include <glib/gi18n.h>
#include <glib/gstdio.h>

#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>


struct _UmRealmManager {
        UmRealmProviderProxy parent;
        guint diagnostics_sig;
        GHashTable *realms;
};

typedef struct {
        UmRealmProviderProxyClass parent_class;
} UmRealmManagerClass;

enum {
        REALM_ADDED,
        NUM_SIGNALS,
};

static gint signals[NUM_SIGNALS] = { 0, };

G_DEFINE_TYPE (UmRealmManager, um_realm_manager, UM_REALM_TYPE_PROVIDER_PROXY);

GQuark
um_realm_error_get_quark (void)
{
        static GQuark quark = 0;
        if (quark == 0)
                quark = g_quark_from_static_string ("um-realm-error");
        return quark;
}

typedef struct {
        UmRealmManager *manager;
        GList *realms;
        gint outstanding;
} LoadClosure;

static void
load_closure_free (gpointer data)
{
        LoadClosure *load = data;
        g_list_free_full (load->realms, g_object_unref);
        g_object_unref (load->manager);
        g_slice_free (LoadClosure, load);
}

static void
on_realm_proxy_created (GObject *source,
                        GAsyncResult *result,
                        gpointer user_data)
{
        GSimpleAsyncResult *async = G_SIMPLE_ASYNC_RESULT (user_data);
        LoadClosure *load = g_simple_async_result_get_op_res_gpointer (async);
        UmRealmManager *self = load->manager;
        UmRealmKerberos *realm;
        UmRealmKerberos *have;
        GError *error = NULL;
        GDBusProxy *proxy;
        GVariant *info;

        realm = um_realm_kerberos_proxy_new_finish (result, &error);
        if (error == NULL) {
                proxy = G_DBUS_PROXY (realm);
                info = g_variant_new ("(sos)",
                                      g_dbus_proxy_get_name (proxy),
                                      g_dbus_proxy_get_object_path (proxy),
                                      g_dbus_proxy_get_interface_name (proxy));

                /* Add it to the manager, unless race */
                have = g_hash_table_lookup (self->realms, info);
                if (have == NULL) {
                        g_dbus_proxy_set_default_timeout (proxy, G_MAXINT);
                        g_hash_table_insert (self->realms,
                                             g_variant_ref_sink (info), realm);
                        g_signal_emit (self, signals[REALM_ADDED], 0, realm);

                } else {
                        g_object_unref (realm);
                        g_variant_unref (info);
                        realm = have;
                }

                load->realms = g_list_prepend (load->realms, g_object_ref (realm));

        } else {
                g_simple_async_result_take_error (async, error);
        }

        if (load->outstanding-- == 1)
                g_simple_async_result_complete (async);

        g_object_unref (async);
}

static void
um_realm_manager_load (UmRealmManager *self,
                       GVariant *realms,
                       GCancellable *cancellable,
                       GAsyncReadyCallback callback,
                       gpointer user_data)
{
        GSimpleAsyncResult *async;
        GDBusConnection *connection;
        LoadClosure *load;
        GVariantIter iter;
        GVariant *info;
        UmRealmKerberos *realm;
        const gchar *path;
        const gchar *iface;
        const gchar *name;

        g_return_if_fail (realms != NULL);

        async = g_simple_async_result_new (G_OBJECT (self), callback, user_data,
                                           um_realm_manager_load);
        load = g_slice_new0 (LoadClosure);
        load->manager = g_object_ref (self);
        g_simple_async_result_set_op_res_gpointer (async, load, load_closure_free);

        connection = g_dbus_proxy_get_connection (G_DBUS_PROXY (self));

        g_variant_iter_init (&iter, realms);
        while (1) {
                info = g_variant_iter_next_value (&iter);
                if (info == NULL)
                        break;
                realm = g_hash_table_lookup (self->realms, info);
                if (realm == NULL) {
                        g_variant_get (info, "(&s&o&s)", &name, &path, &iface);
                        if (g_str_equal (iface, "org.freedesktop.realmd.Kerberos")) {
                                um_realm_kerberos_proxy_new (connection,
                                                             G_DBUS_PROXY_FLAGS_NONE,
                                                             name, path, cancellable,
                                                             on_realm_proxy_created,
                                                             g_object_ref (async));
                                load->outstanding++;
                        }
                } else {
                        load->realms = g_list_prepend (load->realms, g_object_ref (realm));
                }
                g_variant_unref (info);
        }

        if (load->outstanding == 0)
                g_simple_async_result_complete_in_idle (async);
}

static GList *
um_realm_manager_load_finish (UmRealmManager *self,
                              GAsyncResult *result,
                              GError **error)
{
        GSimpleAsyncResult *async = G_SIMPLE_ASYNC_RESULT (result);
        LoadClosure *load;
        GList *realms;

        if (g_simple_async_result_propagate_error (async, error))
                return NULL;

        load = g_simple_async_result_get_op_res_gpointer (async);
        realms = g_list_reverse (load->realms);
        load->realms = NULL;
        return realms;
}

static guint
hash_realm_info (gconstpointer value)
{
        const gchar *name, *path, *iface;
        g_variant_get ((GVariant *)value, "(&s&o&s)", &name, &path, &iface);
        return g_str_hash (name) ^ g_str_hash (path) ^ g_str_hash (iface);
}

static void
um_realm_manager_init (UmRealmManager *self)
{
        self->realms = g_hash_table_new_full (hash_realm_info, g_variant_equal,
                                              (GDestroyNotify)g_variant_unref,
                                              g_object_unref);
}

static void
um_realm_manager_notify (GObject *obj,
                         GParamSpec *spec)
{
        UmRealmManager *self = UM_REALM_MANAGER (obj);
        GVariant *realms;

        if (G_OBJECT_CLASS (um_realm_manager_parent_class)->notify)
                G_OBJECT_CLASS (um_realm_manager_parent_class)->notify (obj, spec);

        if (g_str_equal (spec->name, "realms")) {
                realms = um_realm_provider_get_realms (UM_REALM_PROVIDER (self));
                if (realms != NULL)
                        um_realm_manager_load (self, realms, NULL, NULL, NULL);
        }
}

static void
um_realm_manager_dispose (GObject *obj)
{
        UmRealmManager *self = UM_REALM_MANAGER (obj);
        GDBusConnection *connection;

        if (self->diagnostics_sig) {
                connection = g_dbus_proxy_get_connection (G_DBUS_PROXY (self));
                if (connection != NULL)
                        g_dbus_connection_signal_unsubscribe (connection, self->diagnostics_sig);
                self->diagnostics_sig = 0;
        }

        G_OBJECT_CLASS (um_realm_manager_parent_class)->dispose (obj);
}

static void
um_realm_manager_finalize (GObject *obj)
{
        UmRealmManager *self = UM_REALM_MANAGER (obj);

        g_hash_table_destroy (self->realms);

        G_OBJECT_CLASS (um_realm_manager_parent_class)->finalize (obj);
}

static void
um_realm_manager_class_init (UmRealmManagerClass *klass)
{
        GObjectClass *object_class = G_OBJECT_CLASS (klass);

        object_class->notify = um_realm_manager_notify;
        object_class->dispose = um_realm_manager_dispose;
        object_class->finalize = um_realm_manager_finalize;

        signals[REALM_ADDED] = g_signal_new ("realm-added", UM_TYPE_REALM_MANAGER,
                                             G_SIGNAL_RUN_FIRST, 0, NULL, NULL,
                                             g_cclosure_marshal_generic,
                                             G_TYPE_NONE, 1, UM_REALM_TYPE_KERBEROS);
}

static void
on_realm_diagnostics (GDBusConnection *connection,
                      const gchar *sender_name,
                      const gchar *object_path,
                      const gchar *interface_name,
                      const gchar *signal_name,
                      GVariant *parameters,
                      gpointer user_data)
{
        const gchar *message;
        const gchar *unused;

        if (g_variant_is_of_type (parameters, G_VARIANT_TYPE ("(ss)"))) {
                /* Data is already formatted appropriately for stderr */
                g_variant_get (parameters, "(&s&s)", &message, &unused);
                g_printerr ("%s", message);
        }
}

static gboolean
number_at_least (const gchar *number,
                 guint minimum)
{
        gchar *end;

        if (strtol (number, &end, 10) < (long)minimum)
                return FALSE;
        if (!end || *end != '\0')
                return FALSE;
        return TRUE;
}

static gboolean
version_compare (const char *version,
                 guint req_major,
                 guint req_minor)
{
        gboolean match = FALSE;
        gchar **parts;

        parts = g_strsplit (version, ".", 2);

        if (parts[0] && parts[1]) {
                match = number_at_least (parts[0], req_major) &&
                        number_at_least (parts[1], req_minor);
        }

        g_strfreev (parts);
        return match;
}

static void
on_realm_manager_async_init (GObject *source,
                             GAsyncResult *result,
                             gpointer user_data)
{
        GSimpleAsyncResult *async = G_SIMPLE_ASYNC_RESULT (user_data);
        UmRealmManager *self;
        GError *error = NULL;
        const gchar *version;
        GDBusProxy *proxy;
        GObject *object;
        guint sig;

        object = g_async_initable_new_finish (G_ASYNC_INITABLE (source), result, &error);

        if (error != NULL)
                g_simple_async_result_take_error (async, error);

        /* This is temporary until the dbus interface stabilizes */
        if (object) {
                version = um_realm_provider_get_version (UM_REALM_PROVIDER (object));
                if (!version_compare (version, 0, 2)) {
                        /* No need to bother translators with this temporary message */
                        g_simple_async_result_set_error (async, UM_REALM_ERROR,
                                                         UM_REALM_ERROR_GENERIC,
                                                         "Unsupported version of realmd: %s", version);
                        g_object_unref (object);
                        object = NULL;
                }
        }

        if (object != NULL) {
                proxy = G_DBUS_PROXY (object);
                sig = g_dbus_connection_signal_subscribe (g_dbus_proxy_get_connection (proxy),
                                                          g_dbus_proxy_get_name_owner (proxy),
                                                          "org.freedesktop.realmd.Diagnostics",
                                                          "Diagnostics",
                                                          NULL,
                                                          NULL,
                                                          G_DBUS_SIGNAL_FLAGS_NONE,
                                                          on_realm_diagnostics,
                                                          NULL,
                                                          NULL);

                self = UM_REALM_MANAGER (object);
                self->diagnostics_sig = sig;
                g_simple_async_result_set_op_res_gpointer (async, self, g_object_unref);
        }

        g_simple_async_result_complete (async);
        g_object_unref (async);
}

void
um_realm_manager_new (GCancellable *cancellable,
                      GAsyncReadyCallback callback,
                      gpointer user_data)
{
        GSimpleAsyncResult *async;

        async = g_simple_async_result_new (NULL, callback, user_data,
                                           um_realm_manager_new);

        g_async_initable_new_async (UM_TYPE_REALM_MANAGER,
                                    G_PRIORITY_DEFAULT, cancellable,
                                    on_realm_manager_async_init, g_object_ref (async),
                                    "g-name", "org.freedesktop.realmd",
                                    "g-bus-type", G_BUS_TYPE_SYSTEM,
                                    "g-object-path", "/org/freedesktop/realmd",
                                    "g-interface-name", "org.freedesktop.realmd.Provider",
                                    "g-default-timeout", G_MAXINT,
                                    NULL);

        g_object_unref (async);
}

UmRealmManager *
um_realm_manager_new_finish (GAsyncResult *result,
                             GError **error)
{
        UmRealmManager *self;
        GSimpleAsyncResult *async;

        g_return_val_if_fail (g_simple_async_result_is_valid (result, NULL,
                              um_realm_manager_new), NULL);
        g_return_val_if_fail (error == NULL || *error == NULL, NULL);

        async = G_SIMPLE_ASYNC_RESULT (result);
        if (g_simple_async_result_propagate_error (async, error))
                return NULL;

        self = g_simple_async_result_get_op_res_gpointer (async);
        if (self != NULL)
                g_object_ref (self);
        return self;
}

typedef struct {
        GCancellable *cancellable;
        GList *realms;
} DiscoverClosure;

static void
discover_closure_free (gpointer data)
{
        DiscoverClosure *discover = data;
        g_clear_object (&discover->cancellable);
        g_list_free_full (discover->realms, g_object_unref);
        g_slice_free (DiscoverClosure, discover);
}

static void
on_manager_load (GObject *source,
                 GAsyncResult *result,
                 gpointer user_data)
{
        GSimpleAsyncResult *async = G_SIMPLE_ASYNC_RESULT (user_data);
        DiscoverClosure *discover = g_simple_async_result_get_op_res_gpointer (async);
        GError *error = NULL;

        discover->realms = um_realm_manager_load_finish (UM_REALM_MANAGER (source),
                                                         result, &error);
        if (error != NULL)
                g_simple_async_result_take_error (async, error);
        g_simple_async_result_complete (async);

        g_object_unref (async);
}

static void
on_provider_discover (GObject *source,
                      GAsyncResult *result,
                      gpointer user_data)
{
        GSimpleAsyncResult *async = G_SIMPLE_ASYNC_RESULT (user_data);
        DiscoverClosure *discover = g_simple_async_result_get_op_res_gpointer (async);
        UmRealmManager *self = UM_REALM_MANAGER (source);
        GError *error = NULL;
        GVariant *realms;
        gint relevance;

        um_realm_provider_call_discover_finish (UM_REALM_PROVIDER (self),
                                                &relevance, &realms, result, &error);
        if (error == NULL) {
                um_realm_manager_load (self, realms, discover->cancellable,
                                       on_manager_load, g_object_ref (async));

        } else {
                g_simple_async_result_take_error (async, error);
                g_simple_async_result_complete (async);
        }

        g_object_unref (async);
}

void
um_realm_manager_discover (UmRealmManager *self,
                           const gchar *input,
                           GCancellable *cancellable,
                           GAsyncReadyCallback callback,
                           gpointer user_data)
{
        GSimpleAsyncResult *res;
        DiscoverClosure *discover;

        g_return_if_fail (UM_IS_REALM_MANAGER (self));
        g_return_if_fail (input != NULL);
        g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable));

        res = g_simple_async_result_new (G_OBJECT (self), callback, user_data,
                                         um_realm_manager_discover);
        discover = g_slice_new0 (DiscoverClosure);
        discover->cancellable = cancellable ? g_object_ref (cancellable) : NULL;
        g_simple_async_result_set_op_res_gpointer (res, discover, discover_closure_free);

        um_realm_provider_call_discover (UM_REALM_PROVIDER (self), input, "", cancellable,
                                         on_provider_discover, g_object_ref (res));

        g_object_unref (res);
}

GList *
um_realm_manager_discover_finish (UmRealmManager *self,
                                  GAsyncResult *result,
                                  GError **error)
{
        GSimpleAsyncResult *async;
        DiscoverClosure *discover;
        GList *realms;

        g_return_val_if_fail (UM_IS_REALM_MANAGER (self), NULL);
        g_return_val_if_fail (g_simple_async_result_is_valid (result, G_OBJECT (self),
                              um_realm_manager_discover), NULL);
        g_return_val_if_fail (error == NULL || *error == NULL, NULL);

        async = G_SIMPLE_ASYNC_RESULT (result);
        if (g_simple_async_result_propagate_error (async, error))
                return NULL;

        discover = g_simple_async_result_get_op_res_gpointer (async);
        if (!discover->realms) {
                g_set_error (error, UM_REALM_ERROR, UM_REALM_ERROR_GENERIC,
                             _("No such domain or realm found"));
                return NULL;
        }

        realms = discover->realms;
        discover->realms = NULL;
        return realms;
}

GList *
um_realm_manager_get_realms (UmRealmManager *self)
{
        g_return_val_if_fail (UM_IS_REALM_MANAGER (self), NULL);
        return g_hash_table_get_values (self->realms);
}

void
um_realm_join (UmRealmKerberos *realm,
               GBytes *credentials,
               GCancellable *cancellable,
               GAsyncReadyCallback callback,
               gpointer user_data)
{
        GVariant *options;
        GVariant *creds;

        g_return_if_fail (UM_REALM_IS_KERBEROS (realm));
        g_return_if_fail (credentials != NULL);
        g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable));

        creds = g_variant_new_from_data (G_VARIANT_TYPE ("ay"),
                                         g_bytes_get_data (credentials, NULL),
                                         g_bytes_get_size (credentials),
                                         TRUE, (GDestroyNotify)g_bytes_unref, credentials);
        options = g_variant_new_array (G_VARIANT_TYPE ("{sv}"), NULL, 0);

        um_realm_kerberos_call_enroll_with_credential_cache (realm,
                                                             g_variant_ref_sink (creds),
                                                             g_variant_ref_sink (options),
                                                             "",
                                                             cancellable,
                                                             callback,
                                                             user_data);

        g_variant_unref (options);
        g_variant_unref (creds);
}

gboolean
um_realm_join_finish (UmRealmKerberos *self,
                      GAsyncResult *result,
                      GError **error)
{
        GError *call_error = NULL;
        gchar *dbus_error;

        g_return_val_if_fail (UM_REALM_IS_KERBEROS (self), FALSE);
        g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

        um_realm_kerberos_call_enroll_with_credential_cache_finish (self,
                                                                    result,
                                                                    &call_error);
        if (call_error == NULL)
                return TRUE;

        dbus_error = g_dbus_error_get_remote_error (call_error);
        if (dbus_error == NULL) {
                g_propagate_error (error, call_error);
                return FALSE;
        }

        g_dbus_error_strip_remote_error (call_error);

        if (g_str_equal (dbus_error, "org.freedesktop.realmd.Error.AuthFailed")) {
                g_set_error (error, UM_REALM_ERROR, UM_REALM_ERROR_BAD_LOGIN,
                             "%s", call_error->message);
                g_error_free (call_error);
        } else {
                g_propagate_error (error, call_error);
        }

        g_free (dbus_error);
        return FALSE;
}

typedef struct {
        gchar *domain;
        gchar *realm;
        gchar *user;
        gchar *password;
        GBytes *credentials;
} LoginClosure;

static void
login_closure_free (gpointer data)
{
        LoginClosure *login = data;
        g_free (login->domain);
        g_free (login->realm);
        g_free (login->user);
        g_free (login->password);
        g_bytes_unref (login->credentials);
        g_slice_free (LoginClosure, login);
}

static krb5_error_code
login_perform_kinit (krb5_context k5,
                     const gchar *realm,
                     const gchar *login,
                     const gchar *password,
                     const gchar *filename)
{
        krb5_get_init_creds_opt *opts;
        krb5_error_code code;
        krb5_principal principal;
        krb5_ccache ccache;
        krb5_creds creds;
        gchar *name;

        name = g_strdup_printf ("%s@%s", login, realm);
        code = krb5_parse_name (k5, name, &principal);
        g_free (name);

        if (code != 0)
                return code;

        if (filename == NULL)
                code = krb5_cc_default (k5, &ccache);
        else
                code = krb5_cc_resolve (k5, filename, &ccache);

        if (code != 0) {
                krb5_free_principal (k5, principal);
                return code;
        }

        code = krb5_get_init_creds_opt_alloc (k5, &opts);
        g_return_val_if_fail (code == 0, code);

        code = krb5_get_init_creds_opt_set_out_ccache (k5, opts, ccache);
        g_return_val_if_fail (code == 0, code);

        code = krb5_get_init_creds_password (k5, &creds, principal,
                                             (char *)password,
                                             NULL, 0, 0, NULL, opts);

        krb5_get_init_creds_opt_free (k5, opts);
        krb5_cc_close (k5, ccache);
        krb5_free_principal (k5, principal);

        if (code == 0)
                krb5_free_cred_contents (k5, &creds);

        return code;
}

static void
kinit_thread_func (GSimpleAsyncResult *async,
                   GObject *object,
                   GCancellable *cancellable)
{
        LoginClosure *login = g_simple_async_result_get_op_res_gpointer (async);
        krb5_context k5 = NULL;
        krb5_error_code code;
        GError *error = NULL;
        gchar *filename = NULL;
        gchar *contents;
        gsize length;
        gint temp_fd;

        filename = g_build_filename (g_get_user_runtime_dir (),
                                     "um-krb5-creds.XXXXXX", NULL);
        temp_fd = g_mkstemp_full (filename, O_RDWR, S_IRUSR | S_IWUSR);
        if (temp_fd == -1) {
                g_warning ("Couldn't create credential cache file: %s: %s",
                           filename, g_strerror (errno));
                g_free (filename);
                filename = NULL;
        } else {
                close (temp_fd);
        }

        code = krb5_init_context (&k5);
        if (code == 0) {
                code = login_perform_kinit (k5, login->realm, login->user,
                                            login->password, filename);
        }

        switch (code) {
        case 0:
                if (filename != NULL) {
                        g_file_get_contents (filename, &contents, &length, &error);
                        if (error == NULL) {
                                login->credentials = g_bytes_new_take (contents, length);
                        } else {
                                g_warning ("Couldn't read credential cache: %s", error->message);
                                g_error_free (error);
                        }
                }
                break;

        case KRB5KDC_ERR_C_PRINCIPAL_UNKNOWN:
        case KRB5KDC_ERR_CLIENT_REVOKED:
        case KRB5KDC_ERR_KEY_EXP:
        case KRB5KDC_ERR_POLICY:
        case KRB5KDC_ERR_ETYPE_NOSUPP:
                g_simple_async_result_set_error (async, UM_REALM_ERROR, UM_REALM_ERROR_BAD_LOGIN,
                                                 _("Cannot log in as %s at the %s domain"),
                                                 login->user, login->domain);
                break;
        case KRB5KDC_ERR_PREAUTH_FAILED:
                g_simple_async_result_set_error (async, UM_REALM_ERROR, UM_REALM_ERROR_BAD_PASSWORD,
                                                 _("Invalid password, please try again"));
                break;
        default:
                g_simple_async_result_set_error (async, UM_REALM_ERROR, UM_REALM_ERROR_GENERIC,
                                                 _("Couldn't connect to the %s domain: %s"),
                                                 login->domain, krb5_get_error_message (k5, code));
                break;
        }

        if (filename) {
                g_unlink (filename);
                g_free (filename);
        }

        if (k5)
                krb5_free_context (k5);
}

void
um_realm_login (const gchar *realm,
                const gchar *domain,
                const gchar *user,
                const gchar *password,
                GCancellable *cancellable,
                GAsyncReadyCallback callback,
                gpointer user_data)
{
        GSimpleAsyncResult *async;
        LoginClosure *login;

        g_return_if_fail (realm != NULL);
        g_return_if_fail (user != NULL);
        g_return_if_fail (password != NULL);
        g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable));

        async = g_simple_async_result_new (NULL, callback, user_data,
                                           um_realm_login);
        login = g_slice_new0 (LoginClosure);
        login->domain = g_strdup (domain ? domain : realm);
        login->realm = g_strdup (realm);
        login->user = g_strdup (user);
        login->password = g_strdup (password);
        g_simple_async_result_set_op_res_gpointer (async, login, login_closure_free);

        g_simple_async_result_set_handle_cancellation (async, TRUE);
        g_simple_async_result_run_in_thread (async, kinit_thread_func,
                                             G_PRIORITY_DEFAULT, cancellable);

        g_object_unref (async);
}

gboolean
um_realm_login_finish (GAsyncResult *result,
                       GBytes **credentials,
                       GError **error)
{
        GSimpleAsyncResult *async;
        LoginClosure *login;

        g_return_val_if_fail (g_simple_async_result_is_valid (result, NULL,
                              um_realm_login), FALSE);
        g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

        async = G_SIMPLE_ASYNC_RESULT (result);
        if (g_simple_async_result_propagate_error (async, error))
                return FALSE;

        login = g_simple_async_result_get_op_res_gpointer (async);
        if (credentials) {
                if (login->credentials)
                        *credentials = g_bytes_ref (login->credentials);
                else
                        *credentials = NULL;
        }

        return TRUE;
}
