阻止函數讓您可以執行自定義代碼來修改用戶註冊或登錄到您的應用程序的結果。例如,您可以阻止不滿足特定條件的用戶進行身份驗證,或者在將用戶信息返回到您的客戶端應用程序之前更新用戶信息。
在你開始之前
要使用阻止功能,您必須將您的 Firebase 項目升級到使用 Identity Platform 進行 Firebase 身份驗證。如果您尚未升級,請先升級。
了解阻塞函數
您可以為兩個事件註冊阻塞函數:
beforeCreate
:在將新用戶保存到 Firebase 身份驗證數據庫之前以及將令牌返回到您的客戶端應用程序之前觸發。beforeSignIn
:在驗證用戶憑據之後但在 Firebase 身份驗證將 ID 令牌返回到您的客戶端應用程序之前觸發。如果您的應用程序使用多重身份驗證,則該函數會在用戶驗證其第二個因素後觸發。請注意,除了beforeCreate
之外,創建新用戶還會觸發beforeSignIn
。
使用阻塞函數時請記住以下幾點:
您的函數必須在 7 秒內響應。 7 秒後,Firebase 身份驗證返回錯誤,客戶端操作失敗。
200
以外的 HTTP 響應代碼將傳遞到您的客戶端應用程序。確保您的客戶端代碼處理您的函數可能返回的任何錯誤。函數適用於項目中的所有用戶,包括租戶中包含的任何用戶。 Firebase 身份驗證向您的函數提供有關用戶的信息,包括他們所屬的任何租戶,因此您可以做出相應的響應。
將另一個身份提供者鏈接到一個帳戶會重新觸發任何已註冊的
beforeSignIn
函數。匿名和自定義身份驗證不會觸發阻止功能。
部署並註冊一個阻塞函數
要將您的自定義代碼插入用戶身份驗證流程,請部署和註冊阻止功能。部署和註冊阻止功能後,您的自定義代碼必須成功完成才能成功進行身份驗證和用戶創建。
部署阻塞函數
部署阻塞函數的方式與部署任何函數的方式相同。 (有關詳細信息,請參閱 Cloud Functions入門頁面)。總之:
編寫處理
beforeCreate
事件、beforeSignIn
事件或兩者的 Cloud Functions。例如,要開始,您可以將以下無操作函數添加到
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
之後執行。在beforeCreate
中更新的用戶字段在beforeSignIn
中可見。如果您在兩個事件處理程序中設置的字段不是sessionClaims
, beforeSignIn
中設置的值會覆蓋beforeCreate
中設置的值。僅對於sessionClaims
,它們會傳播到當前會話的令牌聲明,但不會持久化或存儲在數據庫中。
例如,如果設置了任何sessionClaims
, beforeSignIn
將返回它們和任何beforeCreate
聲明,並且它們將被合併。當它們合併時,如果sessionClaims
鍵與customClaims
中的鍵匹配,則匹配的customClaims
將在令牌聲明中被sessionClaims
鍵覆蓋。但是,覆蓋的customClaims
密鑰仍將保留在數據庫中以供將來請求使用。
支持的 OAuth 憑據和數據
您可以將 OAuth 憑據和數據傳遞給來自各種身份提供者的阻止功能。下表顯示了每個身份提供商支持的憑證和數據:
身份提供者 | 身份證 | 訪問令牌 | 到期時間 | 代幣秘密 | 刷新令牌 | 登錄聲明 |
---|---|---|---|---|---|---|
谷歌 | 是的 | 是的 | 是的 | 不 | 是的 | 不 |
不 | 是的 | 是的 | 不 | 不 | 不 | |
推特 | 不 | 是的 | 不 | 是的 | 不 | 不 |
GitHub | 不 | 是的 | 不 | 不 | 不 | 不 |
微軟 | 是的 | 是的 | 是的 | 不 | 是的 | 不 |
領英 | 不 | 是的 | 是的 | 不 | 不 | 不 |
雅虎 | 是的 | 是的 | 是的 | 不 | 是的 | 不 |
蘋果 | 是的 | 是的 | 是的 | 不 | 是的 | 不 |
SAML | 不 | 不 | 不 | 不 | 不 | 是的 |
海外數據中心 | 是的 | 是的 | 是的 | 不 | 是的 | 是的 |
刷新令牌
要在阻止功能中使用刷新令牌,您必須首先選中 Firebase 控制台的阻止功能頁面上的複選框。
當直接使用 OAuth 憑據(例如 ID 令牌或訪問令牌)登錄時,任何身份提供者都不會返回刷新令牌。在這種情況下,相同的客戶端 OAuth 憑據將傳遞給阻止函數。
以下部分描述了每種身份提供者類型及其支持的憑證和數據。
通用 OIDC 提供商
當用戶使用通用 OIDC 提供商登錄時,將傳遞以下憑據:
- ID token :如果選擇了
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 Calendar 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();
});
});
})
}
});