fix(sync): fix Dropbox PKCE auth failure and infinite re-auth loop

- Fix crypto.subtle null check in pkce.util.ts: use != null instead of
  !== undefined to handle Android WebView contexts where crypto.subtle
  is explicitly null, preventing PKCE challenge generation failure

- Keep dialog open when auth is attempted but not completed: enrich
  configuredAuthForSyncProviderIfNecessary return type with authAttempted
  flag so dialog-sync-initial-cfg can distinguish cancelled/failed auth
  from skipped auth, preventing the infinite re-auth loop on Android
  where hardware back button dismisses the auth dialog

The automatic deep-link redirect flow remains disabled (manual code
entry on all platforms) as the redirect URI requires Dropbox developer
console registration and Android may kill the app during auth, losing
the in-memory code verifier.

Fixes #5381, #7131
Related: #7130, #6212, #7139
This commit is contained in:
Johannes Millan 2026-04-14 16:26:10 +02:00
parent 265b44db5d
commit 667a7986fb
5 changed files with 29 additions and 16 deletions

View file

@ -53,7 +53,10 @@ export class DialogGetAndEnterAuthCodeComponent implements OnDestroy {
T: typeof T = T;
token?: string;
// Always use manual code entry flow (show input field)
// Always use manual code entry flow (show input field on all platforms).
// The automatic deep-link redirect flow was reverted: it requires the
// redirect URI to be registered in the Dropbox developer console, and
// Android may kill the app during auth, losing the in-memory code verifier.
readonly isNativePlatform = false;
private _authCodeSub?: Subscription;

View file

@ -222,7 +222,16 @@ export class DialogSyncInitialCfgComponent implements AfterViewInit {
await this.syncConfigService.updateSettingsFromForm(configToSave as SyncConfig, true);
const providerId = toSyncProviderId(this._tmpUpdatedCfg.syncProvider);
if (providerId && this._tmpUpdatedCfg.isEnabled) {
await this.syncWrapperService.configuredAuthForSyncProviderIfNecessary(providerId);
const { wasConfigured, authAttempted } =
await this.syncWrapperService.configuredAuthForSyncProviderIfNecessary(
providerId,
);
// If auth was attempted (dialog opened) but not completed (cancelled or failed),
// keep dialog open so the user can retry without losing their config.
if (authAttempted && !wasConfigured) {
return;
}
}
this._matDialogRef.close();

View file

@ -765,20 +765,20 @@ export class SyncWrapperService {
async configuredAuthForSyncProviderIfNecessary(
providerId: SyncProviderId,
force = false,
): Promise<{ wasConfigured: boolean }> {
): Promise<{ wasConfigured: boolean; authAttempted: boolean }> {
const provider = await this._providerManager.getProviderById(providerId);
if (!provider) {
return { wasConfigured: false };
return { wasConfigured: false, authAttempted: false };
}
if (!provider.getAuthHelper) {
return { wasConfigured: false };
return { wasConfigured: false, authAttempted: false };
}
if (!force && (await provider.isReady())) {
SyncLog.warn('Provider already configured');
return { wasConfigured: false };
return { wasConfigured: false, authAttempted: false };
}
try {
@ -807,9 +807,9 @@ export class SyncWrapperService {
setTimeout(() => {
this.sync();
}, 1000);
return { wasConfigured: true };
return { wasConfigured: true, authAttempted: true };
} else {
return { wasConfigured: false };
return { wasConfigured: false, authAttempted: true };
}
}
} catch (error) {
@ -823,9 +823,9 @@ export class SyncWrapperService {
type: 'ERROR',
config: { duration: 0 },
});
return { wasConfigured: false };
return { wasConfigured: false, authAttempted: true };
}
return { wasConfigured: false };
return { wasConfigured: false, authAttempted: false };
}
/**

View file

@ -314,11 +314,12 @@ export class Dropbox implements SyncProviderServiceInterface<SyncProviderId.Drop
/**
* Gets the OAuth redirect URI.
*
* Always returns null to use Dropbox's manual code entry flow.
* User must copy the code from Dropbox's page and paste it manually.
* This works reliably across all platforms (web, Electron, Android, iOS).
*
* @returns null for manual code entry flow
* Returns null on all platforms to use Dropbox's manual code entry flow.
* User copies the code from Dropbox's page and pastes it manually.
* This is the most reliable cross-platform approach the automatic deep-link
* redirect flow was reverted because the Dropbox developer console redirect URI
* registration is uncertain and Android may kill the app during auth, losing
* the in-memory code verifier.
*/
private _getRedirectUri(): string | null {
return null;

View file

@ -15,7 +15,7 @@ const base64UrlEncode = (buffer: Uint8Array): string => {
* Uses crypto.subtle when available, otherwise falls back to hash-wasm.
*/
const sha256 = async (data: Uint8Array): Promise<ArrayBuffer> => {
if (typeof crypto !== 'undefined' && crypto.subtle !== undefined) {
if (typeof crypto !== 'undefined' && crypto.subtle != null) {
return crypto.subtle.digest('SHA-256', data as BufferSource);
}