Cómo generar URLs pre-firmadas para Firebase Storage con Astro en Cloudflare Workers
LaunchFast Logo LaunchFast

Cómo generar URLs pre-firmadas para Firebase Storage con Astro en Cloudflare Workers

Rishi Raj Jain
How to Generate Pre-signed URLs for Firebase Storage with Cloudflare Workers

En esta guía, aprenderás a generar URLs pre-firmadas para Firebase Storage con Astro en Cloudflare Workers. Pasarás por el proceso de configurar un nuevo proyecto Astro, habilitar la renderización del lado del servidor usando el adaptador de Cloudflare, obtener el JSON de la Cuenta de Servicio de Firebase y luego crear funciones para generar URLs pre-firmadas para la recuperación y carga desde Firebase Storage.

Kits de inicio de alta calidad con flujo de autenticación integrada (Auth.js), carga de objetos (AWS, Clouflare R2, Firebase Storage, Supabase Storage), pagos integrados (Stripe, LemonSqueezy), flujo de verificación de correo electrónico (Resend, Postmark, Sendgrid) y mucho más . Compatible con cualquier base de datos (Redis, Postgres, MongoDB, SQLite, Firestore).

Requisitos previos

Para seguir, necesitarás:

Tabla de Contenidos

Generar un JSON de Cuenta de Servicio de Firebase

  • Ve a la Consola de Firebase y navega por tu proyecto (crea uno si aún no tienes uno).
  • En la Consola de Firebase, navega a Configuración del Proyecto haciendo clic en el ícono de engranaje.
  • Luego, ve a la pestaña Cuentas de servicio.
  • Finalmente, haz clic en Generar nueva clave privada y descarga el archivo JSON. Este archivo contiene las credenciales de tu cuenta de servicio de Firebase.

Crear una nueva aplicación Astro

Comencemos creando un nuevo proyecto Astro. Abre tu terminal y ejecuta el siguiente comando:

Terminal window
npm create astro@latest my-app

npm create astro es la forma recomendada de crear rápidamente un proyecto Astro.

Cuando se te pregunte, elige:

  • Use minimal (empty) template cuando se te pregunte cómo iniciar el nuevo proyecto.
  • Yes cuando se te pregunte si deseas instalar dependencias.
  • Yes cuando se te pregunte si deseas inicializar un repositorio git.

Una vez hecho esto, puedes moverte al directorio del proyecto y comenzar la aplicación:

Terminal window
cd my-app
npm run dev

La aplicación debería estar ejecutándose en localhost:4321. A continuación, ejecuta el siguiente comando para instalar la biblioteca necesaria para construir la aplicación:

Terminal window
npm install jose

La siguiente biblioteca se instala:

  • jose: Una biblioteca para Firmado y Cifrado de Objetos JavaScript (JOSE) utilizada para manejar JWTs y otras operaciones criptográficas.

Integrar el adaptador de Cloudflare en tu proyecto Astro

Para generar URLs pre-firmadas para cada objeto dinámicamente, habilitarás la renderización del lado del servidor en tu proyecto Astro a través del adaptador de Cloudflare. Ejecuta el siguiente comando:

Terminal window
npx astro add cloudflare

Cuando se te pregunte, elige lo siguiente:

  • Y cuando se te pregunte si deseas instalar las dependencias de Cloudflare.
  • Y cuando se te pregunte si deseas realizar cambios en el archivo de configuración de Astro.

Has habilitado con éxito la renderización del lado del servidor en Astro.

Para asegurarte de que la salida sea desplegable en Cloudflare Workers, crea un archivo wrangler.toml en la raíz del proyecto con el siguiente código:

wrangler.toml
name = "firebase-storage-astro-workers"
main = "dist/_worker.js"
compatibility_date = "2025-04-01"
compatibility_flags = [ "nodejs_compat" ]
[assets]
directory="dist"
binding="ASSETS"
[vars]
FIREBASE_CLIENT_ID=""
FIREBASE_PROJECT_ID=""
FIREBASE_PRIVATE_KEY=""
FIREBASE_CLIENT_EMAIL=""
FIREBASE_STORAGE_BUCKET=""
FIREBASE_PRIVATE_KEY_ID=""
FIREBASE_CLIENT_X509_CERT_URL=""

Después de eso, asegúrate de tener tanto un archivo .env como un archivo wrangler.toml con las variables definidas para que puedan ser accedidas durante npm run dev y cuando se despliegue en Cloudflare Workers respectivamente.

Además, actualiza el archivo astro.config.mjs con lo siguiente para poder acceder a estas variables en el código de manera programática:

astro.config.mjs
// ... Importaciones existentes...
import { defineConfig, envField } from 'astro/config'
export default defineConfig({
env: {
schema: {
FIREBASE_PROJECT_ID: envField.string({ context: 'server', access: 'secret', optional: false }),
FIREBASE_CLIENT_X509_CERT_URL: envField.string({ context: 'server', access: 'secret', optional: false }),
FIREBASE_PRIVATE_KEY_ID: envField.string({ context: 'server', access: 'secret', optional: false }),
FIREBASE_PRIVATE_KEY: envField.string({ context: 'server', access: 'secret', optional: false }),
FIREBASE_CLIENT_ID: envField.string({ context: 'server', access: 'secret', optional: false }),
FIREBASE_CLIENT_EMAIL: envField.string({ context: 'server', access: 'secret', optional: false }),
FIREBASE_STORAGE_BUCKET: envField.string({ context: 'server', access: 'secret', optional: false }),
}
}
// adaptador
})

Generar las URLs pre-firmadas

1. Acceder a las Variables de Entorno

El primer paso es acceder a las variables de entorno necesarias durante el tiempo de ejecución para realizar solicitudes fetch para habilitar CORS en el bucket y poder acceder a las URLs pre-firmadas. Desde Astro 5.6 y posteriores, la forma en que deseas acceder a las variables de entorno en tiempo de ejecución en tu código es utilizando la función getSecret de astro:env/server para mantener las cosas agnósticas al proveedor. Esto es crucial para almacenar información sensible de manera segura sin codificarla directamente en tu aplicación. Recuperarás las siguientes variables:

  • FIREBASE_CLIENT_ID
  • FIREBASE_PROJECT_ID
  • FIREBASE_PRIVATE_KEY
  • FIREBASE_CLIENT_EMAIL
  • FIREBASE_STORAGE_BUCKET
  • FIREBASE_PRIVATE_KEY_ID
  • FIREBASE_CLIENT_X509_CERT_URL
src/storage/firebase/config.ts
import { getSecret } from 'astro:env/server'
const project_id = getSecret('FIREBASE_PROJECT_ID')
const private_key_id = getSecret('FIREBASE_PRIVATE_KEY_ID')
const private_key = getSecret('FIREBASE_PRIVATE_KEY')
const client_email = getSecret('FIREBASE_CLIENT_EMAIL')
const client_id = getSecret('FIREBASE_CLIENT_ID')
const storageBucket = getSecret('FIREBASE_STORAGE_BUCKET')
const client_x509_cert_url = getSecret('FIREBASE_CLIENT_X509_CERT_URL')
export default function getFirebaseConfig() {
return {
type: 'service_account',
client_id,
project_id,
private_key,
client_email,
storageBucket,
private_key_id,
client_x509_cert_url,
universe_domain: 'googleapis.com',
token_uri: 'https://oauth2.googleapis.com/token',
auth_uri: 'https://accounts.google.com/o/oauth2/auth',
auth_provider_x509_cert_url: 'https://www.googleapis.com/oauth2/v1/certs',
}
}

2. Generar Token de Google OAuth 2.0

src/storage/firebase/index.ts
import getFirebaseConfig from './config'
import { importPKCS8, SignJWT } from 'jose'
const serviceAccount = getFirebaseConfig()
async function getAccessTokenWithJose() {
const privateKeyPem = serviceAccount.private_key
const clientEmail = serviceAccount.client_email
if (!privateKeyPem) throw new Error(`FIREBASE_PRIVATE_KEY environment variable is not available`)
const iat = Math.floor(Date.now() / 1000)
const exp = iat + 3600
const jwt = await new SignJWT({
iss: clientEmail,
scope: 'https://www.googleapis.com/auth/devstorage.full_control',
aud: 'https://oauth2.googleapis.com/token',
iat,
exp,
})
.setProtectedHeader({ alg: 'RS256' })
.sign(await importPKCS8(privateKeyPem, 'RS256'))
const res = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
assertion: jwt,
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
}),
})
const data = await res.json()
if (!res.ok) throw new Error(`Token error: ${data.error}`)
return data.access_token
}

El código anterior define una función asincrónica getAccessTokenWithJose que genera un JSON Web Token (JWT) usando la biblioteca jose. Firma el JWT con una clave privada obtenida de la configuración de Firebase y luego lo utiliza para solicitar un token de acceso del endpoint de token de Google OAuth 2.0. Este token de acceso es necesario para autenticar las solicitudes a Firebase Storage.

3. Configurar la Política CORS del Bucket de Firebase Storage

src/storage/firebase/index.ts
// ...Importaciones existentes...
// ...Código existente...
async function setBucketCORS(bucketName) {
const accessToken = await getAccessTokenWithJose()
const corsConfig = {
cors: [
{
origin: [
'http://localhost:3000',
'https://your-deployment.workers.dev',
],
maxAgeSeconds: 3600,
method: ['GET', 'PUT', 'POST'],
responseHeader: ['Content-Type', 'Content-MD5', 'Content-Disposition'],
},
],
}
const res = await fetch(`https://storage.googleapis.com/storage/v1/b/${bucketName.replace('gs://', '')}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify(corsConfig),
})
if (!res.ok) {
const err = await res.text()
console.warn(`Failed to update CORS: ${res.statusText}\n${err}`)
}
}

El código anterior define una función asincrónica llamada setBucketCORS para configurar la política de Cross-Origin Resource Sharing (CORS) para un bucket de Firebase Storage especificado. Primero, recupera un token de acceso utilizando la función getAccessTokenWithJose. Luego, construye un objeto de configuración CORS que especifica los orígenes permitidos, métodos HTTP y encabezados de respuesta. La función envía una solicitud PATCH a la API de Google Cloud Storage para actualizar la configuración CORS del bucket, utilizando el token de acceso para la autorización.

4. Crear URLs Pre-firmadas con las APIs de Cloud Storage

src/storage/firebase/index.ts
// ...Importaciones existentes...
import { createHash, createSign } from 'node:crypto'
async function generateSignedUrl(bucketName, objectName, { subresource = null, expiration = 604800, httpMethod = 'GET', queryParameters = {}, headers = {} } = {}) {
if (expiration > 604800) throw new Error("Expiration can't be longer than 7 days (604800 seconds).")
const privateKey = serviceAccount.private_key
if (!privateKey) throw new Error(`FIREBASE_PRIVATE_KEY environment variable is not available`)
const clientEmail = serviceAccount.client_email
const now = new Date()
const datestamp = now.toISOString().slice(0, 10).replace(/-/g, '')
const timestamp = `${datestamp}T${now.toISOString().slice(11, 19).replace(/:/g, '')}Z`
const credentialScope = `${datestamp}/auto/storage/goog4_request`
const credential = `${clientEmail}/${credentialScope}`
const host = `${bucketName.replace('gs://', '')}.storage.googleapis.com`
headers['host'] = host
// Encabezados canónicos
const sortedHeaders = Object.keys(headers)
.sort()
.reduce((obj, key) => {
obj[key.toLowerCase()] = headers[key].toLowerCase()
return obj
}, {})
const canonicalHeaders = Object.entries(sortedHeaders)
.map(([k, v]) => `${k}:${v}\n`)
.join('')
const signedHeaders = Object.keys(sortedHeaders).join(';')
// Cadena de consulta canónica
const fullQueryParams: Record<string, any> = {
...queryParameters,
'X-Goog-Date': timestamp,
'X-Goog-Credential': credential,
'X-Goog-SignedHeaders': signedHeaders,
'X-Goog-Algorithm': 'GOOG4-RSA-SHA256',
'X-Goog-Expires': expiration.toString(),
}
if (subresource) fullQueryParams[subresource] = ''
const orderedQueryParams = Object.keys(fullQueryParams)
.sort()
.map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(fullQueryParams[key])}`)
.join('&')
const canonicalUri = `/${encodeURIComponent(objectName).replace(/%2F/g, '/')}`
const canonicalRequest = [httpMethod, canonicalUri, orderedQueryParams, canonicalHeaders, signedHeaders, 'UNSIGNED-PAYLOAD'].join('\n')
const hash = createHash('sha256').update(canonicalRequest).digest('hex')
const stringToSign = ['GOOG4-RSA-SHA256', timestamp, credentialScope, hash].join('\n')
const signature = createSign('RSA-SHA256').update(stringToSign).sign(privateKey).toString('hex')
return `https://${host}${canonicalUri}?${orderedQueryParams}&X-Goog-Signature=${signature}`
}
// ...Código existente...

El código anterior define una función generateSignedUrl que crea una URL firmada para acceder a objetos en Firebase Storage. Utiliza funciones criptográficas para generar una firma basada en los detalles de la solicitud, incluidos los nombres del bucket y del objeto, el método HTTP y el tiempo de expiración. La función construye encabezados y parámetros de consulta canónicos, luego firma la solicitud utilizando la clave privada de la cuenta de servicio. La URL firmada resultante permite el acceso seguro al objeto especificado en el bucket de almacenamiento.

5. URL Pre-firmada para GET un Objeto de Firebase (recuperar)

La función getFirebaseObject a continuación recupera la URL pre-firmada de un objeto desde Firebase Storage. Genera una solicitud firmada que te permite acceder al archivo de manera segura.

src/storage/firebase/index.ts
// ...Código existente...
export async function getFirebaseObject(Key: string) {
try {
await setBucketCORS(serviceAccount.storageBucket)
return await generateSignedUrl(serviceAccount.storageBucket, Key, {
httpMethod: 'GET',
expiration: 60 * 60 * 24,
})
} catch (e: any) {
const tmp = e.message || e.toString()
console.log(tmp)
return
}
}

6. URL Pre-firmada para PUT un Objeto de Firebase (cargar)

La función uploadFirebaseObject a continuación es responsable de generar una URL pre-firmada para cargar un archivo a Firebase Storage. Sigue una estructura similar a la función getFirebaseObject, generando una URL firmada que te permite cargar archivos de manera segura.

src/storage/firebase/index.ts
// ...Código existente...
export async function uploadFirebaseObject(file: { name: string; type: string }) {
try {
await setBucketCORS(serviceAccount.storageBucket)
return await generateSignedUrl(serviceAccount.storageBucket, file.name, {
httpMethod: 'PUT',
expiration: 60 * 60 * 24,
headers: {
'Content-Type': file.type,
},
})
} catch (e: any) {
const tmp = e.message || e.toString()
console.log(tmp)
return
}
}

7. Crear un Endpoint del Servidor (una Ruta API) en Astro

src/pages/api/storage.ts
import type { APIContext } from 'astro'
import { getFirebaseObject, uploadFirebaseObject } from '../../storage/firebase/index'
// Define una función asincrónica llamada GET que acepta un objeto de solicitud.
export async function GET({ request }: APIContext) {
// Extrae el parámetro 'file' de la URL de la solicitud.
const url = new URL(request.url)
const file = url.searchParams.get('file')
// Verifica si el parámetro 'file' existe en la URL.
if (file) {
try {
const filePublicURL = await getFirebaseObject(file)
// Devuelve una respuesta con la URL pública de la imagen y un código de estado 200.
return new Response(filePublicURL)
} catch (error: any) {
// Si ocurre un error, registra el mensaje de error y devuelve una respuesta con un código de estado 500.
const message = error.message || error.toString()
console.log(message)
return new Response(message, { status: 500 })
}
}
// Si el parámetro 'file' no se encuentra en la URL, devuelve una respuesta con un código de estado 400.
return new Response('Invalid Request.', { status: 400 })
}
export async function POST({ request }: APIContext) {
// Extrae el parámetro 'file' de la URL de la solicitud.
const url = new URL(request.url)
const type = url.searchParams.get('type')
const name = url.searchParams.get('name')
if (!type || !name) return new Response('Invalid Request.', {status:400})
try {
// Genera una URL accesible para el archivo cargado
// Usa esta URL para realizar un GET a este endpoint con el parámetro de consulta file valorado como a continuación
const publicUploadUrl = await uploadFirebaseObject({ type, name })
// Devuelve una respuesta de éxito con un mensaje
return new Response(publicUploadUrl)
} catch (error: any) {
// Si hubo un error durante el proceso de carga, devuelve una respuesta 403 con el mensaje de error
const message = error.message || error.toString()
console.log(message)
return new Response(message, { status: 500 })
}
}

Desplegar en Cloudflare Workers

Para hacer que tu aplicación sea desplegable en Cloudflare Workers, crea un archivo llamado .assetsignore en el directorio public con el siguiente contenido:

_routes.json
_worker.js

A continuación, necesitarás usar la CLI de Wrangler para desplegar tu aplicación en Cloudflare Workers. Ejecuta el siguiente comando para desplegar:

Terminal window
npm run build && npx wrangler@latest deploy

Referencias

Conclusión

En este artículo, aprendiste cómo integrar Firebase Storage con Astro y Cloudflare Workers para cargas y recuperaciones de archivos. Siguiendo los pasos de implementación, puedes cargar y recuperar archivos de Firebase Storage de manera segura, asegurando que tu aplicación web tenga una solución de almacenamiento robusta y flexible.

Si deseas explorar secciones específicas con más detalle, ampliar ciertos conceptos o cubrir temas relacionados adicionales, por favor házmelo saber y estaré encantado de ayudarte.

Learn More Consulta de Cloud Firestore con Astro en Cloudflare Workers
Consulta de Cloud Firestore con Astro en Cloudflare Workers April 25, 2025
Query de Redis con Astro en Cloudflare Workers
Query de Redis con Astro en Cloudflare Workers April 25, 2025
Cómo generar URLs pre-firmadas para Cloudflare R2 con Astro en Cloudflare Workers
Cómo generar URLs pre-firmadas para Cloudflare R2 con Astro en Cloudflare Workers April 23, 2025