封鎖函式可讓您執行自訂程式碼,藉此修改使用者註冊或登入應用程式的結果。舉例來說,您可以禁止使用者驗證不符合特定條件,或是更新使用者資訊後再傳回用戶端應用程式。
事前準備
如要使用封鎖函式,你必須透過 Identity Platform 將 Firebase 專案升級為 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
每次更新函式時,您都必須重新部署函式。
取得使用者和背景資訊
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
| 事件的專屬 ID。 | 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
。例如:
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.
}
});
修改使用者
您可以允許作業繼續,而不封鎖註冊或登入嘗試,而是修改儲存至 Firebase 驗證資料庫並傳回用戶端的 User
物件。
如要修改使用者,請從包含所要修改欄位的事件處理常式傳回物件。您可以修改下列欄位:
displayName
disabled
emailVerified
photoUrl
customClaims
sessionClaims
(僅限beforeSignIn
)
除了 sessionClaims
以外,所有修改過的欄位都會儲存至 Firebase 驗證的資料庫,這代表這些欄位會包含在回應權杖中,且會在使用者工作階段之間保留。
以下範例說明如何設定預設顯示名稱:
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 憑證和資料來封鎖來自各種識別資訊提供者的函式。下表顯示每個識別資訊提供者支援的憑證和資料:
識別資訊提供者 | ID 權杖 | 存取權杖 | 到期時間 | 權杖密鑰 | 更新權杖 | 登入要求 |
---|---|---|---|---|---|---|
是 | 是 | 是 | 否 | 是 | 否 | |
否 | 是 | 是 | 否 | 否 | 否 | |
否 | 是 | 否 | 是 | 否 | 否 | |
GitHub | 否 | 是 | 否 | 否 | 否 | 否 |
Microsoft | 是 | 是 | 是 | 否 | 是 | 否 |
否 | 是 | 是 | 否 | 否 | 否 | |
Yahoo | 是 | 是 | 是 | 否 | 是 | 否 |
Apple | 是 | 是 | 是 | 否 | 是 | 否 |
SAML | 否 | 否 | 否 | 否 | 否 | 是 |
OIDC | 是 | 是 | 是 | 否 | 是 | 是 |
重新整理權杖
如要在封鎖函式中使用更新權杖,您必須先在 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 帳戶登入時,系統會傳送下列憑證:
- ID 權杖
- 存取權杖
- 更新權杖:只有在要求下列自訂參數時才會提供:
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
當使用者透過 Microsoft 登入時,將傳送下列憑證:
- ID 權杖
- 存取權杖
- 重新整理權杖:如果選取
offline_access
範圍,會傳送至封鎖函式。
示例:
const provider = new firebase.auth.OAuthProvider('microsoft.com');
provider.addScope('offline_access');
firebase.auth().signInWithPopup(provider);
Yahoo
使用者透過 Yahoo 登入時,系統會傳遞以下憑證,而不含任何自訂參數或範圍:
- ID 權杖
- 存取權杖
- 重新整理權杖
當使用者透過 LinkedIn 登入時,系統將傳送下列憑證:
- 存取權杖
Apple
當使用者透過 Apple 登入時,系統會傳遞以下憑證,但不會套用任何自訂參數或範圍:
- ID 權杖
- 存取權杖
- 重新整理權杖
常見情境
以下範例說明封鎖函式的部分常見用途:
只允許特定網域註冊
以下範例說明如何防止非 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 驗證存取需要驗證的資源時,請將要求中的 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 Calendar 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 驗證,請參閱「啟用 reCAPTCHA Enterprise」一文。
透過封鎖函式,您可以根據自訂因素允許或封鎖流程,進而覆寫 reCAPTCHA Enterprise 提供的結果。
Node.js
const {
auth,
} = require("firebase-functions/v1");
exports.checkrecaptchaV1 = auth.user().beforeSignIn((userRecord, context) => {
// Allow users with a specific email domain to sign in regardless of their recaptcha score.
if (userRecord.email && userRecord.email.indexOf('@acme.com') === -1) {
return {
recaptchaActionOverride: 'ALLOW',
};
}
// Allow users to sign in with recaptcha score greater than 0.5
if (context.additionalUserInfo.recaptchaScore > 0.5) {
return {
recaptchaActionOverride: 'ALLOW',
};
}
// Block all others.
return {
recaptchaActionOverride: 'BLOCK',
};
});