Como proteger as chaves de API com o Cloud Functions de segunda geração e o Firebase

Tempo de leitura: 7 minutes

O Firebase Cloud Functions permite que você escreva código de backend que pode ser acionado em resposta a eventos, e é muito útil se você quiser adicionar alguma lógica do lado do servidor aos seus aplicativos Flutter.

Mas o Cloud Functions de primeira geração tem algumas limitações, e a introdução da segunda geração do Cloud Functions para o Firebase traz alguns benefícios importantes:

  • Menos cold starts, o que tem sido um grande problema há muito tempo.
  • Maior simultaneidade, uma vez que cada instância de uma função pode agora lidar com várias solicitações.
  • Chaves de API seguras e protegidas por tipo com configuração parametrizada, que são melhores do que as variáveis de ambiente que eram usadas por padrão nas funções de primeira geração.

Este anúncio oficial no blog do Firebase oferece uma boa visão geral dos novos recursos.

Mas depois de tentar migrar um dos meus projetos do Cloud Functions de 1ª geração para o de 2ª geração, fiquei preso na migração das chaves de API e não consegui encontrar facilmente todas as informações de que precisava:

  • Como definir chaves de API com o Firebase CLI a partir da linha de comando
  • Como acessá-las dentro do Cloud Functions de 2ª geração (tanto as funções https quanto os acionadores do Firestore)
  • Como acessar as chaves de API ao testar funções com o Firebase Local Emulator

Portanto, se você também quiser usar o 2nd-gen Cloud Functions e armazenar suas chaves de API com segurança, este guia passo a passo é para você.

Para usar um cenário do mundo real como exemplo, mostrarei como configurar e ler algumas chaves de API do Stripe dentro de uma Cloud Function, mas as mesmas considerações se aplicam a quaisquer outras chaves de API que precisem ser armazenadas em seu backend do Firebase. 👍

Pronto? Vamos lá!

 

A maneira antiga, também conhecida como uso de variáveis de ambiente (e o que há de errado com ela)

Se você já usou funções de 1ª geração no passado, é provável que tenha usado firebase functions:config:set para definir algumas variáveis de ambiente:

# execute isso para definir a variável de ambiente na linha de comando
firebase functions:config:set stripe.secret_key=whsec_YOUR_STRIPE_SECRET_KEY
firebase functions:config:set stripe.webhook_secret_key=whsec_YOUR_STRIPE_WEBHOOK_SECRET_KEY

Para ler essas variáveis dentro do Cloud Functions, basta usar a classe functions.config():

import * as functions from "firebase-functions"
import Stripe from "stripe"

// 1. ler as chaves
const stripeSecretKey = functions.config().stripe?.secret_key
const stripeWebhookSecretKey = functions.config().stripe?.webhook_secret_key

// 2. declarar uma função
exports.createOrderPaymentIntent = functions.https.onCall((data, context) {
  // 3. usar a(s) chave(s) conforme necessário
  if (stripeSecretKey === undefined) {
    throw new functions.https.HttpsError("aborted", "Stripe Secret Key is not set")
  }
  // criar o objeto Stripe, passando a chave como um argumento
  const stripe = new Stripe(stripeSecretKey as string, {
      apiVersion: "2023-08-16",
      typescript: true,
  })
  // use-o conforme necessário
    ...
})

No entanto, essa abordagem não é segura para valores secretos (como chaves de API). E ela não garante se um valor está definido ou qual é o seu tipo.

Portanto, vamos tentar migrar o código acima para usar funções de segunda geração. 👇

 

A nova maneira: funções de segunda geração com a API de segredos

Para começar, a definição de segredos agora é feita usando firebase functions:secrets:set:

# Observação: use MAIÚSCULAS ao definir o nome da chave
firebase functions:secrets:set STRIPE_SECRET_KEY
? Enter a value for STRIPE_SECRET_KEY [input is hidden]

Isso solicitará que você insira o valor da chave secreta (se você estiver usando o Stripe, isso pode ser encontrado aqui).

Mas se for a primeira vez que você faz isso em seu projeto Firebase, você receberá um erro:

Error: HTTP Error: 403, Secret Manager API has not been used in project <your-firebase-project-id> before or it is disabled.
Enable it by visiting https://console.developers.google.com/apis/api/secretmanager.googleapis.com/overview?project=<your-firebase-project-id> then retry.
If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry.

 

Etapa 1: Ativar a API do Secret Manager no Google Cloud

O erro acima contém um link que o levará à página da API do Secret Manager no Console do Google Cloud.

Como alternativa, você pode acessar https://console.cloud.google.com e seguir estas etapas. 👇

Etapa 1a: selecione o projeto correto e, em seguida, navegue até “Enabled APIs & services” (APIs e serviços ativados)

Etapa 1b: Clique em “Enable APIs and Services” (Ativar APIs e serviços)

Etapa 1c: Procure por “Secret Manager API”

Etapa 1d: Clique no Secret Manager API result

Etapa 1e: Ative (Enable) o Secret Manager API

Etapa 2: defina seus segredos com a CLI do Firebase

Depois de ativar a API do Secret Manager, você poderá definir seu segredo com êxito (que pode ser encontrado na página de chaves da API do Stripe):

# Observação: use MAIÚSCULAS ao definir o nome da chave
firebase functions:secrets:set STRIPE_SECRET_KEY
? Enter a value for STRIPE_SECRET_KEY [input is hidden]

Em seguida, você pode fazer o mesmo com o segredo do webhook (que é gerado sempre que você registra um novo webhook do Stripe)

# Observação: use MAIÚSCULAS ao definir o nome da chave
firebase functions:secrets:set STRIPE_WEBHOOK_SECRET_KEY
? Enter a value for STRIPE_WEBHOOK_SECRET_KEY [input is hidden]

O prompt acima mostra como definir as chaves da API do Stripe, mas isso é apenas um exemplo. O ponto principal é que você deve chamar firebase functions:secrets:set para cada chave de API que tiver em seu projeto.

Em seguida, é hora de atualizar o Cloud Functions.

 

Etapa 3: usar a nova API de segredos com funções de nuvem de segunda geração

A nova API de segredos funciona tanto com funções de 1ª geração quanto com funções de 2ª geração (com algumas pequenas diferenças de sintaxe).

Ao usar funções de segunda geração, elas podem ser definidas da seguinte forma:

// 1. importar funções V2
import * as functions from "firebase-functions/v2"
// 2. importar a função defineSecret
import {defineSecret} from "firebase-functions/params"
import Stripe from "stripe"

// 3. definir os segredos, usando as chaves que armazenamos anteriormente com a CLI do Firebase
// https://firebase.google.com/docs/functions/2nd-gen-upgrade#special_case_api_keys
const stripeSecretKey = defineSecret("STRIPE_SECRET_KEY")
const stripeWebhookSecretKey = defineSecret("STRIPE_WEBHOOK_SECRET_KEY")

Em seguida, eles devem ser passados como argumentos ao declarar as funções onCall e onRequest:

// essa função precisa acessar somente a stripeSecretKey
exports.createOrderPaymentIntent = functions.https.onCall({ secrets: [stripeSecretKey] }, (context) => {
  // definido em outro lugar
  return createOrderPaymentIntent(context)
})

// essa função precisa acessar ambas as chaves
exports.stripeWebhook = functions.https.onRequest({ secrets: [stripeSecretKey, stripeWebhookSecretKey] }, (request, response) => {
  // definido em outro lugar
  return stripeWebhook(request, response)
})

Por fim, o valor de cada chave secreta pode ser obtido com o método .value():

// Expor um endpoint como um manipulador de webhook para eventos assíncronos.
// Configure seu webhook no painel de controle do desenvolvedor do Stripe:
// https://dashboard.stripe.com/test/webhooks
async function stripeWebhook(req: functions.https.Request, res: functions.Response<any>) {
  // obter o primeiro valor de chave secreta
  const secretKey = stripeSecretKey.value()
  // se estiver vazio, gera um erro
  if (secretKey.length === 0) {
    console.error("⚠️ Stripe Secret Key is not set")
    res.sendStatus(400)
  }
  // obter o segundo valor de chave secreta
  const webhookSecretKey = stripeWebhookSecretKey.value()
  // se estiver vazio, gera um erro
  if (webhookSecretKey.length === 0) {
    console.error("⚠️ Stripe webhook secret is not set")
    res.sendStatus(400)
  }

  const stripe = new Stripe(secretKey, {
    apiVersion: "2023-08-16",
    typescript: true,
  })
  // fazer a verificação da assinatura do webhook usando a webhookSecretKey
}

Observe como a chamada de secretKey.value() garante o retorno de uma string. Se a chave não for definida corretamente, seu comprimento será 0, e podemos retornar um erro.

Etapa 4: testar nossas funções de nuvem com o emulador local do Firebase

Antes de implementar as funções de nuvem, é sempre uma boa ideia testá-las localmente, e isso pode ser feito com o Firebase Local Emulator.

Mas, conforme explicado em Segredos e credenciais no emulador do Cloud Functions, precisamos executar uma etapa extra.

Isso significa adicionar as chaves em um arquivo .secret.local na pasta functions:

# functions/.secret.local
STRIPE_SECRET_KEY=sk_test_YOUR_STRIPE_SECRET_KEY
STRIPE_WEBHOOK_SECRET_KEY=whsec_YOUR_STRIPE_WEBHOOK_SECRET_KEY

E como essas chaves são muito sensíveis, também queremos adicionar esse arquivo ao .gitignore local:

# Secrets
.secret.local

É isso aí! Agora devemos poder acessar nossas chaves secretas dentro do Cloud Functions. E se tudo funcionar bem, podemos implementá-las. 🚀

 

E quanto aos acionadores do Cloud Firestore?

Como vimos, podemos passar segredos para chamadas https usando essa sintaxe:

import * as functionsV2 from "firebase-functions/v2"

import {defineSecret} from "firebase-functions/params"

const stripeSecretKey = defineSecret("STRIPE_SECRET_KEY")

exports.createOrderPaymentIntent = functionsV2.https.onCall(
  { secrets: [stripeSecretKey] },
  (context) => {
    const secretKey = stripeSecretKey.value()
    if (secretKey.length === 0) {
      throw new https.HttpsError("aborted", "Stripe Secret Key is not set")
    }
    // all good
  },
)

Mas e quanto aos acionadores do Cloud Firestore?

Como se vê, isso pode ser feito passando os argumentos documents e dos secrets como um mapa de pares de valores-chave:

import * as functionsV2 from "firebase-functions/v2"

import {defineSecret} from "firebase-functions/params"

const stripeSecretKey = defineSecret("STRIPE_SECRET_KEY")

exports.onPaymentWritten = functionsV2.firestore.onDocumentWritten(
  {
    document: "/stripe_customers/{stripeId}/payments/{paymentId}",
    secrets: [stripeSecretKey],
  },
  (event) => {
    const secretKey = stripeSecretKey.value()
    if (secretKey.length === 0) {
      throw new https.HttpsError("aborted", "Stripe Secret Key is not set")
    }
    // all good
  },
)

Isso não ficou claro, pois os acionadores do Firestore têm uma API de aparência assustadora em TypeScript:

export declare function onDocumentWritten<Document extends string>(
  opts: DocumentOptions<Document>,
  handler: (event: FirestoreEvent<Change<DocumentSnapshot> | undefined, ParamsOf<Document>>) => any | Promise<any>
): CloudFunction<FirestoreEvent<Change<DocumentSnapshot> | undefined, ParamsOf<Document>>>;

Portanto, se você não quiser se perder em uma grande toca de coelho, basta usar o exemplo acima ao implementar seus próprios acionadores. 👍

Resumo

Vimos agora como definir e acessar segredos com segurança ao usar o Cloud Functions de 2ª geração para o Firebase.

Veja a seguir um resumo das etapas:

  1. Habilite a API do Secret Manager dentro do Google Cloud para o seu projeto do Firebase
  2. Armazene suas chaves executando firebase functions:secrets:set
  3. Use a nova API de segredos com o 2nd-Gen Cloud Functions:
    • Defina os segredos com a API defineSecret
    • Informe às nossas funções onRequest e onCall a quais segredos elas têm acesso
    • Acesse-os com .value() dentro do corpo das funções
  4. Adicione um arquivo .secret.local dentro da pasta de funções, armazene as chaves dentro dele e adicione-o a .gitignore

Embora haja um pouco de trabalho envolvido, essa abordagem é muito mais segura do que a antiga API functions.config() e funciona com o Cloud Functions de 1ª e 2ª geração.

Demorei um pouco para descobrir todos esses detalhes e espero que este guia facilite sua vida. 🙂

 

Recursos adicionais

Aqui estão alguns recursos adicionais que achei úteis ao pesquisar este tópico:

Boa codificação!