運用封鎖函式擴充 Firebase 驗證


封鎖函式可讓你執行自訂程式碼,修改 使用者註冊或登入您的應用程式時。舉例來說 如果使用者不符合特定條件,或無法更新使用者的 再傳回用戶端應用程式。

事前準備

如要使用封鎖函式,您必須將 Firebase 專案升級至 Firebase Authentication with Identity Platform。 若您尚未升級,請先完成升級。

瞭解封鎖函式

您可以為兩個事件註冊封鎖函式:

  • beforeCreate:在系統將新使用者儲存至 Firebase Authentication 資料庫,並在將權杖傳回至 用戶端應用程式。

  • beforeSignIn:在使用者憑證通過驗證後觸發, Firebase Authentication 前,將 ID 符記傳回用戶端應用程式。如果 應用程式採用多重驗證 在使用者驗證雙重驗證後觸發。請注意,建立新的網路時 除了 beforeCreate 以外,使用者也會觸發 beforeSignIn

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

  • 您的函式必須在 7 秒內回應。7 秒後 Firebase Authentication 傳回錯誤,且用戶端作業失敗。

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

  • 功能適用於專案中的所有使用者,包括 用戶群Firebase Authentication 可為您的函式提供使用者相關資訊,包括 因此都能根據情況做出回應。

  • 將其他識別資訊提供者連結至帳戶,都會重新觸發所有已註冊的 beforeSignIn 函式。

  • 匿名和自訂驗證不會觸發封鎖函式。

,瞭解如何調查及移除這項存取權。

部署封鎖函式

如要在使用者驗證流程中插入自訂程式碼,請部署封鎖功能 函式。部署封鎖函式之後,您的自訂程式碼就必須 成功完成驗證和使用者建立作業。

部署封鎖函式的方式與部署任何函式相同。 (請參閱 Cloud Functions 的「開始使用」頁面) )。簡單來說:

  1. 編寫 Cloud Functions 來處理 beforeCreate 事件, beforeSignIn 事件,或同時事件。

    舉例來說,您可以先新增下列免人工管理函式, index.js:

    const functions = require('firebase-functions/v1');
    
    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 事件的專屬 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.
    }
  });

修改使用者

除了封鎖註冊或登入嘗試之外,您可以 作業以繼續,但修改儲存在此物件的 User 物件 Firebase Authentication 的資料庫並傳回用戶端。

如要修改使用者,請從包含 欄位。您可以修改下列欄位:

  • 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 中。如果您設定的欄位 兩個事件處理常式中的 sessionClaimsbeforeSignIn 中設定的值 會覆寫 beforeCreate 中設定的值。僅適用於 sessionClaims: 套用至目前工作階段的權杖憑證附加資訊,但不會保存或 這些資料。

舉例來說,如果設定了任何 sessionClaimsbeforeSignIn 會傳回其 提出任何 beforeCreate著作權聲明,系統就會合併這些著作權聲明。合併後 sessionClaims 鍵與 customClaims 中的鍵相符,即 sessionClaims 將覆寫憑證憑證附加資訊中的 customClaims。 鍵。不過,過度配置的 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 支援流程。
  • 「Refresh token」:如有 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 登入時,將傳送下列憑證:

  • 存取權杖:系統會傳回可交換的存取權杖 再用於其他存取權杖進一步瞭解 存取權杖 以及如何將這些 Cookie 換成 長效型權杖

GitHub

當使用者透過 GitHub 登入時,系統將傳遞以下憑證:

  • 存取權杖:除非撤銷,否則不會過期。

Microsoft

當使用者透過 Microsoft 登入時,將傳送下列憑證:

範例:

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