mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-18 00:46:45 +00:00
fix(sync): add copy-URL fallback for Dropbox auth dialog (#7139)
The "Get Authorization Code" button silently fails on some Linux packagings (Flatpak, AUR) because shell.openExternal() can reject without visible feedback — the wasOpened boolean check in openUrlInBrowser has been dead code since the call was migrated from shell.openPath to shell.openExternal (which returns Promise<void>). - Add a "Copy URL" button to the auth dialog as a reliable fallback users can paste into a manually-opened browser. - Replace the dead wasOpened check with a .catch() that logs the rejection reason, so future portal failures surface in electron logs. - Add regression tests documenting the PKCE verifier/challenge invariants that make stale auth codes fail with "invalid code verifier" when the dialog is reopened.
This commit is contained in:
parent
05cd875dd6
commit
8c3b08e016
6 changed files with 76 additions and 4 deletions
|
|
@ -332,10 +332,11 @@ function initWinEventListeners(app: Electron.App): void {
|
|||
const urlObj = new URL(url);
|
||||
urlObj.pathname = urlObj.pathname.replace('//', '/');
|
||||
const wellFormedUrl = urlObj.toString();
|
||||
const wasOpened = shell.openExternal(wellFormedUrl);
|
||||
if (!wasOpened) {
|
||||
shell.openExternal(wellFormedUrl);
|
||||
}
|
||||
// shell.openExternal returns Promise<void>; surface the failure reason instead
|
||||
// of silently swallowing it (e.g. when xdg-open / Flatpak portal rejects the URI).
|
||||
shell.openExternal(wellFormedUrl).catch((err) => {
|
||||
error('Failed to open external URL via shell.openExternal:', err);
|
||||
});
|
||||
};
|
||||
|
||||
// open new window links in browser
|
||||
|
|
|
|||
|
|
@ -20,6 +20,14 @@
|
|||
<mat-icon>open_in_new</mat-icon>
|
||||
{{ T.F.SYNC.D_AUTH_CODE.GET_AUTH_CODE | translate }}</a
|
||||
>
|
||||
<button
|
||||
mat-button
|
||||
type="button"
|
||||
(click)="copyUrl()"
|
||||
>
|
||||
<mat-icon>content_copy</mat-icon>
|
||||
{{ T.F.SYNC.D_AUTH_CODE.COPY_URL | translate }}
|
||||
</button>
|
||||
|
||||
@if (!isNativePlatform) {
|
||||
<br />
|
||||
|
|
|
|||
|
|
@ -97,4 +97,16 @@ export class DialogGetAndEnterAuthCodeComponent implements OnDestroy {
|
|||
close(token?: string): void {
|
||||
this._matDialogRef.close(token?.trim());
|
||||
}
|
||||
|
||||
async copyUrl(): Promise<void> {
|
||||
try {
|
||||
await navigator.clipboard.writeText(this.data.url);
|
||||
this._snackService.open(T.GLOBAL_SNACK.COPY_TO_CLIPPBOARD);
|
||||
} catch {
|
||||
this._snackService.open({
|
||||
type: 'ERROR',
|
||||
msg: T.PS.FAILED_TO_COPY_TO_CLIPBOARD,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
import { Dropbox } from './dropbox';
|
||||
import { generateCodeChallenge } from '../../../../util/pkce.util';
|
||||
|
||||
/**
|
||||
* Regression/demonstration tests for issue #7139 — the secondary PKCE-mismatch bug.
|
||||
*
|
||||
* Scenario: If the auth dialog is opened twice (e.g. user closes it after the
|
||||
* "Get Authorization Code" button fails to open the system browser), each call
|
||||
* to `getAuthHelper()` generates a fresh `codeVerifier` + matching `code_challenge`.
|
||||
* An auth code obtained from the FIRST authorization URL cannot be exchanged
|
||||
* using the SECOND helper's verifier — Dropbox rejects with `invalid_grant:
|
||||
* invalid code verifier`, which is exactly what the user reports.
|
||||
*/
|
||||
describe('Dropbox.getAuthHelper — PKCE lifecycle (issue #7139)', () => {
|
||||
it('returned authUrl must carry a code_challenge derived from the returned codeVerifier', async () => {
|
||||
const dropbox = new Dropbox({ appKey: 'test-key', basePath: '/' });
|
||||
|
||||
const helper = await dropbox.getAuthHelper();
|
||||
const authUrl = helper.authUrl as string;
|
||||
const codeVerifier = helper.codeVerifier as string;
|
||||
|
||||
const challengeInUrl = new URL(authUrl).searchParams.get('code_challenge');
|
||||
const expectedChallenge = await generateCodeChallenge(codeVerifier);
|
||||
|
||||
expect(challengeInUrl).toBe(expectedChallenge);
|
||||
});
|
||||
|
||||
it('regenerates codeVerifier on every call — stale auth codes will fail token exchange', async () => {
|
||||
const dropbox = new Dropbox({ appKey: 'test-key', basePath: '/' });
|
||||
|
||||
const first = await dropbox.getAuthHelper();
|
||||
const second = await dropbox.getAuthHelper();
|
||||
|
||||
expect(first.codeVerifier).not.toBe(second.codeVerifier);
|
||||
|
||||
const c1 = new URL(first.authUrl as string).searchParams.get('code_challenge');
|
||||
const c2 = new URL(second.authUrl as string).searchParams.get('code_challenge');
|
||||
expect(c1).not.toBe(c2);
|
||||
|
||||
// This is the crux of the user-visible bug: if the user ever opens the
|
||||
// dialog twice (common when the "Get Authorization Code" button silently
|
||||
// fails in Flatpak), the verifier paired with the first URL's challenge
|
||||
// is GONE. Entering an auth code obtained from the first URL produces
|
||||
// `invalid_grant: invalid code verifier`.
|
||||
const firstChallengeMatchesSecondVerifier =
|
||||
(await generateCodeChallenge(second.codeVerifier as string)) === c1;
|
||||
expect(firstChallengeMatchesSecondVerifier).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
@ -1035,6 +1035,7 @@ const T = {
|
|||
FORCE_UPLOAD: 'F.SYNC.C.FORCE_UPLOAD',
|
||||
},
|
||||
D_AUTH_CODE: {
|
||||
COPY_URL: 'F.SYNC.D_AUTH_CODE.COPY_URL',
|
||||
FOLLOW_LINK: 'F.SYNC.D_AUTH_CODE.FOLLOW_LINK',
|
||||
FOLLOW_LINK_MOBILE: 'F.SYNC.D_AUTH_CODE.FOLLOW_LINK_MOBILE',
|
||||
GET_AUTH_CODE: 'F.SYNC.D_AUTH_CODE.GET_AUTH_CODE',
|
||||
|
|
|
|||
|
|
@ -1018,6 +1018,7 @@
|
|||
"FORCE_UPLOAD": "Forcing an upload of your data could lead to data loss. Are you sure you want to continue?"
|
||||
},
|
||||
"D_AUTH_CODE": {
|
||||
"COPY_URL": "Copy URL",
|
||||
"FOLLOW_LINK": "Please open the following link and copy the auth code provided there into the input field below.",
|
||||
"FOLLOW_LINK_MOBILE": "Please tap the button below to authorize. You will be automatically redirected back to the app.",
|
||||
"GET_AUTH_CODE": "Get Authorization Code",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue