使用封鎖函式自訂驗證流程
本文說明如何使用封鎖 Cloud Run functions 擴充 Identity Platform 驗證。
封鎖函式可讓你執行自訂程式碼,藉此修改使用者註冊或登入應用程式的結果。舉例來說,如果使用者不符合特定條件,您可以禁止他們通過驗證,也可以在將使用者資訊傳回用戶端應用程式前更新資訊。
事前準備
使用 Identity Platform 建立應用程式。如需操作說明,請參閱快速入門。
瞭解封鎖函式
您可以為下列兩個事件註冊封鎖函式:
beforeCreate:在新使用者儲存至 Identity Platform 資料庫之前,以及權杖傳回至用戶端應用程式之前觸發。beforeSignIn:在系統驗證使用者憑證後,但在 Identity Platform 將 ID 權杖傳回給用戶端應用程式前觸發。如果應用程式使用多重驗證,則會在使用者驗證第二個因素後觸發函式。請注意,除了beforeCreate之外,建立新使用者也會觸發beforeSignIn。
使用封鎖函式時,請注意下列事項:
函式必須在七秒內回覆。七秒後,Identity Platform 會傳回錯誤,用戶端作業也會失敗。
系統會將
200以外的 HTTP 回應碼傳遞至用戶端應用程式。請確保用戶端程式碼會處理函式可能傳回的任何錯誤。函式會套用至專案中的所有使用者,包括租戶中的使用者。Identity Platform 會向函式提供使用者相關資訊,包括使用者所屬的任何租戶,方便您做出適當回應。
將其他身分識別提供者連結至帳戶會重新觸發所有已註冊的
beforeSignIn函式。這不包括電子郵件和密碼供應商。匿名和自訂驗證不支援封鎖函式。
如果您也使用非同步函式,非同步函式收到的使用者物件不會包含封鎖函式的更新。
建立封鎖函式
下列步驟說明如何建立封鎖函式:
前往Google Cloud 控制台的 Identity Platform「設定」頁面。
選取「觸發程序」分頁標籤。
視要觸發封鎖功能的時間而定, 從「建立前 (beforeCreate)」 下拉式選單或「登入前 (beforeSignIn)」 下拉式選單中,選取「建立函式」。
在「建立函式」窗格中,執行下列步驟:
在「函式名稱」欄位中,輸入函式的名稱。
在「Region」(區域) 清單中選取區域。
複製觸發網址。
在「執行階段、建構作業、連線和安全性設定」專區中,前往各個分頁並設定必要設定。
在「原始碼」部分中,建構原始碼。
在「執行階段」清單中選取執行階段。
在「進入點」欄位中,輸入程式碼的進入點。
使用內嵌編輯器開啟
index.js。刪除範例helloWorld程式碼,並替換成下列其中一個程式碼:如何回應註冊要求:
import * as gcipCloudFunctions from 'gcip-cloud-functions'; const authClient = new gcipCloudFunctions.Auth(); exports.beforeCreate = authClient.functions().beforeCreateHandler((user, context) => { });如何回應登入要求:
import * as gcipCloudFunctions from 'gcip-cloud-functions'; const authClient = new gcipCloudFunctions.Auth(); exports.beforeSignIn = authClient.functions().beforeSignInHandler((user, context) => { });開啟
package.json並新增下列依附元件區塊: 如需最新版 SDK,請參閱gcip-cloud-functions。{ "type": "module", "name": ..., "version": ..., "dependencies": { "gcip-cloud-functions": "^0.2.0" } }
如要發布函式,請按一下「Deploy function」(部署函式)。
選取要傳遞至函式的 OAuth 權杖憑證。
在「設定」頁面中,按一下「儲存」。
您也可以使用 Google Cloud CLI 或 REST API 建立及管理函式。如需詳細的操作說明,請參閱 Cloud Run functions 說明文件。
取得使用者和內容資訊
beforeSignIn 和 beforeCreate 事件會提供 User 和 EventContext 物件,其中包含登入使用者的相關資訊。您可以在程式碼中使用這些值,判斷是否允許作業繼續進行。
如要查看 User 物件可用的屬性清單,請參閱 UserRecord API 參考資料。
EventContext 物件包含下列屬性:
| 名稱 | 說明 | 範例 | 
|---|---|---|
locale | 
    應用程式語言代碼。您可以使用用戶端 SDK 設定語言代碼,或在 REST API 中傳遞語言代碼標頭。 | fr或sv-SE | 
  
ipAddress | 
    使用者註冊或登入裝置的 IP 位址。 | 114.14.200.1 | 
  
userAgent | 
    觸發封鎖函式的使用者代理程式。 | Mozilla/5.0 (X11; Linux x86_64) | 
  
eventId | 
    活動的專屬 ID。 | rWsyPtolplG2TBFoOkkgyg | 
  
eventType | 
    
      事件類型。這項資訊會提供事件名稱,例如 beforeSignIn 或 beforeCreate,以及使用的相關登入方法,例如 Google 或電子郵件/密碼。
     | 
    
      providers/cloud.auth/eventTypes/user.beforeSignIn:password
     | 
  
authType | 
    一律 USER。 | 
    
      USER
     | 
  
resource | 
    Identity Platform 專案或租戶。 | 
      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 gcipCloudFunctions.https.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 gcipCloudFunctions.https.HttpsError('permission-denied', 'Unauthorized request origin!');
以下範例說明如何封鎖特定網域以外的使用者註冊應用程式:
Node.js
// Import the Cloud Auth Admin module.
import * as gcipCloudFunctions from 'gcip-cloud-functions';
// Initialize the Auth client.
const authClient = new gcipCloudFunctions.Auth();
// Http trigger with Cloud Run functions.
exports.beforeCreate = authClient.functions().beforeCreateHandler((user, context) => {
  // If the user is authenticating within a tenant context, the tenant ID can be determined from
  // user.tenantId or from context.resource, eg. 'projects/project-id/tenant/tenant-id-1'
  // Only users of a specific domain can sign up.
  if (!user.email.endsWith('@acme.com')) {
    throw new gcipCloudFunctions.https.HttpsError('invalid-argument', `Unauthorized email "${user.email}"`);
  }
});
無論您使用預設或自訂訊息,Cloud Run 函式都會包裝錯誤,並以內部錯誤的形式傳回給用戶端。舉例來說,如果您在函式中引發下列錯誤:
throw new gcipCloudFunctions.https.HttpsError('invalid-argument', `Unauthorized email user@evil.com}`);
系統會將類似下列內容的錯誤傳回給用戶端應用程式 (如果您使用用戶端 SDK,錯誤會包裝為內部錯誤):
{
  "error": {
    "code": 400,
    "message": "BLOCKING_FUNCTION_ERROR_RESPONSE : HTTP Cloud Function returned an error. Code: 400, Status: \"INVALID_ARGUMENT\", Message: \"Unauthorized email user@evil.com\"",
    "errors": [
      {
        "message": "BLOCKING_FUNCTION_ERROR_RESPONSE : HTTP Cloud Function returned an error. Code: 400, Status: \"INVALID_ARGUMENT\", Message: \"Unauthorized email user@evil.com\"",
        "domain": "global",
        "reason": "invalid"
      }
    ]
  }
}
應用程式應擷取錯誤,並視情況處理。例如:
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.
    }
  });
修改使用者
您可以允許作業繼續進行,但修改儲存至 Identity Platform 資料庫並傳回給用戶端的 User 物件,而非封鎖註冊或登入嘗試。
如要修改使用者,請從事件處理常式傳回包含要修改欄位的物件。您可以修改下列欄位:
displayNamedisabledemailVerifiedphotoURLcustomClaimssessionClaims(僅限beforeSignIn)
除了 sessionClaims 以外,所有修改過的欄位都會儲存到 Identity Platform 的資料庫,也就是說,這些欄位會納入回應權杖,並在使用者工作階段之間保留。
以下範例說明如何設定預設顯示名稱:
Node.js
exports.beforeCreate = authClient.functions().beforeCreateHandler((user, context) => {
  return {
    // If no display name is provided, set it to "guest".
    displayName: user.displayName || 'guest'
  };
});
如果您同時為 beforeCreate 和 beforeSignIn 註冊事件處理常式,請注意 beforeSignIn 會在 beforeCreate 之後執行。在 beforeCreate 中更新的使用者欄位會顯示在 beforeSignIn 中。如果您在兩個事件處理常式中設定 sessionClaims 以外的欄位,則 beforeSignIn 中設定的值會覆寫 beforeCreate 中設定的值。如果是 sessionClaims,系統只會將這些屬性傳播至目前工作階段的權杖聲明,但不會保留或儲存在資料庫中。
舉例來說,如果設定了任何 sessionClaims,beforeSignIn 會連同任何 beforeCreate 聲明一併傳回,並合併這些聲明。合併時,如果 sessionClaims 鍵與 customClaims 中的鍵相符,則權杖聲明中的相符 customClaims 會由 sessionClaims 鍵覆寫。不過,覆寫的 customClaims 金鑰仍會保留在資料庫中,以供日後要求使用。
支援的 OAuth 憑證和資料
您可以將 OAuth 憑證和資料從各種身分識別提供者傳遞至封鎖函式。下表列出各身分識別提供者支援的憑證和資料:
| 識別資訊提供者 | ID 權杖 | 存取權杖 | 到期時間 | 權杖密鑰 | 更新權杖 | 登入聲明 | 
|---|---|---|---|---|---|---|
| 是 | 是 | 是 | 否 | 是 | 否 | |
| 否 | 是 | 是 | 否 | 否 | 否 | |
| 否 | 是 | 否 | 是 | 否 | 否 | |
| GitHub | 否 | 是 | 否 | 否 | 否 | 否 | 
| Microsoft | 是 | 是 | 是 | 否 | 是 | 否 | 
| 否 | 是 | 是 | 否 | 否 | 否 | |
| Yahoo | 是 | 是 | 是 | 否 | 是 | 否 | 
| Apple | 是 | 是 | 是 | 否 | 是 | 否 | 
| SAML | 否 | 否 | 否 | 否 | 否 | 是 | 
| OIDC | 是 | 是 | 是 | 否 | 是 | 是 | 
更新權杖
如要在封鎖函式中使用重新整理權杖,您必須先在 Google Cloud 控制台的「Include token credentials」(包含權杖憑證)下拉式選單中,選取「Triggers」(觸發條件)部分的核取方塊。
如果直接使用 OAuth 憑證 (例如 ID 權杖或存取權杖) 登入,任何身分識別提供者都不會傳回更新權杖。在這種情況下,系統會將相同的用戶端 OAuth 憑證傳遞至封鎖函式。不過,如果是 3 腿流程,如果身分識別提供者支援,或許可以使用重新整理權杖。
以下各節將說明各身分識別提供者類型,以及支援的憑證和資料。
一般 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=offlineprompt=consent:如果使用者先前已同意,且未要求任何新範圍
 
範例:
const provider = new firebase.auth.GoogleAuthProvider();
provider.setCustomParameters({
  'access_type': 'offline',
  'prompt': 'consent'
});
firebase.auth().signInWithPopup(provider);
進一步瞭解 Google 重新整理權杖。
使用者透過 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 登入時,系統會傳遞下列憑證:
- 存取權杖
 
Apple
使用者透過 Apple 登入時,系統會傳遞下列憑證,不含任何自訂參數或範圍:
- ID 權杖
 - 存取權杖
 - 更新權杖
 
常見情境
下列範例為一些封鎖函式的使用案例:
允許特定網域註冊
以下範例說明如何防止不屬於 example.com 網域的使用者向應用程式註冊:
Node.js
exports.beforeCreate = authClient.functions().beforeCreateHandler((user, context) => {
  if (!user.email || user.email.indexOf('@example.com') === -1) {
    throw new gcipCloudFunctions.https.HttpsError(
      'invalid-argument', `Unauthorized email "${user.email}"`);
  }
});
禁止電子郵件未經驗證的使用者註冊
以下範例說明如何禁止電子郵件未經驗證的使用者註冊應用程式:
Node.js
exports.beforeCreate = authClient.functions().beforeCreateHandler((user, context) => {
  if (user.email && !user.emailVerified) {
    throw new gcipCloudFunctions.https.HttpsError(
      'invalid-argument', `Unverified email "${user.email}"`);
  }
});
註冊時必須進行電子郵件驗證
以下範例說明如何要求使用者在註冊後驗證電子郵件地址:
Node.js
exports.beforeCreate = authClient.functions().beforeCreateHandler((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 = authClient.functions().beforeSignInHandler((user, context) => {
 if (user.email && !user.emailVerified) {
   throw new gcipCloudFunctions.https.HttpsError(
     'invalid-argument', `"${user.email}" needs to be verified before access is granted.`);
  }
});
將特定身分識別提供者的電子郵件視為已驗證
以下範例說明如何將特定身分識別提供者的使用者電子郵件視為已驗證:
Node.js
exports.beforeCreate = authClient.functions().beforeCreateHandler((user, context) => {
  if (user.email && !user.emailVerified && context.eventType.indexOf(':facebook.com') !== -1) {
    return {
      emailVerified: true,
    };
  }
});
封鎖來自特定 IP 位址的登入要求
以下範例說明如何封鎖特定 IP 位址範圍的登入要求。
Node.js
exports.beforeSignIn = authClient.functions().beforeSignInHandler((user, context) => {
  if (isSuspiciousIpAddress(context.ipAddress)) {
    throw new gcipCloudFunctions.https.HttpsError(
      'permission-denied', 'Unauthorized access!');
  }
});
設定自訂和工作階段聲明
以下範例說明如何設定自訂和工作階段聲明:
Node.js
exports.beforeCreate = authClient.functions().beforeCreateHandler((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 位址來自不同地理區域),您可以要求使用者重新登入。
使用工作階段聲明追蹤使用者登入時所用的 IP 位址:
Node.js
exports.beforeSignIn = authClient.functions().beforeSignInHandler((user, context) => { return { sessionClaims: { signInIpAddress: context.ipAddress, }, }; });當使用者嘗試存取需要透過 Identity Platform 驗證的資源時,請比較要求中的 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 = authClient.functions().beforeCreateHandler((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 Calendar API。系統會儲存重新整理權杖,以供離線存取。
Node.js
const {OAuth2Client} = require('google-auth-library');
const {google} = require('googleapis');
const gcipCloudFunctions = require('gcip-cloud-functions');
// ...
// Initialize Google OAuth client.
const keys = require('./oauth2.keys.json');
const oAuth2Client = new OAuth2Client(
  keys.web.client_id,
  keys.web.client_secret
);
exports.beforeCreate = authClient.functions().beforeCreateHandler((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();
          });
      });
    })
  }
});
後續步驟
- 使用非同步函式擴充驗證。
 - 進一步瞭解 Cloud Run 函式。