驗證封鎖觸發條件 (第 1 代)

封鎖函式可讓你執行自訂程式碼,藉此修改使用者註冊或登入應用程式的結果。舉例來說,如果使用者不符合特定條件,您可以禁止他們通過驗證,也可以在將使用者資訊傳回用戶端應用程式前更新資訊。

事前準備

如要使用封鎖函式,請將 Firebase 專案升級至 Firebase Authentication with Identity Platform。 如果尚未升級,請先升級。

瞭解封鎖函式

您可以為下列事件註冊封鎖函式:

  • beforeCreate:在新使用者儲存至 Firebase Authentication 資料庫之前,以及權杖傳回至用戶端應用程式之前觸發。

  • beforeSignIn:使用者憑證通過驗證後觸發,但在 Firebase Authentication 將 ID 權杖傳回用戶端應用程式前觸發。如果應用程式使用多重驗證,則會在使用者驗證第二個因素後觸發。請注意,除了 beforeCreate 之外,建立新使用者也會觸發 beforeSignIn

  • beforeEmail (僅限 Node.js):在系統傳送電子郵件給使用者前觸發,例如登入或重設密碼電子郵件。

  • beforeSms (僅限 Node.js):在系統傳送簡訊給使用者之前觸發,適用於多重驗證等情況。

使用封鎖函式時,請注意下列事項:

  • 函式必須在 7 秒內回覆。7 秒後,Firebase Authentication 會傳回錯誤,用戶端作業也會失敗。

  • 系統會將 200 以外的 HTTP 回應碼傳遞至用戶端應用程式。請確保用戶端程式碼會處理函式可能傳回的任何錯誤。

  • 函式會套用至專案中的所有使用者,包括租戶中的使用者。 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. 使用 Firebase CLI 部署函式:

    firebase deploy --only functions
    

    每次更新函式後,都必須重新部署。

取得使用者和內容資訊

beforeSignInbeforeCreate 事件會提供 UserEventContext 物件,其中包含登入使用者的相關資訊。您可以在程式碼中使用這些值,判斷是否允許作業繼續進行。

如要查看 User 物件可用的屬性清單,請參閱 UserRecord API 參考資料

EventContext 物件包含下列屬性:

名稱 說明 範例
locale 應用程式語言代碼。您可以透過用戶端 SDK 設定語言代碼,或在 REST API 中傳遞語言代碼標頭。 frsv-SE
ipAddress 使用者註冊或登入裝置時所用的 IP 位址。 114.14.200.1
userAgent 觸發封鎖功能的使用者代理程式。 Mozilla/5.0 (X11; Linux x86_64)
eventId 活動的專屬 ID。 rWsyPtolplG2TBFoOkkgyg
eventType 事件類型。這項資訊會提供事件名稱,例如 beforeSignInbeforeCreate,以及使用的相關登入方法,例如 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.
    }
  });

修改使用者

您可以允許作業繼續進行,但修改儲存至 Firebase Authentication 資料庫並傳回給用戶端的 User 物件,而非封鎖註冊或登入嘗試。

如要修改使用者,請從事件處理常式傳回包含要修改欄位的物件。您可以修改下列欄位:

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

如果您同時為 beforeCreatebeforeSignIn 註冊事件處理常式,請注意 beforeSignIn 會在 beforeCreate 之後執行。在 beforeCreate 中更新的使用者欄位會顯示在 beforeSignIn 中。如果您在兩個事件處理常式中設定 sessionClaims 以外的欄位,則 beforeSignIn 中設定的值會覆寫 beforeCreate 中設定的值。如果是 sessionClaims,系統只會將這些屬性傳播至目前工作階段的權杖聲明,但不會保留或儲存在資料庫中。

舉例來說,如果設定了任何 sessionClaimsbeforeSignIn 會連同任何 beforeCreate 聲明一併傳回,並合併這些聲明。合併時,如果 sessionClaims 鍵與 customClaims 中的鍵相符,權杖聲明中的相符 customClaims 鍵就會遭到 sessionClaims 鍵覆寫。不過,覆寫的 customClaims 金鑰仍會保留在資料庫中,以供日後要求使用。

支援的 OAuth 憑證和資料

您可以將 OAuth 憑證和資料從各種身分識別供應商傳遞至封鎖函式。下表列出各識別資訊提供者支援的憑證和資料:

識別資訊提供者 ID 權杖 存取權杖 到期時間 權杖密鑰 更新權杖 登入聲明
Google
Facebook
Twitter
GitHub
Microsoft
LinkedIn
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

使用者透過 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

使用者透過 Facebook 登入時,系統會傳遞下列憑證:

  • 存取權杖:系統會傳回存取權杖,可換取另一個存取權杖。進一步瞭解 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

使用者透過 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 位址來自不同地理區域),您可以要求使用者重新登入。

  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 驗證整合,請參閱「啟用 reCAPTCHA Enterprise」。

您可以根據自訂因素使用封鎖函式允許或封鎖流程,藉此覆寫 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',
 }
});