Quellcode durchsuchen

fix: allow re-authorize on auth error

Gerald vor 3 Wochen
Ursprung
Commit
622f4d3c52

+ 13 - 7
src/background/sync/base.js

@@ -252,9 +252,10 @@ export const BaseService = serviceFactory({
   setUserConfig: noop,
   requestAuth: noop,
   authorize: noop,
-  finishAuth: noop,
   revoke: noop,
   authorized: noop,
+  matchAuth: noop,
+  finishAuth: noop,
   metaError: noop,
   hasAuth() {
     const token = this.config.get('token');
@@ -324,17 +325,23 @@ export const BaseService = serviceFactory({
       status: SYNC_UNAUTHORIZED,
     });
   },
-  async checkAuth(url) {
+  async handleAuth(payload) {
     setSyncState({ status: SYNC_AUTHORIZING });
-    let result;
     try {
-      result = await this.finishAuth(url);
+      await this.finishAuth(payload);
     } catch (err) {
       setSyncState({ status: SYNC_ERROR_AUTH });
       throw err;
     }
-    setSyncState({ status: result ? SYNC_AUTHORIZED : SYNC_UNAUTHORIZED });
-    return result;
+    setSyncState({ status: SYNC_AUTHORIZED });
+    autoSync();
+  },
+  checkAuth(url) {
+    const payload = this.matchAuth(url);
+    if (payload) {
+      this.handleAuth(payload);
+      return true;
+    }
   },
   getMeta() {
     return this.get({ name: this.metaFile })
@@ -675,7 +682,6 @@ export async function openAuthPage(url, redirectUri) {
       browser.tabs.remove(tabId);
       // If we unregister without setTimeout, API will ignore { cancel: true }
       setTimeout(unregister, 0);
-      autoSync();
       return { cancel: true };
     }
   };

+ 8 - 3
src/background/sync/dropbox.js

@@ -135,20 +135,25 @@ const Dropbox = BaseService.extend({
       refresh_token: data.refresh_token || params.refresh_token,
     });
   },
-  async finishAuth(url) {
+  matchAuth(url) {
     const redirectUri = `${config.redirect_uri}?`;
     if (!url.startsWith(redirectUri)) return;
     const query = loadQuery(url.slice(redirectUri.length));
     const { state, codeVerifier } = this.session || {};
     this.session = null;
     if (query.state !== state || !query.code) return;
-    await this.authorized({
+    return {
       code: query.code,
       code_verifier: codeVerifier,
+    };
+  },
+  async finishAuth(payload) {
+    await this.authorized({
+      code: payload.code,
+      code_verifier: payload.code_verifier,
       grant_type: 'authorization_code',
       redirect_uri: config.redirect_uri,
     });
-    return true;
   },
   revoke() {
     this.config.set({

+ 12 - 4
src/background/sync/googledrive.js

@@ -1,6 +1,10 @@
 // Reference:
 // - https://developers.google.com/identity/protocols/oauth2/native-app
 // - https://developers.google.com/drive/v3/reference/files
+//
+// Note:
+// - Use a native app approach for longer authorization periods,
+// - Web app refresh tokens have short expiration and require frequent user reauthorization.
 import { dumpQuery, getUniqId, loadQuery } from '@/common';
 import { CHARSET_UTF8, FORM_URLENCODED } from '@/common/consts';
 import { objectGet } from '@/common/object';
@@ -21,7 +25,6 @@ import {
 const config = {
   client_id: process.env.SYNC_GOOGLE_DESKTOP_ID,
   client_secret: process.env.SYNC_GOOGLE_DESKTOP_SECRET,
-  // We use native app approach with code challenge for better security.
   // Google OAuth for native app only allows loopback IP address for callback URL.
   // The URL will be intercepted and blocked so the port doesn't matter.
   redirect_uri: 'http://127.0.0.1:45678/',
@@ -116,20 +119,25 @@ const GoogleDrive = BaseService.extend({
     )}`;
     openAuthPage(url, config.redirect_uri);
   },
-  async finishAuth(url) {
+  matchAuth(url) {
     const redirectUri = `${config.redirect_uri}?`;
     if (!url.startsWith(redirectUri)) return;
     const query = loadQuery(url.slice(redirectUri.length));
     const { state, codeVerifier } = this.session || {};
     this.session = null;
     if (query.state !== state || !query.code) return;
-    await this.authorized({
+    return {
       code: query.code,
       code_verifier: codeVerifier,
+    };
+  },
+  async finishAuth(payload) {
+    await this.authorized({
+      code: payload.code,
+      code_verifier: payload.code_verifier,
       grant_type: 'authorization_code',
       redirect_uri: config.redirect_uri,
     });
-    return true;
   },
   revoke() {
     this.config.set({

+ 17 - 10
src/background/sync/onedrive.js

@@ -1,5 +1,9 @@
 // References
 // - https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-auth-code-flow
+//
+// Note:
+// - SPA refresh tokens expire after 24h.
+// - Microsoft does not allow browser extensions to use the native app flow.
 import { dumpQuery, getUniqId, loadQuery, noop } from '@/common';
 import { FORM_URLENCODED, VM_HOME } from '@/common/consts';
 import { objectGet } from '@/common/object';
@@ -98,22 +102,25 @@ const OneDrive = BaseService.extend({
     )}`;
     openAuthPage(url, config.redirect_uri);
   },
-  async finishAuth(url) {
+  matchAuth(url) {
     const redirectUri = `${config.redirect_uri}?`;
     if (!url.startsWith(redirectUri)) return;
     const query = loadQuery(url.slice(redirectUri.length));
     const { state, codeVerifier } = this.session || {};
     this.session = null;
     if (query.state !== state || !query.code) return;
-    if (url.startsWith(redirectUri)) {
-      await this.authorized({
-        code: query.code,
-        code_verifier: codeVerifier,
-        grant_type: 'authorization_code',
-        redirect_uri: config.redirect_uri,
-      });
-      return true;
-    }
+    return {
+      code: query.code,
+      code_verifier: codeVerifier,
+    };
+  },
+  async finishAuth(payload) {
+    await this.authorized({
+      code: payload.code,
+      code_verifier: payload.code_verifier,
+      grant_type: 'authorization_code',
+      redirect_uri: config.redirect_uri,
+    });
   },
   revoke() {
     this.config.set({

+ 1 - 1
src/background/sync/state-machine.js

@@ -18,7 +18,7 @@ const stateMap = {
   [SYNC_AUTHORIZED]: [SYNC_UNAUTHORIZED, SYNC_INITIALIZING, SYNC_IN_PROGRESS],
   [SYNC_IN_PROGRESS]: [SYNC_AUTHORIZED, SYNC_ERROR],
   [SYNC_ERROR]: [SYNC_UNAUTHORIZED, SYNC_AUTHORIZING, SYNC_INITIALIZING],
-  [SYNC_ERROR_INIT]: [SYNC_INITIALIZING, SYNC_UNAUTHORIZED],
+  [SYNC_ERROR_INIT]: [SYNC_UNAUTHORIZED, SYNC_AUTHORIZING, SYNC_INITIALIZING],
   [SYNC_ERROR_AUTH]: [SYNC_UNAUTHORIZED, SYNC_AUTHORIZING],
 };
 

+ 1 - 0
src/options/views/tab-settings/vm-sync.vue

@@ -209,6 +209,7 @@ function setRefs(srv) {
     SYNC_UNAUTHORIZED,
     SYNC_ERROR,
     SYNC_ERROR_INIT,
+    SYNC_ERROR_AUTH,
   ].includes(status);
   rCanRevoke.value =
     hasAuth &&