Предоставьте доступ к вашим облачным функциям Firebase только для аутентифицированных пользователей.

Вчера я начал рефакторинг одной из основных функций DeckDeckGo, объявив новые Облачные функции Firebase, которые можно запускать с помощью HTTP-запросов.

Поскольку я стремился защитить его доступ, чтобы избежать скрытых запросов, я следил за одним из моих предыдущих сообщений в блоге, чтобы защитить его с помощью носителя.

После того, как я протестировал этот первый шаг функции, я действительно заметил, что это не подходящее решение для моего варианта использования. Мне скорее пришлось предоставить доступ с помощью токенов пользователей.

Проверка токенов пользователей в облачных функциях

Вероятно, это будет звучать глупо для тех, кто знает решение, но на самом деле мне потребовалось некоторое время, чтобы найти, как проверить токены пользователей в Firebase Cloud Functions.

Я ошибся в начале, пытаясь реализовать решение, поскольку я реализовывал его в бэкэнде, соответственно, как показано в Аутентификации с бэкэнд-сервером с использованием библиотеки google-auth-library. Я потратил время на реализацию решения и поиск, где я мог бы найти запрошенную информацию OAuth CLIENT_ID моих проектов, чтобы наконец столкнуться со следующей ошибкой, пока я пробовал процесс:

No pem found for envelope: {"alg":"RS256","kid":"...","typ":"JWT"}

Наконец, после многих попыток я принял поражение и поискал решения в Google. К счастью для меня, в конце вопроса о Stackoverflow я обнаружил, благодаря ответу Will, что есть способ более простой проверки токенов.

Действительно, если бы я знал документацию администратора, я бы обнаружил, что Firebase является встроенным методом для решения этого требования.

В Firebase Admin SDK есть встроенный метод проверки и декодирования токенов идентификаторов. Если предоставленный токен идентификатора имеет правильный формат, срок его действия не истек и он правильно подписан, метод возвращает декодированный токен идентификатора. Вы можете получить uid пользователя или устройства из декодированного токена.

Как только я обнаружил этот драгоценный камень и когда мой мозг наконец щелкнул, я смог реализовать небольшую служебную функцию:

import * as admin from 'firebase-admin';
import * as functions from 'firebase-functions';

export async function verifyToken(
                request: functions.Request): Promise<boolean> {
  try {
    const token: string | undefined = await getToken(request);

    if (!token) {
      return false;
    }

    const payload: admin.auth.DecodedIdToken = 
                   await admin.auth().verifyIdToken(token);
    return payload !== null;
  } catch (err) {
    return false;
  }
}
async function getToken(request: functions.Request): 
                       Promise<string | undefined> {
  if (!request.headers.authorization) {
    return undefined;
  }

  const token: string = 
        request.headers.authorization.replace(/^Bearer\s/, '');

  return token;
}

Обратите внимание, что я проверяю, является ли payload не null, чтобы считать токен действительным, но я думаю, что он может быть не нужен. Метод verifyIdToken выдает ошибку, если он недействителен.

Кроме того, вы также можете заметить, что я за исключением токена пользователя, который должен быть передан, как в headers HTTP-запроса и с префиксом ключевого слова Bearer.

Учитывая, например, идентификатор токена 975dd9f6, запрос HTTP POST будет выглядеть следующим образом:

#!/bin/sh
curl -i
     -H "Accept: application/json"
     -H "Authorization: Bearer 975dd9f6"
     -X POST https://us-central1-yolo.cloudfunctions.net/helloWorld

Предоставлять только не анонимным пользователям

Кто угодно может попробовать DeckDeckGo, здесь нет обязательного входа в систему или авторизации заранее, если вы просто хотите попробовать. Для нас это действительно важно, мы не гонимся за данными или количеством пользователей, мы разрабатываем редактор презентаций для пользователей, которые хотят им пользоваться или нет 😉.

При этом, если пользователи хотят публично публиковать свои презентации, потому что мы не хотим, чтобы публично публиковалось слишком много колод «Это тест» или «Йоло», соответственно избегая, если возможно, бессмысленного общедоступного контента, мы ограничиваем наши «Процесс публикации» (тот, в котором мы трансформируем и развертываем онлайн-презентации как прогрессивные веб-приложения) для подписанных пользователей.

Для этих процессов мы используем предоставленную Firebase возможность использовать анонимных пользователей.

Вот почему, помимо проверки токенов, я также добавляю эту информацию для проверки. К счастью, это также можно легко решить, поскольку payload, предоставляемый функцией verifyToken, действительно содержит такую ​​информацию.

const payload: admin.auth.DecodedIdToken = 
                   await admin.auth().verifyIdToken(token);
return payload !== null &&
       payload.firebase.sign_in_provider !== 'anonymous';

Вызов функции с носителем

Если вам будет интересно, вот как я передаю вышеуказанный bearer вызову функции в TypeScript и в приложении, которое использует Firebase Auth.

import * as firebase from 'firebase/app';
import 'firebase/auth';
helloWorld(): Promise<void> {
  return new Promise<void>(async (resolve, reject) => {
    try {
      const token: string = 
            await firebase.auth().currentUser.getIdToken();
      const functionsUrl: string = 
           'https://us-central1-yolo.cloudfunctions.net';
      const rawResponse: Response = 
            await fetch(`${functionsUrl}/helloWorld`, {
        method: 'POST',
        headers: {
          Accept: 'application/json',
          'Content-Type': 'application/json',
          Authorization: `Bearer ${token}`,
        },
        body: JSON.stringify({
          something: 'a value'
        }),
      });

      if (!rawResponse || !rawResponse.ok) {
        reject('Post failed etc.');
        return;
      }

      resolve();
    } catch (err) {
      reject(err);
    }
  });
}

Вишня сверху: CORS

Поскольку я реализовал нашу первую функцию для обработки HTTP-запроса, мне пришлось иметь дело с CORS. Быстрый поиск в Google и Gist, предоставленный CoderTonyB, предоставили решение.

В проект функций должен быть установлен expressjs / cors.

npm i cors --save && npm i @types/cors --save-dev

Наконец, перед эффективной реализацией следует использовать обработчик для обработки запроса CORS.

import * as functions from 'firebase-functions';
import * as cors from 'cors';
export const helloWorld = functions.https.onRequest(myHelloWorld);
async function helloWorld(request: functions.Request,
                          response: functions.Response<any>) {
  const corsHandler = cors({origin: true});

  corsHandler(request, response, async () => {
      response.send('Yolo');
  });
}

Забрать

Излишне говорить, что на самом деле легко начать разработку новой функции неправильно и быстро потерять время. Я хотел бы сказать, что сделать глубокий вдох или сделать перерыв - это ключ, но время от времени это случается, что-то случается 😉. Тем не менее, если у вас есть отличные советы и рекомендации, чтобы избежать такого сценария, дайте мне знать, мне любопытно услышать об этом!

Если вам интересен результат, подпишитесь на нас в Twitter, так как на следующей неделе мы можем выпустить супер классную функцию для разработчиков 🚀.

Бесконечность не предел!

Дэйвид