Skip to content

Commit 471d1ca

Browse files
fix(companion): Fix Gmail OAuth integration (calcom#25978)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
1 parent 15b1675 commit 471d1ca

6 files changed

Lines changed: 520 additions & 51 deletions

File tree

companion/contexts/AuthContext.tsx

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,15 +84,29 @@ export function AuthProvider({ children }: AuthProviderProps) {
8484
}
8585
};
8686

87-
// Save OAuth tokens to storage
8887
const saveOAuthTokens = async (tokens: OAuthTokens) => {
8988
await storage.set(OAUTH_TOKENS_KEY, JSON.stringify(tokens));
9089
await storage.set(AUTH_TYPE_KEY, "oauth");
90+
91+
if (oauthService) {
92+
try {
93+
await oauthService.syncTokensToExtension(tokens);
94+
} catch (error) {
95+
console.warn("Failed to sync tokens to extension:", error);
96+
}
97+
}
9198
};
9299

93-
// Clear all auth data from storage
94100
const clearAuth = async () => {
95101
await storage.removeAll([ACCESS_TOKEN_KEY, REFRESH_TOKEN_KEY, OAUTH_TOKENS_KEY, AUTH_TYPE_KEY]);
102+
103+
if (oauthService) {
104+
try {
105+
await oauthService.clearTokensFromExtension();
106+
} catch (error) {
107+
console.warn("Failed to clear tokens from extension:", error);
108+
}
109+
}
96110
};
97111

98112
// Reset all auth state
@@ -147,11 +161,13 @@ export function AuthProvider({ children }: AuthProviderProps) {
147161

148162
// Refresh token if expired
149163
let tokens = storedTokens;
164+
let tokensWereRefreshed = false;
150165
if (oauthService.isTokenExpired(storedTokens) && storedTokens.refreshToken) {
151166
try {
152167
console.log("Access token expired, refreshing...");
153168
tokens = await oauthService.refreshAccessToken(storedTokens.refreshToken);
154169
await saveOAuthTokens(tokens);
170+
tokensWereRefreshed = true;
155171
} catch (refreshError) {
156172
console.error("Failed to refresh token:", refreshError);
157173
await clearAuth();
@@ -170,6 +186,14 @@ export function AuthProvider({ children }: AuthProviderProps) {
170186
if (tokens.refreshToken) {
171187
setupRefreshTokenFunction(oauthService);
172188
}
189+
190+
if (!tokensWereRefreshed) {
191+
try {
192+
await oauthService.syncTokensToExtension(tokens);
193+
} catch (error) {
194+
console.warn("Failed to sync tokens to extension on init:", error);
195+
}
196+
}
173197
};
174198

175199
// Handle web session authentication

companion/extension/entrypoints/background/index.ts

Lines changed: 117 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,67 @@ export default defineBackground(() => {
9595
return true;
9696
}
9797

98+
if (message.action === "sync-oauth-tokens") {
99+
const tokens = message.tokens as OAuthTokens | null;
100+
101+
if (isRateLimited()) {
102+
devLog.warn("Token sync rate limited");
103+
sendResponse({ success: false, error: "Rate limited. Please try again later." });
104+
return true;
105+
}
106+
107+
if (!tokens || !tokens.accessToken) {
108+
sendResponse({ success: false, error: "No valid tokens provided" });
109+
return true;
110+
}
111+
112+
validateTokens(tokens)
113+
.then((isValid) => {
114+
if (!isValid) {
115+
devLog.warn("Token sync rejected: invalid tokens");
116+
sendResponse({ success: false, error: "Invalid tokens" });
117+
return;
118+
}
119+
120+
recordTokenOperation();
121+
chrome.storage.local.set({ cal_oauth_tokens: JSON.stringify(tokens) }, () => {
122+
if (chrome.runtime.lastError) {
123+
devLog.error("Failed to sync OAuth tokens:", chrome.runtime.lastError.message);
124+
sendResponse({ success: false, error: chrome.runtime.lastError.message });
125+
} else {
126+
devLog.log("OAuth tokens synced to chrome.storage.local (validated)");
127+
sendResponse({ success: true });
128+
}
129+
});
130+
})
131+
.catch((error) => {
132+
devLog.error("Token validation failed:", error);
133+
sendResponse({ success: false, error: "Token validation failed" });
134+
});
135+
136+
return true;
137+
}
138+
139+
if (message.action === "clear-oauth-tokens") {
140+
if (isRateLimited()) {
141+
devLog.warn("Token clear rate limited");
142+
sendResponse({ success: false, error: "Rate limited. Please try again later." });
143+
return true;
144+
}
145+
146+
recordTokenOperation();
147+
chrome.storage.local.remove(["cal_oauth_tokens", "oauth_state"], () => {
148+
if (chrome.runtime.lastError) {
149+
devLog.error("Failed to clear OAuth tokens:", chrome.runtime.lastError.message);
150+
sendResponse({ success: false, error: chrome.runtime.lastError.message });
151+
} else {
152+
devLog.log("OAuth tokens cleared from chrome.storage.local");
153+
sendResponse({ success: true });
154+
}
155+
});
156+
return true;
157+
}
158+
98159
return false;
99160
});
100161
});
@@ -168,13 +229,22 @@ async function handleTokenExchange(
168229

169230
const tokenData = await response.json();
170231

171-
return {
232+
const tokens: OAuthTokens = {
172233
accessToken: tokenData.access_token,
173234
refreshToken: tokenData.refresh_token,
174235
tokenType: tokenData.token_type || "Bearer",
175236
expiresAt: tokenData.expires_in ? Date.now() + tokenData.expires_in * 1000 : undefined,
176237
scope: tokenData.scope,
177238
};
239+
240+
try {
241+
await chrome.storage.local.set({ cal_oauth_tokens: JSON.stringify(tokens) });
242+
devLog.log("OAuth tokens stored in chrome.storage.local");
243+
} catch (storageError) {
244+
devLog.error("Failed to store OAuth tokens:", storageError);
245+
}
246+
247+
return tokens;
178248
}
179249

180250
async function validateOAuthState(state: string): Promise<void> {
@@ -201,6 +271,52 @@ async function validateOAuthState(state: string): Promise<void> {
201271

202272
const API_BASE_URL = "https://api.cal.com/v2";
203273

274+
const tokenOperationTimestamps: number[] = [];
275+
const TOKEN_RATE_LIMIT_WINDOW_MS = 60000;
276+
const TOKEN_RATE_LIMIT_MAX_OPS = 5;
277+
278+
function isRateLimited(): boolean {
279+
const now = Date.now();
280+
while (
281+
tokenOperationTimestamps.length > 0 &&
282+
tokenOperationTimestamps[0] < now - TOKEN_RATE_LIMIT_WINDOW_MS
283+
) {
284+
tokenOperationTimestamps.shift();
285+
}
286+
return tokenOperationTimestamps.length >= TOKEN_RATE_LIMIT_MAX_OPS;
287+
}
288+
289+
function recordTokenOperation(): void {
290+
tokenOperationTimestamps.push(Date.now());
291+
}
292+
293+
async function validateTokens(tokens: OAuthTokens): Promise<boolean> {
294+
if (!tokens.accessToken) {
295+
return false;
296+
}
297+
298+
try {
299+
const response = await fetch(`${API_BASE_URL}/me`, {
300+
headers: {
301+
Authorization: `Bearer ${tokens.accessToken}`,
302+
"Content-Type": "application/json",
303+
"cal-api-version": "2024-06-11",
304+
},
305+
});
306+
307+
if (response.ok) {
308+
devLog.log("Token validation successful");
309+
return true;
310+
}
311+
312+
devLog.warn("Token validation failed:", response.status);
313+
return false;
314+
} catch (error) {
315+
devLog.error("Token validation error:", error);
316+
return false;
317+
}
318+
}
319+
204320
async function getAuthHeader(): Promise<string> {
205321
const result = await chrome.storage.local.get(["cal_oauth_tokens"]);
206322
const oauthTokens = result.cal_oauth_tokens

0 commit comments

Comments
 (0)