different-ai/openwork · error · Error
Google Workspace refresh token is missing. Reconnect Google
Error message
Google Workspace refresh token is missing. Reconnect Google Workspace.
What it means
refreshGoogleWorkspaceVault() refreshes the stored Google OAuth tokens when the cached access token is expired (or expiring within 60s). If the stored vault record has no refreshToken string, it cannot get a new access token, so it throws immediately. This happens when the original OAuth grant did not return a refresh token (e.g. offline access was not granted or the user re-consented without it) or the vault record is corrupt/partial.
Source
Thrown at apps/server/src/extensions/google-workspace.ts:600
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
client_id: clientId,
client_secret: clientSecret,
code: input.code,
code_verifier: input.verifier,
grant_type: "authorization_code",
redirect_uri: input.redirectUri,
}),
});
}
async function refreshGoogleWorkspaceVault(record: Record<string, unknown>) {
const token = isRecord(record.token) ? record.token : null;
const expiresAt = Number(token?.expiresAt ?? 0);
const accessToken = typeof token?.accessToken === "string" ? token.accessToken : "";
const refreshToken = typeof token?.refreshToken === "string" ? token.refreshToken : "";
if (accessToken && expiresAt > Date.now() + 60_000) return record;
if (!refreshToken) throw new Error("Google Workspace refresh token is missing. Reconnect Google Workspace.");
const { clientId, clientSecret, tokenBrokerUrl, missing } = googleWorkspaceCredentials();
if (missing.length > 0) throw new Error(`Missing Google OAuth configuration: ${missing.join(", ")}`);
const refreshed = tokenBrokerUrl
? await fetchGoogleWorkspaceTokenBrokerJson(tokenBrokerUrl, { grantType: "refresh_token", provider: GOOGLE_WORKSPACE_EXTENSION_ID, clientId, refreshToken })
: await fetchGoogleJson("https://oauth2.googleapis.com/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({ client_id: clientId, client_secret: clientSecret, grant_type: "refresh_token", refresh_token: refreshToken }),
});
if (!isRecord(refreshed) || typeof refreshed.access_token !== "string") throw new Error("Google OAuth refresh did not return an access token.");
const next = {
...record,
scopes: typeof refreshed.scope === "string" ? refreshed.scope.split(/\s+/).filter(Boolean) : record.scopes,
token: {
accessToken: refreshed.access_token,
refreshToken: typeof refreshed.refresh_token === "string" ? refreshed.refresh_token : refreshToken,
expiresAt: Date.now() + Number(refreshed.expires_in ?? 3600) * 1000,
},View on GitHub (pinned to 2b7df46e8a)
Solutions
- Reconnect Google Workspace via the Settings connect flow so a fresh grant stores a new refresh token
- Delete the stale oauth.vault record for the account and redo the OAuth connect from scratch
- Verify the vault JSON has token.refreshToken as a string before relying on long-lived sessions
- Ensure the connect flow requests offline access / refresh tokens so Google returns one on first consent
Example fix
// before (corrupt record in oauth.vault)
{ "token": { "accessToken": "ya29...", "expiresAt": 1700000000 } }
// after: reconnect via Settings; record now contains
{ "token": { "accessToken": "ya29...", "refreshToken": "1//0g...", "expiresAt": 1700000000 } } Defensive patterns
Strategy: try-catch
Validate before calling
const vault = JSON.parse(await readFile(vaultPath, "utf8"));
const token = vault?.token;
if (!token || typeof token.refreshToken !== "string" || token.refreshToken.length === 0) {
await promptReconnect();
} Type guard
function hasRefreshToken(record: unknown): record is { token: { refreshToken: string } } {
return typeof record === "object" && record !== null &&
typeof (record as { token?: unknown }).token === "object" && (record as { token?: unknown }).token !== null &&
typeof ((record as { token?: { refreshToken?: unknown } }).token).refreshToken === "string";
} Try / catch
try {
const { accessToken } = await googleWorkspaceAccessToken(config);
await callGoogleTool(accessToken);
} catch (err) {
if (err instanceof Error && err.message.includes("refresh token is missing")) {
await triggerReconnectFlow(); // surface "Reconnect Google Workspace" to the user
return;
}
throw err;
} Prevention
- Ensure the initial OAuth grant requests offline access so Google always returns a refresh token
- After connect, assert the stored vault record contains token.refreshToken before proceeding
- Back up the oauth.vault and vault-key files; partial writes leave records without refresh tokens
- Surface reconnect prompts proactively via the extension status payload instead of failing tool calls
When it happens
Trigger: Any Google Workspace tool call routed through googleWorkspaceAccessToken() after the stored access token has expired (expiresAt <= Date.now()+60s) while record.token.refreshToken is missing or not a string in the oauth vault.
Common situations: Vault file written by an older extension version without a refresh token; token object corrupted by a partial write; initial grant omitted the refresh token because consent/refresh scope settings changed; user manually edited or truncated the vault file.
Related errors
- Google OAuth refresh did not return an access token.
- Missing Google OAuth configuration: ${missing.join(", ")}
- google_workspace_not_connected
- MCP_OAUTH_CREDENTIAL_EXPIRED
- `${t("providers.no_oauth_prefix")} ${resolved}. ${t("provide
AI-assisted analysis of different-ai/openwork@2b7df46e8a (2026-09-01).
Data as JSON: /api/errors/9e3df151db418b62.
Report an issue: GitHub.