Skip to content
Merged
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
57 changes: 57 additions & 0 deletions cloud_function/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -254,3 +254,60 @@ functions.http('oauthCallback', async (req, res) => {
res.status(500).send('An error occurred during the token exchange. Check function logs for details.');
}
});

/**
* HTTP Cloud Function that handles token refresh.
* Accepts a refresh_token and returns a new access_token.
*/
functions.http('refreshToken', async (req, res) => {
// Only accept POST requests
if (req.method !== 'POST') {
console.error('Invalid method for refreshToken:', req.method);
return res.status(405).send('Method Not Allowed');
}

const { refresh_token } = req.body;
console.log(`Received refresh request with refresh_token: ${refresh_token ? 'present' : 'missing'}`);

if (!refresh_token) {
console.error('Missing refresh_token in request body');
return res.status(400).send('Error: Missing refresh_token in request body.');
}

try {
const clientSecret = await getClientSecret();

console.log('Refreshing access token via Google OAuth API...');
const tokenResponse = await axios.post('https://oauth2.googleapis.com/token', {
client_id: CLIENT_ID,
client_secret: clientSecret,
refresh_token: refresh_token,
grant_type: 'refresh_token',
});

console.log('Token refresh successful.');
const { access_token, expires_in, scope, token_type } = tokenResponse.data;

// Calculate expiry_date (timestamp in milliseconds)
const expiry_date = Date.now() + (expires_in * 1000);

// Return the new credentials
// Note: Google does NOT return a new refresh_token on refresh
// The client must preserve the original refresh_token
res.status(200).json({
access_token,
expiry_date,
token_type,
scope,
});

} catch (error) {
if (axios.isAxiosError(error) && error.response) {
console.error('Error during token refresh:', error.response.data);
res.status(error.response.status).json(error.response.data);
} else {
console.error('Error during token refresh:', error instanceof Error ? error.message : error);
res.status(500).send('An error occurred during token refresh.');
}
}
});
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"test:coverage": "npm run test:coverage --workspaces --if-present",
"test:ci": "npm run test:ci --workspaces --if-present",
"start": "npm run start --workspaces --if-present",
"clear-auth": "npm run build:clear-auth -w workspace-mcp-server && node scripts/clear-auth.js",
"auth-utils": "npm run build:auth-utils -w workspace-mcp-server && node scripts/auth-utils.js",
"clean": "rm -rf release && npm run clean --workspaces --if-present",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
Expand Down
119 changes: 119 additions & 0 deletions scripts/auth-utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

const { OAuthCredentialStorage } = require('../workspace-mcp-server/dist/auth-utils.js');

async function clearAuth() {
try {
await OAuthCredentialStorage.clearCredentials();
console.log('✅ Authentication credentials cleared successfully.');
} catch (error) {
console.error('❌ Failed to clear authentication credentials:', error);
process.exit(1);
}
}

async function expireToken() {
try {
const credentials = await OAuthCredentialStorage.loadCredentials();
if (!credentials) {
console.log('ℹ️ No credentials found to expire.');
return;
}

// Set expiry to 1 second ago
credentials.expiry_date = Date.now() - 1000;
await OAuthCredentialStorage.saveCredentials(credentials);
console.log('✅ Access token expired successfully.');
console.log(' Next API call will trigger proactive refresh.');
} catch (error) {
console.error('❌ Failed to expire token:', error);
process.exit(1);
}
}

async function showStatus() {
try {
const credentials = await OAuthCredentialStorage.loadCredentials();
if (!credentials) {
console.log('ℹ️ No credentials found.');
return;
}

const now = Date.now();
const expiry = credentials.expiry_date;
const hasRefreshToken = !!credentials.refresh_token;
const hasAccessToken = !!credentials.access_token;
const isExpired = expiry ? expiry < now : false;

console.log('📊 Auth Status:');
console.log(` Access Token: ${hasAccessToken ? '✅ Present' : '❌ Missing'}`);
console.log(` Refresh Token: ${hasRefreshToken ? '✅ Present' : '❌ Missing'}`);
if (expiry) {
console.log(` Expiry: ${new Date(expiry).toISOString()}`);
console.log(` Status: ${isExpired ? '❌ EXPIRED' : '✅ Valid'}`);
if (!isExpired) {
const minutesLeft = Math.floor((expiry - now) / 1000 / 60);
console.log(` Time left: ~${minutesLeft} minutes`);
}
} else {
console.log(` Expiry: ⚠️ Unknown`);
}
} catch (error) {
console.error('❌ Failed to get auth status:', error);
process.exit(1);
}
}

function showHelp() {
console.log(`
Auth Management CLI

Usage: node scripts/auth-utils.js <command>

Commands:
clear Clear all authentication credentials
expire Force the access token to expire (for testing refresh)
status Show current authentication status
help Show this help message

Examples:
node scripts/auth-utils.js clear
node scripts/auth-utils.js expire
node scripts/auth-utils.js status
`);
}

async function main() {
const command = process.argv[2];

switch (command) {
case 'clear':
await clearAuth();
break;
case 'expire':
await expireToken();
break;
case 'status':
await showStatus();
break;
case 'help':
case '--help':
case '-h':
showHelp();
break;
default:
if (!command) {
console.error('❌ No command specified.');
} else {
console.error(`❌ Unknown command: ${command}`);
}
showHelp();
process.exit(1);
}
}

main();
19 changes: 0 additions & 19 deletions scripts/clear-auth.js

This file was deleted.

5 changes: 5 additions & 0 deletions workspace-mcp-server/WORKSPACE-Context.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,11 @@ Choose output format based on use case:

## 🔍 Error Handling Patterns

### Authentication Errors
- If any tool returns `{"error":"invalid_request"}`, it likely indicates an expired or invalid session.
- **Action:** Call `auth.clear` to reset credentials and force a re-login.
- Inform the user that you are resetting authentication due to an error.

### Graceful Degradation
- If a folder doesn't exist, offer to create it
- If search returns no results, suggest alternatives
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@
const esbuild = require('esbuild');
const path = require('node:path');

async function buildClearAuth() {
async function buildAuthUtils() {
try {
await esbuild.build({
entryPoints: ['src/auth/token-storage/oauth-credential-storage.ts'],
bundle: true,
platform: 'node',
target: 'node20',
outfile: 'dist/clear-auth.js',
outfile: 'dist/auth-utils.js',
minify: true,
sourcemap: true,
external: [
Expand All @@ -24,11 +24,11 @@ async function buildClearAuth() {
logLevel: 'info',
});

console.log('Clear Auth build completed successfully!');
console.log('Auth Utils build completed successfully!');
} catch (error) {
console.error('Clear Auth build failed:', error);
console.error('Auth Utils build failed:', error);
process.exit(1);
}
}

buildClearAuth();
buildAuthUtils();
2 changes: 1 addition & 1 deletion workspace-mcp-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"start": "ts-node src/index.ts",
"clean": "rm -rf dist",
"build": "node esbuild.config.js",
"build:clear-auth": "node esbuild.clear-auth.js"
"build:auth-utils": "node esbuild.auth-utils.js"
},
"keywords": [],
"author": "",
Expand Down
Loading