diff --git a/src/app/pages/Router.tsx b/src/app/pages/Router.tsx index 1976ae289..8834efbcd 100644 --- a/src/app/pages/Router.tsx +++ b/src/app/pages/Router.tsx @@ -156,7 +156,14 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize) const mobile = screenSize === ScreenSize.Mobile; const routes = createRoutesFromElements( - + + + + + } + > { @@ -192,7 +199,6 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize) > {null}}> <> - diff --git a/src/app/pages/TauriDeepLinkBridge.test.tsx b/src/app/pages/TauriDeepLinkBridge.test.tsx index bacec3cf2..5167e6248 100644 --- a/src/app/pages/TauriDeepLinkBridge.test.tsx +++ b/src/app/pages/TauriDeepLinkBridge.test.tsx @@ -38,6 +38,12 @@ describe('mapDeepLinkToLoginPath', () => { expect(path).toContain('state=s1'); }); + it('passes normalized moe.sable.app://login OIDC callback through', () => { + const path = mapDeepLinkToLoginPath('moe.sable.app://login?code=c1&state=s1'); + expect(path).toContain('code=c1'); + expect(path).toContain('state=s1'); + }); + it('returns undefined for an unrelated url', () => { expect(mapDeepLinkToLoginPath('https://example.com/whatever')).toBeUndefined(); }); diff --git a/src/app/pages/auth/SSOTauri.test.ts b/src/app/pages/auth/SSOTauri.test.ts index c16e9082b..577cfa841 100644 --- a/src/app/pages/auth/SSOTauri.test.ts +++ b/src/app/pages/auth/SSOTauri.test.ts @@ -79,14 +79,31 @@ describe('parseTauriSsoCallback', () => { }); describe('parseTauriOidcCallback', () => { - it('parses code and state', () => { + it('parses code and state from single-slash format', () => { expect(parseTauriOidcCallback('moe.sable.app:/login?code=c1&state=s1')).toEqual({ code: 'c1', state: 's1', }); }); + it('parses code and state from authority/hostname format (moe.sable.app://login)', () => { + expect(parseTauriOidcCallback('moe.sable.app://login?code=c1&state=s1')).toEqual({ + code: 'c1', + state: 's1', + }); + }); + it('rejects the wrong path', () => { expect(parseTauriOidcCallback('moe.sable.app:/other?code=c1&state=s1')).toBeUndefined(); + expect(parseTauriOidcCallback('moe.sable.app://other?code=c1&state=s1')).toBeUndefined(); + }); + + it('rejects a different protocol', () => { + expect(parseTauriOidcCallback('sable://login?code=c1&state=s1')).toBeUndefined(); + expect(parseTauriOidcCallback('https://login?code=c1&state=s1')).toBeUndefined(); + }); + + it('rejects an unrelated hostname with a login path', () => { + expect(parseTauriOidcCallback('moe.sable.app://evil/login?code=c1&state=s1')).toBeUndefined(); }); }); diff --git a/src/app/pages/auth/SSOTauri.ts b/src/app/pages/auth/SSOTauri.ts index 4100a75d4..11fb3d221 100644 --- a/src/app/pages/auth/SSOTauri.ts +++ b/src/app/pages/auth/SSOTauri.ts @@ -75,7 +75,19 @@ export const parseTauriOidcCallback = ( try { const callbackUrl = new URL(rawUrl); if (callbackUrl.protocol !== TAURI_OIDC_PROTOCOL) return undefined; - if (callbackUrl.pathname !== TAURI_OIDC_PATH) return undefined; + + // `moe.sable.app:/login?...` → hostname === '', pathname === '/login' + // `moe.sable.app://login?...` → hostname === 'login', pathname === '/' or '' + const hasLoginPath = + callbackUrl.pathname === TAURI_OIDC_PATH || callbackUrl.pathname === `${TAURI_OIDC_PATH}/`; + + const isSingleSlashFormat = callbackUrl.hostname === '' && hasLoginPath; + + const isAuthorityFormat = + callbackUrl.hostname === 'login' && + (callbackUrl.pathname === '/' || callbackUrl.pathname === ''); + + if (!isSingleSlashFormat && !isAuthorityFormat) return undefined; const code = callbackUrl.searchParams.get('code'); const state = callbackUrl.searchParams.get('state');