1 Nov 2023
How to authenticate 3-legged token with your desktop applications (Linux)
![Linux user](/sites/default/files/2023-11/Platform-office-2542.jpg)
In previous blogs, we have implemented custom protocols on both Win32 and MacOS. This time, we are going to implement it on Linux with GDBus and GTK3+. I am using Almalinux 8.7. There could be some differences between it and other distros, please check the manual of your targeting distro and make changes accordingly.
To register a protocol for application on Linux, you will need to create a .desktop for your application in /usr/share/applications
[Desktop Entry]
Name=APSShellDemo
Comment=Demo for shell launching with Linux
GenericName=APSShelldemo
Exec=YourPathToExec %U
Type=Application
StartupNotify=true
Categories=Development
MimeType=x-scheme-handler/apsshelldemo
We need to add a %U parameter for our application in Exec option.
The MimeType option will register a protocol for our application. Our protocol is apsshelldemo, so it is x-scheme-handler/apsshelldemo.
After creating it, you may need to update the cache of your desktop system to make it work. Run the following command in the terminal.
sudo update-desktop-database
We'll use GTK+, DBUS, GLIB, GIO, GObject libraries in our sample. Please make sure they are installed on your system.
Let's begin with a simple ui. For AlmaLinux 8, it provides GTK3+. There are some changes in GTK4+, if you are using a newer version of GTK+, please update the code according to the manual.
The sample provided by GTK3+ tutorial is a good start. We'll add a label for displaying the APS client id and update it with 3-legged code after receving it from browser. We'll also create a button that could open a browser for authentication. The client id will be retrieved from APS_CLIENT_ID environment variable, please add it before you are running the application. I usually put them inside etc/profile.d, so I could have environment set after booting up my system.
#include <gtk/gtk.h>
#include <cstdlib>
#include <cstdio>
const char *authAddressFMT = "https://developer.api.autodesk.com/authentication/v2/authorize?response_type=code"
"&client_id=%s"
"&redirect_uri=apsshelldemo://oauth"
"&scope=data:read%%20data:create%%20data:write";
// callback function which is called when button is clicked
static void on_button_clicked(GtkButton *btn, gpointer data) {
// Let's generate the uri.
char buffer[1024];
auto env = std::getenv("APS_CLIENT_ID");
if (NULL == env) {
env = "";
}
std::snprintf(buffer, 1024, authAddressFMT, env);
// Launch our uri in a window.
gtk_show_uri_on_window(NULL, buffer, GDK_CURRENT_TIME, NULL);
}
// callback function which is called when application is first started
static void on_app_activate(GApplication *app, gpointer data) {
// create a new application window for the application
// GtkApplication is sub-class of GApplication
// downcast GApplication* to GtkApplication* with GTK_APPLICATION() macro
GtkWidget *window = gtk_application_window_new(GTK_APPLICATION(app));
GtkWidget *vbox = gtk_box_new(GtkOrientation::GTK_ORIENTATION_VERTICAL, 5);
gtk_widget_set_margin_start(vbox,5);
gtk_widget_set_margin_end(vbox,5);
gtk_widget_set_margin_top(vbox,5);
gtk_widget_set_margin_bottom(vbox,5);
// Check if APS_CLIENT_ID exists
// We can add it through user profile or ide environment.
// If we want to add it system-wide, we can add it to /etc/environment
char buffer[256];
auto env = getenv("APS_CLIENT_ID");
if (NULL == env){
std::sprintf(buffer,"You need to set APS_CLIENT_ID in your environment variables.");
}
else{
std::sprintf(buffer,"CLIENT ID:%s", env);
}
// Create UI
GtkWidget *label = gtk_label_new(buffer);
GtkWidget *btn = gtk_button_new_with_label("Do Auth!");
// connect the event-handler for "clicked" signal of button
g_signal_connect(btn, "clicked", G_CALLBACK(on_button_clicked), NULL);
// add the button to the window
gtk_container_add(GTK_CONTAINER(vbox), btn);
gtk_container_add(GTK_CONTAINER(vbox), label);
gtk_container_add(GTK_CONTAINER(window), vbox);
// display the window
gtk_widget_show_all(GTK_WIDGET(window));
}
int main(int argc, char *argv[]) {
// create new GtkApplication with an unique application ID
GtkApplication *app = gtk_application_new(
"DAS.APSShellDemo",
G_APPLICATION_FLAGS_NONE
);
// connect the event-handler for "activate" signal of GApplication
// G_CALLBACK() macro is used to cast the callback function pointer
// to generic void pointer
g_signal_connect(app, "activate", G_CALLBACK(on_app_activate), NULL);
// start the application, terminate by closing the window
// GtkApplication* is upcast to GApplication* with G_APPLICATION() macro
int status = g_application_run(G_APPLICATION(app), 0, 0);
// deallocate the application object
g_object_unref(app);
return status;
}
The result:
Now we have a basic window. Let's figure out how to make it a single instance app.
On linux, the easiest way to make your program single instance is to use file lock. We'll try to acquire a file lock when the program is started. If we failto acquire the lock, we'll send the url through D-Bus and exit.
#include <fcntl.h>
#include <unistd.h>
...
int main(int argc, char *argv[]) {
// We are using file lock here, alternatives could be socket or a semaphore
int fd = open ("/tmp/apsshelldemo.pid", O_CREAT | O_RDWR, 0666);
if(lockf(fd, F_TLOCK, 0) == 0) {
// create new GtkApplication with an unique application ID
GtkApplication *app = gtk_application_new(
"DAS.APSShellDemo",
G_APPLICATION_FLAGS_NONE
);
...
lockf(fd, F_ULOCK, 0);
close(fd);
return status;
}
else
{
//TODO: Implement D-Bus client.
return 0;
}
}
Next is the IPC part. Like other operating systems, there are several ways to send messages between processes. We'll use D-Bus here. It is widely used and easily to implement (sort of). The naive D-Bus library isn't recommended to use directly as the document suggested
If you use this low-level API directly, you're signing up for some pain.
As an alternative, There are several libraries for using D-Bus(e.g., QDBus, GDBus…). We are going to use GDBus provided by GIO here to make things simpler.
Let's begin with some consts and structs.
#include <gio/gio.h>
...
// Some consts for our well-known D-Bus service
const char *dbus_object_path = "/das/apsshelldemo/object";
const char *dbus_interface_name = "das.apsshelldemo";
const char *dbus_method_name = "doOAuth";
const char *dbus_well_known_name = "das.apshelldemo.dbusserver";
// D-Bus ids.
static guint registration_id;
static guint owner_id;
// Accessing GTK items in the dbus callback
typedef struct {
GApplication *app;
GtkLabel *label;
}UserData;
...
To publish and call a service, you'll need to provide introspection data in XML format for D-Bus. Let's add it after the struct we've defined above.
...
// We'll generate GDBusNodeInfo from XML and store it.
static GDBusNodeInfo *introspection_data = NULL;
// This XML defines our method interface for clients calling
static const gchar introspection_xml[] =
"<node>"
"<interface name='das.apsshelldemo'>"
"<method name='doOAuth'>"
"<arg name='message' type='s' direction='in'/>"
"</method>"
"</interface>"
"</node>";
...
Next, we'll create our function for processing a remote call and add it to a GDBusInterfaceVTable struct. This struct will be used for registering D-Bus service later on. Let's continue with the end of previous XML const.
...
// This is the method for handling dbus method call.
static void handle_method_call (GDBusConnection *connection,
const gchar *sender,
const gchar *object_path,
const gchar *interface_name,
const gchar *method_name,
GVariant *parameters,
GDBusMethodInvocation *invocation,
gpointer user_data) {
// Check which method is calling. We only have one method here.
if (g_strcmp0(method_name, dbus_method_name) == 0)
{
const gchar *message;
g_variant_get (parameters, "(&s)", &message);
if (g_strcmp0 (message, "Return Unregistered") == 0 || g_strcmp0(message, "Return Registered") == 0 ||
g_strcmp0(message, "Return Raw") ==0)
{
// Error, do nothing
}else{
// Unwrap our user data and set the label.
GtkLabel *label = ((UserData *)user_data)->label;
gtk_label_set_text(label, message);
}
}
// Return NULL to allow client continue.
g_dbus_method_invocation_return_value(invocation, NULL);
}
// We only want to handle method calling here
static const GDBusInterfaceVTable interface_vtable =
{
handle_method_call,
NULL,
NULL,
{ 0 }
};
...
Now we have set up the remote interface we are going to create. We'll add some callbacks for setup and cleanup the D-Bus. Let's continue.
...
// Free user data
static void user_data_free(gpointer data){
delete data;
}
// Register D-Bus object after acquring the bus.
static void on_bus_acquired (GDBusConnection *connection,
const gchar *name,
gpointer user_data)
{
registration_id = g_dbus_connection_register_object (connection,
dbus_object_path,
introspection_data->interfaces[0],
&interface_vtable,
user_data, /* user_data */
user_data_free, /* user_data_free_func /
NULL); /* GError** */
}
static void on_name_acquired (GDBusConnection *connection,
const gchar *name,
gpointer user_data)
{
}
// We would like to close our application when name is lost.
static void on_name_lost (GDBusConnection *connection,
const gchar *name,
gpointer user_data)
{
UserData *data = (UserData*)user_data;
// exit
g_application_quit(data->app);
}
...
Let's initialize our D-Bus in the activation of our main window.
...
static void on_app_activate(GApplication *app, gpointer data) {
...
gtk_container_add(GTK_CONTAINER(window), vbox);
// Let's setup our D-Bus through GDBus
// Using XML to create our introspection is easier.
introspection_data = g_dbus_node_info_new_for_xml (introspection_xml, NULL);
// We'll wrap our GTK widgets in user data and use them in the D-Bus callback.
UserData *user_data = new UserData();
user_data->app = app;
user_data->label = (GtkLabel*)label;
// Let's connect to D-Bus!
// We are using the bus of login session.
owner_id = g_bus_own_name(G_BUS_TYPE_SESSION,
dbus_well_known_name,
G_BUS_NAME_OWNER_FLAGS_NONE,
on_bus_acquired,
on_name_acquired,
on_name_lost,
user_data,
user_data_free);
// display the window
gtk_widget_show_all(GTK_WIDGET(window));
}
Finally, let's update our program entry. Because D-Bus is initialized after the UI is activated. We only need to cleanup in the main process.
...
// Deallocate the application object
g_object_unref(app);
g_dbus_connection_unregister_object(g_bus_get_sync(G_BUS_TYPE_SESSION,NULL,NULL), registration_id);
g_bus_unown_name(owner_id);
// Unlock our pid file.
lockf(fd, F_ULOCK, 0);
...
For the client, we'll need to call the doOAuth function through D-Bus.
...
else{
// Check if there is an input for sending through dbus
if (argc < 2) {
return 0;
}
// We already have a window running
// Send a signal through D-Bus
// Connect to the D-Bus login session bus
GError *error = NULL;
GDBusConnection *connection;
connection = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, &error);
if (error) {
g_print("Failed to connect to the D-Bus: %s\n", error->message);
g_error_free(error);
return 1;
}
// Create a new proxy object for the server's D-Bus interface
GDBusProxy *proxy;
proxy = g_dbus_proxy_new_sync(connection,
G_DBUS_PROXY_FLAGS_NONE,
NULL,
dbus_well_known_name,
dbus_object_path,
dbus_interface_name,
NULL,
&error);
if (error) {
g_print("Failed to create D-Bus proxy: %s\n", error->message);
g_error_free(error);
return 1;
}
// Call the oauth method on the server
g_dbus_proxy_call_sync(proxy,
dbus_method_name,
g_variant_new("(s)", argv[1]),
G_DBUS_CALL_FLAGS_NONE,
-1,
NULL,
&error);
if (error) {
g_print("Failed to call D-Bus method: %s\n", error->message);
g_error_free(error);
return 1;
}
// Clean up and exit
g_object_unref(proxy);
g_object_unref(connection);
return 0;
}
}
...
Ok, we are done. Here is the result:
Please check out the sample on Github.