/* bz-auth-state.c * * Copyright 2025 Alexander Vanhee * * 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, see . * * SPDX-License-Identifier: GPL-3.0-or-later */ #include #include "bz-async-texture.h" #include "bz-auth-state.h" #define SECRET_SCHEMA_NAME "io.github.kolunmi.Bazaar.FlathubAuth" #define SECRET_LABEL "Flathub Authentication" struct _BzAuthState { GObject parent_instance; char *name; char *token; char *profile_icon_url; GDateTime *token_expires; BzAsyncTexture *paintable; gboolean loading; guint expiration_timeout_id; }; G_DEFINE_FINAL_TYPE (BzAuthState, bz_auth_state, G_TYPE_OBJECT) enum { PROP_0, PROP_NAME, PROP_TOKEN, PROP_PROFILE_ICON_URL, PROP_AUTHENTICATED, PROP_PAINTABLE, N_PROPS }; static GParamSpec *properties[N_PROPS]; static const SecretSchema * get_secret_schema (void) { static const SecretSchema schema = { SECRET_SCHEMA_NAME, SECRET_SCHEMA_NONE, { { "service", SECRET_SCHEMA_ATTRIBUTE_STRING }, { "NULL", 0 }, } }; return &schema; } static gboolean on_token_expired (gpointer user_data) { BzAuthState *self = BZ_AUTH_STATE (user_data); self->expiration_timeout_id = 0; bz_auth_state_clear (self); return G_SOURCE_REMOVE; } static void schedule_token_expiration (BzAuthState *self) { GDateTime *now; gint64 seconds_until_expiration; if (self->expiration_timeout_id != 0) g_source_remove (self->expiration_timeout_id); self->expiration_timeout_id = 0; if (self->token_expires == NULL) return; now = g_date_time_new_now_utc (); seconds_until_expiration = g_date_time_difference (self->token_expires, now) / G_TIME_SPAN_SECOND; g_date_time_unref (now); if (seconds_until_expiration <= 0) { bz_auth_state_clear (self); return; } if (seconds_until_expiration > G_MAXUINT / 1000) self->expiration_timeout_id = g_timeout_add_seconds (G_MAXUINT / 1000, on_token_expired, self); else self->expiration_timeout_id = g_timeout_add_seconds (seconds_until_expiration, on_token_expired, self); } static void save_to_secrets (BzAuthState *self) { GHashTable *attributes; GError *error = NULL; g_autoptr (GVariantBuilder) builder = NULL; g_autoptr (GVariant) variant = NULL; g_autofree char *serialized = NULL; if (self->loading) return; attributes = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); g_hash_table_insert (attributes, g_strdup ("service"), g_strdup ("flathub")); builder = g_variant_builder_new (G_VARIANT_TYPE_VARDICT); if (self->name != NULL) g_variant_builder_add (builder, "{sv}", "name", g_variant_new_string (self->name)); if (self->token != NULL) g_variant_builder_add (builder, "{sv}", "token", g_variant_new_string (self->token)); if (self->token_expires != NULL) { g_autofree char *expires = g_date_time_format_iso8601 (self->token_expires); g_variant_builder_add (builder, "{sv}", "token-expires", g_variant_new_string (expires)); } if (self->profile_icon_url != NULL) g_variant_builder_add (builder, "{sv}", "profile-icon-url", g_variant_new_string (self->profile_icon_url)); variant = g_variant_builder_end (builder); serialized = g_variant_print (variant, FALSE); secret_password_storev_sync ( get_secret_schema (), attributes, NULL, SECRET_LABEL, serialized, NULL, &error); g_hash_table_unref (attributes); if (error != NULL) { g_warning ("Failed to save authentication to secrets: %s", error->message); g_error_free (error); } } static void load_from_secrets (BzAuthState *self) { GHashTable *attributes; GError *error = NULL; g_autofree char *secret = NULL; attributes = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); g_hash_table_insert (attributes, g_strdup ("service"), g_strdup ("flathub")); secret = secret_password_lookupv_sync ( get_secret_schema (), attributes, NULL, &error); g_hash_table_unref (attributes); if (error != NULL) { if (!g_error_matches (error, SECRET_ERROR, SECRET_ERROR_NO_SUCH_OBJECT)) g_warning ("Failed to load authentication from secrets: %s", error->message); g_error_free (error); return; } if (secret != NULL) { g_autoptr (GVariant) variant = NULL; g_autoptr (GVariantIter) iter = NULL; variant = g_variant_parse (G_VARIANT_TYPE_VARDICT, secret, NULL, NULL, &error); if (error != NULL) { g_warning ("Failed to parse secret: %s", error->message); g_error_free (error); return; } if (variant != NULL) { iter = g_variant_iter_new (variant); for (;;) { g_autofree char *key = NULL; g_autoptr (GVariant) value = NULL; if (!g_variant_iter_next (iter, "{sv}", &key, &value)) break; if (g_strcmp0 (key, "name") == 0) { g_clear_pointer (&self->name, g_free); self->name = g_variant_dup_string (value, NULL); } else if (g_strcmp0 (key, "token") == 0) { g_clear_pointer (&self->token, g_free); self->token = g_variant_dup_string (value, NULL); } else if (g_strcmp0 (key, "token-expires") == 0) { g_autoptr (GDateTime) dt = g_date_time_new_from_iso8601 (g_variant_get_string (value, NULL), NULL); if (dt != NULL) { g_clear_pointer (&self->token_expires, g_date_time_unref); self->token_expires = g_steal_pointer (&dt); } } else if (g_strcmp0 (key, "profile-icon-url") == 0) { g_clear_pointer (&self->profile_icon_url, g_free); self->profile_icon_url = g_variant_dup_string (value, NULL); g_clear_object (&self->paintable); if (self->profile_icon_url != NULL && self->profile_icon_url[0] != '\0') { g_autoptr (GFile) file = g_file_new_for_uri (self->profile_icon_url); self->paintable = bz_async_texture_new (file, NULL); } } } } } if (self->token_expires != NULL) { GDateTime *now = g_date_time_new_now_utc (); if (g_date_time_compare (now, self->token_expires) >= 0) { g_date_time_unref (now); bz_auth_state_clear (self); return; } g_date_time_unref (now); schedule_token_expiration (self); } } static void clear_secrets (BzAuthState *self) { gboolean result = FALSE; g_autoptr (GError) local_error = NULL; g_autoptr (GHashTable) attributes = NULL; attributes = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); g_hash_table_replace (attributes, g_strdup ("service"), g_strdup ("flathub")); result = secret_password_clearv_sync ( get_secret_schema (), attributes, NULL, &local_error); if (!result) g_warning ("Failed to clear auth values from secrets: %s", local_error->message); } static void bz_auth_state_dispose (GObject *object) { BzAuthState *self = BZ_AUTH_STATE (object); g_clear_handle_id (&self->expiration_timeout_id, g_source_remove); g_clear_pointer (&self->name, g_free); g_clear_pointer (&self->token, g_free); g_clear_pointer (&self->profile_icon_url, g_free); g_clear_pointer (&self->token_expires, g_date_time_unref); g_clear_object (&self->paintable); G_OBJECT_CLASS (bz_auth_state_parent_class)->dispose (object); } static void bz_auth_state_finalize (GObject *object) { BzAuthState *self = BZ_AUTH_STATE (object); g_clear_pointer (&self->name, g_free); g_clear_pointer (&self->token, g_free); g_clear_pointer (&self->profile_icon_url, g_free); g_clear_pointer (&self->token_expires, g_date_time_unref); G_OBJECT_CLASS (bz_auth_state_parent_class)->finalize (object); } static void bz_auth_state_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { BzAuthState *self = BZ_AUTH_STATE (object); switch (prop_id) { case PROP_NAME: g_value_set_string (value, self->name); break; case PROP_TOKEN: g_value_set_string (value, self->token); break; case PROP_PROFILE_ICON_URL: g_value_set_string (value, self->profile_icon_url); break; case PROP_AUTHENTICATED: g_value_set_boolean (value, bz_auth_state_is_authenticated (self)); break; case PROP_PAINTABLE: g_value_set_object (value, bz_auth_state_get_paintable (self)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); } } static void bz_auth_state_class_init (BzAuthStateClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->dispose = bz_auth_state_dispose; object_class->finalize = bz_auth_state_finalize; object_class->get_property = bz_auth_state_get_property; properties[PROP_NAME] = g_param_spec_string ( "name", NULL, NULL, NULL, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); properties[PROP_TOKEN] = g_param_spec_string ( "token", NULL, NULL, NULL, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); properties[PROP_PROFILE_ICON_URL] = g_param_spec_string ( "profile-icon-url", NULL, NULL, NULL, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); properties[PROP_AUTHENTICATED] = g_param_spec_boolean ( "authenticated", NULL, NULL, FALSE, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); properties[PROP_PAINTABLE] = g_param_spec_object ( "paintable", NULL, NULL, GDK_TYPE_PAINTABLE, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); g_object_class_install_properties (object_class, N_PROPS, properties); } static void bz_auth_state_init (BzAuthState *self) { self->loading = TRUE; load_from_secrets (self); self->loading = FALSE; } BzAuthState * bz_auth_state_new (void) { return g_object_new (BZ_TYPE_AUTH_STATE, NULL); } const char * bz_auth_state_get_name (BzAuthState *self) { g_return_val_if_fail (BZ_IS_AUTH_STATE (self), NULL); return self->name; } const char * bz_auth_state_get_token (BzAuthState *self) { g_return_val_if_fail (BZ_IS_AUTH_STATE (self), NULL); return self->token; } const char * bz_auth_state_get_profile_icon_url (BzAuthState *self) { g_return_val_if_fail (BZ_IS_AUTH_STATE (self), NULL); return self->profile_icon_url; } gboolean bz_auth_state_is_authenticated (BzAuthState *self) { g_return_val_if_fail (BZ_IS_AUTH_STATE (self), FALSE); return self->token != NULL && self->token[0] != '\0'; } GdkPaintable * bz_auth_state_get_paintable (BzAuthState *self) { g_return_val_if_fail (BZ_IS_AUTH_STATE (self), NULL); return (GdkPaintable *) self->paintable; } void bz_auth_state_set_authenticated (BzAuthState *self, const char *name, const char *token, GDateTime *token_expires, const char *profile_icon_url) { gboolean was_authenticated = FALSE; gboolean name_changed = FALSE; gboolean token_changed = FALSE; gboolean icon_changed = FALSE; g_return_if_fail (BZ_IS_AUTH_STATE (self)); was_authenticated = bz_auth_state_is_authenticated (self); if (g_strcmp0 (self->name, name) != 0) { g_clear_pointer (&self->name, g_free); self->name = g_strdup (name); name_changed = TRUE; } if (g_strcmp0 (self->token, token) != 0) { g_clear_pointer (&self->token, g_free); self->token = g_strdup (token); token_changed = TRUE; } g_clear_pointer (&self->token_expires, g_date_time_unref); if (token_expires != NULL) self->token_expires = g_date_time_ref (token_expires); if (g_strcmp0 (self->profile_icon_url, profile_icon_url) != 0) { g_clear_pointer (&self->profile_icon_url, g_free); self->profile_icon_url = g_strdup (profile_icon_url); icon_changed = TRUE; g_clear_object (&self->paintable); if (profile_icon_url != NULL && profile_icon_url[0] != '\0') { g_autoptr (GFile) file = g_file_new_for_uri (profile_icon_url); self->paintable = bz_async_texture_new (file, NULL); } g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_PAINTABLE]); } if (name_changed) g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_NAME]); if (token_changed) g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_TOKEN]); if (icon_changed) g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_PROFILE_ICON_URL]); if (!!was_authenticated != !!bz_auth_state_is_authenticated (self)) g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_AUTHENTICATED]); schedule_token_expiration (self); save_to_secrets (self); } void bz_auth_state_clear (BzAuthState *self) { g_return_if_fail (BZ_IS_AUTH_STATE (self)); g_clear_handle_id (&self->expiration_timeout_id, g_source_remove); clear_secrets (self); bz_auth_state_set_authenticated (self, NULL, NULL, NULL, NULL); }