Firebase is back at Google I/O on May 10! Register now

通過阻止 Cloud Functions 擴展 Firebase 身份驗證

透過集合功能整理內容 你可以依據偏好儲存及分類內容。

如果您已使用 Identity Platform 升級到 Firebase 身份驗證,則可以使用阻止Cloud Functions擴展 Firebase 身份驗證。

阻塞函數讓您可以執行自定義代碼來修改用戶註冊或登錄到您的應用程序的結果。例如,如果用戶不符合特定條件,您可以阻止他們進行身份驗證,或者在將用戶信息返回到您的客戶端應用程序之前更新用戶信息。

在你開始之前

要使用阻止功能,您必須將您的 Firebase 項目升級到使用 Identity Platform 的 Firebase 身份驗證。如果您尚未升級,請先升級。

了解阻塞函數

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

  • beforeCreate :在將新用戶保存到 Firebase 身份驗證數據庫之前以及將令牌返回到您的客戶端應用之前觸發。

  • beforeSignIn :在驗證用戶憑據之後但在 Firebase 身份驗證向您的客戶端應用返回 ID 令牌之前觸發。如果您的應用程序使用多因素身份驗證,則該功能會在用戶驗證其第二個因素後觸發。請注意,除了beforeCreate beforeSignIn

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

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

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

  • 函數適用於項目中的所有用戶,包括租戶中包含的任何用戶。 Firebase 身份驗證為您的函數提供有關用戶的信息,包括他們所屬的任何租戶,因此您可以做出相應的響應。

  • 將另一個身份提供者鏈接到帳戶會重新觸發任何已註冊的beforeSignIn功能。

  • 匿名和自定義身份驗證不會觸發阻止功能。

部署和註冊一個阻塞函數

要將您的自定義代碼插入用戶身份驗證流程,請部署和註冊阻止功能。部署和註冊阻止功能後,您的自定義代碼必須成功完成,身份驗證和用戶創建才能成功。

部署阻塞功能

部署阻塞函數的方式與部署任何函數的方式相同。 (有關詳細信息,請參閱 Cloud Functions入門頁面)。總之:

  1. 編寫處理beforeCreate事件、 beforeSignIn事件或兩者的雲函數。

    例如,要開始使用,您可以將以下無操作函數添加到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 。例如:

節點.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超過請求期限。

您還可以指定自定義錯誤消息:

節點.js

throw new functions.auth.HttpsError('permission-denied', 'Unauthorized request origin!');

以下示例顯示瞭如何阻止不在特定域內的用戶註冊您的應用程序:

節點.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
  • sessionClaimsbeforeSignIn

除了sessionClaims之外,所有修改的字段都保存到 Firebase 身份驗證的數據庫中,這意味著它們包含在響應令牌中並在用戶會話之間持續存在。

以下示例顯示如何設置默認顯示名稱:

節點.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之後執行。在beforeSignIn中更新的用戶字段在beforeCreate中可見。如果您在兩個事件處理程序中設置了sessionClaims以外的字段,則beforeCreate中設置的值將覆蓋beforeSignIn中設置的值。僅對於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 登錄時,將傳遞以下憑據:

  • 身份令牌
  • 訪問令牌
  • 刷新令牌:僅在請求以下自定義參數時提供:
    • 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 登錄時,將傳遞以下憑據:

  • 身份令牌
  • 訪問令牌
  • 刷新令牌:如果選擇了offline_access範圍,則傳遞給阻塞函數。

例子:

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

雅虎

當用戶使用 Yahoo 登錄時,將傳遞以下憑據,而無需任何自定義參數或範圍:

  • 身份令牌
  • 訪問令牌
  • 刷新令牌

領英

當用戶使用 LinkedIn 登錄時,將傳遞以下憑據:

  • 訪問令牌

蘋果

當用戶使用 Apple 登錄時,將傳遞以下憑據,而無需任何自定義參數或範圍:

  • 身份令牌
  • 訪問令牌
  • 刷新令牌

常見場景

以下示例演示了阻塞函數的一些常見用例:

只允許從特定域註冊

以下示例顯示瞭如何防止不屬於example.com域的用戶註冊您的應用程序:

節點.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}"`);
  }
});

阻止未驗證電子郵件的用戶註冊

以下示例展示瞭如何防止未驗證電子郵件的用戶在您的應用中註冊:

節點.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}"`);
  }
});

註冊時要求電子郵件驗證

以下示例顯示瞭如何要求用戶在註冊後驗證其電子郵件:

節點.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.`);
  }
});

將某些身份提供者電子郵件視為已驗證

以下示例顯示瞭如何將來自某些身份提供商的用戶電子郵件視為已驗證:

節點.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 地址範圍的登錄:

節點.js

exports.beforeSignIn = functions.auth.user().beforeSignIn((user, context) => {
  if (isSuspiciousIpAddress(context.ipAddress)) {
    throw new functions.auth.HttpsError(
      'permission-denied', 'Unauthorized access!');
  }
});

設置自定義和會話聲明

以下示例顯示瞭如何設置自定義和會話聲明:

節點.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 地址:

    節點.js

    exports.beforeSignIn = functions.auth.user().beforeSignIn((user, context) => {
      return {
        sessionClaims: {
          signInIpAddress: context.ipAddress,
        },
      };
    });
    
  2. 當用戶嘗試訪問需要使用 Firebase 身份驗證進行身份驗證的資源時,請將請求中的 IP 地址與用於登錄的 IP 進行比較:

    節點.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!' })
          });
        }
      });
    });
    

篩選用戶照片

以下示例顯示瞭如何清理用戶的個人資料照片:

節點.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。存儲刷新令牌以供離線訪問。

節點.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();
          });
      });
    })
  }
});