如果您已使用 Identity Platform 升级到 Firebase 身份验证,则可以使用阻止Cloud Functions扩展 Firebase 身份验证。
阻塞函数让您可以执行自定义代码来修改用户注册或登录到您的应用程序的结果。例如,如果用户不符合特定条件,您可以阻止他们进行身份验证,或者在将用户信息返回到您的客户端应用程序之前更新用户信息。
在你开始之前
要使用阻止功能,您必须将您的 Firebase 项目升级到使用 Identity Platform 的 Firebase 身份验证。如果您尚未升级,请先升级。
了解阻塞函数
您可以为两个事件注册阻塞函数:
beforeCreate
:在将新用户保存到 Firebase 身份验证数据库之前以及将令牌返回到您的客户端应用之前触发。beforeSignIn
:在验证用户凭据之后但在 Firebase 身份验证向您的客户端应用返回 ID 令牌之前触发。如果您的应用程序使用多因素身份验证,则该功能会在用户验证其第二个因素后触发。请注意,除了beforeCreate
beforeSignIn
使用阻塞函数时请记住以下几点:
您的函数必须在 7 秒内响应。 7 秒后,Firebase 身份验证返回错误,客户端操作失败。
200
以外的 HTTP 响应代码将传递到您的客户端应用程序。确保您的客户端代码处理您的函数可能返回的任何错误。函数适用于项目中的所有用户,包括租户中包含的任何用户。 Firebase 身份验证为您的函数提供有关用户的信息,包括他们所属的任何租户,因此您可以做出相应的响应。
将另一个身份提供者链接到帐户会重新触发任何已注册的
beforeSignIn
功能。匿名和自定义身份验证不会触发阻止功能。
部署和注册一个阻塞函数
要将您的自定义代码插入用户身份验证流程,请部署和注册阻止功能。部署和注册阻止功能后,您的自定义代码必须成功完成,身份验证和用户创建才能成功。
部署阻塞功能
部署阻塞函数的方式与部署任何函数的方式相同。 (有关详细信息,请参阅 Cloud Functions入门页面)。总之:
编写处理
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 });
上面的例子省略了自定义认证逻辑的实现。请参阅以下部分以了解如何实现您的阻塞功能和特定示例的常见场景。
使用 Firebase CLI 部署您的函数:
firebase deploy --only functions
每次更新函数时都必须重新部署它们。
注册一个阻塞函数
转到 Firebase 控制台中的Firebase 身份验证设置页面。
选择阻塞功能选项卡。
通过从创建帐户之前 (beforeCreate)或登录之前(beforeSignIn) 下的下拉菜单中选择它来注册您的阻止功能。
保存您的更改。
获取用户和上下文信息
beforeSignIn
和beforeCreate
事件提供包含有关用户登录信息的User
和EventContext
对象。在代码中使用这些值来确定是否允许操作继续进行。
有关User
对象上可用属性的列表,请参阅UserRecord
API 参考。
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 身份验证项目或租户。 | projects/ project-id /tenants/ tenant-id |
timestamp | 触发事件的时间,格式为RFC 3339字符串。 | Tue, 23 Jul 2019 21:10:57 GMT |
additionalUserInfo | 包含用户信息的对象。 | AdditionalUserInfo |
credential | 包含有关用户凭据的信息的对象。 | AuthCredential |
阻止注册或登录
要阻止注册或登录尝试,请在您的函数中抛出HttpsError
。例如:
节点.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 | 超过请求期限。 |
您还可以指定自定义错误消息:
节点.js
throw new functions.auth.HttpsError('permission-denied', 'Unauthorized request origin!');
以下示例显示了如何阻止不在特定域内的用户注册您的应用程序:
节点.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.
}
});
修改用户
您可以允许操作继续进行,而不是阻止注册或登录尝试,但修改保存到 Firebase 身份验证数据库并返回给客户端的User
对象。
要修改用户,请从包含要修改的字段的事件处理程序中返回一个对象。您可以修改以下字段:
-
displayName
-
disabled
-
emailVerified
-
photoURL
-
customClaims
-
sessionClaims
(beforeSignIn
)
除了sessionClaims
之外,所有修改的字段都保存到 Firebase 身份验证的数据库中,这意味着它们包含在响应令牌中并在用户会话之间持续存在。
以下示例显示如何设置默认显示名称:
节点.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
之后执行。在beforeSignIn
中更新的用户字段在beforeCreate
中可见。如果您在两个事件处理程序中设置了sessionClaims
以外的字段,则beforeCreate
中设置的值将覆盖beforeSignIn
中设置的值。仅对于sessionClaims
,它们会传播到当前会话的令牌声明,但不会持久化或存储在数据库中。
例如,如果设置了任何sessionClaims
, beforeSignIn
将返回它们以及任何beforeCreate
声明,并且它们将被合并。当它们合并时,如果sessionClaims
键与customClaims
中的键匹配,则匹配的customClaims
将在令牌声明中被sessionClaims
键覆盖。但是,被覆盖的customClaims
键仍将保留在数据库中以供将来请求。
支持的 OAuth 凭据和数据
您可以将 OAuth 凭据和数据传递给来自各种身份提供者的阻止功能。下表显示了每个身份提供者支持的凭证和数据:
身份提供者 | 身份令牌 | 访问令牌 | 到期时间 | 令牌秘密 | 刷新令牌 | 登录声明 |
---|---|---|---|---|---|---|
谷歌 | 是的 | 是的 | 是的 | 不 | 是的 | 不 |
不 | 是的 | 是的 | 不 | 不 | 不 | |
推特 | 不 | 是的 | 不 | 是的 | 不 | 不 |
GitHub | 不 | 是的 | 不 | 不 | 不 | 不 |
微软 | 是的 | 是的 | 是的 | 不 | 是的 | 不 |
领英 | 不 | 是的 | 是的 | 不 | 不 | 不 |
雅虎 | 是的 | 是的 | 是的 | 不 | 是的 | 不 |
苹果 | 是的 | 是的 | 是的 | 不 | 是的 | 不 |
SAML | 不 | 不 | 不 | 不 | 不 | 是的 |
国际数据中心 | 是的 | 是的 | 是的 | 不 | 是的 | 是的 |
刷新令牌
要在阻止函数中使用刷新令牌,您必须首先选中 Firebase 控制台的阻止函数页面上的复选框。
使用 OAuth 凭据(例如 ID 令牌或访问令牌)直接登录时,任何身份提供者都不会返回刷新令牌。在这种情况下,相同的客户端 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 登录时,将传递以下凭据:
- 身份令牌
- 访问令牌
- 刷新令牌:仅在请求以下自定义参数时提供:
-
access_type=offline
-
prompt=consent
,如果用户之前同意并且没有请求新的范围
-
例子:
const provider = new firebase.auth.GoogleAuthProvider();
provider.setCustomParameters({
'access_type': 'offline',
'prompt': 'consent'
});
firebase.auth().signInWithPopup(provider);
了解有关Google 刷新令牌的更多信息。
当用户使用 Facebook 登录时,将传递以下凭据:
GitHub
当用户使用 GitHub 登录时,将传递以下凭据:
- 访问令牌:除非被撤销,否则不会过期。
微软
当用户使用 Microsoft 登录时,将传递以下凭据:
- 身份令牌
- 访问令牌
- 刷新令牌:如果选择了
offline_access
范围,则传递给阻塞函数。
例子:
const provider = new firebase.auth.OAuthProvider('microsoft.com');
provider.addScope('offline_access');
firebase.auth().signInWithPopup(provider);
雅虎
当用户使用 Yahoo 登录时,将传递以下凭据,而无需任何自定义参数或范围:
- 身份令牌
- 访问令牌
- 刷新令牌
领英
当用户使用 LinkedIn 登录时,将传递以下凭据:
- 访问令牌
苹果
当用户使用 Apple 登录时,将传递以下凭据,而无需任何自定义参数或范围:
- 身份令牌
- 访问令牌
- 刷新令牌
常见场景
以下示例演示了阻塞函数的一些常见用例:
只允许从特定域注册
以下示例显示了如何防止不属于example.com
域的用户注册您的应用程序:
节点.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}"`);
}
});
阻止未验证电子邮件的用户注册
以下示例展示了如何防止未验证电子邮件的用户在您的应用中注册:
节点.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}"`);
}
});
注册时要求电子邮件验证
以下示例显示了如何要求用户在注册后验证其电子邮件:
节点.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.`);
}
});
将某些身份提供者电子邮件视为已验证
以下示例显示了如何将来自某些身份提供商的用户电子邮件视为已验证:
节点.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 地址范围的登录:
节点.js
exports.beforeSignIn = functions.auth.user().beforeSignIn((user, context) => {
if (isSuspiciousIpAddress(context.ipAddress)) {
throw new functions.auth.HttpsError(
'permission-denied', 'Unauthorized access!');
}
});
设置自定义和会话声明
以下示例显示了如何设置自定义和会话声明:
节点.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 地址:
节点.js
exports.beforeSignIn = functions.auth.user().beforeSignIn((user, context) => { return { sessionClaims: { signInIpAddress: context.ipAddress, }, }; });
当用户尝试访问需要使用 Firebase 身份验证进行身份验证的资源时,请将请求中的 IP 地址与用于登录的 IP 进行比较:
节点.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!' }) }); } }); });
筛选用户照片
以下示例显示了如何清理用户的个人资料照片:
节点.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。存储刷新令牌以供离线访问。
节点.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();
});
});
})
}
});
如果您已使用 Identity Platform 升级到 Firebase 身份验证,则可以使用阻止Cloud Functions扩展 Firebase 身份验证。
阻塞函数让您可以执行自定义代码来修改用户注册或登录到您的应用程序的结果。例如,如果用户不符合特定条件,您可以阻止他们进行身份验证,或者在将用户信息返回到您的客户端应用程序之前更新用户信息。
在你开始之前
要使用阻止功能,您必须将您的 Firebase 项目升级到使用 Identity Platform 的 Firebase 身份验证。如果您尚未升级,请先升级。
了解阻塞函数
您可以为两个事件注册阻塞函数:
beforeCreate
:在将新用户保存到 Firebase 身份验证数据库之前以及将令牌返回到您的客户端应用之前触发。beforeSignIn
:在验证用户凭据之后但在 Firebase 身份验证向您的客户端应用返回 ID 令牌之前触发。如果您的应用程序使用多因素身份验证,则该功能会在用户验证其第二个因素后触发。请注意,除了beforeCreate
beforeSignIn
使用阻塞函数时请记住以下几点:
您的函数必须在 7 秒内响应。 7 秒后,Firebase 身份验证返回错误,客户端操作失败。
200
以外的 HTTP 响应代码将传递到您的客户端应用程序。确保您的客户端代码处理您的函数可能返回的任何错误。函数适用于项目中的所有用户,包括租户中包含的任何用户。 Firebase 身份验证为您的函数提供有关用户的信息,包括他们所属的任何租户,因此您可以做出相应的响应。
将另一个身份提供者链接到帐户会重新触发任何已注册的
beforeSignIn
功能。匿名和自定义身份验证不会触发阻止功能。
部署和注册一个阻塞函数
要将您的自定义代码插入用户身份验证流程,请部署和注册阻止功能。部署和注册阻止功能后,您的自定义代码必须成功完成,身份验证和用户创建才能成功。
部署阻塞功能
部署阻塞函数的方式与部署任何函数的方式相同。 (有关详细信息,请参阅 Cloud Functions入门页面)。总之:
编写处理
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 });
上面的例子省略了自定义认证逻辑的实现。请参阅以下部分以了解如何实现您的阻塞功能和特定示例的常见场景。
使用 Firebase CLI 部署您的函数:
firebase deploy --only functions
每次更新函数时都必须重新部署它们。
注册一个阻塞函数
转到 Firebase 控制台中的Firebase 身份验证设置页面。
选择阻塞功能选项卡。
通过从创建帐户之前 (beforeCreate)或登录之前(beforeSignIn) 下的下拉菜单中选择它来注册您的阻止功能。
保存您的更改。
获取用户和上下文信息
beforeSignIn
和beforeCreate
事件提供包含有关用户登录信息的User
和EventContext
对象。在代码中使用这些值来确定是否允许操作继续进行。
有关User
对象上可用属性的列表,请参阅UserRecord
API 参考。
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 身份验证项目或租户。 | projects/ project-id /tenants/ tenant-id |
timestamp | 触发事件的时间,格式为RFC 3339字符串。 | Tue, 23 Jul 2019 21:10:57 GMT |
additionalUserInfo | 包含用户信息的对象。 | AdditionalUserInfo |
credential | 包含有关用户凭据的信息的对象。 | AuthCredential |
阻止注册或登录
要阻止注册或登录尝试,请在您的函数中抛出HttpsError
。例如:
节点.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 | 超过请求期限。 |
您还可以指定自定义错误消息:
节点.js
throw new functions.auth.HttpsError('permission-denied', 'Unauthorized request origin!');
以下示例显示了如何阻止不在特定域内的用户注册您的应用程序:
节点.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.
}
});
修改用户
您可以允许操作继续进行,而不是阻止注册或登录尝试,但修改保存到 Firebase 身份验证数据库并返回给客户端的User
对象。
要修改用户,请从包含要修改的字段的事件处理程序中返回一个对象。您可以修改以下字段:
-
displayName
-
disabled
-
emailVerified
-
photoURL
-
customClaims
-
sessionClaims
(beforeSignIn
)
除了sessionClaims
之外,所有修改的字段都保存到 Firebase 身份验证的数据库中,这意味着它们包含在响应令牌中并在用户会话之间持续存在。
以下示例显示如何设置默认显示名称:
节点.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
之后执行。在beforeSignIn
中更新的用户字段在beforeCreate
中可见。如果您在两个事件处理程序中设置了sessionClaims
以外的字段,则beforeCreate
中设置的值将覆盖beforeSignIn
中设置的值。仅对于sessionClaims
,它们会传播到当前会话的令牌声明,但不会持久化或存储在数据库中。
例如,如果设置了任何sessionClaims
, beforeSignIn
将返回它们以及任何beforeCreate
声明,并且它们将被合并。当它们合并时,如果sessionClaims
键与customClaims
中的键匹配,则匹配的customClaims
将在令牌声明中被sessionClaims
键覆盖。但是,被覆盖的customClaims
键仍将保留在数据库中以供将来请求。
支持的 OAuth 凭据和数据
您可以将 OAuth 凭据和数据传递给来自各种身份提供者的阻止功能。下表显示了每个身份提供者支持的凭证和数据:
身份提供者 | 身份令牌 | 访问令牌 | 到期时间 | 令牌秘密 | 刷新令牌 | 登录声明 |
---|---|---|---|---|---|---|
谷歌 | 是的 | 是的 | 是的 | 不 | 是的 | 不 |
不 | 是的 | 是的 | 不 | 不 | 不 | |
推特 | 不 | 是的 | 不 | 是的 | 不 | 不 |
GitHub | 不 | 是的 | 不 | 不 | 不 | 不 |
微软 | 是的 | 是的 | 是的 | 不 | 是的 | 不 |
领英 | 不 | 是的 | 是的 | 不 | 不 | 不 |
雅虎 | 是的 | 是的 | 是的 | 不 | 是的 | 不 |
苹果 | 是的 | 是的 | 是的 | 不 | 是的 | 不 |
SAML | 不 | 不 | 不 | 不 | 不 | 是的 |
国际数据中心 | 是的 | 是的 | 是的 | 不 | 是的 | 是的 |
刷新令牌
要在阻止函数中使用刷新令牌,您必须首先选中 Firebase 控制台的阻止函数页面上的复选框。
使用 OAuth 凭据(例如 ID 令牌或访问令牌)直接登录时,任何身份提供者都不会返回刷新令牌。在这种情况下,相同的客户端 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 登录时,将传递以下凭据:
- 身份令牌
- 访问令牌
- 刷新令牌:仅在请求以下自定义参数时提供:
-
access_type=offline
-
prompt=consent
,如果用户之前同意并且没有请求新的范围
-
例子:
const provider = new firebase.auth.GoogleAuthProvider();
provider.setCustomParameters({
'access_type': 'offline',
'prompt': 'consent'
});
firebase.auth().signInWithPopup(provider);
了解有关Google 刷新令牌的更多信息。
当用户使用 Facebook 登录时,将传递以下凭据:
GitHub
当用户使用 GitHub 登录时,将传递以下凭据:
- 访问令牌:除非被撤销,否则不会过期。
微软
当用户使用 Microsoft 登录时,将传递以下凭据:
- 身份令牌
- 访问令牌
- 刷新令牌:如果选择了
offline_access
范围,则传递给阻塞函数。
例子:
const provider = new firebase.auth.OAuthProvider('microsoft.com');
provider.addScope('offline_access');
firebase.auth().signInWithPopup(provider);
雅虎
当用户使用 Yahoo 登录时,将传递以下凭据,而无需任何自定义参数或范围:
- 身份令牌
- 访问令牌
- 刷新令牌
领英
当用户使用 LinkedIn 登录时,将传递以下凭据:
- 访问令牌
苹果
当用户使用 Apple 登录时,将传递以下凭据,而无需任何自定义参数或范围:
- 身份令牌
- 访问令牌
- 刷新令牌
常见场景
以下示例演示了阻塞函数的一些常见用例:
只允许从特定域注册
以下示例显示了如何防止不属于example.com
域的用户注册您的应用程序:
节点.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}"`);
}
});
阻止未验证电子邮件的用户注册
以下示例展示了如何防止未验证电子邮件的用户在您的应用中注册:
节点.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}"`);
}
});
注册时要求电子邮件验证
以下示例显示了如何要求用户在注册后验证其电子邮件:
节点.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.`);
}
});
将某些身份提供者电子邮件视为已验证
以下示例显示了如何将来自某些身份提供商的用户电子邮件视为已验证:
节点.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 地址范围的登录:
节点.js
exports.beforeSignIn = functions.auth.user().beforeSignIn((user, context) => {
if (isSuspiciousIpAddress(context.ipAddress)) {
throw new functions.auth.HttpsError(
'permission-denied', 'Unauthorized access!');
}
});
设置自定义和会话声明
以下示例显示了如何设置自定义和会话声明:
节点.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 地址:
节点.js
exports.beforeSignIn = functions.auth.user().beforeSignIn((user, context) => { return { sessionClaims: { signInIpAddress: context.ipAddress, }, }; });
当用户尝试访问需要使用 Firebase 身份验证进行身份验证的资源时,请将请求中的 IP 地址与用于登录的 IP 进行比较:
节点.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!' }) }); } }); });
筛选用户照片
以下示例显示了如何清理用户的个人资料照片:
节点.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。存储刷新令牌以供离线访问。
节点.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();
});
});
})
}
});