UNPKG

firebase-auth-cloudflare-workers

Version:

Zero-dependencies firebase auth library for Cloudflare Workers.

141 lines (140 loc) 5.94 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ServiceAccountCredential = void 0; const base64_1 = require("./base64"); const errors_1 = require("./errors"); const validator_1 = require("./validator"); const GOOGLE_TOKEN_AUDIENCE = 'https://accounts.google.com/o/oauth2/token'; const GOOGLE_AUTH_TOKEN_HOST = 'accounts.google.com'; const GOOGLE_AUTH_TOKEN_PATH = '/o/oauth2/token'; /** * Implementation of Credential that uses a service account. */ class ServiceAccountCredential { projectId; privateKey; clientEmail; /** * Creates a new ServiceAccountCredential from the given parameters. * * @param serviceAccountJson - Service account json content. * * @constructor */ constructor(serviceAccountJson) { const serviceAccount = ServiceAccount.fromJSON(serviceAccountJson); this.projectId = serviceAccount.projectId; this.privateKey = serviceAccount.privateKey; this.clientEmail = serviceAccount.clientEmail; } async getAccessToken() { const header = (0, base64_1.encodeObjectBase64Url)({ alg: 'RS256', typ: 'JWT', }).replace(/=/g, ''); const iat = Math.round(Date.now() / 1000); const exp = iat + 3600; const claim = (0, base64_1.encodeObjectBase64Url)({ iss: this.clientEmail, scope: ['https://www.googleapis.com/auth/cloud-platform', 'https://www.googleapis.com/auth/identitytoolkit'].join(' '), aud: GOOGLE_TOKEN_AUDIENCE, exp, iat, }).replace(/=/g, ''); const unsignedContent = `${header}.${claim}`; // This method is actually synchronous so we can capture and return the buffer. const signature = await this.sign(unsignedContent, this.privateKey); const jwt = `${unsignedContent}.${signature}`; const body = `grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&assertion=${jwt}`; const url = `https://${GOOGLE_AUTH_TOKEN_HOST}${GOOGLE_AUTH_TOKEN_PATH}`; const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Cache-Control': 'no-cache', Host: 'oauth2.googleapis.com', }, body, }); const json = (await res.json()); if (!json.access_token || !json.expires_in) { throw new errors_1.FirebaseAppError(errors_1.AppErrorCodes.INVALID_CREDENTIAL, `Unexpected response while fetching access token: ${JSON.stringify(json)}`); } return json; } async sign(content, privateKey) { const buf = this.str2ab(content); const binaryKey = (0, base64_1.decodeBase64)(privateKey); const signer = await crypto.subtle.importKey('pkcs8', binaryKey, { name: 'RSASSA-PKCS1-V1_5', hash: { name: 'SHA-256' }, }, false, ['sign']); const binarySignature = await crypto.subtle.sign({ name: 'RSASSA-PKCS1-V1_5' }, signer, buf); return (0, base64_1.encodeBase64Url)(binarySignature).replace(/=/g, ''); } str2ab(str) { const buf = new ArrayBuffer(str.length); const bufView = new Uint8Array(buf); for (let i = 0, strLen = str.length; i < strLen; i += 1) { bufView[i] = str.charCodeAt(i); } return buf; } } exports.ServiceAccountCredential = ServiceAccountCredential; /** * A struct containing the properties necessary to use service account JSON credentials. */ class ServiceAccount { projectId; privateKey; clientEmail; static fromJSON(text) { try { return new ServiceAccount(JSON.parse(text)); } catch (error) { // Throw a nicely formed error message if the file contents cannot be parsed throw new errors_1.FirebaseAppError(errors_1.AppErrorCodes.INVALID_CREDENTIAL, 'Failed to parse service account json file: ' + error); } } constructor(json) { if (!(0, validator_1.isNonNullObject)(json)) { throw new errors_1.FirebaseAppError(errors_1.AppErrorCodes.INVALID_CREDENTIAL, 'Service account must be an object.'); } copyAttr(this, json, 'projectId', 'project_id'); copyAttr(this, json, 'privateKey', 'private_key'); copyAttr(this, json, 'clientEmail', 'client_email'); let errorMessage; if (!(0, validator_1.isNonEmptyString)(this.projectId)) { errorMessage = 'Service account object must contain a string "project_id" property.'; } else if (!(0, validator_1.isNonEmptyString)(this.privateKey)) { errorMessage = 'Service account object must contain a string "private_key" property.'; } else if (!(0, validator_1.isNonEmptyString)(this.clientEmail)) { errorMessage = 'Service account object must contain a string "client_email" property.'; } if (typeof errorMessage !== 'undefined') { throw new errors_1.FirebaseAppError(errors_1.AppErrorCodes.INVALID_CREDENTIAL, errorMessage); } this.privateKey = this.privateKey.replace(/-+(BEGIN|END).*/g, '').replace(/\s/g, ''); } } /** * Copies the specified property from one object to another. * * If no property exists by the given "key", looks for a property identified by "alt", and copies it instead. * This can be used to implement behaviors such as "copy property myKey or my_key". * * @param to - Target object to copy the property into. * @param from - Source object to copy the property from. * @param key - Name of the property to copy. * @param alt - Alternative name of the property to copy. */ function copyAttr(to, from, key, alt) { const tmp = from[key] || from[alt]; if (typeof tmp !== 'undefined') { to[key] = tmp; } }