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 00000000..9ab73334 Binary files /dev/null and b/apps/docs/docs/img/google-setup/07-enter-credentials.png differ diff --git a/apps/x/apps/main/src/ipc.ts b/apps/x/apps/main/src/ipc.ts index e05b57b3..a2230eda 100644 --- a/apps/x/apps/main/src/ipc.ts +++ b/apps/x/apps/main/src/ipc.ts @@ -460,7 +460,10 @@ export function setupIpcHandlers() { return { success: true }; }, 'oauth:connect': async (_event, args) => { - 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. ---