通過阻止 Cloud Functions 擴展 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
    

    每次更新函數時都必須重新部署它們。

註冊一個阻塞函數

  1. 轉到 Firebase 控制台中的Firebase 身份驗證設置頁面。

  2. 選擇阻止功能選項卡。

  3. 通過從帳戶創建之前 (beforeCreate)登錄之前 (beforeSignIn)下的下拉菜單中選擇來註冊您的阻止功能。

  4. 保存您的更改。

獲取用戶和上下文信息

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 憑據和數據傳遞給來自各種身份提供商的阻止功能。下表顯示了每個身份提供商支持的憑據和數據:

身份提供者身份令牌訪問令牌到期時間令牌秘密刷新令牌登錄索賠
谷歌是的是的是的是的
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 登錄時,將傳遞以下憑據:

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