Расширьте аутентификацию Firebase, заблокировав облачные функции


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

Прежде чем вы начнете

Чтобы использовать функции блокировки, вы должны обновить свой проект Firebase до Firebase Authentication with Identity Platform. Если вы еще не обновились, сделайте это в первую очередь.

Понимание функций блокировки

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

  • beforeCreate : срабатывает до того, как новый пользователь будет сохранен в базе данных Firebase Authentication и до того, как токен будет возвращен вашему клиентскому приложению.

  • beforeSignIn : срабатывает после проверки учетных данных пользователя, но до того, как проверка подлинности Firebase вернет токен идентификатора в ваше клиентское приложение. Если ваше приложение использует многофакторную аутентификацию, функция срабатывает после того, как пользователь подтвердит свой второй фактор. Обратите внимание, что создание нового пользователя также вызывает действие beforeSignIn в дополнение к beforeCreate .

При использовании блокирующих функций помните следующее:

  • Ваша функция должна ответить в течение 7 секунд. Через 7 секунд проверка подлинности Firebase возвращает ошибку, и операция клиента завершается сбоем.

  • Коды ответов HTTP, отличные от 200 , передаются вашим клиентским приложениям. Убедитесь, что ваш клиентский код обрабатывает любые ошибки, которые может возвращать ваша функция.

  • Функции применяются ко всем пользователям в вашем проекте, включая всех, содержащихся в арендаторе . Firebase Authentication предоставляет информацию о пользователях вашей функции, в том числе обо всех арендаторах, которым они принадлежат, чтобы вы могли реагировать соответствующим образом.

  • Привязка другого поставщика удостоверений к учетной записи повторно запускает все зарегистрированные функции beforeSignIn .

  • Анонимная и пользовательская аутентификация не запускают функции блокировки.

Разверните и зарегистрируйте функцию блокировки

Чтобы вставить собственный код в потоки аутентификации пользователей, разверните и зарегистрируйте блокирующие функции. После развертывания и регистрации функций блокировки ваш пользовательский код должен успешно завершиться для успешной аутентификации и создания пользователя.

Разверните функцию блокировки

Вы развертываете блокирующую функцию так же, как и любую другую функцию. (подробности см. на странице «Приступая к работе с облачными функциями»). В итоге:

  1. Напишите облачные функции, которые обрабатывают событие beforeCreate , событие beforeSignIn или и то, и другое.

    Например, для начала вы можете добавить в index.js следующие недействующие функции:

    const functions = require('firebase-functions');
    
    exports.beforeCreate = functions.auth.user().beforeCreate((user, context) => {
      // TODO
    });
    
    exports.beforeSignIn = functions.auth.user().beforeSignIn((user, context) => {
      // TODO
    });
    

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

  2. Разверните свои функции с помощью интерфейса командной строки Firebase:

    firebase deploy --only functions
    

    Вы должны повторно развертывать свои функции каждый раз, когда вы их обновляете.

Зарегистрируйте функцию блокировки

  1. Перейдите на страницу настроек аутентификации Firebase в консоли Firebase.

  2. Выберите вкладку Блокирующие функции .

  3. Зарегистрируйте функцию блокировки, выбрав ее в раскрывающемся меню перед созданием учетной записи (beforeCreate) или перед входом в систему (beforeSignIn) .

  4. Сохраните изменения.

Получение информации о пользователе и контексте

События beforeSignIn и beforeCreate предоставляют объекты User и EventContext , которые содержат информацию о входе пользователя. Используйте эти значения в своем коде, чтобы определить, следует ли разрешить выполнение операции.

Список свойств, доступных для объекта User , см. в справочнике по API UserRecord .

Объект EventContext содержит следующие свойства:

Имя Описание Пример
locale Локаль приложения. Вы можете установить локаль с помощью клиентского SDK или передав заголовок локали в REST API. fr или sv-SE
ipAddress IP-адрес устройства, с которого регистрируется или с которого входит конечный пользователь. 114.14.200.1
userAgent Пользовательский агент запускает функцию блокировки. Mozilla/5.0 (X11; Linux x86_64)
eventId Уникальный идентификатор события. rWsyPtolplG2TBFoOkkgyg
eventType Тип события. Это предоставляет информацию об имени события, таком как beforeSignIn или beforeCreate , и связанном используемом методе входа, таком как Google или электронная почта/пароль. providers/cloud.auth/eventTypes/user.beforeSignIn:password
authType Всегда USER . USER
resource Проект или клиент Firebase Authentication. projects/ project-id /tenants/ tenant-id
timestamp Время возникновения события в формате строки RFC 3339 . Tue, 23 Jul 2019 21:10:57 GMT
additionalUserInfo Объект, содержащий информацию о пользователе. AdditionalUserInfo
credential Объект, содержащий информацию об учетных данных пользователя. AuthCredential

Блокировка регистрации или входа

Чтобы заблокировать попытку регистрации или входа в систему, вызовите HttpsError в своей функции. Например:

Node.js

throw new functions.auth.HttpsError('permission-denied');

В следующей таблице перечислены ошибки, которые вы можете вызвать, а также их сообщения об ошибках по умолчанию:

Имя Код Сообщение
invalid-argument 400 Клиент указал недопустимый аргумент.
failed-precondition 400 Запрос не может быть выполнен в текущем состоянии системы.
out-of-range 400 Клиент указал недопустимый диапазон.
unauthenticated 401 Отсутствует, недействителен или просрочен токен OAuth.
permission-denied 403 У клиента недостаточно прав.
not-found 404 Указанный ресурс не найден.
aborted 409 Конфликт параллелизма, например конфликт чтения-модификации-записи.
already-exists 409 Ресурс, который пытался создать клиент, уже существует.
resource-exhausted 429 Либо вне квоты ресурсов, либо достигнуто ограничение скорости.
cancelled 499 Запрос отменен клиентом.
data-loss 500 Невосстановимая потеря данных или повреждение данных.
unknown 500 Неизвестная ошибка сервера.
internal 500 Внутренняя ошибка сервера.
not-implemented 501 Метод API не реализован сервером.
unavailable 503 Сервис недоступен.
deadline-exceeded 504 Превышен срок запроса.

Вы также можете указать собственное сообщение об ошибке:

Node.js

throw new functions.auth.HttpsError('permission-denied', 'Unauthorized request origin!');

В следующем примере показано, как заблокировать пользователей, не принадлежащих к определенному домену, от регистрации в вашем приложении.

Node.js

exports.beforeCreate = functions.auth.user().beforeCreate((user, context) => {
  // (If the user is authenticating within a tenant context, the tenant ID can be determined from
  // user.tenantId or from context.resource, e.g. 'projects/project-id/tenant/tenant-id-1')

  // Only users of a specific domain can sign up.
  if (user.email.indexOf('@acme.com') === -1) {
    throw new functions.auth.HttpsError('invalid-argument', `Unauthorized email "${user.email}"`);
  }
});

Независимо от того, используете ли вы сообщение по умолчанию или пользовательское сообщение, Cloud Functions упаковывает ошибку и возвращает ее клиенту как внутреннюю ошибку. Например:

throw new functions.auth.HttpsError('invalid-argument', `Unauthorized email user@evil.com}`);

Ваше приложение должно поймать ошибку и обработать ее соответствующим образом. Например:

JavaScript

// Blocking functions can also be triggered in a multi-tenant context before user creation.
// firebase.auth().tenantId = 'tenant-id-1';
firebase.auth().createUserWithEmailAndPassword('johndoe@example.com', 'password')
  .then((result) => {
    result.user.getIdTokenResult()
  })
  .then((idTokenResult) => {
    console.log(idTokenResult.claim.admin);
  })
  .catch((error) => {
    if (error.code !== 'auth/internal-error' && error.message.indexOf('Cloud Function') !== -1) {
      // Display error.
    } else {
      // Registration succeeds.
    }
  });

Изменение пользователя

Вместо того, чтобы блокировать регистрацию или попытку входа, вы можете разрешить продолжение операции, но изменить объект User , который будет сохранен в базе данных Firebase Authentication и возвращен клиенту.

Чтобы изменить пользователя, верните объект из обработчика событий, содержащий поля для изменения. Вы можете изменить следующие поля:

  • displayName
  • disabled
  • emailVerified
  • photoUrl
  • customClaims
  • sessionClaims (только beforeSignIn )

За исключением sessionClaims , все измененные поля сохраняются в базе данных Firebase Authentication, что означает, что они включаются в токен ответа и сохраняются между сеансами пользователя.

В следующем примере показано, как установить отображаемое имя по умолчанию:

Node.js

exports.beforeCreate = functions.auth.user().beforeCreate((user, context) => {
  return {
    // If no display name is provided, set it to "Guest".
    displayName: user.displayName || 'Guest';
  };
});

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

Например, если установлены какие-либо sessionClaims , beforeSignIn вернет их с любыми утверждениями beforeCreate , и они будут объединены. Когда они объединены, если ключ sessionClaims совпадает с ключом в customClaims , соответствующие customClaims будут перезаписаны в утверждениях токена ключом sessionClaims . Однако перезаписанный ключ customClaims по-прежнему будет храниться в базе данных для будущих запросов.

Поддерживаемые учетные данные и данные OAuth

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

Поставщик удостоверений Идентификационный токен Токен доступа Время истечения Секрет токена Обновить токен Заявки на вход
Google Да Да Да Нет Да Нет
Фейсбук Нет Да Да Нет Нет Нет
Твиттер Нет Да Нет Да Нет Нет
Гитхаб Нет Да Нет Нет Нет Нет
Майкрософт Да Да Да Нет Да Нет
LinkedIn Нет Да Да Нет Нет Нет
Yahoo Да Да Да Нет Да Нет
Яблоко Да Да Да Нет Да Нет
SAML Нет Нет Нет Нет Нет Да
ОИДК Да Да Да Нет Да Да

Обновить токены

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

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

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

Универсальные поставщики OIDC

Когда пользователь входит в систему с помощью универсального поставщика OIDC, будут переданы следующие учетные данные:

  • Токен ID : Предоставляется, если выбран поток id_token .
  • Маркер доступа : предоставляется, если выбран поток кода. Обратите внимание, что поток кода в настоящее время поддерживается только через REST API.
  • Токен обновления : предоставляется, если выбрана область offline_access .

Пример:

const provider = new firebase.auth.OAuthProvider('oidc.my-provider');
provider.addScope('offline_access');
firebase.auth().signInWithPopup(provider);

Google

Когда пользователь входит в систему с помощью Google, будут переданы следующие учетные данные:

  • Идентификационный токен
  • Токен доступа
  • Токен обновления : предоставляется только в том случае, если запрашиваются следующие пользовательские параметры:
    • access_type=offline
    • prompt=consent , если пользователь ранее дал согласие и не запрашивалась новая область

Пример:

const provider = new firebase.auth.GoogleAuthProvider();
provider.setCustomParameters({
  'access_type': 'offline',
  'prompt': 'consent'
});
firebase.auth().signInWithPopup(provider);

Узнайте больше о токенах обновления Google .

Фейсбук

Когда пользователь входит в систему через Facebook, ему передаются следующие учетные данные:

  • Токен доступа : возвращается токен доступа, который можно обменять на другой токен доступа. Узнайте больше о различных типах токенов доступа , поддерживаемых Facebook, и о том, как вы можете обменять их на долгоживущие токены .

Гитхаб

Когда пользователь входит в GitHub, ему передаются следующие учетные данные:

  • Маркер доступа : не истекает, если не отозван.

Майкрософт

Когда пользователь входит в систему Microsoft, будут переданы следующие учетные данные:

  • Идентификационный токен
  • Токен доступа
  • Токен обновления : передается функции блокировки, если выбрана область offline_access .

Пример:

const provider = new firebase.auth.OAuthProvider('microsoft.com');
provider.addScope('offline_access');
firebase.auth().signInWithPopup(provider);

Yahoo

Когда пользователь входит в систему Yahoo, следующие учетные данные будут переданы без каких-либо настраиваемых параметров или областей действия:

  • Идентификационный токен
  • Токен доступа
  • Обновить токен

LinkedIn

Когда пользователь входит в LinkedIn, ему передаются следующие учетные данные:

  • Токен доступа

Яблоко

Когда пользователь входит в систему Apple, следующие учетные данные будут переданы без каких-либо настраиваемых параметров или областей:

  • Идентификационный токен
  • Токен доступа
  • Обновить токен

Общие сценарии

Следующие примеры демонстрируют некоторые распространенные варианты использования блокирующих функций:

Разрешить регистрацию только с определенного домена

В следующем примере показано, как запретить пользователям, которые не являются частью домена example.com , регистрироваться в вашем приложении:

Node.js

exports.beforeCreate = functions.auth.user().beforeCreate((user, context) => {
  if (!user.email || user.email.indexOf('@example.com') === -1) {
    throw new functions.auth.HttpsError(
      'invalid-argument', `Unauthorized email "${user.email}"`);
  }
});

Блокировка регистрации пользователей с непроверенными адресами электронной почты

В следующем примере показано, как запретить пользователям с непроверенными адресами электронной почты регистрироваться в вашем приложении:

Node.js

exports.beforeCreate = functions.auth.user().beforeCreate((user, context) => {
  if (user.email && !user.emailVerified) {
    throw new functions.auth.HttpsError(
      'invalid-argument', `Unverified email "${user.email}"`);
  }
});

Требование подтверждения электронной почты при регистрации

В следующем примере показано, как потребовать от пользователя подтвердить свою электронную почту после регистрации:

Node.js

exports.beforeCreate = functions.auth.user().beforeCreate((user, context) => {
  const locale = context.locale;
  if (user.email && !user.emailVerified) {
    // Send custom email verification on sign-up.
    return admin.auth().generateEmailVerificationLink(user.email).then((link) => {
      return sendCustomVerificationEmail(user.email, link, locale);
    });
  }
});

exports.beforeSignIn = functions.auth.user().beforeSignIn((user, context) => {
 if (user.email && !user.emailVerified) {
   throw new functions.auth.HttpsError(
     'invalid-argument', `"${user.email}" needs to be verified before access is granted.`);
  }
});

Обработка определенных электронных писем поставщика удостоверений как проверенных

В следующем примере показано, как обрабатывать электронные письма пользователей от определенных поставщиков удостоверений как проверенные:

Node.js

exports.beforeCreate = functions.auth.user().beforeCreate((user, context) => {
  if (user.email && !user.emailVerified && context.eventType.indexOf(':facebook.com') !== -1) {
    return {
      emailVerified: true,
    };
  }
});

Блокировка входа с определенных IP-адресов

В следующем примере блокируется вход с определенных диапазонов IP-адресов:

Node.js

exports.beforeSignIn = functions.auth.user().beforeSignIn((user, context) => {
  if (isSuspiciousIpAddress(context.ipAddress)) {
    throw new functions.auth.HttpsError(
      'permission-denied', 'Unauthorized access!');
  }
});

Настройка пользовательских и сеансовых утверждений

В следующем примере показано, как задать настраиваемые утверждения и утверждения сеанса.

Node.js

exports.beforeCreate = functions.auth.user().beforeCreate((user, context) => {
  if (context.credential &&
      context.credential.providerId === 'saml.my-provider-id') {
    return {
      // Employee ID does not change so save in persistent claims (stored in
      // Auth DB).
      customClaims: {
        eid: context.credential.claims.employeeid,
      },
      // Copy role and groups to token claims. These will not be persisted.
      sessionClaims: {
        role: context.credential.claims.role,
        groups: context.credential.claims.groups,
      }
    }
  }
});

Отслеживание IP-адресов для отслеживания подозрительной активности

Вы можете предотвратить кражу токенов, отслеживая IP-адрес, с которого входит пользователь, и сравнивая его с IP-адресом при последующих запросах. Если запрос кажется подозрительным — например, IP-адреса из разных географических регионов — вы можете попросить пользователя снова войти в систему.

  1. Используйте утверждения сеанса для отслеживания IP-адреса, с которым пользователь входит в систему:

    Node.js

    exports.beforeSignIn = functions.auth.user().beforeSignIn((user, context) => {
      return {
        sessionClaims: {
          signInIpAddress: context.ipAddress,
        },
      };
    });
    
  2. Когда пользователь пытается получить доступ к ресурсам, требующим аутентификации с помощью Firebase Authentication, сравните IP-адрес в запросе с IP-адресом, используемым для входа:

    Node.js

    app.post('/getRestrictedData', (req, res) => {
      // Get the ID token passed.
      const idToken = req.body.idToken;
      // Verify the ID token, check if revoked and decode its payload.
      admin.auth().verifyIdToken(idToken, true).then((claims) => {
        // Get request IP address
        const requestIpAddress = req.connection.remoteAddress;
        // Get sign-in IP address.
        const signInIpAddress = claims.signInIpAddress;
        // Check if the request IP address origin is suspicious relative to
        // the session IP addresses. The current request timestamp and the
        // auth_time of the ID token can provide additional signals of abuse,
        // especially if the IP address suddenly changed. If there was a sudden
        // geographical change in a short period of time, then it will give
        // stronger signals of possible abuse.
        if (!isSuspiciousIpAddressChange(signInIpAddress, requestIpAddress)) {
          // Suspicious IP address change. Require re-authentication.
          // You can also revoke all user sessions by calling:
          // admin.auth().revokeRefreshTokens(claims.sub).
          res.status(401).send({error: 'Unauthorized access. Please login again!'});
        } else {
          // Access is valid. Try to return data.
          getData(claims).then(data => {
            res.end(JSON.stringify(data);
          }, error => {
            res.status(500).send({ error: 'Server error!' })
          });
        }
      });
    });
    

Просмотр фотографий пользователей

В следующем примере показано, как очистить фотографии профилей пользователей:

Node.js

exports.beforeCreate = functions.auth.user().beforeCreate((user, context) => {
  if (user.photoURL) {
    return isPhotoAppropriate(user.photoURL)
      .then((status) => {
        if (!status) {
          // Sanitize inappropriate photos by replacing them with guest photos.
          // Users could also be blocked from sign-up, disabled, etc.
          return {
            photoUrl: PLACEHOLDER_GUEST_PHOTO_URL,
          };
        }
      });
});

Чтобы узнать больше о том, как обнаруживать и очищать изображения, см. документацию Cloud Vision .

Доступ к учетным данным OAuth поставщика удостоверений пользователя

В следующем примере показано, как получить маркер обновления для пользователя, выполнившего вход в Google, и использовать его для вызова API Календаря Google. Маркер обновления хранится для автономного доступа.

Node.js

const {OAuth2Client} = require('google-auth-library');
const {google} = require('googleapis');
// ...
// Initialize Google OAuth client.
const keys = require('./oauth2.keys.json');
const oAuth2Client = new OAuth2Client(
  keys.web.client_id,
  keys.web.client_secret
);

exports.beforeCreate = functions.auth.user().beforeCreate((user, context) => {
  if (context.credential &&
      context.credential.providerId === 'google.com') {
    // Store the refresh token for later offline use.
    // These will only be returned if refresh tokens credentials are included
    // (enabled by Cloud console).
    return saveUserRefreshToken(
        user.uid,
        context.credential.refreshToken,
        'google.com'
      )
      .then(() => {
        // Blocking the function is not required. The function can resolve while
        // this operation continues to run in the background.
        return new Promise((resolve, reject) => {
          // For this operation to succeed, the appropriate OAuth scope should be requested
          // on sign in with Google, client-side. In this case:
          // https://www.googleapis.com/auth/calendar
          // You can check granted_scopes from within:
          // context.additionalUserInfo.profile.granted_scopes (space joined list of scopes).

          // Set access token/refresh token.
          oAuth2Client.setCredentials({
            access_token: context.credential.accessToken,
            refresh_token: context.credential.refreshToken,
          });
          const calendar = google.calendar('v3');
          // Setup Onboarding event on user's calendar.
          const event = {/** ... */};
          calendar.events.insert({
            auth: oauth2client,
            calendarId: 'primary',
            resource: event,
          }, (err, event) => {
            // Do not fail. This is a best effort approach.
            resolve();
          });
      });
    })
  }
});