Skip to content

Commit cabee7c

Browse files
committed
fix(oauth): reliable callback delivery without manual token
1 parent a4b44bc commit cabee7c

3 files changed

Lines changed: 62 additions & 7 deletions

File tree

app/src/app.d.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ declare global {
1616
interface ImportMetaEnv {
1717
readonly PUBLIC_GITHUB_CLIENT_ID?: string;
1818
readonly PUBLIC_GITHUB_OAUTH_PROXY_URL?: string;
19-
readonly PUBLIC_GITHUB_ALLOW_MANUAL_TOKEN?: string;
2019
}
2120

2221
interface ImportMeta {

app/src/lib/policies/github/oauthPkce.ts

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { base } from '$app/paths';
2-
import { browser, dev } from '$app/environment';
2+
import { browser } from '$app/environment';
33
import { env as publicEnv } from '$env/dynamic/public';
44
import { z } from 'zod';
55
import { setGithubToken } from '../../stores/githubAuth';
66

77
const SESSION_PREFIX = 'github:oauth:pkce:';
8+
const CALLBACK_KEY = 'github:oauth:pkce:callback';
9+
const CALLBACK_TTL_MS = 2 * 60 * 1000;
810

911
const GithubPkceSessionSchema = z.object({
1012
verifier: z.string().min(32),
@@ -62,11 +64,6 @@ export function getGithubPkceAvailability(): GithubPkceAvailability {
6264

6365
export type GithubPkceCodePayload = { code: string; state: string };
6466

65-
export function getGithubManualTokenAllowed() {
66-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
67-
return Boolean(dev) || Boolean(publicEnv.PUBLIC_GITHUB_ALLOW_MANUAL_TOKEN);
68-
}
69-
7067
export type GithubPkceStartResult =
7168
| { ok: true }
7269
| { ok: false; errorKey: 'panels.sync.githubMissing' | 'errors.githubPopupBlocked' | 'errors.githubPkceUnsupported' };
@@ -264,6 +261,7 @@ function attachPopupCallbackCoordinator(popup: Window, redirectUri: string) {
264261
if (done) return;
265262
done = true;
266263
window.removeEventListener('message', handleMessage);
264+
window.removeEventListener('storage', handleStorage);
267265
channel?.removeEventListener('message', handleChannelMessage as any);
268266
channel?.close();
269267
clearInterval(interval);
@@ -309,13 +307,45 @@ function attachPopupCallbackCoordinator(popup: Window, redirectUri: string) {
309307
cleanup();
310308
};
311309

310+
const readCallbackFromStorage = () => {
311+
try {
312+
const raw = localStorage.getItem(CALLBACK_KEY);
313+
if (!raw) return null;
314+
const parsed = JSON.parse(raw);
315+
const code = typeof parsed?.code === 'string' ? parsed.code.trim() : '';
316+
const state = typeof parsed?.state === 'string' ? parsed.state.trim() : '';
317+
const createdAt = typeof parsed?.createdAt === 'number' ? parsed.createdAt : 0;
318+
if (!code || !state) return null;
319+
if (!createdAt || Date.now() - createdAt > CALLBACK_TTL_MS) return null;
320+
return { code, state };
321+
} catch {
322+
return null;
323+
}
324+
};
325+
326+
const clearCallbackStorage = () => {
327+
try {
328+
localStorage.removeItem(CALLBACK_KEY);
329+
} catch {
330+
// ignore
331+
}
332+
};
333+
312334
const handleMessage = (event: MessageEvent) => {
313335
if (event.origin !== window.location.origin) return;
314336
if (event.data?.type === 'github-oauth-code' && event.data.code && event.data.state) {
315337
void onCallback({ code: event.data.code, state: event.data.state });
316338
}
317339
};
318340

341+
const handleStorage = (event: StorageEvent) => {
342+
if (event.key !== CALLBACK_KEY) return;
343+
const payload = readCallbackFromStorage();
344+
if (!payload) return;
345+
clearCallbackStorage();
346+
void onCallback(payload);
347+
};
348+
319349
const handleChannelMessage = (event: MessageEvent) => {
320350
const data = (event as MessageEvent).data as any;
321351
if (data?.type === 'github-oauth-code' && data.code && data.state) {
@@ -324,6 +354,7 @@ function attachPopupCallbackCoordinator(popup: Window, redirectUri: string) {
324354
};
325355

326356
window.addEventListener('message', handleMessage);
357+
window.addEventListener('storage', handleStorage);
327358
channel?.addEventListener('message', handleChannelMessage as any);
328359

329360
const interval = window.setInterval(() => {
@@ -344,6 +375,12 @@ function attachPopupCallbackCoordinator(popup: Window, redirectUri: string) {
344375
} catch {
345376
// ignore (cross-origin until it returns to our site)
346377
}
378+
379+
const payload = readCallbackFromStorage();
380+
if (payload) {
381+
clearCallbackStorage();
382+
void onCallback(payload);
383+
}
347384
}, 350);
348385

349386
const timeout = window.setTimeout(() => cleanup(), 2 * 60 * 1000);

app/src/routes/auth/callback/+page.svelte

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,25 @@
4747
const state = String(payload.state || '').trim();
4848
if (!code || !state) return;
4949
50+
try {
51+
localStorage.setItem(
52+
'github:oauth:pkce:callback',
53+
JSON.stringify({ code, state, createdAt: Date.now() })
54+
);
55+
setTimeout(() => {
56+
try {
57+
const raw = localStorage.getItem('github:oauth:pkce:callback');
58+
if (!raw) return;
59+
const parsed = JSON.parse(raw);
60+
if (parsed?.code === code && parsed?.state === state) localStorage.removeItem('github:oauth:pkce:callback');
61+
} catch {
62+
// ignore
63+
}
64+
}, 10_000);
65+
} catch {
66+
// ignore
67+
}
68+
5069
try {
5170
window.opener?.postMessage({ type: 'github-oauth-code', code, state }, window.location.origin);
5271
} catch {

0 commit comments

Comments
 (0)