Функции блокировки позволяют выполнять собственный код, который изменяет результат регистрации или входа пользователя в ваше приложение. Например, вы можете запретить пользователю аутентификацию, если он не соответствует определенным критериям, или обновить информацию о пользователе, прежде чем вернуть ее в клиентское приложение.
Подготовка
Чтобы использовать функции блокировки, необходимо обновить проект 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 Начало работы. В целом можно выделить следующее.
Напишите функцию, которая обрабатывает целевое событие.
Например, чтобы начать, вы можете добавить в
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.
Разверните функции с помощью интерфейса командной строки 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 и возвращается клиенту.
Чтобы изменить пользователя, верните из обработчика событий объект, содержащий поля, которые нужно изменить. Вы можете изменить следующие поля:
displayNamedisabledemailVerifiedphotoUrlcustomClaimssessionClaims(только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 и данные в блокирующие функции от разных поставщиков идентификационной информации. В таблице ниже указано, какие учетные данные и данные поддерживаются для каждого поставщика идентификационной информации:
| Поставщик идентификации | Токен идентификации | Токен доступа | Время окончания срока действия | Секрет токена | Токен обновления | Заявки на вход |
|---|---|---|---|---|---|---|
| Да | Да | Да | Нет | Да | Нет | |
| Нет | Да | Да | Нет | Нет | Нет | |
| Нет | Да | Нет | Да | Нет | Нет | |
| GitHub | Нет | Да | Нет | Нет | Нет | Нет |
| Microsoft | Да | Да | Да | Нет | Да | Нет |
| Нет | Да | Да | Нет | Нет | Нет | |
| 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, передаются следующие учетные данные:
- Токен идентификатора
- Токен доступа
- Маркер обновления. Предоставляется, только если запрошены следующие специальные параметры:
access_type=offlineprompt=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, передаются следующие учетные данные:
- Токен доступа
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-адреса относятся к разным географическим регионам, вы можете попросить пользователя войти в аккаунт ещё раз.
Используйте утверждения сеанса, чтобы отслеживать IP-адрес, с которого пользователь выполняет вход:
Node.js
exports.beforeSignIn = functions.auth.user().beforeSignIn((user, context) => { return { sessionClaims: { signInIpAddress: context.ipAddress, }, }; });Когда пользователь пытается получить доступ к ресурсам, для которых требуется аутентификация с помощью 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',
}
});