diff --git a/data/meson.build b/data/meson.build
index 41db2e04..11571c9e 100644
--- a/data/meson.build
+++ b/data/meson.build
@@ -3,6 +3,13 @@
 # SPDX-License-Identifier: GPL-3.0-or-later
 # SPDX-FileCopyrightText: Michael Terry
 
+google_client_id_parts = get_option('google_client_id').split('.')
+google_client_id_parts_reversed = []
+foreach part : google_client_id_parts
+    google_client_id_parts_reversed = [part] + google_client_id_parts_reversed
+endforeach
+google_reversedns = '.'.join(google_client_id_parts_reversed)
+
 conf_data = configuration_data()
 conf_data.set('appid', application_id)
 conf_data.set('bindir', bindir)
@@ -11,6 +18,7 @@ conf_data.set('gsettingspath', profile == '' ? 'deja-dup' : 'deja-dup-' + profil
 conf_data.set('icon', application_id)
 conf_data.set('pkglibexecdir', pkglibexecdir)
 conf_data.set('profile', profile)
+conf_data.set('scheme_google', google_reversedns)
 conf_data.set('version', meson.project_version())
 
 install_data(join_paths('icons', '@0@.svg'.format(application_id)),
diff --git a/data/org.gnome.DejaDup.desktop.in b/data/org.gnome.DejaDup.desktop.in
index afa30e4a..5784ccef 100644
--- a/data/org.gnome.DejaDup.desktop.in
+++ b/data/org.gnome.DejaDup.desktop.in
@@ -7,7 +7,7 @@ Comment=Change your backup settings
 
 Icon=@icon@
 
-Exec=deja-dup
+Exec=deja-dup %u
 
 StartupNotify=true
 DBusActivatable=true
@@ -15,6 +15,9 @@ DBusActivatable=true
 Type=Application
 Categories=Utility;Archiving;GNOME;GTK;X-GNOME-Utilities;
 
+# Used for oauth flows (server redirects to this custom scheme and we catch it)
+MimeType=x-scheme-handler/@scheme_google@;
+
 # Translators: Add whatever keywords you want in your language, separated by semicolons
 # These keywords are used when searching for applications in dashes, etc.
 Keywords=déjà;deja;dup;
diff --git a/data/post-install.sh b/data/post-install.sh
index 2de522d8..7648584b 100644
--- a/data/post-install.sh
+++ b/data/post-install.sh
@@ -12,4 +12,7 @@ if [ -z "$DESTDIR" ]; then
 
   echo "Updating gsettings cache..."
   glib-compile-schemas "$datadir/glib-2.0/schemas"
+
+  echo "Updating desktop mime cache..."
+  update-desktop-database -q "$datadir/applications"
 fi
diff --git a/deja-dup/AssistantOperation.vala b/deja-dup/AssistantOperation.vala
index 08e77654..543c6c89 100644
--- a/deja-dup/AssistantOperation.vala
+++ b/deja-dup/AssistantOperation.vala
@@ -103,6 +103,11 @@ public abstract class AssistantOperation : Assistant
     delete_event.connect(do_minimize_to_tray);
   }
 
+  public DejaDup.Backend? get_backend()
+  {
+    return op == null ? null : op.backend;
+  }
+
   /*
    * Creates confirmation page for particular assistant
    *
diff --git a/deja-dup/main.vala b/deja-dup/main.vala
index 93ef0adb..0bceba80 100644
--- a/deja-dup/main.vala
+++ b/deja-dup/main.vala
@@ -43,8 +43,14 @@ public class DejaDupApp : Gtk.Application
 
   public DejaDupApp()
   {
-    Object(application_id: Config.APPLICATION_ID,
-           flags: ApplicationFlags.HANDLES_COMMAND_LINE);
+    Object(
+      application_id: Config.APPLICATION_ID,
+      flags: ApplicationFlags.HANDLES_COMMAND_LINE |
+             // HANDLES_OPEN is required to support Open calls over dbus, which
+             // we use for our registered custom schemes (which support our
+             // oauth2 workflow).
+             ApplicationFlags.HANDLES_OPEN
+    );
     add_main_option_entries(OPTIONS);
   }
 
@@ -61,10 +67,11 @@ public class DejaDupApp : Gtk.Application
   {
     var options = command_line.get_options_dict();
 
-    string[] filenames = {};
+    File[] files = {};
     if (options.contains("")) {
       var variant = options.lookup_value("", VariantType.BYTESTRING_ARRAY);
-      filenames = variant.get_bytestring_array();
+      foreach (var filename in variant.get_bytestring_array())
+        files += command_line.create_file_for_arg(filename);
     }
 
     if (options.contains("restore")) {
@@ -74,10 +81,9 @@ public class DejaDupApp : Gtk.Application
       }
 
       List<File> file_list = new List<File>();
-      if (filenames.length > 0) {
-        int i = 0;
-        while (filenames[i] != null)
-          file_list.append(command_line.create_file_for_arg(filenames[i++]));
+      if (files.length > 0) {
+        foreach (var file in files)
+          file_list.append(file);
       }
 
       restore_full(file_list);
@@ -88,16 +94,16 @@ public class DejaDupApp : Gtk.Application
         return 1;
       }
 
-      if (filenames.length == 0) {
+      if (files.length == 0) {
         command_line.printerr("%s\n", _("No directory provided"));
         return 1;
       }
-      else if (filenames.length > 1) {
+      else if (files.length > 1) {
         command_line.printerr("%s\n", _("Only one directory can be shown at once"));
         return 1;
       }
 
-      File list_directory = command_line.create_file_for_arg(filenames[0]);
+      File list_directory = files[0];
       if (!list_directory.query_exists(null)) {
         command_line.printerr("%s\n", _("Directory does not exist"));
         return 1;
@@ -123,6 +129,14 @@ public class DejaDupApp : Gtk.Application
     }
     else if (options.contains("prompt")) {
       prompt(this);
+    }
+    else if (files.length > 0) {
+      // If we were called without a mode (like --restore) but with file arguments,
+      // let's do our "Open" action (which is mostly used for our oauth flow).
+      // That oauth flow can happen via command line in some environments like
+      // snaps, whereas the dbus Open call might happen for flatpaks. Regardless
+      // of how they come in, treat them the same.
+      open(files, "");
     } else {
       activate();
     }
@@ -182,6 +196,28 @@ public class DejaDupApp : Gtk.Application
     }
   }
 
+  public override void open(GLib.File[] files, string hint)
+  {
+    var google_backend = this.op == null ? null : this.op.get_backend() as DejaDup.BackendGoogle;
+
+    // We might be in middle of oauth flow, and are given an expected redirect uri
+    // like 'com.googleusercontent.apps.123:/oauth2redirect?code=xxx'
+    if (files.length == 1 && google_backend != null)
+    {
+      var provided_uri = files[0].get_uri();
+      // Normalize backend URI through gio, so it matches incoming URI format (slashes after colon, etc)
+      var expected_uri = File.new_for_uri(google_backend.get_redirect_uri()).get_uri();
+      if (provided_uri.has_prefix(expected_uri) && google_backend.continue_authorization(provided_uri)) {
+        activate();
+        return;
+      }
+    }
+
+    // Got passed files, but we don't know what to do with them.
+    foreach (var file in files)
+      warning("Ignoring unexpected file: %s", file.get_parse_name());
+  }
+
   bool exit_cleanly()
   {
     quit();
diff --git a/libdeja/BackendGoogle.vala b/libdeja/BackendGoogle.vala
index 3f79da40..047ddcb1 100644
--- a/libdeja/BackendGoogle.vala
+++ b/libdeja/BackendGoogle.vala
@@ -23,9 +23,7 @@ public const string GOOGLE_SERVER = "google.com";
 
 public class BackendGoogle : Backend
 {
-  Soup.Server server;
   Soup.Session session;
-  string local_address;
   string pkce;
   string credentials_dir;
   string access_token;
@@ -443,7 +441,7 @@ public class BackendGoogle : Backend
       "GET",
       "https://accounts.google.com/o/oauth2/v2/auth",
       "client_id", Config.GOOGLE_CLIENT_ID,
-      "redirect_uri", local_address,
+      "redirect_uri", get_redirect_uri(),
       "response_type", "code",
       "code_challenge", pkce,
       "scope", "https://www.googleapis.com/auth/drive.file"
@@ -457,7 +455,7 @@ public class BackendGoogle : Backend
       "POST",
       "https://www.googleapis.com/oauth2/v4/token",
       "client_id", Config.GOOGLE_CLIENT_ID,
-      "redirect_uri", local_address,
+      "redirect_uri", get_redirect_uri(),
       "grant_type", "authorization_code",
       "code_verifier", pkce,
       "code", code
@@ -477,49 +475,12 @@ public class BackendGoogle : Backend
     yield get_tokens(message);
   }
 
-  void oauth_server_request_received(Soup.Server server, Soup.Message message,
-                                     string path,
-                                     HashTable<string, string>? query,
-                                     Soup.ClientContext client)
-  {
-    if (path != "/") {
-      message.status_code = Soup.Status.NOT_FOUND;
-      return;
-    }
-
-    message.status_code = Soup.Status.ACCEPTED;
-    server = null;
-
-    string? error = query == null ? null : query.lookup("error");
-    if (error != null) {
-      stop_login(error);
-      return;
-    }
-
-    string? code = query == null ? null : query.lookup("code");
-    if (code == null) {
-      stop_login(null);
-      return;
-    }
-
-    // Show consent granted screen
-    var html = DejaDup.get_access_granted_html();
-    message.response_body.append_take(html.data);
-
-    show_oauth_consent_page(null, null); // continue on from paused screen
-    get_credentials.begin(code);
-  }
-
   void start_authorization() throws Error
   {
-    // Start a server and listen on it
-    server = new Soup.Server("server-header",
-                             "%s/%s ".printf(Config.PACKAGE, Config.VERSION));
-    server.listen_local(0, Soup.ServerListenOptions.IPV4_ONLY);
-    local_address = server.get_uris().data.to_string(false);
-
     // Prepare to handle requests that finish the consent process
-    server.add_handler(null, oauth_server_request_received);
+    error_msg = null;
+    code = null;
+    authorization_instance.set(this);
 
     // We need a random string between 43 and 128 chars. UUIDs are an easy way
     // to get random strings, but they are only 37 chars long. So just add two.
@@ -589,6 +550,53 @@ public class BackendGoogle : Backend
     envp.append("GOOGLE_DRIVE_SETTINGS=%s/settings.yaml".printf(credentials_dir));
     envp_ready(true, envp);
   }
+
+  static WeakRef authorization_instance;
+  string error_msg;
+  string code;
+
+  public string get_redirect_uri()
+  {
+    var id_parts = Config.GOOGLE_CLIENT_ID.split(".");
+    string[] reversed = {};
+    for (int i = id_parts.length - 1; i >= 0; i--) {
+      reversed += id_parts[i];
+    }
+    // Exact path does not matter and is optional. But it seems wise to use some
+    // path and this one seems reasonable (and is the example in their oauth docs).
+    return "%s:/oauth2redirect".printf(string.joinv(".", reversed));
+  }
+
+  // Meant to be called externally when the oauth service redirects back to us.
+  // This passed-in uri will hold the code and error values from the service.
+  public bool continue_authorization(string command_line_redirect_uri)
+  {
+    if (authorization_instance.get() == null)
+      return false; // no auth in progress
+
+    var active_backend = (BackendGoogle)authorization_instance.get();
+    active_backend.continue_authorization_helper(command_line_redirect_uri);
+    return true;
+  }
+
+  void continue_authorization_helper(string command_line_redirect_uri)
+  {
+    var uri = new Soup.URI(command_line_redirect_uri);
+    var query = Soup.Form.decode(uri.query);
+
+    if (error_msg == null && query != null)
+      error_msg = query.lookup("error");
+
+    if (error_msg == null && query != null)
+      code = query.lookup("code");
+
+    if (error_msg == null && code == null)
+      error_msg = ""; // default to just a blank contextual error message
+
+    authorization_instance.set(null);
+    show_oauth_consent_page(null, null); // continue on from paused screen
+    get_credentials.begin(code);
+  }
 }
 
 } // end namespace
