以程式輔助方式管理 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 提供者和產生電子郵件連結等作業,您需要目標租戶的 TenantAwareAuth 執行個體 (由專屬 tenantId 識別)。

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

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

請注意,不要將 Tenant 執行個體與 TenantAwareAuth 物件混淆。authInstance.tenantManager().authForTenant() 會傳回擴充 BaseAuthTenantAwareAuth 例項。Auth 類別也會擴充 BaseAuthBaseAuth 提供 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");

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

建立提供者

下列程式碼顯示如何為租戶建立 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 會提供專屬的 uid

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

修改使用者

如要修改現有使用者,請在 updateUser() 方法中指定使用者的 uid

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");

您一次最多可以匯入 1,000 位使用者,但必須使用特定雜湊演算法。

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 權杖。

從用戶端將使用者成功登入特定租戶後,請使用 Client 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
  });

多個租戶可能存取具有不同存取層級的伺服器端資源。由於在這種情況下,租戶 ID 可能無法預先得知,因此可以先在專案層級驗證 ID 權杖。

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

	# 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 權杖」一文。

管理使用者工作階段

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

在某些情況下,基於安全考量,可能需要撤銷使用者的更新權杖,例如使用者回報裝置遺失或遭竊、發現應用程式存在一般安全漏洞,或是大量洩漏有效權杖。管理員 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
  }
}

如要瞭解詳情,請參閱有關管理工作階段的 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"));

詳情請參閱「自訂聲明」一文。

您可以使用 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 測試電話號碼和驗證碼配對數量已超過上限。