From 50bce6c1d676c4ab7e9005ab8438858bf970ae8b Mon Sep 17 00:00:00 2001 From: Ramnique Singh <30795890+ramnique@users.noreply.github.com> Date: Fri, 10 Apr 2026 00:42:59 +0530 Subject: [PATCH] feat(oauth): switch Google OAuth from PKCE to authorization code flow with client secret MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, the Google OAuth integration used a PKCE-only flow (no client secret). This switches to a standard authorization code flow where the user provides both a Client ID and Client Secret from a "Web application" type OAuth client in Google Cloud Console. PKCE is retained alongside the secret for defense in depth. Key changes: - oauth-client.ts: discoverConfiguration() and createStaticConfiguration() now accept an optional clientSecret param. When provided, uses ClientSecretPost instead of None() for client authentication. - oauth-handler.ts: connectProvider() takes a credentials object ({clientId, clientSecret}) instead of a bare clientId. Removed eager persistence of clientId before flow completion — credentials are now only saved after successful token exchange. Renamed resolveClientId to resolveClientCredentials to return both values from a single repo read. - google-client-factory.ts: same resolveClientId → resolveCredentials rename. Passes clientSecret to OAuth2Client constructor and discoverConfiguration for token refresh. - repo.ts: added clientSecret to ProviderConnectionSchema. Not exposed to renderer via ClientFacingConfigSchema (stays main-process only). - IPC: added clientSecret to oauth:connect request schema. Handler builds a credentials object and passes it through. - UI: GoogleClientIdModal now collects both Client ID and Client Secret (password field). Always shown on connect — no in-memory credential caching. Renamed google-client-id-store to google-credentials-store with a unified {clientId, clientSecret} object. - google-setup.md: updated to instruct users to create a "Web application" type OAuth client (instead of UWP), add the localhost redirect URI, and copy both Client ID and Client Secret. Added credentials modal screenshot. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../img/google-setup/07-enter-credentials.png | Bin 0 -> 85260 bytes apps/x/apps/main/src/ipc.ts | 5 +- apps/x/apps/main/src/oauth-handler.ts | 51 +++++++++--------- .../src/components/google-client-id-modal.tsx | 32 +++++++++-- .../src/components/onboarding-modal.tsx | 23 +++----- .../onboarding/use-onboarding-state.ts | 19 +++---- .../apps/renderer/src/hooks/useConnectors.ts | 26 ++++----- apps/x/apps/renderer/src/hooks/useOAuth.ts | 4 +- .../src/lib/google-client-id-store.ts | 17 ------ .../src/lib/google-credentials-store.ts | 23 ++++++++ apps/x/packages/core/src/auth/oauth-client.ts | 16 +++--- apps/x/packages/core/src/auth/repo.ts | 1 + .../src/knowledge/google-client-factory.ts | 49 ++++++++++------- apps/x/packages/shared/src/ipc.ts | 1 + google-setup.md | 26 +++++---- 15 files changed, 155 insertions(+), 138 deletions(-) create mode 100644 apps/docs/docs/img/google-setup/07-enter-credentials.png delete mode 100644 apps/x/apps/renderer/src/lib/google-client-id-store.ts create mode 100644 apps/x/apps/renderer/src/lib/google-credentials-store.ts diff --git a/apps/docs/docs/img/google-setup/07-enter-credentials.png b/apps/docs/docs/img/google-setup/07-enter-credentials.png new file mode 100644 index 0000000000000000000000000000000000000000..9ab73334cd1f36782423fe96048b332735fa5fc1 GIT binary patch literal 85260 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 { - return await connectProvider(args.provider, args.clientId?.trim()); + const credentials = args.clientId && args.clientSecret + ? { clientId: args.clientId.trim(), clientSecret: args.clientSecret.trim() } + : undefined; + return await connectProvider(args.provider, credentials); }, 'oauth:disconnect': async (_event, args) => { return await disconnectProvider(args.provider); diff --git a/apps/x/apps/main/src/oauth-handler.ts b/apps/x/apps/main/src/oauth-handler.ts index 288d3038..483f25ee 100644 --- a/apps/x/apps/main/src/oauth-handler.ts +++ b/apps/x/apps/main/src/oauth-handler.ts @@ -111,19 +111,19 @@ function getClientRegistrationRepo(): IClientRegistrationRepo { /** * Get or create OAuth configuration for a provider */ -async function getProviderConfiguration(provider: string, clientIdOverride?: string): Promise { +async function getProviderConfiguration(provider: string, credentialsOverride?: { clientId: string; clientSecret: string }): Promise { const config = await getProviderConfig(provider); - const resolveClientId = async (): Promise => { + const resolveClientCredentials = async (): Promise<{ clientId: string; clientSecret?: string }> => { if (config.client.mode === 'static' && config.client.clientId) { - return config.client.clientId; + return { clientId: config.client.clientId, clientSecret: credentialsOverride?.clientSecret }; } - if (clientIdOverride) { - return clientIdOverride; + if (credentialsOverride) { + return { clientId: credentialsOverride.clientId, clientSecret: credentialsOverride.clientSecret }; } const oauthRepo = getOAuthRepo(); - const { clientId } = await oauthRepo.read(provider); - if (clientId) { - return clientId; + const connection = await oauthRepo.read(provider); + if (connection.clientId) { + return { clientId: connection.clientId, clientSecret: connection.clientSecret ?? undefined }; } throw new Error(`${provider} client ID not configured. Please provide a client ID.`); }; @@ -132,10 +132,11 @@ async function getProviderConfiguration(provider: string, clientIdOverride?: str if (config.client.mode === 'static') { // Discover endpoints, use static client ID console.log(`[OAuth] ${provider}: Discovery from issuer with static client ID`); - const clientId = await resolveClientId(); + const { clientId, clientSecret } = await resolveClientCredentials(); return await oauthClient.discoverConfiguration( config.discovery.issuer, - clientId + clientId, + clientSecret ); } else { // DCR mode - check for existing registration or register new @@ -172,12 +173,13 @@ async function getProviderConfiguration(provider: string, clientIdOverride?: str } console.log(`[OAuth] ${provider}: Using static endpoints (no discovery)`); - const clientId = await resolveClientId(); + const { clientId, clientSecret } = await resolveClientCredentials(); return oauthClient.createStaticConfiguration( config.discovery.authorizationEndpoint, config.discovery.tokenEndpoint, clientId, - config.discovery.revocationEndpoint + config.discovery.revocationEndpoint, + clientSecret ); } } @@ -185,7 +187,7 @@ async function getProviderConfiguration(provider: string, clientIdOverride?: str /** * Initiate OAuth flow for a provider */ -export async function connectProvider(provider: string, clientId?: string): Promise<{ success: boolean; error?: string }> { +export async function connectProvider(provider: string, credentials?: { clientId: string; clientSecret: string }): Promise<{ success: boolean; error?: string }> { try { console.log(`[OAuth] Starting connection flow for ${provider}...`); @@ -196,18 +198,13 @@ export async function connectProvider(provider: string, clientId?: string): Prom const providerConfig = await getProviderConfig(provider); if (provider === 'google') { - if (!clientId) { - return { success: false, error: 'Google client ID is required to connect.' }; + if (!credentials?.clientId || !credentials?.clientSecret) { + return { success: false, error: 'Google client ID and client secret are required to connect.' }; } } // Get or create OAuth configuration - const config = await getProviderConfiguration(provider, clientId); - - // Persist Google client ID so it survives restarts and failed token exchanges - if (provider === 'google' && clientId) { - await oauthRepo.upsert(provider, { clientId }); - } + const config = await getProviderConfiguration(provider, credentials); // Generate PKCE codes const { verifier: codeVerifier, challenge: codeChallenge } = await oauthClient.generatePKCE(); @@ -258,13 +255,13 @@ export async function connectProvider(provider: string, clientId?: string): Prom state ); - // Save tokens + // Save tokens and credentials console.log(`[OAuth] Token exchange successful for ${provider}`); - await oauthRepo.upsert(provider, { tokens }); - if (provider === 'google' && clientId) { - await oauthRepo.upsert(provider, { clientId }); - } - await oauthRepo.upsert(provider, { error: null }); + await oauthRepo.upsert(provider, { + tokens, + ...(credentials ? { clientId: credentials.clientId, clientSecret: credentials.clientSecret } : {}), + error: null, + }); // Trigger immediate sync for relevant providers if (provider === 'google') { diff --git a/apps/x/apps/renderer/src/components/google-client-id-modal.tsx b/apps/x/apps/renderer/src/components/google-client-id-modal.tsx index 3ef536d9..14e94339 100644 --- a/apps/x/apps/renderer/src/components/google-client-id-modal.tsx +++ b/apps/x/apps/renderer/src/components/google-client-id-modal.tsx @@ -17,7 +17,7 @@ const GOOGLE_CLIENT_ID_SETUP_GUIDE_URL = interface GoogleClientIdModalProps { open: boolean onOpenChange: (open: boolean) => void - onSubmit: (clientId: string) => void + onSubmit: (clientId: string, clientSecret: string) => void isSubmitting?: boolean description?: string } @@ -30,19 +30,22 @@ export function GoogleClientIdModal({ description, }: GoogleClientIdModalProps) { const [clientId, setClientId] = useState("") + const [clientSecret, setClientSecret] = useState("") useEffect(() => { if (!open) { setClientId("") + setClientSecret("") } }, [open]) const trimmedClientId = clientId.trim() - const isValid = trimmedClientId.length > 0 + const trimmedClientSecret = clientSecret.trim() + const isValid = trimmedClientId.length > 0 && trimmedClientSecret.length > 0 const handleSubmit = () => { if (!isValid || isSubmitting) return - onSubmit(trimmedClientId) + onSubmit(trimmedClientId, trimmedClientSecret) } return ( @@ -50,9 +53,9 @@ export function GoogleClientIdModal({
- Google Client ID + Google OAuth Credentials - {description ?? "Enter the client ID for your Google OAuth app to connect."} + {description ?? "Enter the credentials for your Google OAuth app to connect."}
@@ -76,6 +79,25 @@ export function GoogleClientIdModal({ autoFocus /> +
+ + setClientSecret(event.target.value)} + onKeyDown={(event) => { + if (event.key === "Enter") { + event.preventDefault() + handleSubmit() + } + }} + className="font-mono text-xs" + /> +

Need help?{" "} { + const startConnect = useCallback(async (provider: string, credentials?: { clientId: string; clientSecret: string }) => { setProviderStates(prev => ({ ...prev, [provider]: { ...prev[provider], isConnecting: true } })) try { - const result = await window.ipc.invoke('oauth:connect', { provider, clientId }) + const result = await window.ipc.invoke('oauth:connect', { provider, clientId: credentials?.clientId, clientSecret: credentials?.clientSecret }) if (!result.success) { toast.error(result.error || `Failed to connect to ${provider}`) @@ -622,22 +618,17 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) { // Connect to a provider const handleConnect = useCallback(async (provider: string) => { if (provider === 'google') { - const existingClientId = getGoogleClientId() - if (!existingClientId) { - setGoogleClientIdOpen(true) - return - } - await startConnect(provider, existingClientId) + setGoogleClientIdOpen(true) return } await startConnect(provider) }, [startConnect]) - const handleGoogleClientIdSubmit = useCallback((clientId: string) => { - setGoogleClientId(clientId) + const handleGoogleClientIdSubmit = useCallback((clientId: string, clientSecret: string) => { + setGoogleCredentials(clientId, clientSecret) setGoogleClientIdOpen(false) - startConnect('google', clientId) + startConnect('google', { clientId, clientSecret }) }, [startConnect]) // Step indicator - dynamic based on path diff --git a/apps/x/apps/renderer/src/components/onboarding/use-onboarding-state.ts b/apps/x/apps/renderer/src/components/onboarding/use-onboarding-state.ts index 7cc50a90..a55b23fe 100644 --- a/apps/x/apps/renderer/src/components/onboarding/use-onboarding-state.ts +++ b/apps/x/apps/renderer/src/components/onboarding/use-onboarding-state.ts @@ -1,5 +1,5 @@ import { useState, useEffect, useCallback } from "react" -import { getGoogleClientId, setGoogleClientId } from "@/lib/google-client-id-store" +import { setGoogleCredentials } from "@/lib/google-credentials-store" import { toast } from "sonner" export interface ProviderState { @@ -576,14 +576,14 @@ export function useOnboardingState(open: boolean, onComplete: () => void) { return cleanup }, []) - const startConnect = useCallback(async (provider: string, clientId?: string) => { + const startConnect = useCallback(async (provider: string, credentials?: { clientId: string; clientSecret: string }) => { setProviderStates(prev => ({ ...prev, [provider]: { ...prev[provider], isConnecting: true } })) try { - const result = await window.ipc.invoke('oauth:connect', { provider, clientId }) + const result = await window.ipc.invoke('oauth:connect', { provider, clientId: credentials?.clientId, clientSecret: credentials?.clientSecret }) if (!result.success) { toast.error(result.error || `Failed to connect to ${provider}`) @@ -605,22 +605,17 @@ export function useOnboardingState(open: boolean, onComplete: () => void) { // Connect to a provider const handleConnect = useCallback(async (provider: string) => { if (provider === 'google') { - const existingClientId = getGoogleClientId() - if (!existingClientId) { - setGoogleClientIdOpen(true) - return - } - await startConnect(provider, existingClientId) + setGoogleClientIdOpen(true) return } await startConnect(provider) }, [startConnect]) - const handleGoogleClientIdSubmit = useCallback((clientId: string) => { - setGoogleClientId(clientId) + const handleGoogleClientIdSubmit = useCallback((clientId: string, clientSecret: string) => { + setGoogleCredentials(clientId, clientSecret) setGoogleClientIdOpen(false) - startConnect('google', clientId) + startConnect('google', { clientId, clientSecret }) }, [startConnect]) // Switch to rowboat path from BYOK inline callout diff --git a/apps/x/apps/renderer/src/hooks/useConnectors.ts b/apps/x/apps/renderer/src/hooks/useConnectors.ts index aa2942da..7285fe04 100644 --- a/apps/x/apps/renderer/src/hooks/useConnectors.ts +++ b/apps/x/apps/renderer/src/hooks/useConnectors.ts @@ -1,5 +1,5 @@ import { useState, useEffect, useCallback } from "react" -import { getGoogleClientId, setGoogleClientId, clearGoogleClientId } from "@/lib/google-client-id-store" +import { setGoogleCredentials, clearGoogleCredentials } from "@/lib/google-credentials-store" import { toast } from "sonner" export interface ProviderState { @@ -318,14 +318,14 @@ export function useConnectors(active: boolean) { }, [startGmailConnect]) // OAuth connect/disconnect - const startConnect = useCallback(async (provider: string, clientId?: string) => { + const startConnect = useCallback(async (provider: string, credentials?: { clientId: string; clientSecret: string }) => { setProviderStates(prev => ({ ...prev, [provider]: { ...prev[provider], isConnecting: true } })) try { - const result = await window.ipc.invoke('oauth:connect', { provider, clientId }) + const result = await window.ipc.invoke('oauth:connect', { provider, clientId: credentials?.clientId, clientSecret: credentials?.clientSecret }) if (!result.success) { toast.error(result.error || (provider === 'rowboat' ? 'Failed to log in to Rowboat' : `Failed to connect to ${provider}`)) @@ -347,23 +347,18 @@ export function useConnectors(active: boolean) { const handleConnect = useCallback(async (provider: string) => { if (provider === 'google') { setGoogleClientIdDescription(undefined) - const existingClientId = getGoogleClientId() - if (!existingClientId) { - setGoogleClientIdOpen(true) - return - } - await startConnect(provider, existingClientId) + setGoogleClientIdOpen(true) return } await startConnect(provider) }, [startConnect]) - const handleGoogleClientIdSubmit = useCallback((clientId: string) => { - setGoogleClientId(clientId) + const handleGoogleClientIdSubmit = useCallback((clientId: string, clientSecret: string) => { + setGoogleCredentials(clientId, clientSecret) setGoogleClientIdOpen(false) setGoogleClientIdDescription(undefined) - startConnect('google', clientId) + startConnect('google', { clientId, clientSecret }) }, [startConnect]) const handleDisconnect = useCallback(async (provider: string) => { @@ -377,7 +372,7 @@ export function useConnectors(active: boolean) { if (result.success) { if (provider === 'google') { - clearGoogleClientId() + clearGoogleCredentials() } const displayName = provider === 'fireflies-ai' ? 'Fireflies' : provider.charAt(0).toUpperCase() + provider.slice(1) toast.success(provider === 'rowboat' ? 'Logged out of Rowboat' : `Disconnected from ${displayName}`) @@ -426,9 +421,6 @@ export function useConnectors(active: boolean) { try { const result = await window.ipc.invoke('oauth:getState', null) const config = result.config || {} - if (config.google?.clientId) { - setGoogleClientId(config.google.clientId) - } const statusMap: Record = {} for (const provider of providers) { @@ -565,7 +557,7 @@ export function useConnectors(active: boolean) { handleDisconnect, startConnect, - // Google client ID modal + // Google credentials modal googleClientIdOpen, setGoogleClientIdOpen, googleClientIdDescription, diff --git a/apps/x/apps/renderer/src/hooks/useOAuth.ts b/apps/x/apps/renderer/src/hooks/useOAuth.ts index 6f315ec0..05e7e2c7 100644 --- a/apps/x/apps/renderer/src/hooks/useOAuth.ts +++ b/apps/x/apps/renderer/src/hooks/useOAuth.ts @@ -55,10 +55,10 @@ export function useOAuth(provider: string) { return cleanup; }, [provider, checkConnection]); - const connect = useCallback(async (clientId?: string) => { + const connect = useCallback(async (credentials?: { clientId: string; clientSecret: string }) => { try { setIsConnecting(true); - const result = await window.ipc.invoke('oauth:connect', { provider, clientId }); + const result = await window.ipc.invoke('oauth:connect', { provider, clientId: credentials?.clientId, clientSecret: credentials?.clientSecret }); if (result.success) { // OAuth flow started - keep isConnecting state, wait for event // Event listener will handle the actual completion diff --git a/apps/x/apps/renderer/src/lib/google-client-id-store.ts b/apps/x/apps/renderer/src/lib/google-client-id-store.ts deleted file mode 100644 index 78898325..00000000 --- a/apps/x/apps/renderer/src/lib/google-client-id-store.ts +++ /dev/null @@ -1,17 +0,0 @@ -let googleClientId: string | null = null; - -export function getGoogleClientId(): string | null { - return googleClientId; -} - -export function setGoogleClientId(clientId: string): void { - const trimmed = clientId.trim(); - if (!trimmed) { - return; - } - googleClientId = trimmed; -} - -export function clearGoogleClientId(): void { - googleClientId = null; -} diff --git a/apps/x/apps/renderer/src/lib/google-credentials-store.ts b/apps/x/apps/renderer/src/lib/google-credentials-store.ts new file mode 100644 index 00000000..931910ef --- /dev/null +++ b/apps/x/apps/renderer/src/lib/google-credentials-store.ts @@ -0,0 +1,23 @@ +interface GoogleCredentials { + clientId: string; + clientSecret: string; +} + +let credentials: GoogleCredentials | null = null; + +export function getGoogleCredentials(): GoogleCredentials | null { + return credentials; +} + +export function setGoogleCredentials(clientId: string, clientSecret: string): void { + const trimmedId = clientId.trim(); + const trimmedSecret = clientSecret.trim(); + if (!trimmedId || !trimmedSecret) { + return; + } + credentials = { clientId: trimmedId, clientSecret: trimmedSecret }; +} + +export function clearGoogleCredentials(): void { + credentials = null; +} diff --git a/apps/x/packages/core/src/auth/oauth-client.ts b/apps/x/packages/core/src/auth/oauth-client.ts index 16648256..ccabab19 100644 --- a/apps/x/packages/core/src/auth/oauth-client.ts +++ b/apps/x/packages/core/src/auth/oauth-client.ts @@ -37,9 +37,10 @@ function toOAuthTokens(response: client.TokenEndpointResponse): OAuthTokens { */ export async function discoverConfiguration( issuerUrl: string, - clientId: string + clientId: string, + clientSecret?: string ): Promise { - const cacheKey = `${issuerUrl}:${clientId}`; + const cacheKey = `${issuerUrl}:${clientId}:${clientSecret ? 'secret' : 'none'}`; const cached = configCache.get(cacheKey); if (cached) { @@ -50,8 +51,8 @@ export async function discoverConfiguration( const config = await client.discovery( new URL(issuerUrl), clientId, - undefined, // no client_secret (PKCE flow) - client.None(), // PKCE doesn't require client authentication + clientSecret ?? undefined, + clientSecret ? client.ClientSecretPost(clientSecret) : client.None(), { execute: [client.allowInsecureRequests], } @@ -69,7 +70,8 @@ export function createStaticConfiguration( authorizationEndpoint: string, tokenEndpoint: string, clientId: string, - revocationEndpoint?: string + revocationEndpoint?: string, + clientSecret?: string ): client.Configuration { console.log(`[OAuth] Creating static configuration (no discovery)`); @@ -86,8 +88,8 @@ export function createStaticConfiguration( return new client.Configuration( serverMetadata, clientId, - undefined, // no client_secret - client.None() // PKCE auth + clientSecret ?? undefined, + clientSecret ? client.ClientSecretPost(clientSecret) : client.None() ); } diff --git a/apps/x/packages/core/src/auth/repo.ts b/apps/x/packages/core/src/auth/repo.ts index 70eecf0e..5276faea 100644 --- a/apps/x/packages/core/src/auth/repo.ts +++ b/apps/x/packages/core/src/auth/repo.ts @@ -7,6 +7,7 @@ import z from 'zod'; const ProviderConnectionSchema = z.object({ tokens: OAuthTokens.nullable().optional(), clientId: z.string().nullable().optional(), + clientSecret: z.string().nullable().optional(), error: z.string().nullable().optional(), }); diff --git a/apps/x/packages/core/src/knowledge/google-client-factory.ts b/apps/x/packages/core/src/knowledge/google-client-factory.ts index b3bcfe7a..9e0ad2d1 100644 --- a/apps/x/packages/core/src/knowledge/google-client-factory.ts +++ b/apps/x/packages/core/src/knowledge/google-client-factory.ts @@ -18,21 +18,23 @@ export class GoogleClientFactory { client: OAuth2Client | null; tokens: OAuthTokens | null; clientId: string | null; + clientSecret: string | null; } = { config: null, client: null, tokens: null, clientId: null, + clientSecret: null, }; - private static async resolveClientId(): Promise { + private static async resolveCredentials(): Promise<{ clientId: string; clientSecret?: string }> { const oauthRepo = container.resolve('oauthRepo'); - const { clientId } = await oauthRepo.read(this.PROVIDER_NAME); - if (!clientId) { + const connection = await oauthRepo.read(this.PROVIDER_NAME); + if (!connection.clientId) { await oauthRepo.upsert(this.PROVIDER_NAME, { error: 'Google client ID missing. Please reconnect.' }); throw new Error('Google client ID missing. Please reconnect.'); } - return clientId; + return { clientId: connection.clientId, clientSecret: connection.clientSecret ?? undefined }; } /** @@ -82,9 +84,11 @@ export class GoogleClientFactory { // Update cached tokens and recreate client this.cache.tokens = refreshedTokens; if (!this.cache.clientId) { - this.cache.clientId = await this.resolveClientId(); + const creds = await this.resolveCredentials(); + this.cache.clientId = creds.clientId; + this.cache.clientSecret = creds.clientSecret ?? null; } - this.cache.client = this.createClientFromTokens(refreshedTokens, this.cache.clientId); + this.cache.client = this.createClientFromTokens(refreshedTokens, this.cache.clientId, this.cache.clientSecret ?? undefined); console.log(`[OAuth] Token refreshed successfully`); return this.cache.client; } catch (error) { @@ -105,9 +109,11 @@ export class GoogleClientFactory { console.log(`[OAuth] Creating new OAuth2Client instance`); this.cache.tokens = tokens; if (!this.cache.clientId) { - this.cache.clientId = await this.resolveClientId(); + const creds = await this.resolveCredentials(); + this.cache.clientId = creds.clientId; + this.cache.clientSecret = creds.clientSecret ?? null; } - this.cache.client = this.createClientFromTokens(tokens, this.cache.clientId); + this.cache.client = this.createClientFromTokens(tokens, this.cache.clientId, this.cache.clientSecret ?? undefined); return this.cache.client; } @@ -138,19 +144,20 @@ export class GoogleClientFactory { this.cache.client = null; this.cache.tokens = null; this.cache.clientId = null; + this.cache.clientSecret = null; } /** * Initialize cached configuration (called once) */ private static async initializeConfigCache(): Promise { - const clientId = await this.resolveClientId(); + const { clientId, clientSecret } = await this.resolveCredentials(); - if (this.cache.config && this.cache.clientId === clientId) { - return; // Already initialized for this client ID + if (this.cache.config && this.cache.clientId === clientId && this.cache.clientSecret === (clientSecret ?? null)) { + return; // Already initialized for these credentials } - if (this.cache.clientId && this.cache.clientId !== clientId) { + if (this.cache.clientId && (this.cache.clientId !== clientId || this.cache.clientSecret !== (clientSecret ?? null))) { this.clearCache(); } @@ -163,18 +170,19 @@ export class GoogleClientFactory { console.log(`[OAuth] Discovery mode: issuer with static client ID`); this.cache.config = await oauthClient.discoverConfiguration( providerConfig.discovery.issuer, - clientId + clientId, + clientSecret ); } else { // DCR mode - need existing registration console.log(`[OAuth] Discovery mode: issuer with DCR`); const clientRepo = container.resolve('clientRegistrationRepo'); const existingRegistration = await clientRepo.getClientRegistration(this.PROVIDER_NAME); - + if (!existingRegistration) { throw new Error('Google client not registered. Please connect account first.'); } - + this.cache.config = await oauthClient.discoverConfiguration( providerConfig.discovery.issuer, existingRegistration.client_id @@ -185,28 +193,29 @@ export class GoogleClientFactory { if (providerConfig.client.mode !== 'static') { throw new Error('DCR requires discovery mode "issuer", not "static"'); } - + console.log(`[OAuth] Using static endpoints (no discovery)`); this.cache.config = oauthClient.createStaticConfiguration( providerConfig.discovery.authorizationEndpoint, providerConfig.discovery.tokenEndpoint, clientId, - providerConfig.discovery.revocationEndpoint + providerConfig.discovery.revocationEndpoint, + clientSecret ); } this.cache.clientId = clientId; + this.cache.clientSecret = clientSecret ?? null; console.log(`[OAuth] Google OAuth configuration initialized`); } /** * Create OAuth2Client from OAuthTokens */ - private static createClientFromTokens(tokens: OAuthTokens, clientId: string): OAuth2Client { - // Create OAuth2Client directly (PKCE flow doesn't use client secret) + private static createClientFromTokens(tokens: OAuthTokens, clientId: string, clientSecret?: string): OAuth2Client { const client = new OAuth2Client( clientId, - undefined, // client_secret not needed for PKCE + clientSecret ?? undefined, undefined // redirect_uri not needed for token usage ); diff --git a/apps/x/packages/shared/src/ipc.ts b/apps/x/packages/shared/src/ipc.ts index a8709aa2..645e41ff 100644 --- a/apps/x/packages/shared/src/ipc.ts +++ b/apps/x/packages/shared/src/ipc.ts @@ -225,6 +225,7 @@ const ipcSchemas = { req: z.object({ provider: z.string(), clientId: z.string().optional(), + clientSecret: z.string().optional(), }), res: z.object({ success: z.boolean(), diff --git a/google-setup.md b/google-setup.md index 5769aab8..2bab28ba 100644 --- a/google-setup.md +++ b/google-setup.md @@ -1,6 +1,6 @@ # Connecting Google to Rowboat -Rowboat requires a Google OAuth Client ID to connect to Gmail, Calendar, and Drive. Follow the steps below to generate your Client ID correctly. +Rowboat requires Google OAuth credentials (Client ID and Client Secret) to connect to Gmail, Calendar, and Drive. Follow the steps below to generate them. --- @@ -114,34 +114,32 @@ Click **Create Credentials → OAuth Client ID** Select: -**Universal Windows Platform (UWP)** +**Web application** - Name it anything (e.g. `Rowboat Desktop`) -- Store ID can be anything (e.g. `test` ) -- Click **Create** -![Create OAuth Client ID (UWP)](https://raw.githubusercontent.com/rowboatlabs/rowboat/main/apps/docs/docs/img/google-setup/05-create-oauth-client-uwp.png) +### Authorized redirect URIs -### Authorized redirect URIs (if shown) - -If your OAuth client configuration shows **Authorized redirect URIs**, add: +Add the following redirect URI: - `http://localhost:8080/oauth/callback` -Use this exactly: no trailing slash, port **8080**. This must match what the app uses for the OAuth callback. (Some client types, e.g. UWP, may not expose redirect URIs; that is fine.) +Use this exactly: no trailing slash, port **8080**. This must match what the app uses for the OAuth callback. + +Click **Create**. --- -## 7️⃣ Copy the Client ID +## 7️⃣ Copy the Client ID and Client Secret After creation, Google will show: - **Client ID** - **Client Secret** -Copy the **Client ID** and paste it into Rowboat where prompted. +Copy **both values** and paste them into Rowboat when prompted. -![Copy Client ID](https://raw.githubusercontent.com/rowboatlabs/rowboat/main/apps/docs/docs/img/google-setup/06-copy-client-id.png) +![Enter credentials in Rowboat](https://raw.githubusercontent.com/rowboatlabs/rowboat/main/apps/docs/docs/img/google-setup/07-enter-credentials.png) --- @@ -152,7 +150,7 @@ Copy the **Client ID** and paste it into Rowboat where prompted. If the browser shows "Authorization Successful" but the app then shows an error (e.g. "invalid response encountered" or "response parameter \"iss\" (issuer) missing"): 1. **Check the app logs** (e.g. terminal or dev tools) for the full error. The message there will often indicate the cause (e.g. redirect URI mismatch, missing parameter). -2. **Verify redirect URI in Google Cloud Console**: Open [Credentials → your OAuth 2.0 Client ID](https://console.cloud.google.com/auth/clients). If the client type allows **Authorized redirect URIs**, ensure `http://localhost:8080/oauth/callback` is listed exactly. -3. **Client type**: Use **Desktop** or **UWP** as the application type. A "Web application" client may require the redirect URI to be set and can behave differently with localhost. +2. **Verify redirect URI in Google Cloud Console**: Open [Credentials → your OAuth 2.0 Client ID](https://console.cloud.google.com/auth/clients). Ensure `http://localhost:8080/oauth/callback` is listed under **Authorized redirect URIs**. +3. **Client type**: Make sure you selected **Web application** as the application type. Other types (Desktop, UWP) may not provide a client secret or may handle redirect URIs differently. ---