From 6ab55f6ae45d9840aa382c3111e3ce7a881742a4 Mon Sep 17 00:00:00 2001 From: Adam Setch Date: Fri, 16 Oct 2026 23:22:25 -0400 Subject: [PATCH 1/3] refactor(auth): migrate custom OAuth app login to the device flow Replace the client-secret OAuth web flow for user-supplied ("custom") OAuth apps with the OAuth device flow, which authorizes using only a public client ID. - Add a custom OAuth app sign-in that collects a hostname and client ID and reuses the shared device-flow authorization and scope UI; no client secret. - Retire the web flow end to end: the /login/github/oauth-app route, the authorization-code + secret exchange, the oauthWebApp adapter bundle, and the gitify:// custom-protocol callback across the Electron main process, preload, and the shared IPC event. - Rename auth-method labels: GitHub App -> Gitify OAuth App, OAuth App -> Custom OAuth App; persist the custom app's client ID for its settings link. - Migrate persisted accounts on load without forcing re-login. - Surface actionable guidance for device_flow_disabled and unknown client IDs. --- src/main/index.ts | 20 +- src/main/lifecycle/startup.test.ts | 36 +- src/main/lifecycle/startup.ts | 53 +- src/preload/index.ts | 10 - src/renderer/App.tsx | 7 +- src/renderer/__helpers__/hook-mocks.ts | 1 - src/renderer/__helpers__/test-utils.tsx | 1 - src/renderer/__helpers__/visual.setup.ts | 1 - src/renderer/__helpers__/vitest.setup.ts | 1 - src/renderer/__mocks__/account-mocks.ts | 18 +- src/renderer/__mocks__/notifications-mocks.ts | 4 +- .../settings/AppearanceSettings.test.tsx | 8 +- src/renderer/hooks/useLogins.test.tsx | 50 +- src/renderer/hooks/useLogins.ts | 51 +- src/renderer/routes/AccountScopes.test.tsx | 22 +- src/renderer/routes/Accounts.test.tsx | 66 ++- src/renderer/routes/Login.test.tsx | 2 +- .../login-dark-classic-chromium-linux.png | Bin 35196 -> 28527 bytes ...-oauth-app-dark-classic-chromium-linux.png | Bin 0 -> 34397 bytes ...oauth-app-light-classic-chromium-linux.png | Bin 0 -> 33539 bytes ...-oauth-app-dark-classic-chromium-linux.png | Bin 40512 -> 0 bytes ...oauth-app-light-classic-chromium-linux.png | Bin 40626 -> 0 bytes .../login-light-classic-chromium-linux.png | Bin 35628 -> 28372 bytes .../routes/__snapshots__/Login.test.tsx.snap | 4 +- .../github/LoginWithCustomOAuthApp.test.tsx | 104 ++++ .../routes/github/LoginWithCustomOAuthApp.tsx | 164 ++++++ .../github/LoginWithDeviceFlow.test.tsx | 59 ++- .../routes/github/LoginWithDeviceFlow.tsx | 50 +- .../routes/github/LoginWithOAuthApp.test.tsx | 145 ------ .../routes/github/LoginWithOAuthApp.tsx | 216 -------- .../LoginWithOAuthApp.test.tsx.snap | 487 ------------------ src/renderer/routes/routes.visual.test.tsx | 8 +- src/renderer/stores/types.ts | 2 + src/renderer/stores/useAccountsStore.test.ts | 51 +- src/renderer/stores/useAccountsStore.ts | 30 +- src/renderer/types.ts | 9 +- .../utils/auth/loginValidation.test.ts | 37 +- src/renderer/utils/auth/loginValidation.ts | 31 +- src/renderer/utils/auth/state.test.ts | 22 - src/renderer/utils/auth/state.ts | 13 - src/renderer/utils/auth/types.ts | 20 +- .../utils/forges/bitbucket/adapter.test.ts | 2 +- .../utils/forges/gitea/adapter.test.ts | 4 +- .../utils/forges/github/adapter.test.ts | 69 ++- src/renderer/utils/forges/github/adapter.ts | 45 +- src/renderer/utils/forges/github/auth.test.ts | 24 +- src/renderer/utils/forges/github/auth.ts | 21 +- .../utils/forges/github/flows.test.ts | 418 +++++---------- src/renderer/utils/forges/github/flows.ts | 199 ++----- .../utils/forges/github/octokit.test.ts | 4 +- .../utils/forges/gitlab/adapter.test.ts | 2 +- src/renderer/utils/forges/types.ts | 52 +- src/shared/events.ts | 2 - 53 files changed, 917 insertions(+), 1728 deletions(-) create mode 100644 src/renderer/routes/__screenshots__/routes.visual.test.tsx/login-github-custom-oauth-app-dark-classic-chromium-linux.png create mode 100644 src/renderer/routes/__screenshots__/routes.visual.test.tsx/login-github-custom-oauth-app-light-classic-chromium-linux.png delete mode 100644 src/renderer/routes/__screenshots__/routes.visual.test.tsx/login-github-oauth-app-dark-classic-chromium-linux.png delete mode 100644 src/renderer/routes/__screenshots__/routes.visual.test.tsx/login-github-oauth-app-light-classic-chromium-linux.png create mode 100644 src/renderer/routes/github/LoginWithCustomOAuthApp.test.tsx create mode 100644 src/renderer/routes/github/LoginWithCustomOAuthApp.tsx delete mode 100644 src/renderer/routes/github/LoginWithOAuthApp.test.tsx delete mode 100644 src/renderer/routes/github/LoginWithOAuthApp.tsx delete mode 100644 src/renderer/routes/github/__snapshots__/LoginWithOAuthApp.test.tsx.snap delete mode 100644 src/renderer/utils/auth/state.test.ts delete mode 100644 src/renderer/utils/auth/state.ts diff --git a/src/main/index.ts b/src/main/index.ts index 8b9f58646..557b3d008 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -14,16 +14,10 @@ import { registerUpdaterHandlers, } from './handlers'; import { getIdleTrayIcon } from './icons'; -import { - configureWindowEvents, - handleProtocolURL, - initializeAppLifecycle, - onFirstRunMaybe, -} from './lifecycle'; +import { configureWindowEvents, initializeAppLifecycle, onFirstRunMaybe } from './lifecycle'; import MenuBuilder from './menu'; import { applyOzonePlatform } from './ozone'; import AppUpdater from './updater'; -import { isDevMode } from './utils'; // Runs at module load: the Ozone platform is read during app startup, so this // has to happen before `app.whenReady()` below. @@ -48,10 +42,6 @@ const mb = menubar({ const menuBuilder = new MenuBuilder(mb); const contextMenu = menuBuilder.buildMenu(); -// Register your app as the handler for a custom protocol -const protocol = isDevMode() ? 'gitify-dev' : 'gitify'; -app.setAsDefaultProtocolClient(protocol); - const appUpdater = new AppUpdater(mb, menuBuilder); app @@ -59,7 +49,7 @@ app .then(async () => { await onFirstRunMaybe(); - initializeAppLifecycle(mb, contextMenu, protocol); + initializeAppLifecycle(mb, contextMenu); // Configure window event handlers (Escape key, DevTools resize) configureWindowEvents(mb, menuBuilder); @@ -75,9 +65,3 @@ app .catch((err) => { logError('main', 'Failed to initialize application', toError(err)); }); - -// Handle gitify:// custom protocol URL events for OAuth 2.0 callback -app.on('open-url', (event, url) => { - event.preventDefault(); - handleProtocolURL(mb, url, protocol); -}); diff --git a/src/main/lifecycle/startup.test.ts b/src/main/lifecycle/startup.test.ts index f81f9a66f..17ced7ae8 100644 --- a/src/main/lifecycle/startup.test.ts +++ b/src/main/lifecycle/startup.test.ts @@ -1,30 +1,19 @@ import type { Menubar } from 'electron-menubar'; -import { EVENTS } from '../../shared/events'; - -import { handleProtocolURL, initializeAppLifecycle } from './startup'; +import { initializeAppLifecycle } from './startup'; const requestSingleInstanceLockMock = vi.fn(() => true); -const appOnMock = vi.fn(); const appQuitMock = vi.fn(); vi.mock('electron', () => ({ app: { requestSingleInstanceLock: () => requestSingleInstanceLockMock(), - on: (...a: unknown[]) => appOnMock(...a), quit: () => appQuitMock(), - } satisfies Pick, -})); - -const sendRendererEventMock = vi.fn(); -vi.mock('../events', () => ({ - sendRendererEvent: (...a: unknown[]) => sendRendererEventMock(...a), + } satisfies Pick, })); -const logInfoMock = vi.fn(); const logWarnMock = vi.fn(); vi.mock('../../shared/logger', () => ({ - logInfo: (...a: unknown[]) => logInfoMock(...a), logWarn: (...a: unknown[]) => logWarnMock(...a), })); @@ -46,7 +35,7 @@ describe('main/lifecycle/startup.ts', () => { const mb = createMb(); const contextMenu = {} as Electron.Menu; - initializeAppLifecycle(mb as unknown as Menubar, contextMenu, 'gitify'); + initializeAppLifecycle(mb as unknown as Menubar, contextMenu); expect(mb.on).toHaveBeenCalledWith('ready', expect.any(Function)); }); @@ -56,7 +45,7 @@ describe('main/lifecycle/startup.ts', () => { const mb = createMb(); const contextMenu = {} as Electron.Menu; - initializeAppLifecycle(mb as unknown as Menubar, contextMenu, 'gitify'); + initializeAppLifecycle(mb as unknown as Menubar, contextMenu); expect(appQuitMock).toHaveBeenCalled(); expect(logWarnMock).toHaveBeenCalled(); @@ -66,7 +55,7 @@ describe('main/lifecycle/startup.ts', () => { const mb = createMb(); const contextMenu = {} as Electron.Menu; - initializeAppLifecycle(mb as unknown as Menubar, contextMenu, 'gitify'); + initializeAppLifecycle(mb as unknown as Menubar, contextMenu); const readyHandler = (mb.on as unknown as ReturnType).mock.calls[0]?.[1]; expect(readyHandler).toBeDefined(); @@ -75,19 +64,4 @@ describe('main/lifecycle/startup.ts', () => { expect(mb.setContextMenu).toHaveBeenCalledWith(contextMenu); }); }); - - describe('handleProtocolURL', () => { - it('forwards matching protocol URL to renderer as AUTH_CALLBACK', () => { - const mb = createMb(); - const url = 'gitify://callback?code=abc123'; - handleProtocolURL(mb as unknown as Menubar, url, 'gitify'); - expect(sendRendererEventMock).toHaveBeenCalledWith(mb, EVENTS.AUTH_CALLBACK, url); - }); - - it('ignores URLs that do not match the protocol', () => { - const mb = createMb(); - handleProtocolURL(mb as unknown as Menubar, 'https://github.com', 'gitify'); - expect(sendRendererEventMock).not.toHaveBeenCalled(); - }); - }); }); diff --git a/src/main/lifecycle/startup.ts b/src/main/lifecycle/startup.ts index e80a38f71..a7b4faa98 100644 --- a/src/main/lifecycle/startup.ts +++ b/src/main/lifecycle/startup.ts @@ -2,14 +2,11 @@ import { app } from 'electron'; import type { Menubar } from 'electron-menubar'; import { APPLICATION } from '../../shared/constants'; -import { EVENTS } from '../../shared/events'; -import { logInfo, logWarn } from '../../shared/logger'; - -import { sendRendererEvent } from '../events'; +import { logWarn } from '../../shared/logger'; /** - * Set up core application lifecycle events including tray ready setup, - * protocol URL handling, and single-instance enforcement. + * Set up core application lifecycle events: tray ready setup and + * single-instance enforcement. * * The tray's context-menu wiring (Linux `setContextMenu` vs macOS/Windows * right-click popup) and the macOS `setIgnoreDoubleClickEvents` default @@ -18,60 +15,26 @@ import { sendRendererEvent } from '../events'; * * @param mb - The menubar instance to attach lifecycle events to. * @param contextMenu - The tray context menu to attach via `mb.setContextMenu`. - * @param protocol - The custom protocol string (e.g. 'gitify' or 'gitify-dev'). */ -export function initializeAppLifecycle( - mb: Menubar, - contextMenu: Electron.Menu, - protocol: string, -): void { +export function initializeAppLifecycle(mb: Menubar, contextMenu: Electron.Menu): void { mb.on('ready', () => { mb.app.setAppUserModelId(APPLICATION.ID); mb.tray.setToolTip(APPLICATION.NAME); mb.setContextMenu(contextMenu); }); - preventSecondInstance(mb, protocol); -} - -/** - * Handle a gitify:// protocol URL by forwarding it to the renderer process - * as an AUTH_CALLBACK event. - * - * @param mb - The menubar instance to forward the event through. - * @param url - The protocol URL to handle. - * @param protocol - The custom protocol string to match. - */ -export function handleProtocolURL(mb: Menubar, url: string, protocol: string): void { - if (url.startsWith(`${protocol}://`)) { - logInfo('main:handleUrl', `forwarding URL ${url} to renderer process`); - sendRendererEvent(mb, EVENTS.AUTH_CALLBACK, url); - } + preventSecondInstance(); } /** - * Enforce a single application instance. If a second instance is launched, - * any protocol URL in the command line is forwarded to the existing instance. - * - * @param mb - The menubar instance to show when a second instance is detected. - * @param protocol - The custom protocol string to extract from command line args. + * Enforce a single application instance. A second launch quits immediately so + * the already-running tray instance remains the only one. */ -function preventSecondInstance(mb: Menubar, protocol: string): void { +function preventSecondInstance(): void { const gotTheLock = app.requestSingleInstanceLock(); if (!gotTheLock) { logWarn('main:gotTheLock', 'Second instance detected, quitting'); app.quit(); - return; } - - app.on('second-instance', (_event, commandLine) => { - logInfo('main:second-instance', 'Second instance was launched. Extracting command to forward'); - - const url = commandLine.find((arg) => arg.startsWith(`${protocol}://`)); - - if (url) { - handleProtocolURL(mb, url, protocol); - } - }); } diff --git a/src/preload/index.ts b/src/preload/index.ts index f760cd767..b88db7a2a 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -245,16 +245,6 @@ export const api = { return onRendererEvent(EVENTS.SYSTEM_WAKE, () => callback()); }, - /** - * Register a callback invoked when the main process delivers an OAuth callback URL. - * - * @param callback - Called with the full callback URL (e.g. `gitify://oauth?code=...`). - * @returns A function that removes the listener. - */ - onAuthCallback: (callback: (url: string) => void) => { - return onRendererEvent(EVENTS.AUTH_CALLBACK, (_, url) => callback(url)); - }, - /** * Display a native OS notification. * diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 19f4e11ef..29ae186fb 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -11,9 +11,9 @@ import { AccountScopesRoute } from './routes/AccountScopes'; import { BitbucketLoginWithPersonalAccessTokenRoute } from './routes/bitbucket/LoginWithPersonalAccessToken'; import { FiltersRoute } from './routes/Filters'; import { GiteaLoginWithPersonalAccessTokenRoute } from './routes/gitea/LoginWithPersonalAccessToken'; +import { GitHubLoginWithCustomOAuthAppRoute } from './routes/github/LoginWithCustomOAuthApp'; import { GitHubLoginWithDeviceFlowRoute } from './routes/github/LoginWithDeviceFlow'; import { GitHubLoginWithCLIRoute } from './routes/github/LoginWithGitHubCLI'; -import { GitHubLoginWithOAuthAppRoute } from './routes/github/LoginWithOAuthApp'; import { GitHubLoginWithPersonalAccessTokenRoute } from './routes/github/LoginWithPersonalAccessToken'; import { GitLabLoginWithPersonalAccessTokenRoute } from './routes/gitlab/LoginWithPersonalAccessToken'; import { LoginRoute } from './routes/Login'; @@ -108,7 +108,10 @@ export function App() { element={} path="/login/github/personal-access-token" /> - } path="/login/github/oauth-app" /> + } + path="/login/github/custom-oauth-app" + /> } path="/login/gitea/personal-access-token" diff --git a/src/renderer/__helpers__/hook-mocks.ts b/src/renderer/__helpers__/hook-mocks.ts index fcff10597..a99570529 100644 --- a/src/renderer/__helpers__/hook-mocks.ts +++ b/src/renderer/__helpers__/hook-mocks.ts @@ -52,7 +52,6 @@ function buildLoginsDefaults(): LoginsState { loginWithDeviceFlowPoll: vi.fn(), loginWithDeviceFlowComplete: vi.fn(), loginWithCli: vi.fn(), - loginWithOAuthApp: vi.fn(), loginWithPersonalAccessToken: vi.fn(), logoutFromAccount: vi.fn(), }; diff --git a/src/renderer/__helpers__/test-utils.tsx b/src/renderer/__helpers__/test-utils.tsx index 53bf781fd..333e35ab0 100644 --- a/src/renderer/__helpers__/test-utils.tsx +++ b/src/renderer/__helpers__/test-utils.tsx @@ -47,7 +47,6 @@ const LOGIN_KEYS = [ 'loginWithDeviceFlowPoll', 'loginWithDeviceFlowComplete', 'loginWithCli', - 'loginWithOAuthApp', 'loginWithPersonalAccessToken', 'logoutFromAccount', ] as const; diff --git a/src/renderer/__helpers__/visual.setup.ts b/src/renderer/__helpers__/visual.setup.ts index c6bda06ec..3ea8fd8e1 100644 --- a/src/renderer/__helpers__/visual.setup.ts +++ b/src/renderer/__helpers__/visual.setup.ts @@ -105,7 +105,6 @@ function createGitifyBridgeApi(): Window['gitify'] { useUnreadActiveIcon: vi.fn(), }, notificationSoundPath: vi.fn(), - onAuthCallback: vi.fn(() => vi.fn()), onResetApp: vi.fn(() => vi.fn()), onSystemWake: vi.fn(() => vi.fn()), setAutoLaunch: vi.fn(), diff --git a/src/renderer/__helpers__/vitest.setup.ts b/src/renderer/__helpers__/vitest.setup.ts index 87d475fa6..7319d2bf4 100644 --- a/src/renderer/__helpers__/vitest.setup.ts +++ b/src/renderer/__helpers__/vitest.setup.ts @@ -114,7 +114,6 @@ function createGitifyBridgeApi(): Window['gitify'] { useUnreadActiveIcon: vi.fn(), }, notificationSoundPath: vi.fn(), - onAuthCallback: vi.fn(() => vi.fn()), onResetApp: vi.fn(() => vi.fn()), onSystemWake: vi.fn(() => vi.fn()), setAutoLaunch: vi.fn(), diff --git a/src/renderer/__mocks__/account-mocks.ts b/src/renderer/__mocks__/account-mocks.ts index 5c87fd56c..6229c78ad 100644 --- a/src/renderer/__mocks__/account-mocks.ts +++ b/src/renderer/__mocks__/account-mocks.ts @@ -1,14 +1,21 @@ import { Constants } from '../constants'; -import type { Account, AccountNotifications, GitifyError, Hostname, Token } from '../types'; +import type { + Account, + AccountNotifications, + ClientID, + GitifyError, + Hostname, + Token, +} from '../types'; import { getRecommendedScopeNames } from '../utils/auth/scopes'; import { mockGitifyUser } from './user-mocks'; -export const mockGitHubAppAccount: Account = { +export const mockGitifyOAuthAppAccount: Account = { forge: 'github', platform: 'GitHub Cloud', - method: 'GitHub App', + method: 'Gitify OAuth App', token: 'token-987654321' as Token, hostname: Constants.GITHUB_HOSTNAME, user: mockGitifyUser, @@ -37,12 +44,13 @@ export const mockGitHubCliAccount: Account = { scopes: ['gist', 'read:org', 'repo', 'workflow'], }; -export const mockOAuthAccount: Account = { +export const mockCustomOAuthAppAccount: Account = { forge: 'github', platform: 'GitHub Enterprise Server', - method: 'OAuth App', + method: 'Custom OAuth App', token: 'token-1234568790' as Token, hostname: 'github.gitify.io' as Hostname, + clientId: 'CUSTOM_CLIENT_ID_123' as ClientID, user: mockGitifyUser, scopes: getRecommendedScopeNames(), }; diff --git a/src/renderer/__mocks__/notifications-mocks.ts b/src/renderer/__mocks__/notifications-mocks.ts index 6f8d351a1..95560abc0 100644 --- a/src/renderer/__mocks__/notifications-mocks.ts +++ b/src/renderer/__mocks__/notifications-mocks.ts @@ -21,7 +21,7 @@ import { import { mockGiteaAccount, - mockGitHubAppAccount, + mockGitifyOAuthAppAccount, mockGitHubCloudAccount, mockGitHubEnterpriseServerAccount, } from './account-mocks'; @@ -332,7 +332,7 @@ export function mockPartialGitifyNotification( repository?: Partial, ): GitifyNotification { const mockNotification: Partial = { - account: mockGitHubAppAccount, + account: mockGitifyOAuthAppAccount, reason: { code: 'subscribed', title: 'Updated', diff --git a/src/renderer/components/settings/AppearanceSettings.test.tsx b/src/renderer/components/settings/AppearanceSettings.test.tsx index d776e334b..d7d5377dc 100644 --- a/src/renderer/components/settings/AppearanceSettings.test.tsx +++ b/src/renderer/components/settings/AppearanceSettings.test.tsx @@ -3,7 +3,7 @@ import userEvent from '@testing-library/user-event'; import { renderWithProviders } from '../../__helpers__/test-utils'; import { - mockGitHubAppAccount, + mockGitifyOAuthAppAccount, mockGitHubEnterpriseServerAccount, } from '../../__mocks__/account-mocks'; @@ -91,7 +91,7 @@ describe('renderer/components/settings/AppearanceSettings.tsx', () => { ] as const)('should toggle %s checkbox', async (testId, setting) => { await act(async () => { renderWithProviders(, { - accounts: [mockGitHubAppAccount], + accounts: [mockGitifyOAuthAppAccount], }); }); @@ -135,7 +135,7 @@ describe('renderer/components/settings/AppearanceSettings.tsx', () => { it('reactively hides the account header checkbox when a second account is added', async () => { await act(async () => { renderWithProviders(, { - accounts: [mockGitHubAppAccount], + accounts: [mockGitifyOAuthAppAccount], }); }); @@ -143,7 +143,7 @@ describe('renderer/components/settings/AppearanceSettings.tsx', () => { await act(async () => { useAccountsStore.setState({ - accounts: [mockGitHubAppAccount, mockGitHubEnterpriseServerAccount], + accounts: [mockGitifyOAuthAppAccount, mockGitHubEnterpriseServerAccount], }); }); diff --git a/src/renderer/hooks/useLogins.test.tsx b/src/renderer/hooks/useLogins.test.tsx index 28f23fbfd..635034455 100644 --- a/src/renderer/hooks/useLogins.test.tsx +++ b/src/renderer/hooks/useLogins.test.tsx @@ -14,7 +14,7 @@ import { Constants } from '../constants'; import { useAccountsStore } from '../stores'; -import type { ClientID, ClientSecret, Token } from '../types'; +import type { ClientID, Token } from '../types'; import type { DeviceFlowSession } from '../utils/auth/types'; import { getAdapter } from '../utils/forges/registry'; @@ -113,7 +113,7 @@ describe('renderer/hooks/useLogins.ts', () => { expect(pollSpy).toHaveBeenCalledWith('session'); }); - it('loginWithDeviceFlowComplete creates the account', async () => { + it('loginWithDeviceFlowComplete records the built-in Gitify OAuth App account', async () => { const { result } = renderLoginsHook(); await act(async () => { @@ -124,24 +124,36 @@ describe('renderer/hooks/useLogins.ts', () => { ); }); - expect(createAccountSpy).toHaveBeenCalledWith('GitHub App', 'token', 'github.com', 'github'); + expect(createAccountSpy).toHaveBeenCalledWith( + 'Gitify OAuth App', + 'token', + 'github.com', + 'github', + undefined, + undefined, + ); }); - it('loginWithOAuthApp delegates to the forge adapter', async () => { - const adapter = getAdapter('github'); - const oauthSpy = vi.spyOn(adapter.oauthWebApp!, 'performWebOAuth'); - + it('loginWithDeviceFlowComplete records a custom app client ID', async () => { const { result } = renderLoginsHook(); await act(async () => { - void result.current.loginWithOAuthApp('github', { - clientId: 'id' as ClientID, - clientSecret: 'secret' as ClientSecret, - hostname: Constants.GITHUB_HOSTNAME, - }); + await result.current.loginWithDeviceFlowComplete( + 'github', + 'token' as Token, + Constants.GITHUB_HOSTNAME, + 'CUSTOM_CLIENT_ID_123' as ClientID, + ); }); - expect(oauthSpy).toHaveBeenCalled(); + expect(createAccountSpy).toHaveBeenCalledWith( + 'Custom OAuth App', + 'token', + 'github.com', + 'github', + undefined, + 'CUSTOM_CLIENT_ID_123', + ); }); it('loginWithDeviceFlowStart throws when the forge does not support device flow', async () => { @@ -172,18 +184,6 @@ describe('renderer/hooks/useLogins.ts', () => { ).rejects.toThrow(/does not support device flow/); }); - it('loginWithOAuthApp throws when the forge does not support OAuth app login', async () => { - const { result } = renderLoginsHook(); - - await expect( - result.current.loginWithOAuthApp('gitea', { - clientId: 'id' as ClientID, - clientSecret: 'secret' as ClientSecret, - hostname: Constants.GITHUB_HOSTNAME, - }), - ).rejects.toThrow(/OAuth app login is not supported for forge "gitea"/); - }); - it('logoutFromAccount calls removeAccountNotifications, removeAccount', async () => { useAccountsStore.setState({ accounts: [mockGitHubCloudAccount] }); diff --git a/src/renderer/hooks/useLogins.ts b/src/renderer/hooks/useLogins.ts index 24ac225b4..f6743004e 100644 --- a/src/renderer/hooks/useLogins.ts +++ b/src/renderer/hooks/useLogins.ts @@ -4,10 +4,10 @@ import { useQueryClient } from '@tanstack/react-query'; import { useAccountsStore } from '../stores'; -import type { Account, Forge, Hostname, Token } from '../types'; +import type { Account, ClientID, Forge, Hostname, Token } from '../types'; import type { + AuthMethod, DeviceFlowSession, - LoginOAuthWebOptions, LoginPersonalAccessTokenOptions, } from '../utils/auth/types'; @@ -21,11 +21,16 @@ interface LoginsState { forge: Forge, hostname?: Hostname, scopes?: string[], + clientId?: ClientID, ) => Promise; loginWithDeviceFlowPoll: (forge: Forge, session: DeviceFlowSession) => Promise; - loginWithDeviceFlowComplete: (forge: Forge, token: Token, hostname: Hostname) => Promise; + loginWithDeviceFlowComplete: ( + forge: Forge, + token: Token, + hostname: Hostname, + clientId?: ClientID, + ) => Promise; loginWithCli: (forge: Forge, hostname: Hostname) => Promise; - loginWithOAuthApp: (forge: Forge, data: LoginOAuthWebOptions) => Promise; loginWithPersonalAccessToken: (data: LoginPersonalAccessTokenOptions) => Promise; logoutFromAccount: (account: Account) => void; } @@ -63,12 +68,12 @@ export const useLogins = (): LoginsState => { * Initiate an OAuth device-flow session for the given forge. */ const loginWithDeviceFlowStart = useCallback( - async (forge: Forge, hostname?: Hostname, scopes?: string[]) => { + async (forge: Forge, hostname?: Hostname, scopes?: string[], clientId?: ClientID) => { const { deviceFlow } = getAdapter(forge); if (!deviceFlow) { throw new Error(`Device flow is not supported for forge "${forge}".`); } - return await deviceFlow.start(hostname, scopes); + return await deviceFlow.start(hostname, scopes, clientId); }, [], ); @@ -88,19 +93,21 @@ export const useLogins = (): LoginsState => { * Finalise an OAuth device-flow session by recording the account. */ const loginWithDeviceFlowComplete = useCallback( - async (forge: Forge, token: Token, hostname: Hostname) => { + async (forge: Forge, token: Token, hostname: Hostname, clientId?: ClientID) => { const { deviceFlow } = getAdapter(forge); if (!deviceFlow) { throw new Error(`Forge "${forge}" does not support device flow.`); } - const method = deviceFlow.authMethod; + // A supplied client ID means the user authorized their own app; without + // one the flow used Gitify's preconfigured app. + const method: AuthMethod = clientId ? 'Custom OAuth App' : deviceFlow.authMethod; const existingAccount = accounts.find((a) => a.hostname === hostname && a.method === method); if (existingAccount) { removeAccountNotifications(existingAccount); } - await createAccount(method, token, hostname, forge); + await createAccount(method, token, hostname, forge, undefined, clientId); }, [accounts, createAccount, removeAccountNotifications], ); @@ -135,31 +142,6 @@ export const useLogins = (): LoginsState => { [createAccountInStore, queryClient, removeAccountNotifications], ); - /** - * Login with a custom OAuth app on the given forge. - */ - const loginWithOAuthApp = useCallback( - async (forge: Forge, data: LoginOAuthWebOptions) => { - const { oauthWebApp } = getAdapter(forge); - if (!oauthWebApp) { - throw new Error(`OAuth app login is not supported for forge "${forge}".`); - } - - const { authOptions, authCode } = await oauthWebApp.performWebOAuth(data); - const token = await oauthWebApp.exchangeAuthCodeForToken(authCode, authOptions); - - const existingAccount = accounts.find( - (a) => a.hostname === authOptions.hostname && a.method === 'OAuth App', - ); - if (existingAccount) { - removeAccountNotifications(existingAccount); - } - - await createAccount('OAuth App', token, authOptions.hostname, forge); - }, - [accounts, createAccount, removeAccountNotifications], - ); - /** * Login with Personal Access Token (PAT). */ @@ -203,7 +185,6 @@ export const useLogins = (): LoginsState => { loginWithDeviceFlowPoll, loginWithDeviceFlowComplete, loginWithCli, - loginWithOAuthApp, loginWithPersonalAccessToken, logoutFromAccount, }; diff --git a/src/renderer/routes/AccountScopes.test.tsx b/src/renderer/routes/AccountScopes.test.tsx index 9a0677bed..9754f6dee 100644 --- a/src/renderer/routes/AccountScopes.test.tsx +++ b/src/renderer/routes/AccountScopes.test.tsx @@ -3,9 +3,9 @@ import userEvent from '@testing-library/user-event'; import { renderWithProviders } from '../__helpers__/test-utils'; import { - mockGitHubAppAccount, + mockGitifyOAuthAppAccount, mockGitHubCliAccount, - mockOAuthAccount, + mockCustomOAuthAppAccount, mockPersonalAccessTokenAccount, } from '../__mocks__/account-mocks'; @@ -19,7 +19,7 @@ const navigateMock = vi.fn(); // Mutable variable — individual tests reassign this before rendering to simulate // different account states without needing vi.doMock. let mockLocationAccount: Account = { - ...mockGitHubAppAccount, + ...mockGitifyOAuthAppAccount, scopes: ['read:user', 'notifications', 'repo'], }; @@ -35,7 +35,7 @@ vi.mock('react-router-dom', async (importActual) => { describe('renderer/routes/AccountScopes.tsx', () => { beforeEach(() => { mockLocationAccount = { - ...mockGitHubAppAccount, + ...mockGitifyOAuthAppAccount, scopes: ['read:user', 'notifications', 'repo'], }; }); @@ -97,7 +97,7 @@ describe('renderer/routes/AccountScopes.tsx', () => { it('should show all-granted status when account has public_repo instead of repo', async () => { mockLocationAccount = { - ...mockGitHubAppAccount, + ...mockGitifyOAuthAppAccount, scopes: ['read:user', 'notifications', 'public_repo'], }; @@ -112,7 +112,7 @@ describe('renderer/routes/AccountScopes.tsx', () => { it('should show missing indicator when neither repo nor public_repo is granted', async () => { mockLocationAccount = { - ...mockGitHubAppAccount, + ...mockGitifyOAuthAppAccount, scopes: ['read:user', 'notifications'], }; @@ -128,7 +128,7 @@ describe('renderer/routes/AccountScopes.tsx', () => { it('should show missing indicator when notifications scope is absent', async () => { mockLocationAccount = { - ...mockGitHubAppAccount, + ...mockGitifyOAuthAppAccount, scopes: ['read:user', 'repo'], }; @@ -142,7 +142,7 @@ describe('renderer/routes/AccountScopes.tsx', () => { it('should show extra scopes when token has additional permissions', async () => { mockLocationAccount = { - ...mockGitHubAppAccount, + ...mockGitifyOAuthAppAccount, scopes: ['read:user', 'notifications', 'repo', 'write:packages', 'read:org'], }; @@ -156,7 +156,7 @@ describe('renderer/routes/AccountScopes.tsx', () => { }); it('should show no-scopes message when scopes are not loaded', async () => { - mockLocationAccount = { ...mockGitHubAppAccount, scopes: undefined }; + mockLocationAccount = { ...mockGitifyOAuthAppAccount, scopes: undefined }; await act(async () => { renderWithProviders(); @@ -191,8 +191,8 @@ describe('renderer/routes/AccountScopes.tsx', () => { describe('all auth methods show permissions button', () => { it.each([ - ['GitHub App', mockGitHubAppAccount], - ['OAuth App', mockOAuthAccount], + ['Gitify OAuth App', mockGitifyOAuthAppAccount], + ['Custom OAuth App', mockCustomOAuthAppAccount], ['Personal Access Token', mockPersonalAccessTokenAccount], ])('%s account has a permissions button', async (_method, account) => { mockLocationAccount = { diff --git a/src/renderer/routes/Accounts.test.tsx b/src/renderer/routes/Accounts.test.tsx index 5ff682381..fdf784e2c 100644 --- a/src/renderer/routes/Accounts.test.tsx +++ b/src/renderer/routes/Accounts.test.tsx @@ -4,8 +4,8 @@ import userEvent from '@testing-library/user-event'; import { navigateMock, renderWithProviders } from '../__helpers__/test-utils'; import { mockGiteaAccount, - mockGitHubAppAccount, - mockOAuthAccount, + mockGitifyOAuthAppAccount, + mockCustomOAuthAppAccount, mockPersonalAccessTokenAccount, } from '../__mocks__/account-mocks'; @@ -30,7 +30,11 @@ describe('renderer/routes/Accounts.tsx', () => { it('should render itself & its children', async () => { await act(async () => { renderWithProviders(, { - accounts: [mockPersonalAccessTokenAccount, mockOAuthAccount, mockGitHubAppAccount], + accounts: [ + mockPersonalAccessTokenAccount, + mockCustomOAuthAppAccount, + mockGitifyOAuthAppAccount, + ], }); }); @@ -241,8 +245,8 @@ describe('renderer/routes/Accounts.tsx', () => { ...mockPersonalAccessTokenAccount, scopes: ['read:user', 'notifications'], }, - mockOAuthAccount, - mockGitHubAppAccount, + mockCustomOAuthAppAccount, + mockGitifyOAuthAppAccount, ], }); }); @@ -266,7 +270,11 @@ describe('renderer/routes/Accounts.tsx', () => { it('should set account as primary account', async () => { await act(async () => { renderWithProviders(, { - accounts: [mockPersonalAccessTokenAccount, mockOAuthAccount, mockGitHubAppAccount], + accounts: [ + mockPersonalAccessTokenAccount, + mockCustomOAuthAppAccount, + mockGitifyOAuthAppAccount, + ], }); }); @@ -276,7 +284,7 @@ describe('renderer/routes/Accounts.tsx', () => { await userEvent.click(screen.getAllByTestId('account-set-primary')[1]); - expect(useAccountsStore.getState().accounts[0]).toBe(mockOAuthAccount); + expect(useAccountsStore.getState().accounts[0]).toBe(mockCustomOAuthAppAccount); expect(navigateMock).toHaveBeenCalledWith('/accounts', { replace: true }); }); @@ -318,7 +326,11 @@ describe('renderer/routes/Accounts.tsx', () => { it('should show view-scopes button for all auth methods', async () => { await act(async () => { renderWithProviders(, { - accounts: [mockPersonalAccessTokenAccount, mockOAuthAccount, mockGitHubAppAccount], + accounts: [ + mockPersonalAccessTokenAccount, + mockCustomOAuthAppAccount, + mockGitifyOAuthAppAccount, + ], }); }); @@ -344,7 +356,7 @@ describe('renderer/routes/Accounts.tsx', () => { it('should show github login methods after selecting GitHub forge', async () => { await act(async () => { renderWithProviders(, { - accounts: [mockOAuthAccount], + accounts: [mockCustomOAuthAppAccount], }); }); @@ -367,7 +379,7 @@ describe('renderer/routes/Accounts.tsx', () => { it('should show login with personal access token after selecting GitHub forge', async () => { await act(async () => { renderWithProviders(, { - accounts: [mockOAuthAccount], + accounts: [mockCustomOAuthAppAccount], }); }); @@ -384,7 +396,7 @@ describe('renderer/routes/Accounts.tsx', () => { }); }); - it('should show login with oauth app after selecting GitHub forge', async () => { + it('should show login with a custom OAuth app after selecting GitHub forge', async () => { await act(async () => { renderWithProviders(, { accounts: [mockPersonalAccessTokenAccount], @@ -394,12 +406,14 @@ describe('renderer/routes/Accounts.tsx', () => { await userEvent.click(screen.getByTestId('account-add-new')); await userEvent.click(screen.getByTestId('account-add-forge-github')); - expect(screen.getByTestId('account-add-oauth-app')).toHaveTextContent('OAuth App'); + expect(screen.getByTestId('account-add-custom-oauth-app')).toHaveTextContent( + 'Custom OAuth App', + ); - await userEvent.click(screen.getByTestId('account-add-oauth-app')); + await userEvent.click(screen.getByTestId('account-add-custom-oauth-app')); expect(navigateMock).toHaveBeenCalledTimes(1); - expect(navigateMock).toHaveBeenCalledWith('/login/github/oauth-app', { + expect(navigateMock).toHaveBeenCalledWith('/login/github/custom-oauth-app', { replace: true, }); }); @@ -407,7 +421,7 @@ describe('renderer/routes/Accounts.tsx', () => { it('should navigate directly to gitea login (single method forge)', async () => { await act(async () => { renderWithProviders(, { - accounts: [mockOAuthAccount], + accounts: [mockCustomOAuthAppAccount], }); }); @@ -426,7 +440,7 @@ describe('renderer/routes/Accounts.tsx', () => { it('should navigate directly to bitbucket login (single method forge)', async () => { await act(async () => { renderWithProviders(, { - accounts: [mockOAuthAccount], + accounts: [mockCustomOAuthAppAccount], }); }); @@ -472,10 +486,10 @@ describe('renderer/routes/Accounts.tsx', () => { it('should re-authenticate a github app account', async () => { await act(async () => { renderWithProviders(, { - accounts: [mockGitHubAppAccount], + accounts: [mockGitifyOAuthAppAccount], notifications: [ { - account: mockGitHubAppAccount, + account: mockGitifyOAuthAppAccount, notifications: [], error: Errors.BAD_CREDENTIALS, }, @@ -488,17 +502,17 @@ describe('renderer/routes/Accounts.tsx', () => { expect(navigateMock).toHaveBeenCalledTimes(1); expect(navigateMock).toHaveBeenCalledWith('/login/github/device-flow', { replace: true, - state: { account: mockGitHubAppAccount }, + state: { account: mockGitifyOAuthAppAccount }, }); }); - it('should re-authenticate an oauth app account', async () => { + it('should re-authenticate a custom OAuth app account', async () => { await act(async () => { renderWithProviders(, { - accounts: [mockOAuthAccount], + accounts: [mockCustomOAuthAppAccount], notifications: [ { - account: mockOAuthAccount, + account: mockCustomOAuthAppAccount, notifications: [], error: Errors.BAD_CREDENTIALS, }, @@ -509,9 +523,9 @@ describe('renderer/routes/Accounts.tsx', () => { await userEvent.click(screen.getByTestId('account-reauthenticate')); expect(navigateMock).toHaveBeenCalledTimes(1); - expect(navigateMock).toHaveBeenCalledWith('/login/github/oauth-app', { + expect(navigateMock).toHaveBeenCalledWith('/login/github/custom-oauth-app', { replace: true, - state: { account: mockOAuthAccount }, + state: { account: mockCustomOAuthAppAccount }, }); }); @@ -540,10 +554,10 @@ describe('renderer/routes/Accounts.tsx', () => { it('should log and skip re-authenticate when no login method matches', async () => { const rendererLogErrorSpy = vi.spyOn(logger, 'rendererLogError').mockImplementation(vi.fn()); - // Gitea only registers PAT — a GitHub App method has no matching loginMethod. + // Gitea only registers PAT — a Custom OAuth App method has no matching loginMethod. const unsupportedAccount = { ...mockGiteaAccount, - method: 'GitHub App' as const, + method: 'Custom OAuth App' as const, }; await act(async () => { diff --git a/src/renderer/routes/Login.test.tsx b/src/renderer/routes/Login.test.tsx index f907c2747..55db58e0a 100644 --- a/src/renderer/routes/Login.test.tsx +++ b/src/renderer/routes/Login.test.tsx @@ -31,7 +31,7 @@ describe('renderer/routes/Login.tsx', () => { it.each([ ['login-github', '/login/github/device-flow'], ['login-pat', '/login/github/personal-access-token'], - ['login-oauth-app', '/login/github/oauth-app'], + ['login-custom-oauth-app', '/login/github/custom-oauth-app'], ])('navigates %s', async (testId, route) => { renderWithProviders(); diff --git a/src/renderer/routes/__screenshots__/routes.visual.test.tsx/login-dark-classic-chromium-linux.png b/src/renderer/routes/__screenshots__/routes.visual.test.tsx/login-dark-classic-chromium-linux.png index caea7edd6b70921f93c7f67f9fd6f8f4c01b4e68..d710632b4566f410f9c07d03cc3df1074a3286a0 100644 GIT binary patch literal 28527 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

n=Fm-(Cqj?bl^Y3=aaF4Ut`SXx3<1@VF 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

s@kNg*6)4(5Ti?CtG3 z2jr8TqhqY1XwE{_XAHf#h`S0kq9sj0LEdCgBx350^QYf8_-`Yt&ZEG+QahzYmz!m)amw{H(60{mHX zc6YJ$@g92B9ZLY(mzSj6d;@DF`mL@Wf5*GQM|P+qMLFgq^bs6eS|ZZ{?DTSEG&Aio zWxc0#XSqqiUIY~DklW6x!k`___N9`^pA`N^`|cHhU2krl#Ifm%Vc)(qGNF%!jMDnR zz+f&uog<)7IWF_$`tOm*;h=8(wri^Ru=@+y>u*8c-VbR@jn9-3-=xESDF#EB5HlaC zy*Y>UOo>1+-2#paj9qiD6UwuGaX-mvI%~xBCniDf+nrb6G0{r}Q+kYJtoq*Xn=D~bJJ!V{dyxZ9! zS^7wPMNJAR&@6)90_ii0Ge7=b$6WL!#_o#SvF%AktQC)Tm5jEw<%y3An3meiW%(}z zjR}#5Km8@{kg7On9)=|#Ybre_rn&ccwzjrHYyef&@HooF&i-vNA#%0Ah_JVAxYbtzbKb~*1>7_kvLy#^Gz`KL<6r^bn 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 Oz{tFle_JYU;PWqHULq|3 literal 35196 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 YchSiyNgGVyzr2zZl@loy()0a4076q=O8@`> literal 0 HcmV?d00001 diff --git a/src/renderer/routes/__screenshots__/routes.visual.test.tsx/login-github-custom-oauth-app-light-classic-chromium-linux.png b/src/renderer/routes/__screenshots__/routes.visual.test.tsx/login-github-custom-oauth-app-light-classic-chromium-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..e30e7e1f28758dbd5452117ee77ead218d338627 GIT binary patch literal 33539 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
?kR+6yomIas`)+pkqs<&sE@AYH%> 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 aVYn~~IIz$7x4~CnzKY6xE)mxD{yzYCH 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 diff --git a/src/renderer/routes/__screenshots__/routes.visual.test.tsx/login-github-oauth-app-light-classic-chromium-linux.png b/src/renderer/routes/__screenshots__/routes.visual.test.tsx/login-github-oauth-app-light-classic-chromium-linux.png deleted file mode 100644 index cc534051b00d1c15c999cdb6d68f48eafa27abd8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 40626 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 F0RXFcQGWmc diff --git a/src/renderer/routes/__screenshots__/routes.visual.test.tsx/login-light-classic-chromium-linux.png b/src/renderer/routes/__screenshots__/routes.visual.test.tsx/login-light-classic-chromium-linux.png index cb7b1528cf2b025cdf275505dc4414d659f3260d..fbc85fc5909bf9b50ed3fb907bdc0cf3cc421f8e 100644 GIT binary patch literal 28372 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 literal 35628 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 diff --git a/src/renderer/routes/__snapshots__/Login.test.tsx.snap b/src/renderer/routes/__snapshots__/Login.test.tsx.snap index e4af27471..da70146dd 100644 --- a/src/renderer/routes/__snapshots__/Login.test.tsx.snap +++ b/src/renderer/routes/__snapshots__/Login.test.tsx.snap @@ -364,7 +364,7 @@ exports[`renderer/routes/Login.tsx > should render itself & its children 1`] = ` data-component="Button" data-loading="false" data-size="medium" - data-testid="login-oauth-app" + data-testid="login-custom-oauth-app" data-variant="invisible" type="button" > @@ -399,7 +399,7 @@ exports[`renderer/routes/Login.tsx > should render itself & its children 1`] = ` class="prc-Button-Label-FWkx3" data-component="text" > - OAuth App + Custom OAuth App diff --git a/src/renderer/routes/github/LoginWithCustomOAuthApp.test.tsx b/src/renderer/routes/github/LoginWithCustomOAuthApp.test.tsx new file mode 100644 index 000000000..0987888e6 --- /dev/null +++ b/src/renderer/routes/github/LoginWithCustomOAuthApp.test.tsx @@ -0,0 +1,104 @@ +import { screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { renderWithProviders } from '../../__helpers__/test-utils'; +import { mockCustomOAuthAppAccount } from '../../__mocks__/account-mocks'; + +import * as comms from '../../utils/system/comms'; +import { GitHubLoginWithCustomOAuthAppRoute } from './LoginWithCustomOAuthApp'; + +describe('renderer/routes/github/LoginWithCustomOAuthApp.tsx', () => { + const copyToClipboardSpy = vi.spyOn(comms, 'copyToClipboard').mockResolvedValue(); + const openExternalLinkSpy = vi.spyOn(comms, 'openExternalLink').mockImplementation(vi.fn()); + + const session = { + hostname: 'github.example.com', + clientId: 'CUSTOM_CLIENT_ID_123', + deviceCode: 'device-code', + userCode: 'USER-1234', + verificationUri: 'https://github.example.com/login/device', + intervalSeconds: 5, + expiresAt: Date.now() + 900000, + }; + + it('collects a hostname and client ID, and no client secret', () => { + renderWithProviders(); + + expect(screen.getByTestId('login-hostname')).toBeInTheDocument(); + expect(screen.getByTestId('login-clientId')).toBeInTheDocument(); + expect(screen.queryByTestId('login-clientSecret')).not.toBeInTheDocument(); + }); + + it('shows validation errors for an invalid hostname and client ID', async () => { + renderWithProviders(); + + const hostname = screen.getByTestId('login-hostname'); + await userEvent.clear(hostname); + await userEvent.type(hostname, 'test'); + await userEvent.type(screen.getByTestId('login-clientId'), 'short'); + + await userEvent.click(screen.getByTestId('login-submit')); + + expect(screen.getByText('Hostname format is invalid')).toBeInTheDocument(); + expect(screen.getByText('Client ID format is invalid')).toBeInTheDocument(); + // Still on the credentials step — the device flow was not started. + expect(screen.getByTestId('login-submit')).toBeInTheDocument(); + }); + + it('reuses the scope step and starts the device flow with the supplied credentials', async () => { + const loginWithDeviceFlowStartMock = vi.fn().mockResolvedValueOnce(session); + + renderWithProviders(, { + loginWithDeviceFlowStart: loginWithDeviceFlowStartMock, + loginWithDeviceFlowPoll: vi.fn().mockResolvedValue(null), + }); + + const hostname = screen.getByTestId('login-hostname'); + await userEvent.clear(hostname); + await userEvent.type(hostname, 'github.example.com'); + await userEvent.type(screen.getByTestId('login-clientId'), 'CUSTOM_CLIENT_ID_123'); + + await userEvent.click(screen.getByTestId('login-submit')); + + // The shared public/recommended scope step is shown before the session starts. + expect(screen.getByTestId('device-scope-full')).toBeInTheDocument(); + await userEvent.click(screen.getByTestId('device-scope-full')); + + await waitFor(() => { + expect(loginWithDeviceFlowStartMock).toHaveBeenCalledWith( + 'github', + 'github.example.com', + ['notifications', 'read:user', 'repo'], + 'CUSTOM_CLIENT_ID_123', + ); + }); + + await screen.findByTestId('device-user-code'); + expect(copyToClipboardSpy).toHaveBeenCalledWith('USER-1234'); + }); + + it('opens the OAuth app registration page without a callback URL', async () => { + renderWithProviders(); + + await userEvent.click(screen.getByTestId('login-create-oauth-app')); + + expect(openExternalLinkSpy).toHaveBeenCalledWith( + expect.stringContaining('https://github.com/settings/applications/new'), + ); + expect(openExternalLinkSpy).not.toHaveBeenCalledWith(expect.stringContaining('callback_url')); + }); + + it('prefills the hostname and client ID from the re-auth account', () => { + renderWithProviders(, { + initialEntries: [ + { + pathname: '/login/github/custom-oauth-app', + state: { account: mockCustomOAuthAppAccount }, + }, + ], + }); + + expect(screen.getByTestId('login-hostname')).toHaveValue('github.gitify.io'); + expect(screen.getByTestId('login-clientId')).toHaveValue('CUSTOM_CLIENT_ID_123'); + }); +}); diff --git a/src/renderer/routes/github/LoginWithCustomOAuthApp.tsx b/src/renderer/routes/github/LoginWithCustomOAuthApp.tsx new file mode 100644 index 000000000..0c9ebc8e5 --- /dev/null +++ b/src/renderer/routes/github/LoginWithCustomOAuthApp.tsx @@ -0,0 +1,164 @@ +import { useState } from 'react'; +import { useLocation } from 'react-router-dom'; + +import { BookIcon, PersonIcon } from '@primer/octicons-react'; +import { Button, FormControl, Stack, Text, TextInput, Tooltip } from '@primer/react'; + +import { Constants } from '../../constants'; + +import { Contents } from '../../components/layout/Contents'; +import { Page } from '../../components/layout/Page'; +import { Footer } from '../../components/primitives/Footer'; +import { Header } from '../../components/primitives/Header'; + +import type { Account, ClientID, Forge, Hostname } from '../../types'; + +import { + type CustomOAuthAppFormData, + type CustomOAuthAppFormErrors, + validateCustomOAuthAppForm, +} from '../../utils/auth/loginValidation'; +import { getAdapter } from '../../utils/forges/registry'; +import { openExternalLink } from '../../utils/system/comms'; +import { GitHubLoginWithDeviceFlowRoute } from './LoginWithDeviceFlow'; + +interface LocationState { + account?: Account; +} + +/** + * Sign in with a user-supplied GitHub OAuth app. + * + * Collects the app's hostname and client ID (a public identifier — never a + * secret), then hands off to the shared device-flow authorization UI. + */ +export function GitHubLoginWithCustomOAuthAppRoute() { + const location = useLocation(); + const { account: reAuthAccount } = (location.state ?? {}) as LocationState; + + const forge: Forge = 'github'; + + const [formData, setFormData] = useState({ + hostname: (reAuthAccount?.hostname ?? Constants.GITHUB_HOSTNAME) as Hostname, + clientId: (reAuthAccount?.clientId ?? '') as ClientID, + }); + const [errors, setErrors] = useState({}); + const [credentials, setCredentials] = useState(null); + + const handleInputChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormData((prev) => ({ ...prev, [name]: value })); + }; + + const handleSubmit = () => { + const newErrors = validateCustomOAuthAppForm(formData, forge); + + setErrors(newErrors); + + if (!newErrors.hostname && !newErrors.clientId) { + setCredentials(formData); + } + }; + + if (credentials) { + return ( + + ); + } + + return ( + +

Login with Custom OAuth App
+ + + + + Authorize Gitify with your own GitHub OAuth app. Create an app, enable its device flow, + then paste its client ID below. + + + + Hostname + + Change only if you are using GitHub Enterprise Server + + + {errors.hostname && ( + {errors.hostname} + )} + + + + + + and enable its device flow, then use its client ID below. + + + + + Client ID + + {errors.clientId && ( + {errors.clientId} + )} + + + + +
+ + + + + +
+ + ); +} diff --git a/src/renderer/routes/github/LoginWithDeviceFlow.test.tsx b/src/renderer/routes/github/LoginWithDeviceFlow.test.tsx index 8b42a6158..3623b1c0e 100644 --- a/src/renderer/routes/github/LoginWithDeviceFlow.test.tsx +++ b/src/renderer/routes/github/LoginWithDeviceFlow.test.tsx @@ -2,9 +2,9 @@ import { screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { navigateMock, renderWithProviders } from '../../__helpers__/test-utils'; -import { mockGitHubAppAccount } from '../../__mocks__/account-mocks'; +import { mockGitifyOAuthAppAccount } from '../../__mocks__/account-mocks'; -import type { Hostname } from '../../types'; +import type { ClientID, Hostname } from '../../types'; import * as logger from '../../utils/core/logger'; import * as comms from '../../utils/system/comms'; @@ -54,11 +54,12 @@ describe('renderer/routes/github/LoginWithDeviceFlow.tsx', () => { await userEvent.click(screen.getByTestId('device-scope-public')); - expect(loginWithDeviceFlowStartMock).toHaveBeenCalledWith('github', undefined, [ - 'notifications', - 'read:user', - 'public_repo', - ]); + expect(loginWithDeviceFlowStartMock).toHaveBeenCalledWith( + 'github', + 'github.com', + ['notifications', 'read:user', 'public_repo'], + undefined, + ); await screen.findByTestId('device-user-code'); expect(screen.getByTestId('open-browser-button')).toBeInTheDocument(); @@ -85,11 +86,12 @@ describe('renderer/routes/github/LoginWithDeviceFlow.tsx', () => { await userEvent.click(screen.getByTestId('device-scope-full')); - expect(loginWithDeviceFlowStartMock).toHaveBeenCalledWith('github', undefined, [ - 'notifications', - 'read:user', - 'repo', - ]); + expect(loginWithDeviceFlowStartMock).toHaveBeenCalledWith( + 'github', + 'github.com', + ['notifications', 'read:user', 'repo'], + undefined, + ); await screen.findByTestId('device-user-code'); }); @@ -234,6 +236,7 @@ describe('renderer/routes/github/LoginWithDeviceFlow.tsx', () => { 'github', 'access-token', 'github.com', + undefined, ); expect(navigateMock).toHaveBeenCalledWith('/'); }); @@ -314,7 +317,7 @@ describe('renderer/routes/github/LoginWithDeviceFlow.tsx', () => { pathname: '/login/github/device-flow', state: { account: { - ...mockGitHubAppAccount, + ...mockGitifyOAuthAppAccount, hostname: 'github.enterprise.example' as Hostname, }, }, @@ -328,6 +331,36 @@ describe('renderer/routes/github/LoginWithDeviceFlow.tsx', () => { 'github', 'github.enterprise.example', ['notifications', 'read:user', 'repo'], + undefined, + ); + }); + + it('threads a custom client ID and hostname through the device flow', async () => { + const loginWithDeviceFlowStartMock = vi.fn().mockResolvedValueOnce({ + hostname: 'github.example.com', + clientId: 'CUSTOM_CLIENT_ID_123', + deviceCode: 'device-code', + userCode: 'USER-1234', + verificationUri: 'https://github.example.com/login/device', + intervalSeconds: 5, + expiresAt: Date.now() + 900000, + }); + + renderWithProviders( + , + { loginWithDeviceFlowStart: loginWithDeviceFlowStartMock }, + ); + + await userEvent.click(screen.getByTestId('device-scope-full')); + + expect(loginWithDeviceFlowStartMock).toHaveBeenCalledWith( + 'github', + 'github.example.com', + ['notifications', 'read:user', 'repo'], + 'CUSTOM_CLIENT_ID_123', ); }); diff --git a/src/renderer/routes/github/LoginWithDeviceFlow.tsx b/src/renderer/routes/github/LoginWithDeviceFlow.tsx index eca790539..83f8fbebb 100644 --- a/src/renderer/routes/github/LoginWithDeviceFlow.tsx +++ b/src/renderer/routes/github/LoginWithDeviceFlow.tsx @@ -20,11 +20,12 @@ import { Page } from '../../components/layout/Page'; import { Footer } from '../../components/primitives/Footer'; import { Header } from '../../components/primitives/Header'; -import type { Account, Forge, Hostname, Link } from '../../types'; +import type { Account, ClientID, Forge, Hostname, Link } from '../../types'; import type { DeviceFlowSession } from '../../utils/auth/types'; import { getAlternateScopeNames, getRecommendedScopeNames } from '../../utils/auth/scopes'; import { rendererLogError, toError } from '../../utils/core/logger'; +import { describeDeviceFlowError } from '../../utils/forges/github/flows'; import { getAdapter } from '../../utils/forges/registry'; import { copyToClipboard, openExternalLink } from '../../utils/system/comms'; @@ -32,15 +33,32 @@ interface LocationState { account?: Account; } +interface GitHubLoginWithDeviceFlowRouteProps { + /** OAuth app client ID to authorize; omit to use Gitify's preconfigured app. */ + clientId?: ClientID; + /** Hostname to authenticate against; defaults to the re-auth account or GitHub.com. */ + hostname?: Hostname; + /** Page heading; defaults to authorizing Gitify's preconfigured app. */ + title?: string; +} + type ScopeChoice = 'public' | 'full'; -export function GitHubLoginWithDeviceFlowRoute() { +export function GitHubLoginWithDeviceFlowRoute({ + clientId, + hostname, + title, +}: GitHubLoginWithDeviceFlowRouteProps) { const navigate = useNavigate(); const location = useLocation(); const { account: reAuthAccount } = (location.state ?? {}) as LocationState; const forge: Forge = 'github'; + const effectiveHostname = (hostname ?? + reAuthAccount?.hostname ?? + Constants.GITHUB_HOSTNAME) as Hostname; + const { loginWithDeviceFlowStart, loginWithDeviceFlowPoll, loginWithDeviceFlowComplete } = useLogins(); @@ -56,7 +74,12 @@ export function GitHubLoginWithDeviceFlowRoute() { const scopes = scopeChoice === 'public' ? getAlternateScopeNames() : getRecommendedScopeNames(); - const newSession = await loginWithDeviceFlowStart(forge, reAuthAccount?.hostname, scopes); + const newSession = await loginWithDeviceFlowStart( + forge, + effectiveHostname, + scopes, + clientId, + ); setSession(newSession); // Auto-copy the user code to clipboard @@ -66,14 +89,17 @@ export function GitHubLoginWithDeviceFlowRoute() { openExternalLink(newSession.verificationUri as Link); } catch (err) { rendererLogError('LoginWithDeviceFlow', 'Failed to start device flow', toError(err)); - setError('Failed to start authentication. Please try again.'); + setError( + describeDeviceFlowError(err, effectiveHostname, clientId) ?? + 'Failed to start authentication. Please try again.', + ); } }; if (scopeChoice) { void initializeDeviceFlow(); } - }, [loginWithDeviceFlowStart, reAuthAccount, scopeChoice, forge]); + }, [loginWithDeviceFlowStart, effectiveHostname, clientId, scopeChoice, forge]); // Poll for device flow completion useEffect(() => { @@ -98,7 +124,7 @@ export function GitHubLoginWithDeviceFlowRoute() { const token = await loginWithDeviceFlowPoll(forge, session); if (token && isActive) { - await loginWithDeviceFlowComplete(forge, token, session.hostname); + await loginWithDeviceFlowComplete(forge, token, session.hostname, clientId); void navigate('/'); return; } @@ -133,7 +159,7 @@ export function GitHubLoginWithDeviceFlowRoute() { } }; // oxlint-disable-next-line react/exhaustive-deps -- navigate is stable - }, [session, loginWithDeviceFlowPoll, loginWithDeviceFlowComplete, forge]); + }, [session, loginWithDeviceFlowPoll, loginWithDeviceFlowComplete, clientId, forge]); const handleCopyUserCode = useCallback(async () => { if (session?.userCode) { @@ -249,9 +275,7 @@ export function GitHubLoginWithDeviceFlowRoute() { {(() => { const adapter = getAdapter(forge); - const revokeUrl = adapter.deviceFlow?.getRevokeAccessUrl( - (reAuthAccount?.hostname ?? Constants.GITHUB_HOSTNAME) as Hostname, - ); + const revokeUrl = adapter.deviceFlow?.getRevokeAccessUrl(effectiveHostname, clientId); if (!revokeUrl) { return null; } @@ -259,7 +283,9 @@ export function GitHubLoginWithDeviceFlowRoute() { return ( - Note: to change previously granted permissions, revoke Gitify's access at{' '} + {clientId + ? "Note: to change previously granted permissions, revoke your app's access at" + : "Note: to change previously granted permissions, revoke Gitify's access at"}{' '} - - and use your client id & secret below. - - - - Client ID - - {errors.clientId && ( - {errors.clientId} - )} - - - Client Secret - setShouldMaskClientSecret(!shouldMaskClientSecret)} - /> - } - type={shouldMaskClientSecret ? 'password' : 'text'} - value={formData.clientSecret} - /> - {errors.clientSecret && ( - {errors.clientSecret} - )} - - - - -
- - - - - -
- - ); -} diff --git a/src/renderer/routes/github/__snapshots__/LoginWithOAuthApp.test.tsx.snap b/src/renderer/routes/github/__snapshots__/LoginWithOAuthApp.test.tsx.snap deleted file mode 100644 index 1e0136890..000000000 --- a/src/renderer/routes/github/__snapshots__/LoginWithOAuthApp.test.tsx.snap +++ /dev/null @@ -1,487 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`renderer/routes/github/LoginWithOAuthApp.tsx > renders correctly 1`] = ` -
-
-
-
- - -
-
- -

- Login with OAuth App -

-
-
-
-
-
-
-
-
- - - - - - - Change only if you are using GitHub Enterprise Server - - -
-
- - - and use your - - client id & secret - - below. - -
-
- - - - -
-
- - - - - - - - -
-
-
- -
-
-`; diff --git a/src/renderer/routes/routes.visual.test.tsx b/src/renderer/routes/routes.visual.test.tsx index bb95e71b5..2c955687a 100644 --- a/src/renderer/routes/routes.visual.test.tsx +++ b/src/renderer/routes/routes.visual.test.tsx @@ -17,9 +17,9 @@ import { AccountScopesRoute } from './AccountScopes'; import { BitbucketLoginWithPersonalAccessTokenRoute } from './bitbucket/LoginWithPersonalAccessToken'; import { FiltersRoute } from './Filters'; import { GiteaLoginWithPersonalAccessTokenRoute } from './gitea/LoginWithPersonalAccessToken'; +import { GitHubLoginWithCustomOAuthAppRoute } from './github/LoginWithCustomOAuthApp'; import { GitHubLoginWithDeviceFlowRoute } from './github/LoginWithDeviceFlow'; import { GitHubLoginWithCLIRoute } from './github/LoginWithGitHubCLI'; -import { GitHubLoginWithOAuthAppRoute } from './github/LoginWithOAuthApp'; import { GitHubLoginWithPersonalAccessTokenRoute } from './github/LoginWithPersonalAccessToken'; import { GitLabLoginWithPersonalAccessTokenRoute } from './gitlab/LoginWithPersonalAccessToken'; import { LoginRoute } from './Login'; @@ -89,9 +89,9 @@ const ROUTES: RouteCase[] = [ state: { initialEntries: ['/login/github/personal-access-token'] }, }, { - name: 'login-github-oauth-app', - element: , - state: { initialEntries: ['/login/github/oauth-app'] }, + name: 'login-github-custom-oauth-app', + element: , + state: { initialEntries: ['/login/github/custom-oauth-app'] }, }, { name: 'login-gitea-personal-access-token', diff --git a/src/renderer/stores/types.ts b/src/renderer/stores/types.ts index c7ca4575f..287391c18 100644 --- a/src/renderer/stores/types.ts +++ b/src/renderer/stores/types.ts @@ -1,6 +1,7 @@ import type { Account, AccountUUID, + ClientID, FilterStateType, Forge, GitifyError, @@ -40,6 +41,7 @@ export interface AccountsActions { hostname: Hostname, forge?: Forge, username?: string, + clientId?: ClientID, ) => Promise; /** diff --git a/src/renderer/stores/useAccountsStore.test.ts b/src/renderer/stores/useAccountsStore.test.ts index 8a1b49df6..c900c51c9 100644 --- a/src/renderer/stores/useAccountsStore.test.ts +++ b/src/renderer/stores/useAccountsStore.test.ts @@ -1,6 +1,7 @@ import { act, renderHook } from '@testing-library/react'; import { + mockCustomOAuthAppAccount, mockGiteaAccount, mockGitHubCliAccount, mockGitHubCloudAccount, @@ -8,7 +9,8 @@ import { } from '../__mocks__/account-mocks'; import { mockRawUser } from '../utils/forges/github/__mocks__/response-mocks'; -import type { Account, Forge, Hostname, Link, Token } from '../types'; +import type { Account, ClientID, Forge, Hostname, Link, Token } from '../types'; +import type { AuthMethod } from '../utils/auth/types'; import type { GetAuthenticatedUserResponse } from '../utils/forges/github/types'; import { getRecommendedScopeNames } from '../utils/auth/scopes'; @@ -90,6 +92,25 @@ describe('renderer/stores/useAccountsStore.ts', () => { ); }, ); + + it('relabels a legacy GitHub App account as Gitify OAuth App without losing its token', () => { + const [account] = sanitizeAccounts([ + { ...mockGitHubCloudAccount, method: 'GitHub App' as AuthMethod }, + ]); + + expect(account.method).toBe('Gitify OAuth App'); + expect(account.token).toBe(mockGitHubCloudAccount.token); + }); + + it('relabels a legacy OAuth App account as Custom OAuth App and keeps its client ID', () => { + const [account] = sanitizeAccounts([ + { ...mockCustomOAuthAppAccount, method: 'OAuth App' as AuthMethod }, + ]); + + expect(account.method).toBe('Custom OAuth App'); + expect(account.token).toBe(mockCustomOAuthAppAccount.token); + expect(account.clientId).toBe('CUSTOM_CLIENT_ID_123'); + }); }); it.each([ @@ -201,16 +222,24 @@ describe('renderer/stores/useAccountsStore.ts', () => { ]); }); - test('should add oauth app account', async () => { + test('should add a custom OAuth app account with its client ID', async () => { await useAccountsStore .getState() - .createAccount('OAuth App', '123-456' as Token, 'github.com' as Hostname); + .createAccount( + 'Custom OAuth App', + '123-456' as Token, + 'github.com' as Hostname, + 'github', + undefined, + 'CUSTOM_CLIENT_ID_123' as ClientID, + ); expect(useAccountsStore.getState().accounts).toEqual([ { forge: 'github', hostname: 'github.com' as Hostname, - method: 'OAuth App', + method: 'Custom OAuth App', + clientId: 'CUSTOM_CLIENT_ID_123' as ClientID, platform: 'GitHub Cloud', scopes: getRecommendedScopeNames(), token: 'encrypted' as Token, @@ -306,16 +335,24 @@ describe('renderer/stores/useAccountsStore.ts', () => { ]); }); - test('should add oauth app account', async () => { + test('should add a custom OAuth app account', async () => { await useAccountsStore .getState() - .createAccount('OAuth App', '123-456' as Token, 'github.gitify.io' as Hostname); + .createAccount( + 'Custom OAuth App', + '123-456' as Token, + 'github.gitify.io' as Hostname, + 'github', + undefined, + 'CUSTOM_CLIENT_ID_123' as ClientID, + ); expect(useAccountsStore.getState().accounts).toEqual([ { forge: 'github', hostname: 'github.gitify.io' as Hostname, - method: 'OAuth App', + method: 'Custom OAuth App', + clientId: 'CUSTOM_CLIENT_ID_123' as ClientID, platform: 'GitHub Enterprise Server', scopes: getRecommendedScopeNames(), token: 'encrypted' as Token, diff --git a/src/renderer/stores/useAccountsStore.ts b/src/renderer/stores/useAccountsStore.ts index a689d30a8..eb0e67752 100644 --- a/src/renderer/stores/useAccountsStore.ts +++ b/src/renderer/stores/useAccountsStore.ts @@ -3,7 +3,7 @@ import { persist } from 'zustand/middleware'; import { Constants } from '../constants'; -import type { Account, Forge, Hostname, Token } from '../types'; +import type { Account, ClientID, Forge, Hostname, Token } from '../types'; import type { AuthMethod } from '../utils/auth/types'; import type { AccountsState, AccountsStore } from './types'; @@ -13,11 +13,30 @@ import { getAccountAdapter, getAdapter, isKnownForge } from '../utils/forges/reg import { decryptValue, encryptValue } from '../utils/system/comms'; import { DEFAULT_ACCOUNTS_STATE } from './defaults'; +/** + * Map a persisted auth-method label onto the current vocabulary. + * + * Accounts stored before the custom OAuth app flow moved to the device flow + * carry the retired `GitHub App` / `OAuth App` labels; relabel them so their + * existing tokens keep working without forcing a re-login. + */ +function migrateAuthMethod(method: string): AuthMethod { + switch (method) { + case 'GitHub App': + return 'Gitify OAuth App'; + case 'OAuth App': + return 'Custom OAuth App'; + default: + return method as AuthMethod; + } +} + /** * Sanitise persisted accounts state. Persisted JSON is untrusted (XSS, malicious * extension, file-system tampering), so we project only the known fields, - * coerce `forge` to a registered value, and drop accounts with a - * structurally invalid hostname before the renderer ever touches them. + * coerce `forge` to a registered value, migrate retired auth-method labels, and + * drop accounts with a structurally invalid hostname before the renderer ever + * touches them. */ export function sanitizeAccounts(accounts: Account[]): Account[] { return accounts.flatMap((a) => { @@ -27,11 +46,12 @@ export function sanitizeAccounts(accounts: Account[]): Account[] { } const sanitised: Account = { forge, - method: a.method, + method: migrateAuthMethod(a.method as string), platform: a.platform, version: a.version, hostname: a.hostname, token: a.token, + clientId: a.clientId, username: a.username, user: a.user, scopes: a.scopes, @@ -57,6 +77,7 @@ const useAccountsStore = create()( hostname: Hostname, forge: Forge = 'github', username?: string, + clientId?: ClientID, ) => { const encryptedToken = (await encryptValue(token)) as Token; @@ -66,6 +87,7 @@ const useAccountsStore = create()( method: method, platform: getAdapter(forge).getPlatform(hostname), token: encryptedToken, + clientId, username, user: null, // Will be updated during the refresh call below }; diff --git a/src/renderer/types.ts b/src/renderer/types.ts index 74298639c..6b5ba4be0 100644 --- a/src/renderer/types.ts +++ b/src/renderer/types.ts @@ -27,14 +27,10 @@ type Brand = { [__brand]: B }; export type Branded = T & Brand; -export type AuthCode = Branded; - export type Token = Branded; export type ClientID = Branded; -export type ClientSecret = Branded; - export type Hostname = Branded; export type Link = Branded; @@ -73,6 +69,11 @@ export interface Account { version?: string; hostname: Hostname; token: Token; + /** + * OAuth app client ID for an account authorized through a user-supplied + * custom OAuth app; absent for Gitify's own preconfigured app. + */ + clientId?: ClientID; /** Atlassian account email — required for Bitbucket Basic Auth (email:token). */ username?: string; user: GitifyUser | null; diff --git a/src/renderer/utils/auth/loginValidation.test.ts b/src/renderer/utils/auth/loginValidation.test.ts index 6e4e136cc..0e34e1454 100644 --- a/src/renderer/utils/auth/loginValidation.test.ts +++ b/src/renderer/utils/auth/loginValidation.test.ts @@ -1,10 +1,10 @@ -import type { ClientID, ClientSecret, Forge, Hostname, Token } from '../../types'; +import type { ClientID, Forge, Hostname, Token } from '../../types'; import { - type OAuthAppFormData, + type CustomOAuthAppFormData, type PersonalAccessTokenFormData, summarizeLoginError, - validateOAuthAppForm, + validateCustomOAuthAppForm, validatePersonalAccessTokenForm, } from './loginValidation'; @@ -153,29 +153,34 @@ describe('renderer/utils/auth/loginValidation.ts', () => { }); }); - describe('validateOAuthAppForm', () => { - it('reports missing hostname, client ID, and client secret', () => { - const values: OAuthAppFormData = { + describe('validateCustomOAuthAppForm', () => { + it('reports missing hostname and client ID', () => { + const values: CustomOAuthAppFormData = { hostname: null as unknown as Hostname, clientId: null as unknown as ClientID, - clientSecret: null as unknown as ClientSecret, }; - expect(validateOAuthAppForm(values).hostname).toBe('Hostname is required'); - expect(validateOAuthAppForm(values).clientId).toBe('Client ID is required'); - expect(validateOAuthAppForm(values).clientSecret).toBe('Client Secret is required'); + expect(validateCustomOAuthAppForm(values).hostname).toBe('Hostname is required'); + expect(validateCustomOAuthAppForm(values).clientId).toBe('Client ID is required'); }); - it('reports invalid hostname, client ID, and client secret formats', () => { - const values: OAuthAppFormData = { + it('reports invalid hostname and client ID formats', () => { + const values: CustomOAuthAppFormData = { hostname: 'hello' as Hostname, clientId: '!@£INVALID-.1' as ClientID, - clientSecret: '!@£INVALID-.1' as ClientSecret, }; - expect(validateOAuthAppForm(values).hostname).toBe('Hostname format is invalid'); - expect(validateOAuthAppForm(values).clientId).toBe('Client ID format is invalid'); - expect(validateOAuthAppForm(values).clientSecret).toBe('Client Secret format is invalid'); + expect(validateCustomOAuthAppForm(values).hostname).toBe('Hostname format is invalid'); + expect(validateCustomOAuthAppForm(values).clientId).toBe('Client ID format is invalid'); + }); + + it('passes with a valid hostname and client ID and no client secret', () => { + const values: CustomOAuthAppFormData = { + hostname: 'github.com' as Hostname, + clientId: '1234567890_ASDFGHJKL' as ClientID, + }; + + expect(validateCustomOAuthAppForm(values)).toEqual({}); }); }); diff --git a/src/renderer/utils/auth/loginValidation.ts b/src/renderer/utils/auth/loginValidation.ts index e076f9fde..0b7db50af 100644 --- a/src/renderer/utils/auth/loginValidation.ts +++ b/src/renderer/utils/auth/loginValidation.ts @@ -1,5 +1,4 @@ -import type { Forge, Hostname, Token } from '../../types'; -import type { LoginOAuthWebOptions } from './types'; +import type { ClientID, Forge, Hostname, Token } from '../../types'; import { getAdapter } from '../forges/registry'; import { isValidHostname } from './utils'; @@ -20,14 +19,16 @@ export interface PersonalAccessTokenFormErrors { invalidCredentialsDetail?: string; } -/** OAuth app login form values — mirrors LoginOAuthWebOptions so it passes straight through. */ -export type OAuthAppFormData = LoginOAuthWebOptions; +/** Custom OAuth app login form values. */ +export interface CustomOAuthAppFormData { + hostname: Hostname; + clientId: ClientID; +} -/** OAuth app login form errors. */ -export interface OAuthAppFormErrors { +/** Custom OAuth app login form errors. */ +export interface CustomOAuthAppFormErrors { hostname?: string; clientId?: string; - clientSecret?: string; invalidCredentialsForHost?: string; } @@ -74,11 +75,11 @@ export const validatePersonalAccessTokenForm = ( return errors; }; -export const validateOAuthAppForm = ( - values: OAuthAppFormData, +export const validateCustomOAuthAppForm = ( + values: CustomOAuthAppFormData, forge: Forge = 'github', -): OAuthAppFormErrors => { - const errors: OAuthAppFormErrors = {}; +): CustomOAuthAppFormErrors => { + const errors: CustomOAuthAppFormErrors = {}; const adapter = getAdapter(forge); if (!values.hostname) { @@ -89,15 +90,9 @@ export const validateOAuthAppForm = ( if (!values.clientId) { errors.clientId = 'Client ID is required'; - } else if (!adapter.oauthWebApp?.validateClientId(values.clientId)) { + } else if (!adapter.validateClientId?.(values.clientId)) { errors.clientId = 'Client ID format is invalid'; } - if (!values.clientSecret) { - errors.clientSecret = 'Client Secret is required'; - } else if (!adapter.validateToken(values.clientSecret as unknown as Token)) { - errors.clientSecret = 'Client Secret format is invalid'; - } - return errors; }; diff --git a/src/renderer/utils/auth/state.test.ts b/src/renderer/utils/auth/state.test.ts deleted file mode 100644 index 191a792b1..000000000 --- a/src/renderer/utils/auth/state.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { generateOAuthState } from './state'; - -const UUID_V4 = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; - -describe('renderer/utils/auth/state.ts', () => { - describe('generateOAuthState', () => { - it('should return a v4 UUID', () => { - expect(generateOAuthState()).toMatch(UUID_V4); - }); - - it('should return different values across calls', () => { - expect(generateOAuthState()).not.toBe(generateOAuthState()); - }); - - it('should use crypto.randomUUID', () => { - const spy = vi.spyOn(globalThis.crypto, 'randomUUID'); - generateOAuthState(); - expect(spy).toHaveBeenCalledOnce(); - spy.mockRestore(); - }); - }); -}); diff --git a/src/renderer/utils/auth/state.ts b/src/renderer/utils/auth/state.ts deleted file mode 100644 index 3b88de7c3..000000000 --- a/src/renderer/utils/auth/state.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Generate a cryptographically random OAuth 2.0 `state` value. - * - * `crypto.randomUUID()` is backed by the platform CSPRNG and yields 122 random - * bits — ample for a per-attempt, single-use value that an attacker cannot - * guess in order to bind the client to a different account. Using the platform - * primitive avoids hand-rolled random-value encoding entirely. - * - * @returns A random UUID to send as the OAuth `state` parameter. - */ -export function generateOAuthState(): string { - return crypto.randomUUID(); -} diff --git a/src/renderer/utils/auth/types.ts b/src/renderer/utils/auth/types.ts index 60654acc2..d64e6fd1d 100644 --- a/src/renderer/utils/auth/types.ts +++ b/src/renderer/utils/auth/types.ts @@ -1,6 +1,10 @@ -import type { AuthCode, ClientID, ClientSecret, Forge, Hostname, Token } from '../../types'; +import type { ClientID, Forge, Hostname, Token } from '../../types'; -export type AuthMethod = 'GitHub App' | 'GitHub CLI' | 'Personal Access Token' | 'OAuth App'; +export type AuthMethod = + | 'Gitify OAuth App' + | 'Custom OAuth App' + | 'GitHub CLI' + | 'Personal Access Token'; export type PlatformType = | 'Bitbucket Cloud' @@ -11,12 +15,6 @@ export type PlatformType = | 'GitLab Self-Managed' | 'Gitea'; -export interface LoginOAuthWebOptions { - hostname: Hostname; - clientId: ClientID; - clientSecret: ClientSecret; -} - export interface DeviceFlowSession { hostname: Hostname; clientId: ClientID; @@ -35,9 +33,3 @@ export interface LoginPersonalAccessTokenOptions { /** Defaults to GitHub when omitted. */ forge?: Forge; } - -export interface AuthResponse { - authMethod: AuthMethod; - authCode: AuthCode; - authOptions: LoginOAuthWebOptions; -} diff --git a/src/renderer/utils/forges/bitbucket/adapter.test.ts b/src/renderer/utils/forges/bitbucket/adapter.test.ts index 01a78619e..f5936d2d6 100644 --- a/src/renderer/utils/forges/bitbucket/adapter.test.ts +++ b/src/renderer/utils/forges/bitbucket/adapter.test.ts @@ -118,7 +118,7 @@ describe('renderer/utils/forges/bitbucket/adapter.ts', () => { it('returns KeyIcon for every auth method', () => { expect(bitbucketAdapter.getAuthMethodIcon('Personal Access Token')).toBe(KeyIcon); - expect(bitbucketAdapter.getAuthMethodIcon('OAuth App')).toBe(KeyIcon); + expect(bitbucketAdapter.getAuthMethodIcon('Custom OAuth App')).toBe(KeyIcon); }); }); diff --git a/src/renderer/utils/forges/gitea/adapter.test.ts b/src/renderer/utils/forges/gitea/adapter.test.ts index aa5c502f2..bc22f9c11 100644 --- a/src/renderer/utils/forges/gitea/adapter.test.ts +++ b/src/renderer/utils/forges/gitea/adapter.test.ts @@ -74,8 +74,8 @@ describe('renderer/utils/forges/gitea/adapter.ts', () => { it('returns the key icon for every auth method (PAT-only forge today)', () => { expect(giteaAdapter.getAuthMethodIcon('Personal Access Token')).toBe(KeyIcon); - expect(giteaAdapter.getAuthMethodIcon('GitHub App')).toBe(KeyIcon); - expect(giteaAdapter.getAuthMethodIcon('OAuth App')).toBe(KeyIcon); + expect(giteaAdapter.getAuthMethodIcon('Gitify OAuth App')).toBe(KeyIcon); + expect(giteaAdapter.getAuthMethodIcon('Custom OAuth App')).toBe(KeyIcon); }); }); diff --git a/src/renderer/utils/forges/github/adapter.test.ts b/src/renderer/utils/forges/github/adapter.test.ts index b1d588add..510abf19c 100644 --- a/src/renderer/utils/forges/github/adapter.test.ts +++ b/src/renderer/utils/forges/github/adapter.test.ts @@ -1,15 +1,33 @@ -import { AgentIcon, AppsIcon, CopilotIcon, KeyIcon, PersonIcon } from '@primer/octicons-react'; +vi.mock('@octokit/oauth-methods', async () => { + const actual = + await vi.importActual('@octokit/oauth-methods'); + return { + ...actual, + createDeviceCode: vi.fn(), + exchangeDeviceCode: vi.fn(), + }; +}); + +import { AgentIcon, CopilotIcon, KeyIcon, PersonIcon } from '@primer/octicons-react'; + +import { createDeviceCode } from '@octokit/oauth-methods'; + +import type { MockedFunction } from 'vite-plus/test'; import { mockGitHubCliAccount, mockGitHubCloudAccount } from '../../../__mocks__/account-mocks'; import { mockPartialGitifyNotification } from '../../../__mocks__/notifications-mocks'; -import type { GitifyNotificationUser, Hostname, Link, Token } from '../../../types'; +import type { ClientID, GitifyNotificationUser, Hostname, Link, Token } from '../../../types'; import { githubAdapter } from './adapter'; import * as client from './client'; import * as octokit from './octokit'; describe('renderer/utils/forges/github/adapter.ts', () => { + const createDeviceCodeMock = createDeviceCode as unknown as MockedFunction< + typeof createDeviceCode + >; + describe('static fields', () => { it('identifies as github', () => { expect(githubAdapter.id).toBe('github'); @@ -18,7 +36,12 @@ describe('renderer/utils/forges/github/adapter.ts', () => { it('exposes the device-flow / PAT / CLI / OAuth login methods', () => { const ids = githubAdapter.loginMethods.map((m) => m.testId); - expect(ids).toEqual(['login-github', 'login-pat', 'login-github-cli', 'login-oauth-app']); + expect(ids).toEqual([ + 'login-github', + 'login-pat', + 'login-github-cli', + 'login-custom-oauth-app', + ]); }); it('defaults the PAT hostname to github.com', () => { @@ -54,20 +77,44 @@ describe('renderer/utils/forges/github/adapter.ts', () => { }); it('maps each auth method to its icon', () => { - expect(githubAdapter.getAuthMethodIcon('GitHub App')).toBe(AppsIcon); - expect(githubAdapter.getAuthMethodIcon('OAuth App')).toBe(PersonIcon); + expect(githubAdapter.getAuthMethodIcon('Gitify OAuth App')).toBe(PersonIcon); + expect(githubAdapter.getAuthMethodIcon('Custom OAuth App')).toBe(PersonIcon); expect(githubAdapter.getAuthMethodIcon('Personal Access Token')).toBe(KeyIcon); }); - it('wires the device-flow and OAuth-app methods so the context can dispatch via the adapter', () => { - expect(githubAdapter.deviceFlow?.authMethod).toBe('GitHub App'); + it('forwards a custom client ID through the device-flow start', async () => { + createDeviceCodeMock.mockResolvedValueOnce({ + data: { + device_code: 'device-code', + user_code: 'user-code', + verification_uri: 'https://github.example.com/login/device', + expires_in: 600, + interval: 5, + }, + } as unknown as Awaited>); + + const session = await githubAdapter.deviceFlow!.start( + 'github.example.com' as Hostname, + undefined, + 'CUSTOM_CLIENT_ID_123' as ClientID, + ); + + expect(createDeviceCodeMock).toHaveBeenCalledWith({ + clientType: 'oauth-app', + clientId: 'CUSTOM_CLIENT_ID_123', + scopes: expect.anything(), + request: expect.any(Function), + }); + expect(session.clientId).toBe('CUSTOM_CLIENT_ID_123'); + }); + + it('wires the device-flow methods so the context can dispatch via the adapter', () => { + expect(githubAdapter.deviceFlow?.authMethod).toBe('Gitify OAuth App'); expect(githubAdapter.deviceFlow?.start).toBeDefined(); expect(githubAdapter.deviceFlow?.poll).toBeDefined(); expect(githubAdapter.deviceFlow?.getRevokeAccessUrl).toBeDefined(); - expect(githubAdapter.oauthWebApp?.performWebOAuth).toBeDefined(); - expect(githubAdapter.oauthWebApp?.exchangeAuthCodeForToken).toBeDefined(); - expect(githubAdapter.oauthWebApp?.validateClientId).toBeDefined(); - expect(githubAdapter.oauthWebApp?.getNewOAuthAppUrl).toBeDefined(); + expect(githubAdapter.deviceFlow?.getNewOAuthAppUrl).toBeDefined(); + expect(githubAdapter.validateClientId).toBeDefined(); }); }); diff --git a/src/renderer/utils/forges/github/adapter.ts b/src/renderer/utils/forges/github/adapter.ts index 973a2acaf..30aa84f15 100644 --- a/src/renderer/utils/forges/github/adapter.ts +++ b/src/renderer/utils/forges/github/adapter.ts @@ -1,10 +1,4 @@ -import { - AppsIcon, - KeyIcon, - MarkGithubIcon, - PersonIcon, - TerminalIcon, -} from '@primer/octicons-react'; +import { KeyIcon, MarkGithubIcon, PersonIcon, TerminalIcon } from '@primer/octicons-react'; import { Constants, OAUTH_SCOPE } from '../../../constants'; @@ -31,12 +25,7 @@ import { markNotificationThreadAsRead, } from './client'; import { enrichGitHubNotifications } from './enrich'; -import { - exchangeAuthCodeForAccessToken, - performGitHubWebOAuth, - pollGitHubDeviceFlow, - startGitHubDeviceFlow, -} from './flows'; +import { pollGitHubDeviceFlow, startGitHubDeviceFlow } from './flows'; import { createNotificationHandler } from './handlers'; import { clearOctokitClientCacheForAccount, createOctokitClient } from './octokit'; import { getGitHubPlatform } from './platform'; @@ -104,6 +93,7 @@ export const githubAdapter: ForgeAdapter = { defaultHostname: Constants.GITHUB_HOSTNAME, validateToken: isValidToken, + validateClientId: isValidClientId, validateHostname: isValidHostname, getPersonalAccessTokenSettingsUrl: getNewTokenURL, documentationUrl: Constants.GITHUB_DOCS.PAT_URL as Link, @@ -116,7 +106,7 @@ export const githubAdapter: ForgeAdapter = { label: 'GitHub', variant: 'primary', route: '/login/github/device-flow', - authMethod: 'GitHub App', + authMethod: 'Gitify OAuth App', }, { testId: 'login-pat', @@ -133,26 +123,20 @@ export const githubAdapter: ForgeAdapter = { authMethod: 'GitHub CLI', }, { - testId: 'login-oauth-app', + testId: 'login-custom-oauth-app', icon: PersonIcon, - label: 'OAuth App', - route: '/login/github/oauth-app', - authMethod: 'OAuth App', + label: 'Custom OAuth App', + route: '/login/github/custom-oauth-app', + authMethod: 'Custom OAuth App', }, ], deviceFlow: { - authMethod: 'GitHub App', + authMethod: 'Gitify OAuth App', start: startGitHubDeviceFlow, poll: pollGitHubDeviceFlow, - getRevokeAccessUrl: (hostname) => - getDeveloperSettingsURL({ hostname, method: 'GitHub App' } as Account), - }, - - oauthWebApp: { - performWebOAuth: performGitHubWebOAuth, - exchangeAuthCodeForToken: exchangeAuthCodeForAccessToken, - validateClientId: isValidClientId, + getRevokeAccessUrl: (hostname, clientId) => + getDeveloperSettingsURL({ hostname, method: 'Custom OAuth App', clientId } as Account), getNewOAuthAppUrl: getNewOAuthAppURL, }, @@ -231,12 +215,11 @@ function accountHasScopes( function githubAuthMethodIcon(method: AuthMethod) { switch (method) { - case 'GitHub App': - return AppsIcon; + case 'Gitify OAuth App': + case 'Custom OAuth App': + return PersonIcon; case 'GitHub CLI': return TerminalIcon; - case 'OAuth App': - return PersonIcon; default: return KeyIcon; } diff --git a/src/renderer/utils/forges/github/auth.test.ts b/src/renderer/utils/forges/github/auth.test.ts index 618bc917f..07e5442d6 100644 --- a/src/renderer/utils/forges/github/auth.test.ts +++ b/src/renderer/utils/forges/github/auth.test.ts @@ -61,22 +61,32 @@ describe('renderer/utils/forges/github/auth.ts', () => { }); describe('getDeveloperSettingsURL', () => { - it('returns the GitHub App connections URL', () => { + it('returns the connections URL for Gitify OAuth App accounts', () => { expect( getDeveloperSettingsURL({ hostname: 'github.com' as Hostname, - method: 'GitHub App', + method: 'Gitify OAuth App', } as Account), ).toBe('https://github.com/settings/connections/applications/FAKE_CLIENT_ID_123'); }); - it('returns the OAuth App developer URL', () => { + it("returns the connections URL for a custom app's own client ID", () => { expect( getDeveloperSettingsURL({ hostname: 'github.com' as Hostname, - method: 'OAuth App', + method: 'Custom OAuth App', + clientId: 'CUSTOM_CLIENT_ID_123' as ClientID, } as Account), - ).toBe('https://github.com/settings/developers'); + ).toBe('https://github.com/settings/connections/applications/CUSTOM_CLIENT_ID_123'); + }); + + it('falls back to the preconfigured app when a custom account stores no client ID', () => { + expect( + getDeveloperSettingsURL({ + hostname: 'github.com' as Hostname, + method: 'Custom OAuth App', + } as Account), + ).toBe('https://github.com/settings/connections/applications/FAKE_CLIENT_ID_123'); }); it('returns the PAT settings URL', () => { @@ -132,6 +142,10 @@ describe('renderer/utils/forges/github/auth.ts', () => { ), ).toBeTruthy(); }); + + it('does not pre-fill a callback URL for a device-flow custom app', () => { + expect(getNewOAuthAppURL('github.com' as Hostname)).not.toContain('callback_url'); + }); }); describe('isValidClientId', () => { diff --git a/src/renderer/utils/forges/github/auth.ts b/src/renderer/utils/forges/github/auth.ts index 99915c750..c1a155f9d 100644 --- a/src/renderer/utils/forges/github/auth.ts +++ b/src/renderer/utils/forges/github/auth.ts @@ -62,9 +62,9 @@ export function getGitHubAuthBaseUrl(hostname: Hostname): URL { /** * Return the GitHub developer settings URL appropriate for the account's auth method. * - * - GitHub App → application connections page + * - Gitify OAuth App / Custom OAuth App → the OAuth app's connection page for the + * account's own client ID (falling back to Gitify's preconfigured app) * - GitHub CLI → authorized applications page - * - OAuth App → developer settings page * - Personal Access Token → tokens settings page * * @param account - The account whose settings URL to build. @@ -74,15 +74,15 @@ export function getDeveloperSettingsURL(account: Account): Link { const settingsURL = new URL(`https://${account.hostname}`); switch (account.method) { - case 'GitHub App': - settingsURL.pathname = `/settings/connections/applications/${Constants.OAUTH_DEVICE_FLOW_CLIENT_ID}`; + case 'Gitify OAuth App': + case 'Custom OAuth App': + settingsURL.pathname = `/settings/connections/applications/${ + account.clientId ?? Constants.OAUTH_DEVICE_FLOW_CLIENT_ID + }`; break; case 'GitHub CLI': settingsURL.pathname = '/settings/applications'; break; - case 'OAuth App': - settingsURL.pathname = '/settings/developers'; - break; case 'Personal Access Token': settingsURL.pathname = '/settings/tokens'; break; @@ -115,8 +115,10 @@ export function getNewTokenURL(hostname: Hostname): Link { /** * Build a pre-filled URL for registering a new OAuth App. * - * Pre-populates the app name (with creation date), homepage URL, and - * the `gitify://oauth` callback URL. + * Pre-populates the app name (with creation date) and homepage URL. No callback + * URL is set: the custom OAuth app authorizes through the device flow, which + * needs no browser redirect. The user must still enable the device flow in the + * app's settings after registering it. * * @param hostname - The GitHub hostname to register the app on. * @returns The URL with pre-filled query parameters. @@ -129,7 +131,6 @@ export function getNewOAuthAppURL(hostname: Hostname): Link { `${APPLICATION.NAME} (Created on ${date})`, ); newOAuthAppURL.searchParams.append('oauth_application[url]', 'https://gitify.io'); - newOAuthAppURL.searchParams.append('oauth_application[callback_url]', 'gitify://oauth'); return newOAuthAppURL.toString() as Link; } diff --git a/src/renderer/utils/forges/github/flows.test.ts b/src/renderer/utils/forges/github/flows.test.ts index e9adc14fb..5f672853f 100644 --- a/src/renderer/utils/forges/github/flows.test.ts +++ b/src/renderer/utils/forges/github/flows.test.ts @@ -6,23 +6,21 @@ vi.mock('@octokit/oauth-methods', async () => { ...actual, createDeviceCode: vi.fn(), exchangeDeviceCode: vi.fn(), - exchangeWebFlowCode: vi.fn(), }; }); -import { createDeviceCode, exchangeDeviceCode, exchangeWebFlowCode } from '@octokit/oauth-methods'; +import { createDeviceCode, exchangeDeviceCode } from '@octokit/oauth-methods'; import { RequestError } from '@octokit/request-error'; import type { MockedFunction } from 'vite-plus/test'; import { Constants } from '../../../constants'; -import type { AuthCode, ClientID, ClientSecret, Hostname } from '../../../types'; -import type { DeviceFlowSession, LoginOAuthWebOptions } from '../../auth/types'; +import type { ClientID, Hostname } from '../../../types'; +import type { DeviceFlowSession } from '../../auth/types'; import { getRecommendedScopeNames } from '../../auth/scopes'; import * as logger from '../../core/logger'; -import * as comms from '../../system/comms'; import * as flows from './flows'; const createDeviceCodeMock = createDeviceCode as unknown as MockedFunction; @@ -31,327 +29,189 @@ const exchangeDeviceCodeMock = exchangeDeviceCode as unknown as MockedFunction< typeof exchangeDeviceCode >; -const exchangeWebFlowCodeMock = exchangeWebFlowCode as unknown as MockedFunction< - typeof exchangeWebFlowCode ->; - describe('renderer/utils/forges/github/flows.ts', () => { - vi.spyOn(logger, 'rendererLogInfo').mockImplementation(vi.fn()); const rendererLogErrorSpy = vi.spyOn(logger, 'rendererLogError').mockImplementation(vi.fn()); - const rendererLogWarnSpy = vi.spyOn(logger, 'rendererLogWarn').mockImplementation(vi.fn()); - const openExternalLinkSpy = vi.spyOn(comms, 'openExternalLink').mockImplementation(vi.fn()); beforeEach(() => { // Mock OAUTH_DEVICE_FLOW_CLIENT_ID value Constants.OAUTH_DEVICE_FLOW_CLIENT_ID = 'FAKE_CLIENT_ID_123' as ClientID; }); - describe('Gitify GitHub OAuth - Device Code Flow', () => { - describe('startGitHubDeviceFlow', () => { - it('should request a device code and return a session', async () => { - createDeviceCodeMock.mockResolvedValueOnce({ - data: { - device_code: 'device-code-xyz', - user_code: 'user-code-xyz', - verification_uri: 'https://github.com/login/device', - expires_in: 600, - interval: 5, - }, - } as unknown as Awaited>); - - const session = await flows.startGitHubDeviceFlow(); - - expect(createDeviceCodeMock).toHaveBeenCalledWith({ - clientType: 'oauth-app', - clientId: 'FAKE_CLIENT_ID_123', - scopes: getRecommendedScopeNames(), - request: expect.any(Function), - }); - - expect(session.deviceCode).toBe('device-code-xyz'); - expect(session.userCode).toBe('user-code-xyz'); - expect(session.verificationUri).toBe('https://github.com/login/device'); - expect(session.intervalSeconds).toBe(5); - expect(session.expiresAt).toBeGreaterThan(Date.now()); - }); - }); - - describe('pollGitHubDeviceFlow', () => { - const baseSession = { - hostname: 'github.com', - clientId: 'FAKE_CLIENT_ID_123', - deviceCode: 'device-code', - userCode: 'user-code', - verificationUri: 'https://github.com/login/device', - intervalSeconds: 5, - expiresAt: Date.now() + 10000, - } as const; - - it('returns token on successful exchange', async () => { - exchangeDeviceCodeMock.mockResolvedValueOnce({ - authentication: { token: 'device-token-xyz' }, - } as unknown as Awaited>); - - const token = await flows.pollGitHubDeviceFlow(baseSession as DeviceFlowSession); - - expect(exchangeDeviceCodeMock).toHaveBeenCalledWith({ - clientType: 'oauth-app', - clientId: 'FAKE_CLIENT_ID_123', - code: 'device-code', - request: expect.any(Function), - }); - - expect(token).toBe('device-token-xyz'); - }); - - it('returns null and does not change interval when authorization is pending', async () => { - const pendingErr = Object.create(RequestError.prototype); - pendingErr.response = { data: { error: 'authorization_pending' } }; - - exchangeDeviceCodeMock.mockRejectedValueOnce(pendingErr); + describe('startGitHubDeviceFlow', () => { + it('should request a device code and return a session', async () => { + createDeviceCodeMock.mockResolvedValueOnce({ + data: { + device_code: 'device-code-xyz', + user_code: 'user-code-xyz', + verification_uri: 'https://github.com/login/device', + expires_in: 600, + interval: 5, + }, + } as unknown as Awaited>); - const session = { ...baseSession } as DeviceFlowSession; - const token = await flows.pollGitHubDeviceFlow(session); + const session = await flows.startGitHubDeviceFlow(); - expect(token).toBeNull(); - expect(session.intervalSeconds).toBe(5); + expect(createDeviceCodeMock).toHaveBeenCalledWith({ + clientType: 'oauth-app', + clientId: 'FAKE_CLIENT_ID_123', + scopes: getRecommendedScopeNames(), + request: expect.any(Function), }); - it('returns null and increases interval by 5 when slow_down', async () => { - const slowDownErr = Object.create(RequestError.prototype); - slowDownErr.response = { data: { error: 'slow_down' } }; - - exchangeDeviceCodeMock.mockRejectedValueOnce(slowDownErr); - - const session = { ...baseSession } as DeviceFlowSession; - const token = await flows.pollGitHubDeviceFlow(session); - - expect(token).toBeNull(); - expect(session.intervalSeconds).toBe(10); - }); + expect(session.deviceCode).toBe('device-code-xyz'); + expect(session.userCode).toBe('user-code-xyz'); + expect(session.verificationUri).toBe('https://github.com/login/device'); + expect(session.intervalSeconds).toBe(5); + expect(session.expiresAt).toBeGreaterThan(Date.now()); + }); - it('throws on other errors', async () => { - const otherErr = new Error('boom'); + it('authorizes a supplied custom OAuth app client ID', async () => { + createDeviceCodeMock.mockResolvedValueOnce({ + data: { + device_code: 'device-code-xyz', + user_code: 'user-code-xyz', + verification_uri: 'https://github.example.com/login/device', + expires_in: 600, + interval: 5, + }, + } as unknown as Awaited>); - exchangeDeviceCodeMock.mockRejectedValueOnce(otherErr); + const session = await flows.startGitHubDeviceFlow( + 'github.example.com' as Hostname, + getRecommendedScopeNames(), + 'CUSTOM_CLIENT_ID_123' as ClientID, + ); - await expect( - async () => await flows.pollGitHubDeviceFlow(baseSession as DeviceFlowSession), - ).rejects.toThrow('boom'); - expect(rendererLogErrorSpy).toHaveBeenCalledTimes(1); + expect(createDeviceCodeMock).toHaveBeenCalledWith({ + clientType: 'oauth-app', + clientId: 'CUSTOM_CLIENT_ID_123', + scopes: getRecommendedScopeNames(), + request: expect.any(Function), }); + expect(session.clientId).toBe('CUSTOM_CLIENT_ID_123'); + expect(session.hostname).toBe('github.example.com'); }); }); - describe('performGitHubWebOAuth', () => { - const webAuthOptions: LoginOAuthWebOptions = { - hostname: 'github.com' as Hostname, - clientId: 'FAKE_CLIENT_ID_123' as ClientID, - clientSecret: 'FAKE_CLIENT_SECRET_123' as ClientSecret, - }; - - it('should call performGitHubWebOAuth using custom oauth app - success oauth flow', async () => { - let deliverCallback: (callbackUrl: string) => void = () => {}; - window.gitify.onAuthCallback = vi.fn().mockImplementation((callback) => { - deliverCallback = callback; - }); - - const resultPromise = flows.performGitHubWebOAuth({ - clientId: 'BYO_CLIENT_ID' as ClientID, - clientSecret: 'BYO_CLIENT_SECRET' as ClientSecret, - hostname: 'my.git.com' as Hostname, + describe('pollGitHubDeviceFlow', () => { + const baseSession = { + hostname: 'github.com', + clientId: 'FAKE_CLIENT_ID_123', + deviceCode: 'device-code', + userCode: 'user-code', + verificationUri: 'https://github.com/login/device', + intervalSeconds: 5, + expiresAt: Date.now() + 10000, + } as const; + + it('returns token on successful exchange', async () => { + exchangeDeviceCodeMock.mockResolvedValueOnce({ + authentication: { token: 'device-token-xyz' }, + } as unknown as Awaited>); + + const token = await flows.pollGitHubDeviceFlow(baseSession as DeviceFlowSession); + + expect(exchangeDeviceCodeMock).toHaveBeenCalledWith({ + clientType: 'oauth-app', + clientId: 'FAKE_CLIENT_ID_123', + code: 'device-code', + request: expect.any(Function), }); - expect(openExternalLinkSpy).toHaveBeenCalledTimes(1); - expect(openExternalLinkSpy).toHaveBeenCalledWith( - expect.stringContaining( - 'https://my.git.com/login/oauth/authorize?allow_signup=false&client_id=BYO_CLIENT_ID&scope=notifications%2Cread%3Auser%2Crepo', - ), - ); - - // The authorization URL must carry a per-attempt state, and the login - // only completes when the callback echoes that same value back. - const authorizeUrl = new URL(openExternalLinkSpy.mock.calls[0][0] as string); - const state = authorizeUrl.searchParams.get('state'); - expect(state).toBeTruthy(); - - deliverCallback(`gitify://oauth?code=123-456&state=${state}`); - - const res = await resultPromise; - - expect(window.gitify.onAuthCallback).toHaveBeenCalledTimes(1); - expect(window.gitify.onAuthCallback).toHaveBeenCalledWith(expect.any(Function)); - - expect(res.authMethod).toBe('OAuth App'); - expect(res.authCode).toBe('123-456'); + expect(token).toBe('device-token-xyz'); }); - it('does not resolve when the callback state does not match the generated state', async () => { - let deliverCallback: (callbackUrl: string) => void = () => {}; - window.gitify.onAuthCallback = vi.fn().mockImplementation((callback) => { - deliverCallback = callback; - }); - - let resolved = false; - const resultPromise = flows.performGitHubWebOAuth(webAuthOptions).then((res) => { - resolved = true; - return res; + it("uses the session's own client ID when polling a custom app", async () => { + exchangeDeviceCodeMock.mockResolvedValueOnce({ + authentication: { token: 'custom-token' }, + } as unknown as Awaited>); + + const token = await flows.pollGitHubDeviceFlow({ + ...baseSession, + clientId: 'CUSTOM_CLIENT_ID_123', + } as DeviceFlowSession); + + expect(exchangeDeviceCodeMock).toHaveBeenCalledWith({ + clientType: 'oauth-app', + clientId: 'CUSTOM_CLIENT_ID_123', + code: 'device-code', + request: expect.any(Function), }); + expect(token).toBe('custom-token'); + }); - const authorizeUrl = new URL(openExternalLinkSpy.mock.calls[0][0] as string); - const state = authorizeUrl.searchParams.get('state'); + it('returns null and does not change interval when authorization is pending', async () => { + const pendingErr = Object.create(RequestError.prototype); + pendingErr.response = { data: { error: 'authorization_pending' } }; - deliverCallback('gitify://oauth?code=attacker-code&state=not-the-real-state'); - deliverCallback('gitify://oauth?code=attacker-code-no-state'); + exchangeDeviceCodeMock.mockRejectedValueOnce(pendingErr); - // Give any pending microtasks a chance to run, then confirm neither - // injected callback settled the login. - await Promise.resolve(); - await Promise.resolve(); - expect(resolved).toBe(false); - expect(rendererLogWarnSpy).toHaveBeenCalledTimes(2); + const session = { ...baseSession } as DeviceFlowSession; + const token = await flows.pollGitHubDeviceFlow(session); - // The still-pending attempt completes once the matching state arrives. - deliverCallback(`gitify://oauth?code=the-real-code&state=${state}`); - const res = await resultPromise; - expect(res.authCode).toBe('the-real-code'); + expect(token).toBeNull(); + expect(session.intervalSeconds).toBe(5); }); - it('ignores an unparseable callback URL instead of tearing down the pending login', async () => { - let deliverCallback: (callbackUrl: string) => void = () => {}; - window.gitify.onAuthCallback = vi.fn().mockImplementation((callback) => { - deliverCallback = callback; - }); - - let settled = false; - const resultPromise = flows.performGitHubWebOAuth(webAuthOptions).then( - (res) => { - settled = true; - return res; - }, - (err: unknown) => { - settled = true; - throw err; - }, - ); - - // `gitify://[` passes the protocol handler's `gitify://` prefix check but - // is not a parseable URL, so it must be ignored rather than rejecting the - // login before the state is ever compared. - deliverCallback('gitify://['); + it('returns null and increases interval by 5 when slow_down', async () => { + const slowDownErr = Object.create(RequestError.prototype); + slowDownErr.response = { data: { error: 'slow_down' } }; - await Promise.resolve(); - await Promise.resolve(); - expect(settled).toBe(false); - expect(rendererLogWarnSpy).toHaveBeenCalledTimes(1); + exchangeDeviceCodeMock.mockRejectedValueOnce(slowDownErr); - // Settle the still-pending attempt so no listener is left dangling. - const authorizeUrl = new URL(openExternalLinkSpy.mock.calls[0][0] as string); - const state = authorizeUrl.searchParams.get('state'); - deliverCallback(`gitify://oauth?code=real-code&state=${state}`); + const session = { ...baseSession } as DeviceFlowSession; + const token = await flows.pollGitHubDeviceFlow(session); - await expect(resultPromise).resolves.toMatchObject({ authCode: 'real-code' }); + expect(token).toBeNull(); + expect(session.intervalSeconds).toBe(10); }); - it('should call performGitHubWebOAuth - failure', async () => { - let deliverCallback: (callbackUrl: string) => void = () => {}; - window.gitify.onAuthCallback = vi.fn().mockImplementation((callback) => { - deliverCallback = callback; - }); + it('throws on other errors', async () => { + const otherErr = new Error('boom'); - const resultPromise = flows.performGitHubWebOAuth(webAuthOptions); + exchangeDeviceCodeMock.mockRejectedValueOnce(otherErr); - const authorizeUrl = new URL(openExternalLinkSpy.mock.calls[0][0] as string); - const state = authorizeUrl.searchParams.get('state'); + await expect( + async () => await flows.pollGitHubDeviceFlow(baseSession as DeviceFlowSession), + ).rejects.toThrow('boom'); + expect(rendererLogErrorSpy).toHaveBeenCalledTimes(1); + }); + }); - deliverCallback( - `gitify://oauth?state=${state}&error=invalid_request&error_description=The+redirect_uri+is+missing+or+invalid.&error_uri=https://docs.github.com/en/developers/apps/troubleshooting-oauth-errors`, - ); + describe('describeDeviceFlowError', () => { + it('explains how to enable device flow for a custom app', () => { + const err = Object.create(RequestError.prototype); + err.response = { data: { error: 'device_flow_disabled' } }; - await expect(resultPromise).rejects.toEqual( - new Error( - "Oops! Something went wrong and we couldn't log you in using GitHub. Please try again. Reason: The redirect_uri is missing or invalid. Docs: https://docs.github.com/en/developers/apps/troubleshooting-oauth-errors", - ), + const message = flows.describeDeviceFlowError( + err, + 'github.example.com' as Hostname, + 'CUSTOM_CLIENT_ID_123' as ClientID, ); - expect(openExternalLinkSpy).toHaveBeenCalledTimes(1); - expect(openExternalLinkSpy).toHaveBeenCalledWith( - expect.stringContaining( - 'https://github.com/login/oauth/authorize?allow_signup=false&client_id=FAKE_CLIENT_ID_123&scope=notifications%2Cread%3Auser%2Crepo', - ), + expect(message).toContain('Device flow is not enabled'); + expect(message).toContain( + 'https://github.example.com/settings/applications/CUSTOM_CLIENT_ID_123', ); - - expect(window.gitify.onAuthCallback).toHaveBeenCalledTimes(1); - expect(window.gitify.onAuthCallback).toHaveBeenCalledWith(expect.any(Function)); }); - it('ignores a callback error whose state does not match the attempt', async () => { - let deliverCallback: (callbackUrl: string) => void = () => {}; - window.gitify.onAuthCallback = vi.fn().mockImplementation((callback) => { - deliverCallback = callback; - }); - - let settled = false; - const resultPromise = flows.performGitHubWebOAuth(webAuthOptions).then( - (res) => { - settled = true; - return res; - }, - (err: unknown) => { - settled = true; - throw err; - }, - ); - - deliverCallback('gitify://oauth?error=access_denied'); - deliverCallback('gitify://oauth?error=access_denied&state=not-the-real-state'); - - await Promise.resolve(); - await Promise.resolve(); - expect(settled).toBe(false); - expect(rendererLogWarnSpy).toHaveBeenCalledTimes(2); + it('names an unrecognized client ID as invalid for the host', () => { + const err = Object.create(RequestError.prototype); + err.response = { data: { error: 'incorrect_client_credentials' } }; - // Settle the still-pending attempt so no listener is left dangling. - const authorizeUrl = new URL(openExternalLinkSpy.mock.calls[0][0] as string); - const state = authorizeUrl.searchParams.get('state'); - deliverCallback(`gitify://oauth?code=real-code&state=${state}`); - - await expect(resultPromise).resolves.toMatchObject({ authCode: 'real-code' }); + expect( + flows.describeDeviceFlowError( + err, + 'github.example.com' as Hostname, + 'CUSTOM_CLIENT_ID_123' as ClientID, + ), + ).toBe('The client ID is not valid for github.example.com. Check it and try again.'); }); - describe('exchangeAuthCodeForAccessToken', () => { - const authCode = '123-456' as AuthCode; - - it('should exchange auth code for access token', async () => { - exchangeWebFlowCodeMock.mockResolvedValueOnce({ - authentication: { - token: 'this-is-a-token', - }, - } as unknown as Awaited>); - - const res = await flows.exchangeAuthCodeForAccessToken(authCode, { - ...webAuthOptions, - }); - - expect(exchangeWebFlowCodeMock).toHaveBeenCalledWith({ - clientType: 'oauth-app', - clientId: 'FAKE_CLIENT_ID_123', - clientSecret: 'FAKE_CLIENT_SECRET_123', - code: '123-456', - request: expect.any(Function), - }); - expect(res).toBe('this-is-a-token'); - }); - - it('should throw when client secret is missing', async () => { - await expect( - async () => - await flows.exchangeAuthCodeForAccessToken(authCode, { - ...webAuthOptions, - clientSecret: undefined as unknown as ClientSecret, - }), - ).rejects.toThrow('clientSecret is required to exchange an auth code'); - }); + it('returns undefined for a non-RequestError', () => { + expect( + flows.describeDeviceFlowError(new Error('boom'), 'github.com' as Hostname), + ).toBeUndefined(); }); }); }); diff --git a/src/renderer/utils/forges/github/flows.ts b/src/renderer/utils/forges/github/flows.ts index 6b7fe1d22..4c74211f2 100644 --- a/src/renderer/utils/forges/github/flows.ts +++ b/src/renderer/utils/forges/github/flows.ts @@ -1,21 +1,14 @@ -import { - createDeviceCode, - exchangeDeviceCode, - exchangeWebFlowCode, - getWebFlowAuthorizationUrl, -} from '@octokit/oauth-methods'; +import { createDeviceCode, exchangeDeviceCode } from '@octokit/oauth-methods'; import { request } from '@octokit/request'; import { RequestError } from '@octokit/request-error'; import { Constants } from '../../../constants'; -import type { AuthCode, Hostname, Link, Token } from '../../../types'; -import type { AuthResponse, DeviceFlowSession, LoginOAuthWebOptions } from '../../auth/types'; +import type { ClientID, Hostname, Token } from '../../../types'; +import type { DeviceFlowSession } from '../../auth/types'; import { getRecommendedScopeNames } from '../../auth/scopes'; -import { generateOAuthState } from '../../auth/state'; -import { rendererLogError, rendererLogInfo, rendererLogWarn, toError } from '../../core/logger'; -import { openExternalLink } from '../../system/comms'; +import { rendererLogError, toError } from '../../core/logger'; import { getGitHubAuthBaseUrl } from './auth'; type DeviceFlowErrorResponse = { @@ -24,115 +17,6 @@ type DeviceFlowErrorResponse = { error_uri: string; }; -/** - * Initiate a GitHub OAuth Web Flow (OAuth App) authentication. - * - * Opens the GitHub authorization URL in the user's browser, then waits for the - * app's custom `gitify://oauth` callback to receive the authorization code. - * - * The login is bound to a per-attempt random `state` that is sent with the - * authorization request and must be echoed by the callback. A callback - * carrying a missing or mismatched `state` is logged and ignored, so a - * `gitify://oauth` link delivered from outside this attempt cannot bind the - * client to a different GitHub account or tear down the pending login. - * - * @param authOptions - The OAuth App client configuration and target hostname. - * @returns Resolves with the authorization code and options on success. - */ -export function performGitHubWebOAuth(authOptions: LoginOAuthWebOptions): Promise { - return new Promise((resolve, reject) => { - // A per-attempt, unguessable value this app itself tracks and checks on - // callback, so a `gitify://oauth` callback carrying an authorization code - // this specific login attempt never requested (e.g. one an attacker - // obtained for their own GitHub account) is rejected instead of silently - // accepted and exchanged for a token. - // - // Read the state back from the library's result rather than trusting the - // value echoed to us, so the string compared on callback is exactly the one - // the library embedded in the authorization URL. - const { url, state: expectedState } = getWebFlowAuthorizationUrl({ - clientType: 'oauth-app', - clientId: authOptions.clientId, - scopes: getRecommendedScopeNames(), - allowSignup: false, - state: generateOAuthState(), - request: request.defaults({ - baseUrl: `https://${authOptions.hostname}`, - }), - }); - - openExternalLink(url as Link); - - // Assigned once the listener is registered below; optional-chained in - // `handleCallback` so a synchronously-firing callback cannot hit the - // temporal dead zone. - let unsubscribeAuthCallback: (() => void) | undefined; - - const handleCallback = (callbackUrl: string) => { - let url: URL; - try { - url = new URL(callbackUrl); - } catch { - // An unparseable callback cannot carry the state this attempt - // generated, so treat it like any other unmatched callback: log and - // ignore rather than tearing down the pending login. Otherwise a - // forged `gitify://` URL (e.g. `gitify://[`) would abort a legitimate - // login even though the state check never runs. - rendererLogWarn( - 'renderer:auth-callback', - 'Ignored an unparseable authentication callback URL', - ); - return; - } - - const type = url.hostname; - const code = url.searchParams.get('code'); - const state = url.searchParams.get('state'); - const error = url.searchParams.get('error'); - const errorDescription = url.searchParams.get('error_description'); - const errorUri = url.searchParams.get('error_uri'); - - // A callback belongs to this login attempt only when it echoes the state - // this attempt generated. Anything else (missing or mismatched state) is - // logged and ignored, leaving the pending login intact so an injected - // callback can neither bind another account nor tear down the attempt. - if (state !== expectedState) { - rendererLogWarn( - 'renderer:auth-callback', - 'Ignored an authentication callback with a missing or mismatched state parameter', - ); - return; - } - - if (code && type === 'oauth') { - unsubscribeAuthCallback?.(); - resolve({ - authMethod: 'OAuth App', - authCode: code as AuthCode, - authOptions: authOptions, - }); - } else if (error) { - unsubscribeAuthCallback?.(); - reject( - new Error( - `Oops! Something went wrong and we couldn't log you in using GitHub. Please try again. Reason: ${errorDescription} Docs: ${errorUri}`, - ), - ); - } - }; - - // Unsubscribed inside `handleCallback` once the flow settles so repeated - // login attempts do not accumulate stale ipcRenderer listeners. - unsubscribeAuthCallback = window.gitify.onAuthCallback((callbackUrl: string) => { - rendererLogInfo( - 'renderer:auth-callback', - `received authentication callback URL ${callbackUrl}`, - ); - handleCallback(callbackUrl); - }); - }); -} - /** * Start a GitHub Device Flow authorization session. * @@ -141,15 +25,18 @@ export function performGitHubWebOAuth(authOptions: LoginOAuthWebOptions): Promis * * @param hostname - The GitHub hostname to authenticate against. Defaults to github.com. * @param scopes - Array of scope names to request. Defaults to recommended (full) scopes. + * @param clientId - The OAuth app client ID to authorize with. Defaults to Gitify's + * own preconfigured app; a user-supplied custom app passes its own. * @returns The device flow session data. */ export async function startGitHubDeviceFlow( hostname: Hostname = Constants.GITHUB_HOSTNAME, scopes: string[] = getRecommendedScopeNames(), + clientId: ClientID = Constants.OAUTH_DEVICE_FLOW_CLIENT_ID, ): Promise { const deviceCode = await createDeviceCode({ clientType: 'oauth-app' as const, - clientId: Constants.OAUTH_DEVICE_FLOW_CLIENT_ID, + clientId: clientId, scopes: scopes, request: request.defaults({ baseUrl: getGitHubAuthBaseUrl(hostname).toString(), @@ -158,7 +45,7 @@ export async function startGitHubDeviceFlow( return { hostname: hostname, - clientId: Constants.OAUTH_DEVICE_FLOW_CLIENT_ID, + clientId: clientId, deviceCode: deviceCode.data.device_code, userCode: deviceCode.data.user_code, verificationUri: deviceCode.data.verification_uri, @@ -167,6 +54,42 @@ export async function startGitHubDeviceFlow( } as DeviceFlowSession; } +/** + * Describe a device-flow start failure in actionable terms. + * + * A device flow can only start when the app has device flow enabled and the + * client ID is known to the host. Both are common setup mistakes for a + * user-supplied custom app, so they are surfaced with a named cause and fix + * instead of a raw transport error. + * + * @param err - The error thrown while starting the device flow. + * @param hostname - The host the session was started against. + * @param clientId - The client ID supplied for a custom app, when any. + * @returns A user-facing message, or `undefined` for an unrecognised failure. + */ +export function describeDeviceFlowError( + err: unknown, + hostname: Hostname, + clientId?: ClientID, +): string | undefined { + if (!(err instanceof RequestError)) { + return undefined; + } + + const errorCode = (err.response?.data as Partial | undefined)?.error; + + switch (errorCode) { + case 'device_flow_disabled': + return clientId + ? `Device flow is not enabled for this OAuth app. Enable it in that app's settings at https://${hostname}/settings/applications/${clientId}, then try again.` + : `Device flow is not enabled for this OAuth app. Enable it in the app's settings on ${hostname}, then try again.`; + case 'incorrect_client_credentials': + return `The client ID is not valid for ${hostname}. Check it and try again.`; + default: + return undefined; + } +} + /** * Poll GitHub to exchange a device code for an access token. * @@ -209,35 +132,3 @@ export async function pollGitHubDeviceFlow(session: DeviceFlowSession): Promise< throw err; } } - -/** - * Exchange an OAuth authorization code for an access token. - * - * `authOptions.clientSecret` is required; this step must be performed - * server-side or in a trusted context to keep the secret confidential. - * - * @param authCode - The authorization code received from the OAuth callback. - * @param authOptions - The OAuth App options, including the client secret. - * @returns The access token. - * @throws If `clientSecret` is absent. - */ -export async function exchangeAuthCodeForAccessToken( - authCode: AuthCode, - authOptions: LoginOAuthWebOptions, -): Promise { - if (!authOptions.clientSecret) { - throw new Error('clientSecret is required to exchange an auth code'); - } - - const { authentication } = await exchangeWebFlowCode({ - clientType: 'oauth-app', - clientId: authOptions.clientId, - clientSecret: authOptions.clientSecret, - code: authCode, - request: request.defaults({ - baseUrl: getGitHubAuthBaseUrl(authOptions.hostname).toString().replace(/\/$/, ''), - }), - }); - - return authentication.token as Token; -} diff --git a/src/renderer/utils/forges/github/octokit.test.ts b/src/renderer/utils/forges/github/octokit.test.ts index 588c468f4..da1a96693 100644 --- a/src/renderer/utils/forges/github/octokit.test.ts +++ b/src/renderer/utils/forges/github/octokit.test.ts @@ -1,5 +1,5 @@ import { - mockGitHubAppAccount, + mockGitifyOAuthAppAccount, mockGitHubCliAccount, mockGitHubCloudAccount, mockGitHubEnterpriseServerAccount, @@ -113,7 +113,7 @@ describe('renderer/utils/forges/github/octokit.ts', () => { const getGitHubAPIBaseUrlSpy = vi.spyOn(utils, 'getGitHubAPIBaseUrl'); getGitHubAPIBaseUrlSpy.mockReturnValue(new URL('https://api.github.com/')); - const octokit1 = await createOctokitClient(mockGitHubAppAccount, 'rest'); + const octokit1 = await createOctokitClient(mockGitifyOAuthAppAccount, 'rest'); const octokit2 = await createOctokitClient(mockGitHubCloudAccount, 'rest'); diff --git a/src/renderer/utils/forges/gitlab/adapter.test.ts b/src/renderer/utils/forges/gitlab/adapter.test.ts index 1f2ddeddd..275faca28 100644 --- a/src/renderer/utils/forges/gitlab/adapter.test.ts +++ b/src/renderer/utils/forges/gitlab/adapter.test.ts @@ -83,7 +83,7 @@ describe('renderer/utils/forges/gitlab/adapter.ts', () => { it('returns the key icon for every auth method (PAT-only forge today)', () => { expect(gitlabAdapter.getAuthMethodIcon('Personal Access Token')).toBe(KeyIcon); - expect(gitlabAdapter.getAuthMethodIcon('OAuth App')).toBe(KeyIcon); + expect(gitlabAdapter.getAuthMethodIcon('Custom OAuth App')).toBe(KeyIcon); }); it('omits the oauthScopes bundle', () => { diff --git a/src/renderer/utils/forges/types.ts b/src/renderer/utils/forges/types.ts index 745d472d6..96d778c0c 100644 --- a/src/renderer/utils/forges/types.ts +++ b/src/renderer/utils/forges/types.ts @@ -4,7 +4,7 @@ import type { OcticonProps } from '@primer/octicons-react'; import type { Account, - AuthCode, + ClientID, Forge, Hostname, IconColor, @@ -14,13 +14,7 @@ import type { Token, UserType, } from '../../types'; -import type { - AuthMethod, - AuthResponse, - DeviceFlowSession, - LoginOAuthWebOptions, - PlatformType, -} from '../auth/types'; +import type { AuthMethod, DeviceFlowSession, PlatformType } from '../auth/types'; /** * Capability flags exposed by a forge adapter. @@ -153,6 +147,11 @@ export interface ForgeAdapter { getOrigin?(hostname: Hostname): string; /** Whether the supplied token matches the forge's PAT format. */ validateToken(token: Token): boolean; + /** + * Whether the supplied OAuth client ID matches the forge's expected format. + * Omitted by forges with no user-supplied OAuth app flow. + */ + validateClientId?(clientId: string): boolean; /** URL to manage/create a personal access token on the forge. */ getPersonalAccessTokenSettingsUrl(hostname: Hostname): Link; /** Login entries rendered in the Login route. */ @@ -179,12 +178,6 @@ export interface ForgeAdapter { */ deviceFlow?: DeviceFlowSupport; - /** - * Custom-OAuth-app web flow. Forges without OAuth-app support (e.g. Gitea) - * omit this bundle entirely — callers gate the OAuth-app UI on its presence. - */ - oauthWebApp?: OAuthWebAppSupport; - /** * Reuse of a locally installed forge CLI's credential. Forges whose CLI * Gitify cannot read a token from omit this bundle entirely — callers gate @@ -320,22 +313,6 @@ export interface ExternallyManagedScopes { command: string; } -/** - * Custom-OAuth-app web flow capability bundle. Present only on forges that - * support browser-redirect OAuth with user-supplied client credentials - * (GitHub today). - */ -export interface OAuthWebAppSupport { - /** Start the OAuth web flow and return the auth code once the user consents. */ - performWebOAuth(options: LoginOAuthWebOptions): Promise; - /** Exchange an OAuth web-flow authorization code for an access token. */ - exchangeAuthCodeForToken(authCode: AuthCode, options: LoginOAuthWebOptions): Promise; - /** Whether the supplied OAuth client ID matches the forge's expected format. */ - validateClientId(clientId: string): boolean; - /** URL the user visits to create a new OAuth app on the forge. */ - getNewOAuthAppUrl(hostname: Hostname): Link; -} - /** * OAuth device-flow capability bundle. Present only on forges that support * browser-side device authorisation (GitHub today). @@ -343,12 +320,19 @@ export interface OAuthWebAppSupport { export interface DeviceFlowSupport { /** `AuthMethod` recorded on the account when a successful login completes. */ authMethod: AuthMethod; - /** Start a device-flow authorization session. */ - start(hostname?: Hostname, scopes?: string[]): Promise; + /** + * Start a device-flow authorization session. + * + * `clientId` authorizes the user's own OAuth app instead of Gitify's + * preconfigured one; when omitted the forge's built-in app is used. + */ + start(hostname?: Hostname, scopes?: string[], clientId?: ClientID): Promise; /** Poll for completion; resolves to a token when granted. */ poll(session: DeviceFlowSession): Promise; - /** URL the user visits to revoke Gitify's access on this forge. */ - getRevokeAccessUrl(hostname: Hostname): Link; + /** URL the user visits to revoke the authorized OAuth app's access on this forge. */ + getRevokeAccessUrl(hostname: Hostname, clientId?: ClientID): Link; + /** URL the user visits to register an OAuth app that can authorize via the device flow. */ + getNewOAuthAppUrl(hostname: Hostname): Link; } /** diff --git a/src/shared/events.ts b/src/shared/events.ts index 90e88c632..ddac48bd3 100644 --- a/src/shared/events.ts +++ b/src/shared/events.ts @@ -5,7 +5,6 @@ const P = 'gitify:' as const; * Each value is prefixed with `gitify:` to prevent collisions. */ export const EVENTS = { - AUTH_CALLBACK: `${P}auth-callback`, QUIT: `${P}quit`, WINDOW_SHOW: `${P}window-show`, WINDOW_HIDE: `${P}window-hide`, @@ -117,7 +116,6 @@ type AssertEventCoverage> = T; * - For events with no payload, `request` is `undefined`. */ export type EventContracts = AssertEventCoverage<{ - [EVENTS.AUTH_CALLBACK]: { request: string; response: undefined }; [EVENTS.QUIT]: { request: undefined; response: undefined }; [EVENTS.WINDOW_SHOW]: { request: undefined; response: undefined }; [EVENTS.WINDOW_HIDE]: { request: undefined; response: undefined }; From bd3eab624d0fca44a85e28dc46083b01a31b3f49 Mon Sep 17 00:00:00 2001 From: Adam Setch Date: Fri, 16 Oct 2026 23:44:22 -0400 Subject: [PATCH 2/3] refactor(auth): clarify device-flow authorizer and custom-app helpers Follow-up naming pass on the device-flow migration: - Rename describeDeviceFlowError -> describeDeviceFlowStartError; it only covers start-time failures (device_flow_disabled / unknown client ID), not poll errors. - Group the custom OAuth app helpers (authMethod, validateClientId, getNewOAuthAppUrl) into a customOAuthApp adapter bundle, so validateClientId and getNewOAuthAppUrl live together instead of split across the top-level adapter and the deviceFlow bundle. - Have useLogins derive the recorded auth method from the adapter's customOAuthApp bundle rather than hardcoding 'Custom OAuth App'. - Extract GitHubDeviceFlowAuthorize as the shared authorization UI and keep GitHubLoginWithDeviceFlowRoute as the thin built-in route element, so the custom route composes a mechanism-named component rather than a login route. - Rename effectiveHostname -> resolvedHostname and migrateAuthMethod -> migrateLegacyAuthMethod. --- src/renderer/hooks/useLogins.ts | 5 +- ....test.tsx => DeviceFlowAuthorize.test.tsx} | 32 +- .../routes/github/DeviceFlowAuthorize.tsx | 384 +++++++++++++++++ .../routes/github/LoginWithCustomOAuthApp.tsx | 6 +- .../routes/github/LoginWithDeviceFlow.tsx | 394 +----------------- src/renderer/stores/useAccountsStore.ts | 4 +- src/renderer/utils/auth/loginValidation.ts | 2 +- .../utils/forges/github/adapter.test.ts | 7 +- src/renderer/utils/forges/github/adapter.ts | 6 +- .../utils/forges/github/flows.test.ts | 8 +- src/renderer/utils/forges/github/flows.ts | 2 +- src/renderer/utils/forges/types.ts | 29 +- 12 files changed, 455 insertions(+), 424 deletions(-) rename src/renderer/routes/github/{LoginWithDeviceFlow.test.tsx => DeviceFlowAuthorize.test.tsx} (92%) create mode 100644 src/renderer/routes/github/DeviceFlowAuthorize.tsx diff --git a/src/renderer/hooks/useLogins.ts b/src/renderer/hooks/useLogins.ts index f6743004e..e5fae7e03 100644 --- a/src/renderer/hooks/useLogins.ts +++ b/src/renderer/hooks/useLogins.ts @@ -94,13 +94,14 @@ export const useLogins = (): LoginsState => { */ const loginWithDeviceFlowComplete = useCallback( async (forge: Forge, token: Token, hostname: Hostname, clientId?: ClientID) => { - const { deviceFlow } = getAdapter(forge); + const { deviceFlow, customOAuthApp } = getAdapter(forge); if (!deviceFlow) { throw new Error(`Forge "${forge}" does not support device flow.`); } // A supplied client ID means the user authorized their own app; without // one the flow used Gitify's preconfigured app. - const method: AuthMethod = clientId ? 'Custom OAuth App' : deviceFlow.authMethod; + const method: AuthMethod = + clientId && customOAuthApp ? customOAuthApp.authMethod : deviceFlow.authMethod; const existingAccount = accounts.find((a) => a.hostname === hostname && a.method === method); if (existingAccount) { diff --git a/src/renderer/routes/github/LoginWithDeviceFlow.test.tsx b/src/renderer/routes/github/DeviceFlowAuthorize.test.tsx similarity index 92% rename from src/renderer/routes/github/LoginWithDeviceFlow.test.tsx rename to src/renderer/routes/github/DeviceFlowAuthorize.test.tsx index 3623b1c0e..91f989d20 100644 --- a/src/renderer/routes/github/LoginWithDeviceFlow.test.tsx +++ b/src/renderer/routes/github/DeviceFlowAuthorize.test.tsx @@ -8,9 +8,9 @@ import type { ClientID, Hostname } from '../../types'; import * as logger from '../../utils/core/logger'; import * as comms from '../../utils/system/comms'; -import { GitHubLoginWithDeviceFlowRoute } from './LoginWithDeviceFlow'; +import { GitHubDeviceFlowAuthorize } from './DeviceFlowAuthorize'; -describe('renderer/routes/github/LoginWithDeviceFlow.tsx', () => { +describe('renderer/routes/github/DeviceFlowAuthorize.tsx', () => { const copyToClipboardSpy = vi.spyOn(comms, 'copyToClipboard').mockResolvedValue(); const openExternalLinkSpy = vi.spyOn(comms, 'openExternalLink').mockImplementation(vi.fn()); @@ -25,7 +25,7 @@ describe('renderer/routes/github/LoginWithDeviceFlow.tsx', () => { expiresAt: Date.now() + 900000, }); - renderWithProviders(, { + renderWithProviders(, { loginWithDeviceFlowStart: loginWithDeviceFlowStartMock, }); @@ -48,7 +48,7 @@ describe('renderer/routes/github/LoginWithDeviceFlow.tsx', () => { expiresAt: Date.now() + 900000, }); - renderWithProviders(, { + renderWithProviders(, { loginWithDeviceFlowStart: loginWithDeviceFlowStartMock, }); @@ -80,7 +80,7 @@ describe('renderer/routes/github/LoginWithDeviceFlow.tsx', () => { expiresAt: Date.now() + 900000, }); - renderWithProviders(, { + renderWithProviders(, { loginWithDeviceFlowStart: loginWithDeviceFlowStartMock, }); @@ -107,7 +107,7 @@ describe('renderer/routes/github/LoginWithDeviceFlow.tsx', () => { expiresAt: Date.now() + 900000, }); - renderWithProviders(, { + renderWithProviders(, { loginWithDeviceFlowStart: loginWithDeviceFlowStartMock, }); @@ -130,7 +130,7 @@ describe('renderer/routes/github/LoginWithDeviceFlow.tsx', () => { expiresAt: Date.now() + 900000, }); - renderWithProviders(, { + renderWithProviders(, { loginWithDeviceFlowStart: loginWithDeviceFlowStartMock, }); @@ -159,7 +159,7 @@ describe('renderer/routes/github/LoginWithDeviceFlow.tsx', () => { expiresAt: Date.now() + 900000, }); - renderWithProviders(, { + renderWithProviders(, { loginWithDeviceFlowStart: loginWithDeviceFlowStartMock, }); @@ -178,7 +178,7 @@ describe('renderer/routes/github/LoginWithDeviceFlow.tsx', () => { const rendererLogErrorSpy = vi.spyOn(logger, 'rendererLogError').mockImplementation(vi.fn()); const loginWithDeviceFlowStartMock = vi.fn().mockRejectedValueOnce(new Error('Network error')); - renderWithProviders(, { + renderWithProviders(, { loginWithDeviceFlowStart: loginWithDeviceFlowStartMock, }); @@ -199,7 +199,7 @@ describe('renderer/routes/github/LoginWithDeviceFlow.tsx', () => { expiresAt: Date.now() + 900000, }); - renderWithProviders(, { + renderWithProviders(, { loginWithDeviceFlowStart: loginWithDeviceFlowStartMock, }); @@ -221,7 +221,7 @@ describe('renderer/routes/github/LoginWithDeviceFlow.tsx', () => { const loginWithDeviceFlowPollMock = vi.fn().mockResolvedValueOnce('access-token'); const loginWithDeviceFlowCompleteMock = vi.fn().mockResolvedValueOnce(undefined); - renderWithProviders(, { + renderWithProviders(, { loginWithDeviceFlowStart: loginWithDeviceFlowStartMock, loginWithDeviceFlowPoll: loginWithDeviceFlowPollMock, loginWithDeviceFlowComplete: loginWithDeviceFlowCompleteMock, @@ -257,7 +257,7 @@ describe('renderer/routes/github/LoginWithDeviceFlow.tsx', () => { .fn() .mockRejectedValueOnce(new Error('authorization denied')); - renderWithProviders(, { + renderWithProviders(, { loginWithDeviceFlowStart: loginWithDeviceFlowStartMock, loginWithDeviceFlowPoll: loginWithDeviceFlowPollMock, }); @@ -284,7 +284,7 @@ describe('renderer/routes/github/LoginWithDeviceFlow.tsx', () => { }); const loginWithDeviceFlowPollMock = vi.fn(); - renderWithProviders(, { + renderWithProviders(, { loginWithDeviceFlowStart: loginWithDeviceFlowStartMock, loginWithDeviceFlowPoll: loginWithDeviceFlowPollMock, }); @@ -309,7 +309,7 @@ describe('renderer/routes/github/LoginWithDeviceFlow.tsx', () => { expiresAt: Date.now() + 900000, }); - renderWithProviders(, { + renderWithProviders(, { loginWithDeviceFlowStart: loginWithDeviceFlowStartMock, loginWithDeviceFlowPoll: vi.fn().mockResolvedValueOnce(null), initialEntries: [ @@ -347,7 +347,7 @@ describe('renderer/routes/github/LoginWithDeviceFlow.tsx', () => { }); renderWithProviders( - , @@ -365,7 +365,7 @@ describe('renderer/routes/github/LoginWithDeviceFlow.tsx', () => { }); it('should open the revoke-access developer settings link', async () => { - renderWithProviders(); + renderWithProviders(); await userEvent.click(screen.getByTitle('GitHub → Developer Settings')); diff --git a/src/renderer/routes/github/DeviceFlowAuthorize.tsx b/src/renderer/routes/github/DeviceFlowAuthorize.tsx new file mode 100644 index 000000000..eac8ffe0a --- /dev/null +++ b/src/renderer/routes/github/DeviceFlowAuthorize.tsx @@ -0,0 +1,384 @@ +import { type ReactNode, useCallback, useEffect, useState } from 'react'; +import { useLocation, useNavigate } from 'react-router-dom'; + +import { + CheckCircleFillIcon, + CopyIcon, + GlobeIcon, + LinkIcon, + SignInIcon, + SyncIcon, +} from '@primer/octicons-react'; +import { Banner, Button, Heading, IconButton, Stack, Text } from '@primer/react'; + +import { Constants } from '../../constants'; + +import { useLogins } from '../../hooks/useLogins'; + +import { Contents } from '../../components/layout/Contents'; +import { Page } from '../../components/layout/Page'; +import { Footer } from '../../components/primitives/Footer'; +import { Header } from '../../components/primitives/Header'; + +import type { Account, ClientID, Forge, Hostname, Link } from '../../types'; +import type { DeviceFlowSession } from '../../utils/auth/types'; + +import { getAlternateScopeNames, getRecommendedScopeNames } from '../../utils/auth/scopes'; +import { rendererLogError, toError } from '../../utils/core/logger'; +import { describeDeviceFlowStartError } from '../../utils/forges/github/flows'; +import { getAdapter } from '../../utils/forges/registry'; +import { copyToClipboard, openExternalLink } from '../../utils/system/comms'; + +interface LocationState { + account?: Account; +} + +interface GitHubDeviceFlowAuthorizeProps { + /** OAuth app client ID to authorize; omit to use Gitify's preconfigured app. */ + clientId?: ClientID; + /** Hostname to authenticate against; defaults to the re-auth account or GitHub.com. */ + hostname?: Hostname; + /** Page heading; defaults to authorizing Gitify's preconfigured app. */ + title?: string; +} + +type ScopeChoice = 'public' | 'full'; + +export function GitHubDeviceFlowAuthorize({ + clientId, + hostname, + title, +}: GitHubDeviceFlowAuthorizeProps) { + const navigate = useNavigate(); + const location = useLocation(); + const { account: reAuthAccount } = (location.state ?? {}) as LocationState; + + const forge: Forge = 'github'; + + const resolvedHostname = (hostname ?? + reAuthAccount?.hostname ?? + Constants.GITHUB_HOSTNAME) as Hostname; + + const { loginWithDeviceFlowStart, loginWithDeviceFlowPoll, loginWithDeviceFlowComplete } = + useLogins(); + + const [session, setSession] = useState(null); + const [isPolling, setIsPolling] = useState(false); + const [error, setError] = useState(null); + const [scopeChoice, setScopeChoice] = useState(null); + + // Initialize device flow session, copy code, and open browser + useEffect(() => { + const initializeDeviceFlow = async () => { + try { + const scopes = + scopeChoice === 'public' ? getAlternateScopeNames() : getRecommendedScopeNames(); + + const newSession = await loginWithDeviceFlowStart( + forge, + resolvedHostname, + scopes, + clientId, + ); + setSession(newSession); + + // Auto-copy the user code to clipboard + await copyToClipboard(newSession.userCode); + + // Auto-open the verification URL in the browser + openExternalLink(newSession.verificationUri as Link); + } catch (err) { + rendererLogError('LoginWithDeviceFlow', 'Failed to start device flow', toError(err)); + setError( + describeDeviceFlowStartError(err, resolvedHostname, clientId) ?? + 'Failed to start authentication. Please try again.', + ); + } + }; + + if (scopeChoice) { + void initializeDeviceFlow(); + } + }, [loginWithDeviceFlowStart, resolvedHostname, clientId, scopeChoice, forge]); + + // Poll for device flow completion + useEffect(() => { + if (!session) { + return; + } + + let isActive = true; + let timeoutId: NodeJS.Timeout | null = null; + + const startPolling = async () => { + setIsPolling(true); + + const poll = async (): Promise => { + if (!isActive || Date.now() >= session.expiresAt) { + if (isActive) { + setError('Device code expired. Please start again.'); + } + return; + } + + const token = await loginWithDeviceFlowPoll(forge, session); + + if (token && isActive) { + await loginWithDeviceFlowComplete(forge, token, session.hostname, clientId); + void navigate('/'); + return; + } + + const intervalMs = Math.max(5000, session.intervalSeconds * 1000); + await new Promise((resolve) => { + timeoutId = setTimeout(resolve, intervalMs); + }); + await poll(); + }; + + try { + await poll(); + } catch (err) { + if (isActive) { + rendererLogError('LoginWithDeviceFlow', 'Failed to poll device flow', toError(err)); + setError('Authentication failed. Please try again.'); + } + } finally { + if (isActive) { + setIsPolling(false); + } + } + }; + + void startPolling(); + + return () => { + isActive = false; + if (timeoutId) { + clearTimeout(timeoutId); + } + }; + // oxlint-disable-next-line react/exhaustive-deps -- navigate is stable + }, [session, loginWithDeviceFlowPoll, loginWithDeviceFlowComplete, clientId, forge]); + + const handleCopyUserCode = useCallback(async () => { + if (session?.userCode) { + await copyToClipboard(session.userCode); + } + }, [session?.userCode]); + + const handleOpenBrowser = useCallback(() => { + if (session?.verificationUri) { + openExternalLink(session.verificationUri as Link); + } + }, [session?.verificationUri]); + + const handleCopyVerificationLink = useCallback(async () => { + if (session?.verificationUri) { + await copyToClipboard(session.verificationUri); + } + }, [session?.verificationUri]); + + // Render UI states as separate functions for clarity + const renderSessionUI = () => { + if (!session) { + return null; + } + + return ( + + + Authorize the app using this code. + + + + + {session.userCode} + + + + + + + + Code copied to clipboard + + + + + + Opened{' '} + {session.hostname === Constants.GITHUB_HOSTNAME ? 'GitHub.com' : session.hostname} + + + + + + Waiting for authorization... + + + + ); + }; + + const renderScopeChoiceUI = () => ( + + Receive notifications for: + + + + + + + {(() => { + const adapter = getAdapter(forge); + const revokeUrl = adapter.deviceFlow?.getRevokeAccessUrl(resolvedHostname, clientId); + if (!revokeUrl) { + return null; + } + const label = `${adapter.displayName} → Developer Settings`; + return ( + + + {clientId + ? "Note: to change previously granted permissions, revoke your app's access at" + : "Note: to change previously granted permissions, revoke Gitify's access at"}{' '} + + {', then re-authorize above.'} + + + ); + })()} + + + ); + + const renderInitializingUI = () => ( + + + Initializing authentication... + + ); + + let mainContent: ReactNode; + if (session) { + mainContent = renderSessionUI(); + } else if (!scopeChoice) { + mainContent = renderScopeChoiceUI(); + } else { + mainContent = renderInitializingUI(); + } + + return ( + +
{title ?? 'Authorize with GitHub'}
+ + + {error && ( + + + {error} + + + } + hideTitle + title="Login errors" + variant="critical" + /> + )} + {mainContent} + + + {session ? ( +
+ + + + + +
+ ) : ( +
+ +
+ )} +
+ ); +} diff --git a/src/renderer/routes/github/LoginWithCustomOAuthApp.tsx b/src/renderer/routes/github/LoginWithCustomOAuthApp.tsx index 0c9ebc8e5..ab41e6e4d 100644 --- a/src/renderer/routes/github/LoginWithCustomOAuthApp.tsx +++ b/src/renderer/routes/github/LoginWithCustomOAuthApp.tsx @@ -20,7 +20,7 @@ import { } from '../../utils/auth/loginValidation'; import { getAdapter } from '../../utils/forges/registry'; import { openExternalLink } from '../../utils/system/comms'; -import { GitHubLoginWithDeviceFlowRoute } from './LoginWithDeviceFlow'; +import { GitHubDeviceFlowAuthorize } from './DeviceFlowAuthorize'; interface LocationState { account?: Account; @@ -62,7 +62,7 @@ export function GitHubLoginWithCustomOAuthAppRoute() { if (credentials) { return ( - { - const url = getAdapter(forge).deviceFlow?.getNewOAuthAppUrl(formData.hostname); + const url = getAdapter(forge).customOAuthApp?.getNewOAuthAppUrl(formData.hostname); if (url) { openExternalLink(url); } diff --git a/src/renderer/routes/github/LoginWithDeviceFlow.tsx b/src/renderer/routes/github/LoginWithDeviceFlow.tsx index 83f8fbebb..2d7bd7ae5 100644 --- a/src/renderer/routes/github/LoginWithDeviceFlow.tsx +++ b/src/renderer/routes/github/LoginWithDeviceFlow.tsx @@ -1,384 +1,12 @@ -import { type ReactNode, useCallback, useEffect, useState } from 'react'; -import { useLocation, useNavigate } from 'react-router-dom'; - -import { - CheckCircleFillIcon, - CopyIcon, - GlobeIcon, - LinkIcon, - SignInIcon, - SyncIcon, -} from '@primer/octicons-react'; -import { Banner, Button, Heading, IconButton, Stack, Text } from '@primer/react'; - -import { Constants } from '../../constants'; - -import { useLogins } from '../../hooks/useLogins'; - -import { Contents } from '../../components/layout/Contents'; -import { Page } from '../../components/layout/Page'; -import { Footer } from '../../components/primitives/Footer'; -import { Header } from '../../components/primitives/Header'; - -import type { Account, ClientID, Forge, Hostname, Link } from '../../types'; -import type { DeviceFlowSession } from '../../utils/auth/types'; - -import { getAlternateScopeNames, getRecommendedScopeNames } from '../../utils/auth/scopes'; -import { rendererLogError, toError } from '../../utils/core/logger'; -import { describeDeviceFlowError } from '../../utils/forges/github/flows'; -import { getAdapter } from '../../utils/forges/registry'; -import { copyToClipboard, openExternalLink } from '../../utils/system/comms'; - -interface LocationState { - account?: Account; -} - -interface GitHubLoginWithDeviceFlowRouteProps { - /** OAuth app client ID to authorize; omit to use Gitify's preconfigured app. */ - clientId?: ClientID; - /** Hostname to authenticate against; defaults to the re-auth account or GitHub.com. */ - hostname?: Hostname; - /** Page heading; defaults to authorizing Gitify's preconfigured app. */ - title?: string; -} - -type ScopeChoice = 'public' | 'full'; - -export function GitHubLoginWithDeviceFlowRoute({ - clientId, - hostname, - title, -}: GitHubLoginWithDeviceFlowRouteProps) { - const navigate = useNavigate(); - const location = useLocation(); - const { account: reAuthAccount } = (location.state ?? {}) as LocationState; - - const forge: Forge = 'github'; - - const effectiveHostname = (hostname ?? - reAuthAccount?.hostname ?? - Constants.GITHUB_HOSTNAME) as Hostname; - - const { loginWithDeviceFlowStart, loginWithDeviceFlowPoll, loginWithDeviceFlowComplete } = - useLogins(); - - const [session, setSession] = useState(null); - const [isPolling, setIsPolling] = useState(false); - const [error, setError] = useState(null); - const [scopeChoice, setScopeChoice] = useState(null); - - // Initialize device flow session, copy code, and open browser - useEffect(() => { - const initializeDeviceFlow = async () => { - try { - const scopes = - scopeChoice === 'public' ? getAlternateScopeNames() : getRecommendedScopeNames(); - - const newSession = await loginWithDeviceFlowStart( - forge, - effectiveHostname, - scopes, - clientId, - ); - setSession(newSession); - - // Auto-copy the user code to clipboard - await copyToClipboard(newSession.userCode); - - // Auto-open the verification URL in the browser - openExternalLink(newSession.verificationUri as Link); - } catch (err) { - rendererLogError('LoginWithDeviceFlow', 'Failed to start device flow', toError(err)); - setError( - describeDeviceFlowError(err, effectiveHostname, clientId) ?? - 'Failed to start authentication. Please try again.', - ); - } - }; - - if (scopeChoice) { - void initializeDeviceFlow(); - } - }, [loginWithDeviceFlowStart, effectiveHostname, clientId, scopeChoice, forge]); - - // Poll for device flow completion - useEffect(() => { - if (!session) { - return; - } - - let isActive = true; - let timeoutId: NodeJS.Timeout | null = null; - - const startPolling = async () => { - setIsPolling(true); - - const poll = async (): Promise => { - if (!isActive || Date.now() >= session.expiresAt) { - if (isActive) { - setError('Device code expired. Please start again.'); - } - return; - } - - const token = await loginWithDeviceFlowPoll(forge, session); - - if (token && isActive) { - await loginWithDeviceFlowComplete(forge, token, session.hostname, clientId); - void navigate('/'); - return; - } - - const intervalMs = Math.max(5000, session.intervalSeconds * 1000); - await new Promise((resolve) => { - timeoutId = setTimeout(resolve, intervalMs); - }); - await poll(); - }; - - try { - await poll(); - } catch (err) { - if (isActive) { - rendererLogError('LoginWithDeviceFlow', 'Failed to poll device flow', toError(err)); - setError('Authentication failed. Please try again.'); - } - } finally { - if (isActive) { - setIsPolling(false); - } - } - }; - - void startPolling(); - - return () => { - isActive = false; - if (timeoutId) { - clearTimeout(timeoutId); - } - }; - // oxlint-disable-next-line react/exhaustive-deps -- navigate is stable - }, [session, loginWithDeviceFlowPoll, loginWithDeviceFlowComplete, clientId, forge]); - - const handleCopyUserCode = useCallback(async () => { - if (session?.userCode) { - await copyToClipboard(session.userCode); - } - }, [session?.userCode]); - - const handleOpenBrowser = useCallback(() => { - if (session?.verificationUri) { - openExternalLink(session.verificationUri as Link); - } - }, [session?.verificationUri]); - - const handleCopyVerificationLink = useCallback(async () => { - if (session?.verificationUri) { - await copyToClipboard(session.verificationUri); - } - }, [session?.verificationUri]); - - // Render UI states as separate functions for clarity - const renderSessionUI = () => { - if (!session) { - return null; - } - - return ( - - - Authorize the app using this code. - - - - - {session.userCode} - - - - - - - - Code copied to clipboard - - - - - - Opened{' '} - {session.hostname === Constants.GITHUB_HOSTNAME ? 'GitHub.com' : session.hostname} - - - - - - Waiting for authorization... - - - - ); - }; - - const renderScopeChoiceUI = () => ( - - Receive notifications for: - - - - - - - {(() => { - const adapter = getAdapter(forge); - const revokeUrl = adapter.deviceFlow?.getRevokeAccessUrl(effectiveHostname, clientId); - if (!revokeUrl) { - return null; - } - const label = `${adapter.displayName} → Developer Settings`; - return ( - - - {clientId - ? "Note: to change previously granted permissions, revoke your app's access at" - : "Note: to change previously granted permissions, revoke Gitify's access at"}{' '} - - {', then re-authorize above.'} - - - ); - })()} - - - ); - - const renderInitializingUI = () => ( - - - Initializing authentication... - - ); - - let mainContent: ReactNode; - if (session) { - mainContent = renderSessionUI(); - } else if (!scopeChoice) { - mainContent = renderScopeChoiceUI(); - } else { - mainContent = renderInitializingUI(); - } - - return ( - -
{title ?? 'Authorize with GitHub'}
- - - {error && ( - - - {error} - - - } - hideTitle - title="Login errors" - variant="critical" - /> - )} - {mainContent} - - - {session ? ( -
- - - - - -
- ) : ( -
- -
- )} -
- ); +import { GitHubDeviceFlowAuthorize } from './DeviceFlowAuthorize'; + +/** + * Route element for `/login/github/device-flow`. + * + * Authorizes Gitify's preconfigured OAuth app on GitHub.com. A user-supplied + * custom app uses the `/login/github/custom-oauth-app` route instead, which + * composes the same `GitHubDeviceFlowAuthorize` UI with its own client ID. + */ +export function GitHubLoginWithDeviceFlowRoute() { + return ; } diff --git a/src/renderer/stores/useAccountsStore.ts b/src/renderer/stores/useAccountsStore.ts index eb0e67752..e98426ee8 100644 --- a/src/renderer/stores/useAccountsStore.ts +++ b/src/renderer/stores/useAccountsStore.ts @@ -20,7 +20,7 @@ import { DEFAULT_ACCOUNTS_STATE } from './defaults'; * carry the retired `GitHub App` / `OAuth App` labels; relabel them so their * existing tokens keep working without forcing a re-login. */ -function migrateAuthMethod(method: string): AuthMethod { +function migrateLegacyAuthMethod(method: string): AuthMethod { switch (method) { case 'GitHub App': return 'Gitify OAuth App'; @@ -46,7 +46,7 @@ export function sanitizeAccounts(accounts: Account[]): Account[] { } const sanitised: Account = { forge, - method: migrateAuthMethod(a.method as string), + method: migrateLegacyAuthMethod(a.method as string), platform: a.platform, version: a.version, hostname: a.hostname, diff --git a/src/renderer/utils/auth/loginValidation.ts b/src/renderer/utils/auth/loginValidation.ts index 0b7db50af..44289c800 100644 --- a/src/renderer/utils/auth/loginValidation.ts +++ b/src/renderer/utils/auth/loginValidation.ts @@ -90,7 +90,7 @@ export const validateCustomOAuthAppForm = ( if (!values.clientId) { errors.clientId = 'Client ID is required'; - } else if (!adapter.validateClientId?.(values.clientId)) { + } else if (!adapter.customOAuthApp?.validateClientId(values.clientId)) { errors.clientId = 'Client ID format is invalid'; } diff --git a/src/renderer/utils/forges/github/adapter.test.ts b/src/renderer/utils/forges/github/adapter.test.ts index 510abf19c..a9800ed9b 100644 --- a/src/renderer/utils/forges/github/adapter.test.ts +++ b/src/renderer/utils/forges/github/adapter.test.ts @@ -108,13 +108,14 @@ describe('renderer/utils/forges/github/adapter.ts', () => { expect(session.clientId).toBe('CUSTOM_CLIENT_ID_123'); }); - it('wires the device-flow methods so the context can dispatch via the adapter', () => { + it('wires the device-flow and custom-OAuth-app methods so the context can dispatch via the adapter', () => { expect(githubAdapter.deviceFlow?.authMethod).toBe('Gitify OAuth App'); expect(githubAdapter.deviceFlow?.start).toBeDefined(); expect(githubAdapter.deviceFlow?.poll).toBeDefined(); expect(githubAdapter.deviceFlow?.getRevokeAccessUrl).toBeDefined(); - expect(githubAdapter.deviceFlow?.getNewOAuthAppUrl).toBeDefined(); - expect(githubAdapter.validateClientId).toBeDefined(); + expect(githubAdapter.customOAuthApp?.authMethod).toBe('Custom OAuth App'); + expect(githubAdapter.customOAuthApp?.validateClientId).toBeDefined(); + expect(githubAdapter.customOAuthApp?.getNewOAuthAppUrl).toBeDefined(); }); }); diff --git a/src/renderer/utils/forges/github/adapter.ts b/src/renderer/utils/forges/github/adapter.ts index 30aa84f15..08c74e170 100644 --- a/src/renderer/utils/forges/github/adapter.ts +++ b/src/renderer/utils/forges/github/adapter.ts @@ -93,7 +93,6 @@ export const githubAdapter: ForgeAdapter = { defaultHostname: Constants.GITHUB_HOSTNAME, validateToken: isValidToken, - validateClientId: isValidClientId, validateHostname: isValidHostname, getPersonalAccessTokenSettingsUrl: getNewTokenURL, documentationUrl: Constants.GITHUB_DOCS.PAT_URL as Link, @@ -137,6 +136,11 @@ export const githubAdapter: ForgeAdapter = { poll: pollGitHubDeviceFlow, getRevokeAccessUrl: (hostname, clientId) => getDeveloperSettingsURL({ hostname, method: 'Custom OAuth App', clientId } as Account), + }, + + customOAuthApp: { + authMethod: 'Custom OAuth App', + validateClientId: isValidClientId, getNewOAuthAppUrl: getNewOAuthAppURL, }, diff --git a/src/renderer/utils/forges/github/flows.test.ts b/src/renderer/utils/forges/github/flows.test.ts index 5f672853f..b3852cf22 100644 --- a/src/renderer/utils/forges/github/flows.test.ts +++ b/src/renderer/utils/forges/github/flows.test.ts @@ -178,12 +178,12 @@ describe('renderer/utils/forges/github/flows.ts', () => { }); }); - describe('describeDeviceFlowError', () => { + describe('describeDeviceFlowStartError', () => { it('explains how to enable device flow for a custom app', () => { const err = Object.create(RequestError.prototype); err.response = { data: { error: 'device_flow_disabled' } }; - const message = flows.describeDeviceFlowError( + const message = flows.describeDeviceFlowStartError( err, 'github.example.com' as Hostname, 'CUSTOM_CLIENT_ID_123' as ClientID, @@ -200,7 +200,7 @@ describe('renderer/utils/forges/github/flows.ts', () => { err.response = { data: { error: 'incorrect_client_credentials' } }; expect( - flows.describeDeviceFlowError( + flows.describeDeviceFlowStartError( err, 'github.example.com' as Hostname, 'CUSTOM_CLIENT_ID_123' as ClientID, @@ -210,7 +210,7 @@ describe('renderer/utils/forges/github/flows.ts', () => { it('returns undefined for a non-RequestError', () => { expect( - flows.describeDeviceFlowError(new Error('boom'), 'github.com' as Hostname), + flows.describeDeviceFlowStartError(new Error('boom'), 'github.com' as Hostname), ).toBeUndefined(); }); }); diff --git a/src/renderer/utils/forges/github/flows.ts b/src/renderer/utils/forges/github/flows.ts index 4c74211f2..1b49ebf17 100644 --- a/src/renderer/utils/forges/github/flows.ts +++ b/src/renderer/utils/forges/github/flows.ts @@ -67,7 +67,7 @@ export async function startGitHubDeviceFlow( * @param clientId - The client ID supplied for a custom app, when any. * @returns A user-facing message, or `undefined` for an unrecognised failure. */ -export function describeDeviceFlowError( +export function describeDeviceFlowStartError( err: unknown, hostname: Hostname, clientId?: ClientID, diff --git a/src/renderer/utils/forges/types.ts b/src/renderer/utils/forges/types.ts index 96d778c0c..b5da9fe08 100644 --- a/src/renderer/utils/forges/types.ts +++ b/src/renderer/utils/forges/types.ts @@ -147,11 +147,6 @@ export interface ForgeAdapter { getOrigin?(hostname: Hostname): string; /** Whether the supplied token matches the forge's PAT format. */ validateToken(token: Token): boolean; - /** - * Whether the supplied OAuth client ID matches the forge's expected format. - * Omitted by forges with no user-supplied OAuth app flow. - */ - validateClientId?(clientId: string): boolean; /** URL to manage/create a personal access token on the forge. */ getPersonalAccessTokenSettingsUrl(hostname: Hostname): Link; /** Login entries rendered in the Login route. */ @@ -167,9 +162,9 @@ export interface ForgeAdapter { // --- Auth flows --- // Optional because not every forge supports every flow. Gitea today is - // PAT-only and omits both bundles. The orchestrator gates UI on the + // PAT-only and omits every bundle. The orchestrator gates UI on the // presence of these bundles (and on `loginMethods` entries pointing at - // the forge's device-flow or OAuth-app routes). + // the forge's device-flow or custom-OAuth-app routes). /** * OAuth device-flow capability. Forges without device-flow support (e.g. @@ -178,6 +173,13 @@ export interface ForgeAdapter { */ deviceFlow?: DeviceFlowSupport; + /** + * Custom-OAuth-app capability. Forges where a user cannot authorize their + * own OAuth app (e.g. Gitea) omit this bundle entirely — callers gate the + * custom-app UI on its presence. + */ + customOAuthApp?: CustomOAuthAppSupport; + /** * Reuse of a locally installed forge CLI's credential. Forges whose CLI * Gitify cannot read a token from omit this bundle entirely — callers gate @@ -331,7 +333,18 @@ export interface DeviceFlowSupport { poll(session: DeviceFlowSession): Promise; /** URL the user visits to revoke the authorized OAuth app's access on this forge. */ getRevokeAccessUrl(hostname: Hostname, clientId?: ClientID): Link; - /** URL the user visits to register an OAuth app that can authorize via the device flow. */ +} + +/** + * Custom-OAuth-app capability bundle. Present only on forges where a user can + * authorize their own OAuth app (GitHub today). + */ +export interface CustomOAuthAppSupport { + /** `AuthMethod` recorded on accounts created from a user-supplied OAuth app. */ + authMethod: AuthMethod; + /** Whether the supplied OAuth client ID matches the forge's expected format. */ + validateClientId(clientId: string): boolean; + /** URL the user visits to register a new OAuth app on the forge. */ getNewOAuthAppUrl(hostname: Hostname): Link; } From 7c0b8dc0b614f018e603d9c24fc5a529773a7624 Mon Sep 17 00:00:00 2001 From: Adam Setch Date: Sat, 17 Oct 2026 09:17:44 -0400 Subject: [PATCH 3/3] refactor(auth): build OAuth app connections URL without a synthetic account `deviceFlow.getRevokeAccessUrl` fabricated an Account (inventing a `method` that was not the account's real one) purely to call `getDeveloperSettingsURL`. Extract `getAuthorizedOAuthAppURL(hostname, clientId?)` as the single builder for the `/settings/connections/applications/:client_id` page, use it directly as the revoke-access URL, and have `getDeveloperSettingsURL` delegate to it for the OAuth-app methods. This removes the cast and keeps the revoke link and the account settings link on one source of truth. --- src/renderer/utils/forges/github/adapter.ts | 4 +- src/renderer/utils/forges/github/auth.test.ts | 18 +++++++++ src/renderer/utils/forges/github/auth.ts | 37 +++++++++++-------- 3 files changed, 42 insertions(+), 17 deletions(-) diff --git a/src/renderer/utils/forges/github/adapter.ts b/src/renderer/utils/forges/github/adapter.ts index 08c74e170..05b01973e 100644 --- a/src/renderer/utils/forges/github/adapter.ts +++ b/src/renderer/utils/forges/github/adapter.ts @@ -9,6 +9,7 @@ import type { ForgeAdapter, NotificationDisplayHelpers, RefreshAccountData } fro import { isValidHostname } from '../../auth/utils'; import { extractHostVersion, + getAuthorizedOAuthAppURL, getDeveloperSettingsURL, getNewOAuthAppURL, getNewTokenURL, @@ -134,8 +135,7 @@ export const githubAdapter: ForgeAdapter = { authMethod: 'Gitify OAuth App', start: startGitHubDeviceFlow, poll: pollGitHubDeviceFlow, - getRevokeAccessUrl: (hostname, clientId) => - getDeveloperSettingsURL({ hostname, method: 'Custom OAuth App', clientId } as Account), + getRevokeAccessUrl: getAuthorizedOAuthAppURL, }, customOAuthApp: { diff --git a/src/renderer/utils/forges/github/auth.test.ts b/src/renderer/utils/forges/github/auth.test.ts index 07e5442d6..fb922a6e9 100644 --- a/src/renderer/utils/forges/github/auth.test.ts +++ b/src/renderer/utils/forges/github/auth.test.ts @@ -5,6 +5,7 @@ import type { AuthMethod } from '../../auth/types'; import { extractHostVersion, + getAuthorizedOAuthAppURL, getDeveloperSettingsURL, getGitHubAuthBaseUrl, getNewOAuthAppURL, @@ -60,6 +61,23 @@ describe('renderer/utils/forges/github/auth.ts', () => { }); }); + describe('getAuthorizedOAuthAppURL', () => { + it("builds the connections URL for a custom app's own client ID", () => { + expect( + getAuthorizedOAuthAppURL( + 'github.example.com' as Hostname, + 'CUSTOM_CLIENT_ID_123' as ClientID, + ), + ).toBe('https://github.example.com/settings/connections/applications/CUSTOM_CLIENT_ID_123'); + }); + + it("falls back to Gitify's preconfigured client ID when none is supplied", () => { + expect(getAuthorizedOAuthAppURL('github.com' as Hostname)).toBe( + 'https://github.com/settings/connections/applications/FAKE_CLIENT_ID_123', + ); + }); + }); + describe('getDeveloperSettingsURL', () => { it('returns the connections URL for Gitify OAuth App accounts', () => { expect( diff --git a/src/renderer/utils/forges/github/auth.ts b/src/renderer/utils/forges/github/auth.ts index c1a155f9d..f10568654 100644 --- a/src/renderer/utils/forges/github/auth.ts +++ b/src/renderer/utils/forges/github/auth.ts @@ -59,11 +59,27 @@ export function getGitHubAuthBaseUrl(hostname: Hostname): URL { return url; } +/** + * Build the URL where a user reviews and revokes an authorized OAuth app's access. + * + * Omit `clientId` for Gitify's preconfigured app; pass one for a user-supplied app. + * + * @param hostname - The GitHub hostname the app is authorized on. + * @param clientId - The OAuth app's client ID, when the account supplied one. + * @returns The authorized-applications URL for that client ID. + */ +export function getAuthorizedOAuthAppURL(hostname: Hostname, clientId?: ClientID): Link { + const url = new URL(`https://${hostname}`); + url.pathname = `/settings/connections/applications/${ + clientId ?? Constants.OAUTH_DEVICE_FLOW_CLIENT_ID + }`; + return url.toString() as Link; +} + /** * Return the GitHub developer settings URL appropriate for the account's auth method. * - * - Gitify OAuth App / Custom OAuth App → the OAuth app's connection page for the - * account's own client ID (falling back to Gitify's preconfigured app) + * - Gitify OAuth App / Custom OAuth App → the OAuth app's authorized-applications page * - GitHub CLI → authorized applications page * - Personal Access Token → tokens settings page * @@ -71,26 +87,17 @@ export function getGitHubAuthBaseUrl(hostname: Hostname): URL { * @returns The URL to the relevant GitHub developer settings page. */ export function getDeveloperSettingsURL(account: Account): Link { - const settingsURL = new URL(`https://${account.hostname}`); - switch (account.method) { case 'Gitify OAuth App': case 'Custom OAuth App': - settingsURL.pathname = `/settings/connections/applications/${ - account.clientId ?? Constants.OAUTH_DEVICE_FLOW_CLIENT_ID - }`; - break; + return getAuthorizedOAuthAppURL(account.hostname, account.clientId); case 'GitHub CLI': - settingsURL.pathname = '/settings/applications'; - break; + return `https://${account.hostname}/settings/applications` as Link; case 'Personal Access Token': - settingsURL.pathname = '/settings/tokens'; - break; + return `https://${account.hostname}/settings/tokens` as Link; default: - settingsURL.pathname = '/settings'; - break; + return `https://${account.hostname}/settings` as Link; } - return settingsURL.toString() as Link; } /**