google-api-fetch
Version:
A lightweight Google API client using fetch for edge environments
164 lines (140 loc) • 4.58 kB
JavaScript
let cryptoModule;
if (typeof crypto !== 'undefined') {
cryptoModule = crypto;
} else if (typeof globalThis !== 'undefined' && globalThis.crypto) {
cryptoModule = globalThis.crypto;
} else {
try {
const nodeCrypto = require('crypto');
cryptoModule = nodeCrypto.webcrypto;
} catch (e) {
throw new Error('Web Crypto API is not available in this environment');
}
}
export default class AuthClient {
constructor(params) {
let credentials;
if (params.credentials) {
credentials = params.credentials;
} else if (params.private_key && params.client_email) {
credentials = {
private_key: params.private_key,
client_email: params.client_email
};
}
if (!credentials) {
throw new Error('Credentials required');
}
if (!credentials.client_email || !credentials.private_key) {
throw new Error('Service account credentials must include client_email and private_key');
}
this.credentials = credentials;
this.token = null;
this.tokenExpiry = 0;
}
async getAccessToken() {
if (this.token && Date.now() < this.tokenExpiry - 60000) {
return this.token;
}
try {
const now = Math.floor(Date.now() / 1000);
const expiryTime = now + 3600;
const header = {
alg: 'RS256',
typ: 'JWT'
};
const payload = {
iss: this.credentials.client_email,
scope: 'https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/documents',
aud: 'https://oauth2.googleapis.com/token',
exp: expiryTime,
iat: now
};
const signedJwt = await this.createSignedJwt(header, payload, this.credentials.private_key);
const response = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
assertion: signedJwt
}).toString()
});
if (!response.ok) {
const error = await response.json();
throw new Error(`Authentication failed: ${JSON.stringify(error)}`);
}
const tokenData = await response.json();
this.token = tokenData.access_token;
this.tokenExpiry = Date.now() + (tokenData.expires_in * 1000);
return this.token;
} catch (error) {
console.error('Failed to obtain access token:', error);
throw error;
}
}
async createSignedJwt(header, payload, privateKey) {
const base64Url = (str) => {
return btoa(str)
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
};
const headerEncoded = base64Url(JSON.stringify(header));
const payloadEncoded = base64Url(JSON.stringify(payload));
const toSign = `${headerEncoded}.${payloadEncoded}`;
const privateKeyFormatted = this.formatPrivateKey(privateKey);
const keyData = await this.importPrivateKey(privateKeyFormatted);
const signature = await this.signData(toSign, keyData);
return `${toSign}.${signature}`;
}
formatPrivateKey(privateKey) {
return privateKey
.replace('-----BEGIN PRIVATE KEY-----', '')
.replace('-----END PRIVATE KEY-----', '')
.replace(/\s+/g, '');
}
async importPrivateKey(privateKeyBase64) {
const binaryString = atob(privateKeyBase64);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
const cryptoImpl = cryptoModule;
return await cryptoImpl.subtle.importKey(
'pkcs8',
bytes.buffer,
{
name: 'RSASSA-PKCS1-v1_5',
hash: {name: 'SHA-256'},
},
false,
['sign']
);
}
async signData(data, key) {
const encoder = new TextEncoder();
const encodedData = encoder.encode(data);
const cryptoImpl = cryptoModule;
const signature = await cryptoImpl.subtle.sign(
{ name: 'RSASSA-PKCS1-v1_5' },
key,
encodedData
);
return btoa(String.fromCharCode(...new Uint8Array(signature)))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
}
async getAuthHeaders() {
const token = await this.getAccessToken();
return {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
};
}
async getRequestHeaders() {
return this.getAuthHeaders();
}
}