Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ Shows your schedule for today or a specified date.

Searches your Google Drive for files matching the given query.

## Deployment

If you want to host your own version of this extension's infrastructure, see the [GCP Recreation Guide](docs/GCP-RECREATION.md).

## Resources

- [Documentation](docs/index.md): Detailed documentation on all the available tools.
Expand Down
81 changes: 81 additions & 0 deletions docs/GCP-RECREATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Recreating the GCP Project

This guide provides step-by-step instructions to recreate the Google Cloud Platform (GCP) project and infrastructure required for the Google Workspace Extension.

## Overview

The extension uses a "Hybrid" OAuth flow for security:
1. **Local Client**: Requests authorization from the user.
2. **Cloud Function**: Acts as a secure proxy to exchange the authorization code for tokens. It holds the `CLIENT_SECRET` securely in Secret Manager.
3. **Secret Manager**: Stores the OAuth Client Secret.

## Prerequisites

- A Google Cloud Project with billing enabled.
- [Google Cloud CLI (gcloud)](https://cloud.google.com/sdk/docs/install) installed and authenticated.
- Node.js and npm installed.

## Step 1: Automated Infrastructure Setup

We provide a script to automate most of the GCP setup, including enabling APIs, creating secrets, and deploying the Cloud Function.

1. Set your project ID:
```bash
gcloud config set project YOUR_PROJECT_ID
```
2. Run the setup script:
```bash
./scripts/setup-gcp.sh
```
3. Follow the prompts to enter your **OAuth Client ID** and **Client Secret**. (If you don't have them yet, see Step 2 below first).

## Step 2: Manual OAuth Configuration

Some steps must be performed manually in the Google Cloud Console for security and policy reasons.

### 1. Configure OAuth Consent Screen
1. Go to [APIs & Services > OAuth consent screen](https://console.cloud.google.com/apis/credentials/consent).
2. Select **Internal** (if you are a Google Workspace user) or **External**.
3. Fill in the required App information.
4. **Scopes**: Add the following scopes:
- `https://www.googleapis.com/auth/documents`
- `https://www.googleapis.com/auth/drive`
- `https://www.googleapis.com/auth/calendar`
- `https://www.googleapis.com/auth/chat.spaces`
- `https://www.googleapis.com/auth/chat.messages`
- `https://www.googleapis.com/auth/chat.memberships`
- `https://www.googleapis.com/auth/userinfo.profile`
- `https://www.googleapis.com/auth/gmail.modify`
- `https://www.googleapis.com/auth/directory.readonly`
- `https://www.googleapis.com/auth/presentations.readonly`
- `https://www.googleapis.com/auth/spreadsheets.readonly`

### 2. Create OAuth 2.0 Client ID
1. Go to [APIs & Services > Credentials](https://console.cloud.google.com/apis/credentials).
2. Click **Create Credentials > OAuth client ID**.
3. Select **Web application** as the Application type.
4. Name it (e.g., "Workspace Extension").
5. **Authorized redirect URIs**: Add the URL of your deployed Cloud Function (provided by the setup script).
- Format: `https://[REGION]-[PROJECT_ID].cloudfunctions.net/workspace-oauth-handler`
6. Click **Create**.
7. Copy the **Client ID** and **Client Secret**.

## Step 3: Local Configuration

After running the script and configuring the OAuth client, you need to tell the local extension where to find your infrastructure.

Set the following environment variables in your shell (e.g., in `.zshrc` or `.bashrc`):

```bash
export WORKSPACE_CLIENT_ID="your-client-id"
export WORKSPACE_CLOUD_FUNCTION_URL="https://your-cloud-function-url"
```

Alternatively, you can modify the `DEFAULT_CONFIG` in `workspace-server/src/utils/config.ts`.

## Why a Cloud Function?

The extension uses a Cloud Function to protect your `CLIENT_SECRET`.
- If the `CLIENT_SECRET` were included in the local extension code, anyone with access to the extension could steal it.
- By using a Cloud Function, the secret stays in your GCP project and is only used server-side during the token exchange.
- The local client only ever sees the resulting tokens, never the secret.
142 changes: 142 additions & 0 deletions scripts/setup-gcp.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
#!/bin/bash

# GCP Setup Script for Google Workspace Extension
# This script enables necessary APIs and helps set up Secret Manager and Cloud Functions.

set -e

# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color

echo -e "${YELLOW}Starting Google Cloud Platform setup...${NC}"

# Check if gcloud is installed
if ! command -v gcloud &> /dev/null; then
echo -e "${RED}Error: gcloud CLI is not installed. Please install it first.${NC}"
exit 1
fi

# Get current project ID
PROJECT_ID=$(gcloud config get-value project)
if [ -z "$PROJECT_ID" ]; then
echo -e "${RED}Error: No Google Cloud project is currently set.${NC}"
echo "Please run: gcloud config set project [PROJECT_ID]"
exit 1
fi

echo -e "Using project: ${GREEN}$PROJECT_ID${NC}"

# 1. Enable Required APIs
echo -e "\n${YELLOW}Step 1: Enabling Required APIs...${NC}"
APIS=(
"drive.googleapis.com"
"docs.googleapis.com"
"calendar-json.googleapis.com"
"chat.googleapis.com"
"gmail.googleapis.com"
"people.googleapis.com"
"slides.googleapis.com"
"sheets.googleapis.com"
"admin.googleapis.com"
"secretmanager.googleapis.com"
"cloudfunctions.googleapis.com"
"cloudbuild.googleapis.com"
)

for api in "${APIS[@]}"; do
echo "Enabling $api..."
gcloud services enable "$api"
done

echo -e "${GREEN}APIs enabled successfully.${NC}"

# 2. Setup Secret Manager
echo -e "\n${YELLOW}Step 2: Setting up Secret Manager...${NC}"
SECRET_ID="workspace-oauth-client-secret"

if gcloud secrets describe "$SECRET_ID" &> /dev/null; then
echo "Secret $SECRET_ID already exists."
else
echo "Creating secret $SECRET_ID..."
gcloud secrets create "$SECRET_ID" --replication-policy=\"automatic\"
fi

echo -e "${YELLOW}Please enter your OAuth 2.0 Client Secret (from Google Cloud Console):${NC}"
read -s CLIENT_SECRET
echo "$CLIENT_SECRET" | gcloud secrets versions add "$SECRET_ID" --data-file=-

echo -e "${GREEN}Secret stored successfully.${NC}"

# 3. Deploy Cloud Function
echo -e "\n${YELLOW}Step 3: Deploying Cloud Function...${NC}"
echo -e "${YELLOW}Please enter the OAuth 2.0 Client ID:${NC}"
read CLIENT_ID

echo -e "${YELLOW}Please enter the GCP region for your Cloud Function (e.g., us-central1):${NC}"
read REGION
if [ -z "$REGION" ]; then
REGION="us-central1"
echo -e "${YELLOW}No region entered, defaulting to $REGION.${NC}"
fi
FUNCTION_NAME="workspace-oauth-handler"
FUNCTION_URL="https://${REGION}-${PROJECT_ID}.cloudfunctions.net/${FUNCTION_NAME}"

echo "Deploying Cloud Function..."
gcloud functions deploy "$FUNCTION_NAME" \
--gen2 \
--runtime=nodejs20 \
--region="$REGION" \
--source="./cloud_function" \
--entry-point=oauthHandler \
--trigger-http \
--allow-unauthenticated \
--set-env-vars "CLIENT_ID=$CLIENT_ID,SECRET_NAME=projects/$PROJECT_ID/secrets/$SECRET_ID/versions/latest,REDIRECT_URI=$FUNCTION_URL"

echo "Deploying Cloud Function..."
gcloud functions deploy "$FUNCTION_NAME" \
--gen2 \
--runtime=nodejs20 \
--region="$REGION" \
--source="./cloud_function" \
--entry-point=oauthHandler \
--trigger-http \
--allow-unauthenticated \
--set-env-vars "CLIENT_ID=$CLIENT_ID,SECRET_NAME=projects/$PROJECT_ID/secrets/$SECRET_ID/versions/latest"

# Get the URL of the deployed function
FUNCTION_URL=$(gcloud functions describe "$FUNCTION_NAME" --region="$REGION" --format='value(serviceConfig.uri)')

echo -e "${GREEN}Cloud Function deployed at: $FUNCTION_URL${NC}"

# Update REDIRECT_URI env var now that we have the URL
echo "Updating REDIRECT_URI environment variable..."
gcloud functions deploy "$FUNCTION_NAME" \
--gen2 \
--region="$REGION" \
--update-env-vars "REDIRECT_URI=$FUNCTION_URL"

# 4. Grant Permissions
echo -e "\n${YELLOW}Step 4: Granting Secret Manager Access to Cloud Function...${NC}"
# Get the service account used by the Cloud Function
SERVICE_ACCOUNT=$(gcloud functions describe "$FUNCTION_NAME" --region="$REGION" --format='value(serviceConfig.serviceAccountEmail)')

gcloud secrets add-iam-policy-binding "$SECRET_ID" \
--member="serviceAccount:$SERVICE_ACCOUNT" \
--role="roles/secretmanager.secretAccessor"

echo -e "${GREEN}Permissions granted successfully.${NC}"

echo -e "\n${GREEN}GCP Setup Complete!${NC}"
echo -e "---------------------------------------------------"
echo -e "${YELLOW}Next Steps:${NC}"
echo "1. Go to Google Cloud Console > APIs & Services > Credentials."
echo "2. Edit your OAuth 2.0 Client ID."
echo "3. Add the following to 'Authorized redirect URIs':"
echo -e " ${GREEN}$FUNCTION_URL${NC}"
echo "4. Set the following environment variables in your local environment:"
echo -e " ${GREEN}export WORKSPACE_CLIENT_ID=\"$CLIENT_ID\"${NC}"
echo -e " ${GREEN}export WORKSPACE_CLOUD_FUNCTION_URL=\"$FUNCTION_URL\"${NC}"
echo -e "---------------------------------------------------"
11 changes: 6 additions & 5 deletions workspace-server/src/auth/AuthManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ import { logToFile } from '../utils/logger';
import open from '../utils/open-wrapper';
import { shouldLaunchBrowser } from '../utils/secure-browser-launcher';
import { OAuthCredentialStorage } from './token-storage/oauth-credential-storage';
import { loadConfig } from '../utils/config';

// The Client ID for the OAuth flow.
// The secret is handled by the cloud function, not in the client.
const CLIENT_ID = '338689075775-o75k922vn5fdl18qergr96rp8g63e4d7.apps.googleusercontent.com';
const config = loadConfig();
const CLIENT_ID = config.clientId;
const CLOUD_FUNCTION_URL = config.cloudFunctionUrl;
const TOKEN_EXPIRY_BUFFER_MS = 5 * 60 * 1000; // 5 minutes

/**
Expand Down Expand Up @@ -201,7 +202,7 @@ export class AuthManager {

// Call the cloud function refresh endpoint
// The cloud function has the client secret needed for token refresh
const response = await fetch('https://google-workspace-extension.geminicli.com/refreshToken', {
const response = await fetch(`${CLOUD_FUNCTION_URL}/refreshToken`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Expand Down Expand Up @@ -287,7 +288,7 @@ export class AuthManager {
const state = Buffer.from(JSON.stringify(statePayload)).toString('base64');

// The redirect URI for Google's auth server is the cloud function
const cloudFunctionRedirectUri = 'https://google-workspace-extension.geminicli.com';
const cloudFunctionRedirectUri = CLOUD_FUNCTION_URL;

const authUrl = client.generateAuthUrl({
redirect_uri: cloudFunctionRedirectUri, // Tell Google to go to the cloud function
Expand Down
31 changes: 31 additions & 0 deletions workspace-server/src/utils/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import { logToFile } from './logger';

export interface WorkspaceConfig {
clientId: string;
cloudFunctionUrl: string;
}

const DEFAULT_CONFIG: WorkspaceConfig = {
clientId: '338689075775-o75k922vn5fdl18qergr96rp8g63e4d7.apps.googleusercontent.com',
cloudFunctionUrl: 'https://google-workspace-extension.geminicli.com'
};

/**
* Loads the configuration. Currently uses defaults, but can be extended
* to read from environment variables or a configuration file.
*/
export function loadConfig(): WorkspaceConfig {
const config: WorkspaceConfig = {
clientId: process.env['WORKSPACE_CLIENT_ID'] || DEFAULT_CONFIG.clientId,
cloudFunctionUrl: process.env['WORKSPACE_CLOUD_FUNCTION_URL'] || DEFAULT_CONFIG.cloudFunctionUrl
};

logToFile(`Loaded config: clientId=${config.clientId}, cloudFunctionUrl=${config.cloudFunctionUrl}`);
return config;
}
Loading