透過程式輔助方式管理 Identity Platform 租戶

本文說明如何使用 Identity Platform Admin SDK,以程式輔助方式管理租戶和使用者。管理員可以執行的活動包括:

  • 使用者管理:建立、更新、刪除及列出特定租戶的使用者。

  • 身分驗證:識別應用程式使用者,限制他們存取您伺服器上的資源。

  • 匯入使用者:從外部驗證系統或其他 Identity Platform 專案/租戶遷移使用者。

  • 使用自訂宣告控管存取權:為特定租戶的使用者帳戶定義自訂屬性,並實作各種存取控管策略,例如角色式存取控管。

  • 使用者工作階段管理:撤銷使用者在特定租戶的更新權杖。

  • 電子郵件動作連結:為特定租戶的使用者產生自訂電子郵件連結,用於重設密碼、電子郵件連結登入和電子郵件驗證。

  • 租戶管理:建立、列出、取得、更新、刪除特定 Identity Platform 專案的租戶。

  • 管理租戶的 OIDC 和 SAML 供應商:以程式輔助方式管理指定租戶的 OIDC 和 SAML 設定。

事前準備

支援功能

下表列出多用戶群環境中各 SDK 支援的功能:

功能 Node.js Java Python Go C#
自訂權杖鑄造
驗證 ID 權杖
管理使用者
使用自訂聲明控管存取權
撤銷更新權杖
匯入使用者
產生電子郵件動作連結
多重驗證
管理 SAML/OIDC 提供者設定
工作階段 Cookie 管理

下表列出您可以使用 Admin SDK 和 Google Cloud 控制台,在特定租戶環境中設定的登入方式:

功能 Google Cloud 控制台 Admin SDK
電子郵件
OIDC
SAML
社交
電話
多重驗證
匿名

租戶管理

您可以使用 Admin SDK,從安全的伺服器環境以程式輔助方式管理租戶,而不必使用 Google Cloud 控制台。包括建立、列出、取得、修改或刪除租戶。

每個租戶都有自己的識別資訊提供者、設定和使用者。您可以使用 admin.auth().tenantManager(),從上層專案執行個體執行租戶設定管理作業 (CRUD)。

租戶設定會提供租戶的相關資訊,例如顯示名稱、租戶 ID 和電子郵件驗證設定。

租戶的所有其他設定 (例如許可網域和經過驗證的重新導向 URI) 均沿用自上層專案。這些設定必須使用 Google Cloud 控制台管理。

如要執行租戶專屬使用者管理、設定 OIDC/SAML 供應商,以及產生電子郵件連結等作業,您需要為目標租戶 (由其專屬 tenantId 識別) 建立 TenantAwareAuth 執行個體。

Node.js

const tenantManager = admin.auth().tenantManager();
const tenantAuth = tenantManager.authForTenant(tenantId);

Python

from firebase_admin import tenant_mgt

tenant_client = tenant_mgt.auth_for_tenant(tenant_id)

Java

FirebaseAuth auth = FirebaseAuth.getInstance();
TenantManager tenantManager = auth.getTenantManager();
TenantAwareFirebaseAuth tenantAuth = tenantManager.getAuthForTenant(tenantId);

所有對使用者管理 API、OIDC/SAML 供應商管理 API 和電子郵件連結產生 API 的呼叫,都會在這個租戶的範圍內 (使用其 TenantAwareAuth 執行個體)。

取得現有租戶

Admin SDK 提供 getTenant() 方法,可根據租戶的 tenantId (租戶的專屬 ID) 擷取相關資訊。

Node.js

admin.auth().tenantManager().getTenant(tenantId)
  .then((tenant) => {
    console.log(tenant.toJSON());
  })
  .catch((error) => {
    // Handle error.
  });

Python

tenant = tenant_mgt.get_tenant(tenant_id)

print('Retrieved tenant:', tenant.tenant_id)

Java

Tenant tenant = FirebaseAuth.getInstance().getTenantManager().getTenant(tenantId);
System.out.println("Retrieved tenant: " + tenant.getTenantId());

這個方法會傳回與 tenantId 對應的 Tenant 物件。如果提供的 tenantId 不屬於現有租戶,傳回的 Promise 會遭到拒絕,並顯示 auth/tenant-not-found 錯誤。

請小心不要混淆 Tenant 執行個體和 TenantAwareAuth 物件。authInstance.tenantManager().authForTenant() 會傳回擴充 BaseAuth 的 TenantAwareAuth 例項。Auth 類別也會擴充 BaseAuth。BaseAuth 提供 API,可在不同情境下管理使用者、設定 OIDC/SAML 供應商。如果是 Auth,脈絡則位於上層專案層級。如果是 TenantAwareAuth,脈絡則位於租戶層級 (租戶由租戶 ID 決定)。getTenant() 方法會解析基本租戶資訊 (例如租戶 ID、顯示名稱和電子郵件服務供應商設定),但如要呼叫該租戶的 API,則需使用 authForTenant(tenantFromGetTenant.tenantId)。

建立租戶

使用 createTenant() 方法建立新的租戶設定:

Node.js

admin.auth().tenantManager().createTenant({
  displayName: 'myTenant1',
  emailSignInConfig: {
    enabled: true,
    passwordRequired: false, // Email link sign-in enabled.
  },
  // TODO: Remove if you don't want to enable multi-factor authentication.
  multiFactorConfig: {
    state: 'ENABLED',
    factorIds: ['phone']
  },
  // TODO: Remove if you don't want to register test phone numbers for use
  // with multi-factor authentication.
  testPhoneNumbers: {
    '+16505551234': '145678',
    '+16505550000': '123456'
  },
})
.then((createdTenant) => {
  console.log(createdTenant.toJSON());
})
.catch((error) => {
  // Handle error.
});

Python

tenant = tenant_mgt.create_tenant(
    display_name='myTenant1',
    enable_email_link_sign_in=True,
    allow_password_sign_up=True)

print('Created tenant:', tenant.tenant_id)

Java

Tenant.CreateRequest request = new Tenant.CreateRequest()
    .setDisplayName("myTenant1")
    .setEmailLinkSignInEnabled(true)
    .setPasswordSignInAllowed(true);
Tenant tenant = FirebaseAuth.getInstance().getTenantManager().createTenant(request);
System.out.println("Created tenant: " + tenant.getTenantId());

您可以提供下列屬性的任意組合:

屬性 類型 說明
displayName
string
    
租戶顯示名稱。長度必須介於 4 至 20 個半形字元之間,只能使用英文字母、數字和連字號,且開頭須為英文字母。
emailSignInConfig
{
  enable: boolean,
  passwordRequired: boolean
}
    
電子郵件登入供應商設定。包括是否啟用電子郵件服務供應商,以及是否需要密碼才能透過電子郵件登入。 如果不需要,則可使用密碼或電子郵件連結登入。
multiFactorConfig
{
  state: 'DISABLED' | 'ENABLED',
  factorIds: string[]
}
    
是否為租戶啟用多重驗證,以及允許哪些驗證類型。目前唯一支援的因素 ID 為 phone。
testPhoneNumbers
{
  string: string
}
  
電話號碼和相關聯多重驗證碼的對應關係,用於註冊測試用途。最多只能有 10 個項目。如要移除所有測試電話號碼,請將這個欄位設為 null。

這個方法會為新建立的租戶傳回 Tenant 物件。

更新租戶

使用 updateTenant() 方法修改現有租戶的資料。您必須指定 tenantId,以及要為該租戶更新的屬性。

Node.js

admin.auth().tenantManager().updateTenant(tenantId, {
  displayName: 'updatedName',
  emailSignInConfig: {
    enabled: false, // Disable email provider.
  },
  // Enable multi-factor authentication.
  multiFactorConfig: {
    state: 'ENABLED',
    factorIds: ['phone']
  },
  // Register phone numbers for testing.
  testPhoneNumbers: {
    '+16505551234': '145678',
    '+16505550000': '123456'
  },
})
.then((updatedTenant) => {
  console.log(updatedTenant.toJSON());
})
.catch((error) => {
  // Handle error.
});

Python

tenant = tenant_mgt.update_tenant(
    tenant_id,
    display_name='updatedName',
    allow_password_sign_up=False) # Disable email provider

print('Updated tenant:', tenant.tenant_id)

Java

Tenant.UpdateRequest request = new Tenant.UpdateRequest(tenantId)
    .setDisplayName("updatedName")
    .setPasswordSignInAllowed(false);
Tenant tenant = FirebaseAuth.getInstance().getTenantManager().updateTenant(request);
System.out.println("Updated tenant: " + tenant.getTenantId());

updateTenant() 接受與 createTenant() 相同的屬性。所有屬性皆為選用。如未指定屬性,現有值就不會修改。

方法完成後,會傳回更新後的 Tenant 物件。如果提供的 tenantId 不屬於現有租戶,傳回的 Promise 會遭到拒絕,並顯示 auth/tenant-not-found 錯誤。

刪除租戶

您可以使用租戶的 tenantId 刪除租戶:

Node.js

admin.auth().tenantManager().deleteTenant(tenantId)
  .then(() => {
    // Tenant deleted.
  })
  .catch((error) => {
    // Handle error.
  });

Python

tenant_mgt.delete_tenant(tenant_id)

Java

FirebaseAuth.getInstance().getTenantManager().deleteTenant(tenantId);

刪除作業成功完成後,這個方法會傳回空白結果。如果提供的 tenantId 不屬於現有租戶,傳回的 Promise 會遭到拒絕,並顯示 auth/tenant-not-found 錯誤。

列出租戶

使用 listTenants() 方法列出現有租戶:

Node.js

function listAllTenants(nextPageToken) {
  return admin.auth().tenantManager().listTenants(100, nextPageToken)
    .then((result) => {
      result.tenants.forEach((tenant) => {
        console.log(tenant.toJSON());
      });
      if (result.pageToken) {
        return listAllTenants(result.pageToken);
      }
    });
}

listAllTenants();

Python

for tenant in tenant_mgt.list_tenants().iterate_all():
    print('Retrieved tenant:', tenant.tenant_id)

Java

ListTenantsPage page = FirebaseAuth.getInstance().getTenantManager().listTenants(null);
for (Tenant tenant : page.iterateAll()) {
  System.out.println("Retrieved tenant: " + tenant.getTenantId());
}

每批結果都包含租戶清單,以及用於列出下一批租戶的下一頁權杖。如果所有租戶都已列出,則不會傳回 pageToken。

如未指定 maxResults 欄位,則預設為每批 1000 個租戶。這也是一次可列出的租戶數量上限。如果值大於上限,就會擲回引數錯誤。如未指定 pageToken,這個方法會從頭列出所有租戶。

透過程式管理 SAML 和 OIDC 供應商

Admin SDK 提供 API,可從安全的伺服器環境,以程式輔助方式管理安全宣告標記語言 (SAML) 2.0 和 OpenID Connect (OIDC) 供應商設定。

您可以使用 Admin SDK,管理特定租戶的這些供應商。這與管理專案層級 OIDC 和 SAML 供應商類似。

如要管理租戶的供應商,請先建立 TenantAwareAuth 執行個體:

Node.js

const tenantAuth = admin.auth().tenantManager().authForTenant('TENANT-ID');

Python

tenant_client = tenant_mgt.auth_for_tenant('TENANT-ID')

Java

TenantAwareFirebaseAuth tenantAuth = FirebaseAuth.getInstance().getTenantManager()
    .getAuthForTenant("TENANT-ID");

接著,您可以執行常見作業,例如為租戶建立、修改或刪除供應商。

建立提供者

下列程式碼說明如何為租戶建立 SAML 供應商:

Node.js

const newConfig = {
  displayName: 'SAML provider name',
  enabled: true,
  providerId: 'saml.myProvider',
  idpEntityId: 'IDP_ENTITY_ID',
  ssoURL: 'https://example.com/saml/sso/1234/',
  x509Certificates: [
    '-----BEGIN CERTIFICATE-----\nCERT1...\n-----END CERTIFICATE-----',
    '-----BEGIN CERTIFICATE-----\nCERT2...\n-----END CERTIFICATE-----'
  ],
  rpEntityId: 'RP_ENTITY_ID',
  // Using the default callback URL.
  callbackURL: 'https://project-id.firebaseapp.com/__/auth/handler'
};

tenantAuth.createProviderConfig(newConfig).then(() => {
  // Successful creation.
}).catch((error) => {
  // Handle error.
});

Python

saml = tenant_client.create_saml_provider_config(
    display_name='SAML provider name',
    enabled=True,
    provider_id='saml.myProvider',
    idp_entity_id='IDP_ENTITY_ID',
    sso_url='https://example.com/saml/sso/1234/',
    x509_certificates=[
        '-----BEGIN CERTIFICATE-----\nCERT1...\n-----END CERTIFICATE-----',
        '-----BEGIN CERTIFICATE-----\nCERT2...\n-----END CERTIFICATE-----',
    ],
    rp_entity_id='P_ENTITY_ID',
    callback_url='https://project-id.firebaseapp.com/__/auth/handler')

print('Created new SAML provider:', saml.provider_id)

Java

SamlProviderConfig.CreateRequest request = new SamlProviderConfig.CreateRequest()
    .setDisplayName("SAML provider name")
    .setEnabled(true)
    .setProviderId("saml.myProvider")
    .setIdpEntityId("IDP_ENTITY_ID")
    .setSsoUrl("https://example.com/saml/sso/1234/")
    .addX509Certificate("-----BEGIN CERTIFICATE-----\nCERT1...\n-----END CERTIFICATE-----")
    .addX509Certificate("-----BEGIN CERTIFICATE-----\nCERT2...\n-----END CERTIFICATE-----")
    .setRpEntityId("RP_ENTITY_ID")
    .setCallbackUrl("https://project-id.firebaseapp.com/__/auth/handler");
SamlProviderConfig saml = tenantAuth.createSamlProviderConfig(request);
System.out.println("Created new SAML provider: " + saml.getProviderId());

修改提供者

以下程式碼說明如何修改供應商:

Node.js

const updatedConfig = {
  x509Certificates: [
    '-----BEGIN CERTIFICATE-----\nCERT2...\n-----END CERTIFICATE-----',
    '-----BEGIN CERTIFICATE-----\nCERT3...\n-----END CERTIFICATE-----',
  ],
};
tenantAuth.updateProviderConfig('saml.myProvider', updatedConfig).then(() => {
  // Successful update.
}).catch((error) => {
  // Handle error.
});

Python

saml = tenant_client.update_saml_provider_config(
    'saml.myProvider',
    x509_certificates=[
        '-----BEGIN CERTIFICATE-----\nCERT2...\n-----END CERTIFICATE-----',
        '-----BEGIN CERTIFICATE-----\nCERT3...\n-----END CERTIFICATE-----',
    ])

print('Updated SAML provider:', saml.provider_id)

Java

SamlProviderConfig.UpdateRequest request =
    new SamlProviderConfig.UpdateRequest("saml.myProvider")
      .addX509Certificate("-----BEGIN CERTIFICATE-----\nCERT2...\n-----END CERTIFICATE-----")
      .addX509Certificate("-----BEGIN CERTIFICATE-----\nCERT3...\n-----END CERTIFICATE-----");
SamlProviderConfig saml = tenantAuth.updateSamlProviderConfig(request);
System.out.println("Updated SAML provider: " + saml.getProviderId());

取得提供者

下列程式碼說明如何使用供應商 ID,擷取特定租戶的供應商設定:

Node.js

tenantAuth.getProviderConfig('saml.myProvider').then((config) => {
  // Get display name and whether it is enabled.
  console.log(config.displayName, config.enabled);
}).catch((error) => {
  // Handle error. Common error is that config is not found.
});

Python

saml = tennat_client.get_saml_provider_config('saml.myProvider')
print(saml.display_name, saml.enabled)

Java

SamlProviderConfig saml = tenantAuth.getSamlProviderConfig("saml.myProvider");

// Get display name and whether it is enabled.
System.out.println(saml.getDisplayName() + " " + saml.isEnabled());

列出供應商

下列程式碼說明如何列出特定租戶的供應商設定:

Node.js

// Returns 10 SAML provider configs starting from the specified nextPageToken offset.
tenantAuth.listProviderConfigs({type: 'saml', maxResults: 10, pageToken: 'nextPageToken'}).then((results) => {
  results.providerConfigs.forEach((config) => {
    console.log(config.providerId);
  });
  // To list the next 10:
  // return tenantAuth.listProviderConfigs(
  //     {type: 'saml', maxResults: 10, pageToken: results.pageToken});
}).catch((error) => {
  // Handle error.
});

Python

for saml in tenant_client.list_saml_provider_configs('nextPageToken').iterate_all():
    print(saml.provider_id)

Java

ListProviderConfigsPage<SamlProviderConfig> page = tenantAuth.listSamlProviderConfigs(
    "nextPageToken");
for (SamlProviderConfig saml : page.iterateAll()) {
  System.out.println(saml.getProviderId());
}

刪除提供者

下列程式碼說明如何刪除供應商:

Node.js

tenantAuth.deleteProviderConfig('saml.myProvider').then(() => {
  // Successful deletion.
}).catch((error) => {
  // Handle error.
});

Python

tenant_client.delete_saml_provider_config('saml.myProvider')

Java

tenantAuth.deleteSamlProviderConfig("saml.myProvider");

OIDC 供應商的管理方式與專案層級 OIDC 供應商類似,但可以從對應的 TenantAwareAuth 執行個體管理,而非 Auth 專案層級執行個體。

詳情請參閱「以程式輔助方式管理 SAML 和 OIDC 供應商」。

管理租戶專屬使用者

您可以使用 Admin SDK 建立、擷取、更新、刪除及列出特定租戶的所有使用者。

首先,您需要為對應的租戶建立 TenantAwareAuth 執行個體:

Node.js

const tenantAuth = admin.auth().tenantManager().authForTenant('TENANT-ID');

Python

tenant_client = tenant_mgt.auth_for_tenant('TENANT-ID')

Java

TenantAwareFirebaseAuth tenantAuth = FirebaseAuth.getInstance().getTenantManager()
    .getAuthForTenant("TENANT-ID");

取得使用者

您可以使用 uid 識別碼擷取特定租戶的使用者:

Node.js

tenantAuth.getUser(uid)
  .then((userRecord) => {
    // See the UserRecord reference documentation to learn more.
    console.log('Successfully fetched user data:', userRecord.toJSON());
    // Tenant ID will be reflected in userRecord.tenantId.
  })
  .catch((error) => {
    console.log('Error fetching user data:', error);
  });

Python

	# Get an auth.Client from tenant_mgt.auth_for_tenant()
    user = tenant_client.get_user(uid)
    print('Successfully fetched user data:', user.uid)

Java

// Get an auth client from the firebase.App
UserRecord user = tenantAuth.getUser(uid);
System.out.println("Successfully fetched user data: " + user.getDisplayName());

您也可以透過電子郵件識別使用者:

Node.js

tenantAuth.getUserByEmail(email)
  .then((userRecord) => {
    // See the UserRecord reference documentation to learn more.
    console.log('Successfully fetched user data:', userRecord.toJSON());
    // Tenant ID will be reflected in userRecord.tenantId.
  })
  .catch((error) => {
    console.log('Error fetching user data:', error);
  });

Python

user = tenant_client.get_user_by_email(email)
print('Successfully fetched user data:', user.uid)

Java

// Get an auth client from the firebase.App
UserRecord user = tenantAuth.getUserByEmail(email);
System.out.println("Successfully fetched user data: " + user.getDisplayName());

建立使用者

使用 createUser() 方法為特定租戶建立新使用者。建立新使用者時,uid 為選填項目;如未指定,Identity Platform 會佈建專屬 ID。

Node.js

tenantAuth.createUser({
  email: 'user@example.com',
  emailVerified: false,
  phoneNumber: '+11234567890',
  password: 'secretPassword',
  displayName: 'John Doe',
  photoURL: 'http://www.example.com/12345678/photo.png',
  disabled: false
})
.then((userRecord) => {
  // See the UserRecord reference documentation to learn more.
  console.log('Successfully created new user:', userRecord.uid);
  // Tenant ID will be reflected in userRecord.tenantId.
})
.catch((error) => {
  console.log('Error creating new user:', error);
});

Python

user = tenant_client.create_user(
    email='user@example.com',
    email_verified=False,
    phone_number='+15555550100',
    password='secretPassword',
    display_name='John Doe',
    photo_url='http://www.example.com/12345678/photo.png',
    disabled=False)
print('Sucessfully created new user:', user.uid)

Java

UserRecord.CreateRequest request = new UserRecord.CreateRequest()
    .setEmail("user@example.com")
    .setEmailVerified(false)
    .setPhoneNumber("+15555550100")
    .setPassword("secretPassword")
    .setDisplayName("John Doe")
    .setPhotoUrl("http://www.example.com/12345678/photo.png")
    .setDisabled(false);
UserRecord user = tenantAuth.createUser(request);
System.out.println("Successfully created user: " + user.getDisplayName());

修改使用者

您可以將現有使用者的 uid 指定給 updateUser() 方法,藉此修改使用者:

Node.js

tenantAuth.updateUser(uid, {
  email: 'modifiedUser@example.com',
  phoneNumber: '+11234567890',
  emailVerified: true,
  password: 'newPassword',
  displayName: 'Jane Doe',
  photoURL: 'http://www.example.com/12345678/photo.png',
  disabled: true
})
.then((userRecord) => {
  // See the UserRecord reference documentation to learn more.
  console.log('Successfully updated user', userRecord.toJSON());
})
.catch((error) => {
  console.log('Error updating user:', error);
});

Python

user = tenant_client.update_user(
    uid,
    email='user@example.com',
    phone_number='+15555550100',
    email_verified=True,
    password='newPassword',
    display_name='John Doe',
    photo_url='http://www.example.com/12345678/photo.png',
    disabled=True)
print('Sucessfully updated user:', user.uid)

Java

UserRecord.UpdateRequest request = new UserRecord.UpdateRequest(uid)
    .setEmail("user@example.com")
    .setEmailVerified(true)
    .setPhoneNumber("+15555550100")
    .setPassword("newPassword")
    .setDisplayName("John Doe")
    .setPhotoUrl("http://www.example.com/12345678/photo.png")
    .setDisabled(true);
UserRecord user = tenantAuth.updateUser(request);
System.out.println("Successfully updated user: " + user.getDisplayName());

刪除使用者

以下範例說明如何根據使用者的 uid 刪除使用者:

Node.js

tenantAuth.deleteUser(uid)
  .then(() => {
    console.log('Successfully deleted user');
  })
  .catch((error) => {
    console.log('Error deleting user:', error);
  });

Python

tenant_client.delete_user(uid)
print('Successfully deleted user')

Java

tenantAuth.deleteUser(uid);

System.out.println("Successfully deleted user: " + uid);

列出使用者

如要批次擷取特定租戶的完整使用者清單,請使用 listUsers() 方法。每個批次都會包含使用者記錄清單,如果還有其他使用者,則會加上下一頁權杖。

Node.js

function listAllUsers(nextPageToken) {
  // List batch of users, 1000 at a time.
  tenantAuth.listUsers(1000, nextPageToken)
    .then((listUsersResult) => {
      listUsersResult.users.forEach((userRecord) => {
        console.log('user', userRecord.toJSON());
        // Tenant ID will be reflected in userRecord.tenantId.
      });
      if (listUsersResult.pageToken) {
        // List next batch of users.
        listAllUsers(listUsersResult.pageToken);
      }
    })
    .catch((error) => {
      console.log('Error listing users:', error);
    });
}
// Start listing users from the beginning, 1000 at a time.
listAllUsers();

Python

	# Note, behind the scenes, the iterator will retrive 1000 users at a time through the API
    for user in tenant_client.list_users().iterate_all():
        print('User: ' + user.uid)

	# Iterating by pages of 1000 users at a time.
    page = tenant_client.list_users()
    while page:
        for user in page.users:
            print('User: ' + user.uid)
        # Get next batch of users.
        page = page.get_next_page()

Java

// Note, behind the scenes, the ListUsersPage retrieves 1000 Users at a time
// through the API
ListUsersPage  page = tenantAuth.listUsers(null);
for (ExportedUserRecord user : page.iterateAll()) {
  System.out.println("User: " + user.getUid());
}

// Iterating by pages 100 users at a time.
page = tenantAuth.listUsers(null, 100);
while (page != null) {
  for (ExportedUserRecord user : page.getValues()) {
    System.out.println("User: " + user.getUid());
  }

  page = page.getNextPage();
}

詳情請參閱 Admin SDK 說明文件中的「管理使用者」一節。

匯入使用者

您可以使用 Admin SDK,將使用者大量匯入具有進階權限的特定租戶。這項功能有許多優點,例如可從其他 Identity Platform 產品、其他租戶,或使用不同雜湊演算法的外部驗證系統遷移使用者。您也可以直接大量匯入具有聯合提供者 (例如 SAML 和 OIDC) 和自訂聲明的使用者。

首先,請取得對應租戶的 TenantAwareAuth 執行個體:

Node.js

const tenantAuth = admin.auth().tenantManager().authForTenant('TENANT-ID');

Python

tenant_client = tenant_mgt.auth_for_tenant('TENANT-ID')

Java

TenantAwareFirebaseAuth tenantAuth = FirebaseAuth.getInstance().getTenantManager()
    .getAuthForTenant("TENANT-ID");

您可以使用特定雜湊演算法,一次匯入最多 1000 位使用者。

Node.js

tenantAuth.importUsers([{
  uid: 'uid1',
  email: 'user1@example.com',
  // Must be provided in a byte buffer.
  passwordHash: Buffer.from('password-hash-1'),
  // Must be provided in a byte buffer.
  passwordSalt: Buffer.from('salt1')
},
{
  uid: 'uid2',
  email: 'user2@example.com',
  // Must be provided in a byte buffer.
  passwordHash: Buffer.from('password-hash-2'),
  // Must be provided in a byte buffer.
  passwordSalt: Buffer.from('salt2')

}], {
  hash: {
    algorithm: 'HMAC_SHA256',
    // Must be provided in a byte buffer.
    key: Buffer.from('secret')
  }
})
.then((results) => {
  results.errors.forEach(function(indexedError) {
  console.log('Error importing user ' + indexedError.index);
  });
})
.catch((error) => {
  console.log('Error importing users:', error);
});

Python

users = [
    auth.ImportUserRecord(
        uid='uid1',
        email='user1@example.com',
        password_hash=b'password_hash_1',
        password_salt=b'salt1'
    ),
    auth.ImportUserRecord(
        uid='uid2',
        email='user2@example.com',
        password_hash=b'password_hash_2',
        password_salt=b'salt2'
    ),
]

hash_alg = auth.UserImportHash.hmac_sha256(key=b'secret')
try:
    result = tenant_client.import_users(users, hash_alg=hash_alg)
    for err in result.errors:
        print('Failed to import user:', err.reason)
except exceptions.FirebaseError as error:
    print('Error importing users:', error)

Java

List<ImportUserRecord> users = new ArrayList<>();
users.add(ImportUserRecord.builder()
    .setUid("uid1")
    .setEmail("user1@example.com")
    .setPasswordHash("password-hash-1".getBytes())
    .setPasswordSalt("salt1".getBytes())
    .build());
users.add(ImportUserRecord.builder()
    .setUid("uid2")
    .setEmail("user2@example.com")
    .setPasswordHash("password-hash-2".getBytes())
    .setPasswordSalt("salt2".getBytes())
    .build());
UserImportHash hmacSha256 = HmacSha256.builder()
    .setKey("secret".getBytes())
    .build();
UserImportResult result = tenantAuth.importUsers(users, UserImportOptions.withHash(hmacSha256));

for (ErrorInfo error : result.getErrors()) {
  System.out.println("Failed to import user: " + error.getReason());
}

所有匯入的使用者都會將tenantId設為tenantAuth.tenantId。

您也可以將沒有密碼的使用者匯入特定租戶。這些使用者可透過聯合供應商和自訂宣告匯入。

Node.js

tenantAuth.importUsers([{
  uid: 'some-uid',
  displayName: 'John Doe',
  email: 'johndoe@acme.com',
  photoURL: 'http://www.example.com/12345678/photo.png',
  emailVerified: true,
  phoneNumber: '+11234567890',
  // Set this user as admin.
  customClaims: {admin: true},
  // User with SAML provider.
  providerData: [{
    uid: 'saml-uid',
    email: 'johndoe@acme.com',
    displayName: 'John Doe',
    photoURL: 'http://www.example.com/12345678/photo.png',
    providerId: 'saml.acme'
  }]
}])
.then(function(results) {
  results.errors.forEach(function(indexedError) {
  console.log('Error importing user ' + indexedError.index);
  });
})
.catch(function(error) {
  console.log('Error importing users:', error);
});

Python

users = [
    auth.ImportUserRecord(
        uid='some-uid',
        display_name='John Doe',
        email='johndoe@gmail.com',
        photo_url='http://www.example.com/12345678/photo.png',
        email_verified=True,
        phone_number='+11234567890',
        custom_claims={'admin': True}, # set this user as admin
        provider_data=[ # user with SAML provider
            auth.UserProvider(
                uid='saml-uid',
                email='johndoe@gmail.com',
                display_name='John Doe',
                photo_url='http://www.example.com/12345678/photo.png',
                provider_id='saml.acme'
            )
        ],
    ),
]
try:
    result = tenant_client.import_users(users)
    for err in result.errors:
        print('Failed to import user:', err.reason)
except exceptions.FirebaseError as error:
    print('Error importing users:', error)

Java

List<ImportUserRecord> users = new ArrayList<>();
users.add(ImportUserRecord.builder()
    .setUid("some-uid")
    .setDisplayName("John Doe")
    .setEmail("johndoe@acme.com")
    .setPhotoUrl("https://www.example.com/12345678/photo.png")
    .setEmailVerified(true)
    .setPhoneNumber("+11234567890")
    // Set this user as admin.
    .putCustomClaim("admin", true)
    // User with SAML provider.
    .addUserProvider(UserProvider.builder()
        .setUid("saml-uid")
        .setEmail("johndoe@acme.com")
        .setDisplayName("John Doe")
        .setPhotoUrl("https://www.example.com/12345678/photo.png")
        .setProviderId("saml.acme")
        .build())
    .build());

UserImportResult result = tenantAuth.importUsers(users);

for (ErrorInfo error : result.getErrors()) {
  System.out.println("Failed to import user: " + error.getReason());
}

詳情請參閱 Admin SDK 說明文件中的「匯入使用者」。

身分驗證

當 Identity Platform 用戶端應用程式與自訂後端伺服器通訊時,必須在該伺服器上識別目前登入的使用者。使用者成功登入後,您可以透過安全連線將使用者的 ID 權杖傳送至伺服器,安全地完成這項操作。接著,伺服器可以驗證 ID 權杖的完整性和真實性。

Admin SDK 內建驗證及解碼特定租戶 ID 權杖的方法。

成功將使用者從用戶端登入特定租用戶後,請使用用戶端 SDK 擷取使用者的 ID 權杖:

auth.tenantId = 'TENANT-ID';
auth.signInWithEmailAndPassword('user@example.com', 'password')
  .then((userCredential) => {
    return userCredential.user.getIdToken();
  })
  .then((idToken) => {
    // Send the ID token to server for verification. ID token should be scoped to TENANT-ID.
  });

在伺服器上建立 TenantAwareAuth 執行個體:

Node.js

const tenantAuth = admin.auth().tenantManager().authForTenant('TENANT-ID');

Python

tenant_client = tenant_mgt.auth_for_tenant('TENANT-ID')

Java

TenantAwareFirebaseAuth tenantAuth = FirebaseAuth.getInstance().getTenantManager()
    .getAuthForTenant("TENANT-ID");

接著,您可以驗證該特定租戶的 ID 權杖:

Node.js

// idToken comes from the client app
tenantAuth.verifyIdToken(idToken)
  .then((decodedToken) => {
    let uid = decodedToken.uid;
    // This should be set to TENANT-ID. Otherwise auth/mismatching-tenant-id error thrown.
    console.log(decodedToken.firebase.tenant);
    // ...
  }).catch((error) => {
    // Handle error
  });

Python

	# id_token comes from the client app
    try:
        decoded_token = tenant_client.verify_id_token(id_token)

        # This should be set to TENANT-ID. Otherwise TenantIdMismatchError error raised.
        print('Verified ID token from tenant:', decoded_token['firebase']['tenant'])
    except tenant_mgt.TenantIdMismatchError:
        # Token revoked, inform the user to reauthenticate or signOut().
        pass

Java

try {
  // idToken comes from the client app
  FirebaseToken token = tenantAuth.verifyIdToken(idToken);
  // TenantId on the FirebaseToken should be set to TENANT-ID.
  // Otherwise "tenant-id-mismatch" error thrown.
  System.out.println("Verified ID token from tenant: " + token.getTenantId());
} catch (FirebaseAuthException e) {
  System.out.println("error verifying ID token: " + e.getMessage());
}

伺服器端資源可能可供多個租戶存取,但存取層級不同。在這種情況下,您可能無法事先得知租戶 ID,因此可以先在專案層級驗證 ID 權杖:

Node.js

admin.auth().verifyIdToken(idToken)
  .then((decodedToken) => {
    if (decodedToken.firebase.tenant === 'TENANT-ID1') {
      // Allow appropriate level of access for TENANT-ID1.
    } else if (decodedToken.firebase.tenant === 'TENANT-ID2') {
      // Allow appropriate level of access for TENANT-ID2.
    } else {
      // Block access for all other tenants.
      throw new Error('Access not allowed.');
    }
  }).catch((error) => {
    // Handle error
  });

Python

    decoded_token = auth.verify_id_token(id_token)

    tenant = decoded_token['firebase']['tenant']
    if tenant == 'TENANT-ID1':
		# Allow appropriate level of access for TENANT-ID1.
        pass
    elif tenant == 'TENANT-ID2':
		# Allow appropriate level of access for TENANT-ID2.
        pass
    else:
		# Access not allowed -- Handle error
        pass

Java

try {
  // idToken comes from the client app
  FirebaseToken token = tenantAuth.verifyIdToken(idToken);
  if ("TENANT-ID1".equals(token.getTenantId())) {
    // Allow appropriate level of access for TENANT-ID1.
  } else if ("TENANT-ID2".equals(token.getTenantId())) {
    // Allow appropriate level of access for TENANT-ID2.
  } else {
    // Access not allowed -- Handle error
  }
} catch (FirebaseAuthException e) {
  System.out.println("error verifying ID token: " + e.getMessage());
}

詳情請參閱 Admin SDK 說明文件中的驗證 ID 權杖。

管理使用者工作階段

Identity Platform 工作階段的效期很長。使用者每次登入時,系統都會在 Identity Platform 伺服器上驗證使用者的憑證,然後換成短期 ID 權杖和長期更新權杖。ID 權杖的效期為一小時。更新權杖不會過期,除非使用者遭到停用、刪除,或帳戶發生重大變更 (例如電子郵件或密碼更新)。

在某些情況下,基於安全考量,可能需要撤銷使用者的更新權杖,例如使用者回報裝置遺失或遭竊、發現應用程式內的一般安全漏洞,或有效權杖大規模外洩。Admin SDK 提供 API,可撤銷特定租戶指定使用者發出的所有更新權杖。

首先,您需要 TenantAwareAuth 執行個體:

Node.js

const tenantAuth = admin.auth().tenantManager().authForTenant('TENANT-ID');

Python

tenant_client = tenant_mgt.auth_for_tenant('TENANT-ID')

Java

TenantAwareFirebaseAuth tenantAuth = FirebaseAuth.getInstance().getTenantManager()
    .getAuthForTenant("TENANT-ID");

接著,您可以指定該使用者的 uid,撤銷更新權杖:

Node.js

// Revoke all refresh tokens for a specified user in a specified tenant for whatever reason.
// Retrieve the timestamp of the revocation, in seconds since the epoch.
tenantAuth.revokeRefreshTokens(uid)
  .then(() => {
    return tenantAuth.getUser(uid);
  })
  .then((userRecord) => {
    return new Date(userRecord.tokensValidAfterTime).getTime() / 1000;
  })
  .then((timestamp) => {
    console.log('Tokens revoked at: ', timestamp);
  });

Python

	# Revoke all refresh tokens for a specified user in a specified tenant for whatever reason.
	# Retrieve the timestamp of the revocation, in seconds since the epoch.
    tenant_client.revoke_refresh_tokens(uid)

    user = tenant_client.get_user(uid)
    # Convert to seconds as the auth_time in the token claims is in seconds.
    revocation_second = user.tokens_valid_after_timestamp / 1000
    print(f'Tokens revoked at: {revocation_second}')

Java

// Revoke all refresh tokens for a specified user in a specified tenant for whatever reason.
// Retrieve the timestamp of the revocation, in seconds since the epoch.
tenantAuth.revokeRefreshTokens(uid);

// accessing the user's TokenValidAfter
UserRecord user = tenantAuth.getUser(uid);


long timestamp = user.getTokensValidAfterTimestamp() / 1000;
System.out.println("the refresh tokens were revoked at: " + timestamp + " (UTC seconds)");

撤銷更新權杖後,系統就不會再為該使用者核發新的 ID 權杖,直到使用者重新驗證為止。不過,現有的 ID 權杖會維持有效狀態,直到自然到期 (一小時) 為止。

您可以指定選用的 checkRevoked 參數,確認未過期的有效 ID 權杖未遭撤銷;這項操作會檢查權杖的完整性和真實性是否經過驗證,以及權杖是否遭到撤銷。

Node.js

// Verify the ID token for a specific tenant while checking if the token is revoked by passing
// checkRevoked true.
let checkRevoked = true;
tenantAuth.verifyIdToken(idToken, checkRevoked)
  .then(payload => {
    // Token is valid.
  })
  .catch(error => {
    if (error.code == 'auth/id-token-revoked') {
      // Token has been revoked. Inform the user to re-authenticate or
      // signOut() the user.
    } else {
      // Token is invalid.
    }
  });

Python

	# Verify the ID token for a specific tenant while checking if the token is revoked.
    try:
        # Verify the ID token while checking if the token is revoked by
        # passing check_revoked=True.
        decoded_token = tenant_client.verify_id_token(id_token, check_revoked=True)
        # Token is valid and not revoked.
        uid = decoded_token['uid']
    except tenant_mgt.TenantIdMismatchError:
        # Token belongs to a different tenant.
        pass
    except auth.RevokedIdTokenError:
        # Token revoked, inform the user to reauthenticate or signOut().
        pass
    except auth.UserDisabledError:
        # Token belongs to a disabled user record.
        pass
    except auth.InvalidIdTokenError:
        # Token is invalid
        pass

Java

// Verify the ID token for a specific tenant while checking if the token is revoked.
boolean checkRevoked = true;
try {
  FirebaseToken token = tenantAuth.verifyIdToken(idToken, checkRevoked);
  System.out.println("Verified ID token for: " + token.getUid());
} catch (FirebaseAuthException e) {
  if ("id-token-revoked".equals(e.getErrorCode())) {
    // Token is revoked. Inform the user to re-authenticate or signOut() the user.
  } else {
    // Token is invalid
  }
}

如果不知道租戶 ID,請先在專案層級驗證權杖,擷取租戶 ID。接著,使用對應的租戶感知用戶端驗證撤銷:

Node.js

admin.auth().verifyIdToken(idToken)
  .then((decodedToken) => {
    const tenantId = decodedToken.firebase.tenant;
    if (tenantId) {
      const tenantAuth = admin.auth().tenantManager().authForTenant(tenantId);
      return tenantAuth.verifyIdToken(idToken, true);
    }
    return admin.auth().verifyIdToken(idToken, true);
  })
  .then((payload) => {
    // Token is valid and not revoked.
  })
  .catch((error) => {
    if (error.code === 'auth/id-token-revoked') {
      // Token has been revoked. Inform the user to re-authenticate or
      // signOut() the user.
    } else {
      // Token is invalid.
    }
  });

Python

try:
    decoded_token = auth.verify_id_token(id_token)
    tenant_id = decoded_token['firebase'].get('tenant')
    if tenant_id:
        tenant_client = tenant_mgt.auth_for_tenant(tenant_id)
        decoded_token = tenant_client.verify_id_token(id_token, check_revoked=True)
    else:
        decoded_token = auth.verify_id_token(id_token, check_revoked=True)
    # Token is valid and not revoked.
    uid = decoded_token['uid']
except auth.RevokedIdTokenError:
    # Token revoked, inform the user to reauthenticate or signOut().
    pass
except auth.UserDisabledError:
    # Token belongs to a disabled user record.
    pass
except auth.InvalidIdTokenError:
    # Token is invalid.
    pass

Java

try {
  FirebaseAuth auth = FirebaseAuth.getInstance();
  FirebaseToken decodedToken = auth.verifyIdToken(idToken);
  String tenantId = decodedToken.getTenantId();
  if (tenantId != null && !tenantId.isEmpty()) {
    TenantAwareFirebaseAuth tenantAuth =
        auth.getTenantManager().getAuthForTenant(tenantId);
    decodedToken = tenantAuth.verifyIdToken(idToken, true);
  } else {
    decodedToken = auth.verifyIdToken(idToken, true);
  }
  // Token is valid and not revoked.
  String uid = decodedToken.getUid();
} catch (FirebaseAuthException e) {
  if (e.getAuthErrorCode() == AuthErrorCode.REVOKED_ID_TOKEN) {
    // Token has been revoked. Inform the user to re-authenticate or
    // signOut() the user.
  } else {
    // Token is invalid.
  }
}

詳情請參閱 Admin SDK 說明文件中的管理工作階段。

使用自訂聲明控管存取權

Admin SDK 支援為特定租戶的使用者帳戶定義自訂屬性。這些屬性可讓您實施不同的存取控管策略,例如角色式存取控管。這些屬性可用於根據應用程式的安全規則,授予使用者不同層級的存取權。

首先,請取得對應租戶的 TenantAwareAuth 執行個體:

Node.js

const tenantAuth = admin.auth().tenantManager().authForTenant('TENANT-ID');

Python

tenant_client = tenant_mgt.auth_for_tenant('TENANT-ID')

Java

TenantAwareFirebaseAuth tenantAuth = FirebaseAuth.getInstance().getTenantManager()
    .getAuthForTenant("TENANT-ID");

自訂聲明可能包含機密資料,因此只能使用 Admin SDK 從具備權限的伺服器環境設定。

Node.js

// Set admin privilege on the user corresponding to uid for a specific tenant.
tenantAuth.setCustomUserClaims(uid, {admin: true}).then(() => {
  // The new custom claims will propagate to the user's ID token the
  // next time a new one is issued.
});

Python

# Set admin privilege on the user corresponding to uid.
tenant_client.set_custom_user_claims(uid, {'admin': True})
# The new custom claims will propagate to the user's ID token the
# next time a new one is issued.

Java

// Set admin privilege on the user corresponding to uid in a specific tenant.
Map<String, Object> claims = new HashMap<>();
claims.put("admin", true);
tenantAuth.setCustomUserClaims(uid, claims);
// The new custom claims will propagate to the user's ID token the
// next time a new one is issued.

使用者下次登入或在現有工作階段重新整理 ID 權杖時,新設定的自訂屬性會顯示在權杖酬載的頂層屬性中。在上述範例中,ID 權杖包含額外的宣告:{admin: true}。

驗證 ID 權杖並解碼其酬載後,即可檢查其他自訂宣告,以強制執行存取控管。

Node.js

// Verify the ID token first.
tenantAuth.verifyIdToken(idToken).then((claims) => {
  if (claims.admin === true) {
    // Allow access to requested admin resource.
  }
});

Python

# Verify the ID token first.
claims = tenant_client.verify_id_token(id_token)
if claims['admin'] is True:
    # Allow access to requested admin resource.
    pass

Java

// Verify the ID token first.
FirebaseToken token = tenantAuth.verifyIdToken(idToken);
if (Boolean.TRUE.equals(token.getClaims().get("admin"))) {
  //Allow access to requested admin resource.
}
// Verify the ID token first.
FirebaseToken decoded = tenantAuth.verifyIdToken(idToken);
if (Boolean.TRUE.equals(decoded.getClaims().get("admin"))) {
  // Allow access to requested admin resource.
}

特定租戶現有使用者的自訂聲明,也可做為使用者記錄的屬性。

Node.js

// Lookup the user associated with the specified uid.
tenantAuth.getUser(uid).then((userRecord) => {
  // The claims can be accessed on the user record.
  console.log(userRecord.customClaims.admin);
});

Python

	# Lookup the user associated with the specified uid.
    user = tenant_client.get_user(uid)

	# The claims can be accessed on the user record.
    print(user.custom_claims.get('admin'))

Java

// Lookup the user associated with the specified uid in a specific tenant.
UserRecord user = tenantAuth.getUser(uid);
System.out.println(user.getCustomClaims().get("admin"));

詳情請參閱 Admin SDK 說明文件中的「自訂宣告」一節。

您可以使用 Identity Platform 用戶端 SDK,向特定租戶的使用者傳送電子郵件,其中包含可用於重設密碼、驗證電子郵件地址和透過電子郵件登入的連結。這些電子郵件由 Google 傳送,自訂選項有限。

您可以使用 Admin SDK,在特定租戶範圍內,透過程式輔助方式產生這些連結。

首先,請取得對應租戶的 TenantAwareAuth 執行個體:

Node.js

const tenantAuth = admin.auth().tenantManager().authForTenant('TENANT-ID');

Python

tenant_client = tenant_mgt.auth_for_tenant('TENANT-ID')

Java

TenantAwareFirebaseAuth tenantAuth = FirebaseAuth.getInstance().getTenantManager()
    .getAuthForTenant("TENANT-ID");

以下範例說明如何產生連結,驗證指定租用戶的使用者電子郵件:

Node.js

const actionCodeSettings = {
  // URL you want to redirect back to. The domain (www.example.com) for
  // this URL must be whitelisted in the Cloud console.
  url: 'https://www.example.com/checkout?cartId=1234',
  // This must be true for email link sign-in.
  handleCodeInApp: true,
  iOS: {
    bundleId: 'com.example.ios'
  },
  android: {
    packageName: 'com.example.android',
    installApp: true,
    minimumVersion: '12'
  },
  // FDL custom domain.
  dynamicLinkDomain: 'coolapp.page.link'
};

// Admin SDK API to generate the email verification link.
const userEmail = 'user@example.com';
tenantAuth.generateEmailVerificationLink(userEmail, actionCodeSettings)
  .then((link) => {
    // Construct email verification template, embed the link and send
    // using custom SMTP server.
    return sendCustomVerificationEmail(userEmail, displayName, link);
  })
  .catch((error) => {
    // Some error occurred.
  });

Python

action_code_settings = auth.ActionCodeSettings(
    url='https://www.example.com/checkout?cartId=1234',
    handle_code_in_app=True,
    ios_bundle_id='com.example.ios',
    android_package_name='com.example.android',
    android_install_app=True,
    android_minimum_version='12',
    # FDL custom domain.
    dynamic_link_domain='coolapp.page.link',
)

email = 'user@example.com'
link = tenant_client.generate_email_verification_link(email, action_code_settings)
# Construct email from a template embedding the link, and send
# using a custom SMTP server.
send_custom_email(email, link)

Java

ActionCodeSettings actionCodeSettings = ActionCodeSettings.builder()
    // URL you want to redirect back to. The domain (www.example.com) for
    // this URL must be whitelisted in the GCP Console.
    .setUrl("https://www.example.com/checkout?cartId=1234")
    // This must be true for email link sign-in.
    .setHandleCodeInApp(true)
    .setIosBundleId("com.example.ios")
    .setAndroidPackageName("com.example.android")
    .setAndroidInstallApp(true)
    .setAndroidMinimumVersion("12")
    // FDL custom domain.
    .setDynamicLinkDomain("coolapp.page.link")
    .build();

String link = tenantAuth.generateEmailVerificationLink(email, actionCodeSettings);

// Construct email verification template, embed the link and send
// using custom SMTP server.
sendCustomEmail(email, displayName, link);

您也可以使用類似機制,產生重設密碼和電子郵件登入連結。請注意,在租戶環境中產生電子郵件動作連結時,必須先從連結剖析租戶 ID,並在用戶端 Auth 執行個體上設定,才能套用程式碼。

const actionCodeUrl = firebase.auth.ActionCodeURL.parseLink(window.location.href);
// A one-time code, used to identify and verify a request.
const code = actionCodeUrl.code;
// The tenant ID being used to trigger the email action.
const tenantId = actionCodeUrl.tenantId;
auth.tenantId = tenantId;

// Apply the action code.
auth.applyActionCode(actionCode)
  .then(() => {
    // User's email is now verified.
  })
  .catch((error) => {
    // Handle error.
  });

詳情請參閱 Admin SDK 說明文件中的「電子郵件動作連結」。

錯誤訊息

下表列出您可能會遇到的常見錯誤訊息。

錯誤代碼 說明和解決步驟
auth/billing-not-enabled 必須啟用計費功能,才能使用這項功能。
auth/invalid-display-name displayName 欄位必須是有效字串。
auth/invalid-name 提供無效的資源名稱。
auth/invalid-page-token 頁面權杖必須是有效的非空白字串。
auth/invalid-project-id 上層專案無效。上層專案未啟用多用戶群,或未啟用多用戶群。
auth/invalid-tenant-id 租戶 ID 必須是有效的非空白字串。
auth/mismatching-tenant-id 使用者租戶 ID 與目前的 TenantAwareAuth 租戶 ID 不符。
auth/missing-display-name 正在建立或編輯的資源缺少有效的顯示名稱。
auth/insufficient-permission 使用者權限不足,無法存取要求的資源或執行特定租戶作業。
auth/quota-exceeded 指定作業的專案配額已超過上限。
auth/tenant-not-found 沒有與所提供 ID 對應的租戶。
auth/unsupported-tenant-operation 多租戶環境不支援這項作業。
auth/invalid-testing-phone-number 提供的測試用電話號碼或測試碼無效。
auth/test-phone-number-limit-exceeded 測試電話號碼和驗證碼組合數量已超過上限。