import { createHash, randomBytes } from 'crypto';
export interface OAuthConfig {
clientId: string;
clientSecret: string;
redirectUri: string;
scopes: string[];
}
export interface OAuthTokens {
access_token: string;
refresh_token: string;
expires_at: number;
scope: string;
}
export class GoogleOAuth {
private config: OAuthConfig;
constructor(config: OAuthConfig) {
this.config = config;
}
async initialize(): Promise<void> {
}
generateAuthUrl(): { url: string; codeVerifier: string; state: string } {
const codeVerifier = this.generateCodeVerifier();
const codeChallenge = this.generateCodeChallenge(codeVerifier);
const state = this.generateState();
const params = new URLSearchParams({
client_id: this.config.clientId,
redirect_uri: this.config.redirectUri,
scope: this.config.scopes.join(' '),
code_challenge: codeChallenge,
code_challenge_method: 'S256',
state,
response_type: 'code'
});
const url = `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`;
return { url, codeVerifier, state };
}
async exchangeCodeForTokens(
code: string,
codeVerifier: string,
state: string
): Promise<any> {
const response = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
client_id: this.config.clientId,
client_secret: this.config.clientSecret,
code,
grant_type: 'authorization_code',
redirect_uri: this.config.redirectUri,
code_verifier: codeVerifier
})
});
if (!response.ok) {
throw new Error(`Token exchange failed: ${response.statusText}`);
}
return await response.json();
}
async refreshAccessToken(refreshToken: string): Promise<any> {
const response = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
client_id: this.config.clientId,
client_secret: this.config.clientSecret,
refresh_token: refreshToken,
grant_type: 'refresh_token'
})
});
if (!response.ok) {
throw new Error(`Token refresh failed: ${response.statusText}`);
}
return await response.json();
}
extractTokens(tokenSet: any): OAuthTokens {
if (!tokenSet.access_token) {
throw new Error('No access token in token set');
}
if (!tokenSet.refresh_token) {
throw new Error('No refresh token in token set');
}
return {
access_token: tokenSet.access_token,
refresh_token: tokenSet.refresh_token,
expires_at: tokenSet.expires_at || Date.now() + 3600000, // Default 1 hour
scope: tokenSet.scope || this.config.scopes.join(' ')
};
}
isTokenExpired(tokens: OAuthTokens): boolean {
return Date.now() >= tokens.expires_at;
}
needsRefresh(tokens: OAuthTokens): boolean {
const bufferMs = 5 * 60 * 1000; // 5 minutes
return Date.now() >= (tokens.expires_at - bufferMs);
}
private generateCodeVerifier(): string {
return randomBytes(32).toString('base64url');
}
private generateCodeChallenge(verifier: string): string {
return createHash('sha256').update(verifier).digest('base64url');
}
private generateState(): string {
return randomBytes(16).toString('hex');
}
}