Триггеры блокировки аутентификации (первое поколение)

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

Подготовка

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

Функции блокировки

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

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

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

  • beforeEmail (только для Node.js). Триггер срабатывает до того, как пользователю будет отправлено электронное письмо (например,
    письмо для входа в аккаунт или сброса пароля).

  • beforeSms (только для Node.js). Срабатывает перед отправкой пользователю SMS-сообщения, например для многофакторной аутентификации.

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

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

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

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

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

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

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

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

Блокирующая функция развертывается так же, как и любая другая. Подробную информацию можно найти на странице Cloud Functions Начало работы. В целом можно выделить следующее.

  1. Напишите функцию, которая обрабатывает целевое событие.

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

    const functions = require('firebase-functions/v1');
    
    exports.beforeCreate = functions.auth.user().beforeCreate((user, context) => {
      // TODO
    });
    
    The above example has omitted the implementation of custom auth logic. See
    the following sections to learn how to implement your blocking functions and
    [Common scenarios](#common-scenarios) for specific examples.
    
  1. Разверните функции с помощью интерфейса командной строки Firebase:

    firebase deploy --only functions
    

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

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

События 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, то ключ sessionClaims перезапишет ключ customClaims в утверждениях токена. Однако перезаписанный ключ customClaims будет сохранен в базе данных для будущих запросов.

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

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

Поставщик идентификации Токен идентификации Токен доступа Время окончания срока действия Секрет токена Токен обновления Заявки на вход
Google Да Да Да Нет Да Нет
Facebook Нет Да Да Нет Нет Нет
Twitter Нет Да Нет Да Нет Нет
GitHub Нет Да Нет Нет Нет Нет
Microsoft Да Да Да Нет Да Нет
LinkedIn Нет Да Да Нет Нет Нет
Yahoo Да Да Да Нет Да Нет
Apple Да Да Да Нет Да Нет
SAML Нет Нет Нет Нет Нет Да
OIDC Да Да Да Нет Да Да

Токены обновления

Чтобы использовать токен обновления в блокирующей функции, сначала установите флажок на странице Блокирующие функции консоли Firebase (перейдите на вкладку Настройки в разделе Безопасность > Аутентификация > Настройки).

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

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

Поставщики Generic OIDC

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

  • Токен идентификатора предоставляется, если выбран поток 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, передаются следующие учетные данные:

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

GitHub

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

  • Токен доступа не имеет срока действия, если не отозван.

Microsoft

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

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

Пример:

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

Yahoo

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

  • Токен идентификатора
  • Токен доступа
  • Токен обновления

LinkedIn

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

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

Apple

Когда пользователь входит в аккаунт с помощью 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, и использовать его для вызова Google Календаря API. Токен обновления хранится для офлайн-доступа.

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();
          });
      });
    })
  }
});

Переопределение вердикта reCAPTCHA Enterprise для операции пользователя

В примере ниже показано, как переопределить вердикт reCAPTCHA Enterprise для поддерживаемых сценариев взаимодействия с пользователем.

Подробнее о том, как интегрировать reCAPTCHA Enterprise с Firebase Authentication…

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

Node.js

const functions = require("firebase-functions/v1");
exports.beforesmsv1 = functions.auth.user().beforeSms((context) => {
 if (
   context.smsType === "SIGN_IN_OR_SIGN_UP" &&
   context.additionalUserInfo.phoneNumber.includes('+91')
 ) {
   return {
     recaptchaActionOverride: "ALLOW",
   };
 }

 // Allow users to sign in with recaptcha score greater than 0.5
 if (event.additionalUserInfo.recaptchaScore > 0.5) {
   return {
     recaptchaActionOverride: 'ALLOW',
   };
 }

 // Block all others.
 return  {
   recaptchaActionOverride: 'BLOCK',
 }
});