使用阻止功能擴充 Firebase 驗證


阻止函數可讓您執行自訂程式碼來修改使用者註冊或登入應用程式的結果。例如,您可以阻止使用者在不符合特定條件的情況下進行身份驗證,或在將使用者資訊傳回用戶端應用程式之前更新使用者資訊。

在你開始之前

若要使用封鎖功能,您必須將 Firebase 專案升級至使用 Identity Platform 進行 Firebase 驗證。如果您尚未升級,請先升級。

了解阻塞函數

您可以為兩個事件註冊阻塞函數:

  • beforeCreate :在將新使用者儲存到 Firebase 驗證資料庫之前以及將令牌返回到客戶端應用程式之前觸發。

  • beforeSignIn :在驗證使用者憑證後、Firebase 驗證將 ID 令牌傳回客戶端應用之前觸發。如果您的應用程式使用多重身份驗證,則函數會在使用者驗證第二個因素後觸發。請注意,除了beforeCreate之外,建立新使用者還會觸發beforeSignIn

使用阻塞函數時請記住以下幾點:

  • 您的函數必須在 7 秒內響應。 7 秒後,Firebase 驗證傳回錯誤,用戶端操作失敗。

  • 200以外的 HTTP 回應代碼將傳遞到您的客戶端應用程式。確保您的客戶端程式碼能夠處理函數可能傳回的任何錯誤。

  • 函數適用於專案中的所有用戶,包括租戶中包含的任何用戶。 Firebase 身份驗證會為您的函數提供有關使用者的信息,包括他們所屬的任何租戶,以便您可以做出相應的回應。

  • 將另一個身分提供者連結到帳戶會重新觸發任何已註冊的beforeSignIn函數。

  • 匿名和自訂身份驗證不會觸發封鎖功能。

部署阻塞功能

若要將自訂程式碼插入到使用者驗證流程中,請部署封鎖功能。部署封鎖功能後,您的自訂程式碼必須成功完成才能成功進行身份驗證和使用者建立。

部署阻塞函數的方式與部署任何函數的方式相同。 (詳情請參閱雲端函數入門頁面)。總之:

  1. 編寫處理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
    });
    

    上面的範例省略了自訂身份驗證邏輯的實作。請參閱以下部分以了解如何實現您的封鎖功能以及特定範例的常見場景

  2. 使用 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事件的唯一識別碼。 rWsyPtolplG2TBFoOkkgyg
eventType事件類型。這提供了有關事件名稱的信息,例如beforeSignInbeforeCreate ,以及所使用的關聯登入方法,例如 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';
  };
});

如果您為beforeCreatebeforeSignIn註冊事件處理程序,請注意beforeSignInbeforeCreate之後執行。 beforeCreate中更新的使用者欄位在beforeSignIn中可見。如果您在兩個事件處理程序中設定sessionClaims以外的字段,則beforeSignIn中設定的值將覆蓋beforeCreate中設定的值。僅對於sessionClaims ,它們會傳播到當前會話的令牌聲明,但不會持久保存或儲存在資料庫中。

例如,如果設定了任何sessionClaimsbeforeSignIn將傳回它們以及任何beforeCreate聲明,並且它們將合併。合併時,如果sessionClaims鍵與customClaims中的鍵匹配,則令牌聲明中匹配的customClaims將被sessionClaims鍵覆蓋​​。但是,被覆蓋的customClaims鍵仍將保留在資料庫中以供將來的請求。

支援的 OAuth 憑證和數據

您可以將 OAuth 憑證和資料傳遞給來自各種身分提供者的封鎖功能。下表顯示了每個身分提供者支援的憑證和資料:

身分提供者身份令牌訪問令牌到期時間令牌秘密刷新令牌登入索賠
Google是的是的是的是的
Facebook是的是的
推特是的是的
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

當使用者使用 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 登入時,將傳遞以下憑證:

例子:

const provider = new firebase.auth.OAuthProvider('microsoft.com');
provider.addScope('offline_access');
firebase.auth().signInWithPopup(provider);

雅虎

當使用者使用 Yahoo 登入時,將傳遞以下憑證,不帶任何自訂參數或範圍:

  • ID令牌
  • 訪問令牌
  • 刷新令牌

領英

當使用者使用 LinkedIn 登入時,將傳遞以下憑證:

  • 訪問令牌

蘋果

當使用者使用 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 驗證進行驗證的資源時,請將請求中的 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以了解有關將 reCAPTCHA Enterprise 與 Firebase 身份驗證整合的詳細資訊。

阻止函數可用於根據自訂因素允許或阻止流,從而覆寫 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',
 };
});