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

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

Подготовка

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

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

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

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

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

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

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

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

  • Функция должна ответить в течение семи секунд. Через 7 секунд 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. Разверните функции с помощью CLI 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, то совпадающий ключ customClaims будет перезаписан в утверждениях токена ключом sessionClaims. Однако перезаписанный ключ customClaims будет сохранен в базе данных для будущих запросов.

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

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

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

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

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

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

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

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

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',
 }
});