멀티테넌시로 인증

이 문서에서는 멀티 테넌트 Identity Platform 환경에서 사용자를 인증하는 방법을 설명합니다.

시작하기 전에

프로젝트에 멀티테넌시를 사용 설정하고 테넌트를 구성해야 합니다. 자세한 방법은 멀티테넌시 시작하기를 참조하세요.

또한 앱에 클라이언트 SDK를 추가해야 합니다.

  1. Google Cloud Console에서 Identity Platform 페이지로 이동합니다.
    Identity Platform 사용자 페이지로 이동

  2. 오른쪽 상단에서 애플리케이션 설정 세부정보를 클릭합니다.

  3. 코드를 웹 앱에 복사합니다. 예를 들면 다음과 같습니다.

웹 버전 9

import { initializeApp } from "firebase/app";

const firebaseConfig = {
  apiKey: "...",
  // By default, authDomain is '[YOUR_APP].firebaseapp.com'.
  // You may replace it with a custom domain.
  authDomain: '[YOUR_CUSTOM_DOMAIN]'
};
const firebaseApp = initializeApp(firebaseConfig);

웹 버전 8

firebase.initializeApp({
  apiKey: '...',
  // By default, authDomain is '[YOUR_APP].firebaseapp.com'.
  // You may replace it with a custom domain.
  authDomain: '[YOUR_CUSTOM_DOMAIN]'
});

테넌트로 로그인

테넌트에 로그인하려면 테넌트 ID를 auth 객체에 전달해야 합니다. tenantId는 페이지를 새로고침하면 사라집니다.

웹 버전 9

import { getAuth } from "firebase/auth";
const auth = getAuth();
const tenantId = "TENANT_ID1";
auth.tenantId = tenantId;

웹 버전 8

const tenantId = "TENANT_ID1";
firebase.auth().tenantId = tenantId;

auth 인스턴스의 향후 로그인 요청에는 테넌트 ID를 변경하거나 재설정할 때까지 테넌트 ID(앞의 예시에서 TENANT_ID1)가 포함됩니다.

하나 또는 여러 개의 auth 인스턴스를 사용하여 여러 테넌트로 작업할 수 있습니다.

단일 auth 인스턴스를 사용하려면 테넌트 간에 전환할 때마다 tenantId 속성을 수정합니다. 프로젝트 수준의 IdP로 되돌리려면 tenantIdnull로 설정합니다.

웹 버전 9

// One Auth instance
// Switch to tenant1
auth.tenantId = "TENANT_ID1";
// Switch to tenant2
auth.tenantId = "TENANT_ID2";
// Switch back to project level IdPs
auth.tenantId = null;

웹 버전 8

// One Auth instance
// Switch to tenant1
firebase.auth().tenantId = "TENANT_ID1";
// Switch to tenant2
firebase.auth().tenantId = "TENANT_ID2";
// Switch back to project level IdPs
firebase.auth().tenantId = null;

여러 인스턴스를 사용하려면 각 테넌트의 새 auth 인스턴스를 만들고 다른 ID를 할당합니다.

웹 버전 9

// Multiple Auth instances
import { initializeApp } from "firebase/app";
import { getAuth } from "firebase/auth";
const firebaseApp1 = initializeApp(firebaseConfig1, 'app1_for_tenantId1');
const firebaseApp2 = initializeApp(firebaseConfig2, 'app2_for_tenantId2');

const auth1 = getAuth(firebaseApp1);
const auth2 = getAuth(firebaseApp2);

auth1.tenantId = "TENANT_ID1";
auth2.tenantId = "TENANT_ID2";

웹 버전 8

// Multiple Auth instances
firebase.initializeApp(config, 'app1_for_tenantId1');
firebase.initializeApp(config, 'app2_for_tenantId2');

const auth1 = firebase.app('app1').auth();
const auth2 = firebase.app('app2').auth();

auth1.tenantId = "TENANT_ID1";
auth2.tenantId = "TENANT_ID2";

테넌트로 로그인하면 user.tenantId가 해당 테넌트로 설정된 테넌트 사용자가 반환됩니다. 나중에 auth 인스턴스에서 tenantId를 전환해도 currentUser 속성은 변경되지 않으며 여전히 이전 테넌트와 동일한 사용자를 가리킵니다.

웹 버전 9

import { signInWithEmailAndPassword, onAuthStateChanged } from "firebase/auth";
// Switch to TENANT_ID1
auth.tenantId = 'TENANT_ID1';

// Sign in with tenant
signInWithEmailAndPassword(auth, email, password)
  .then((userCredential) => {
    // User is signed in.
    const user = userCredential.user;
    // user.tenantId is set to 'TENANT_ID1'.
    // Switch to 'TENANT_ID2'.
    auth.tenantId = 'TENANT_ID2';
    // auth.currentUser still points to the user.
    // auth.currentUser.tenantId is 'TENANT_ID1'.
  });

// You could also get the current user from Auth state observer.
onAuthStateChanged(auth, (user) => {
  if (user) {
    // User is signed in.
    // user.tenantId is set to 'TENANT_ID1'.
  } else {
    // No user is signed in.
  }
});

웹 버전 8

// Switch to TENANT_ID1
firebase.auth().tenantId = 'TENANT_ID1';

// Sign in with tenant
firebase.auth().signInWithEmailAndPassword(email, password)
  .then((result) => {
    const user = result.user;
    // user.tenantId is set to 'TENANT_ID1'.
    // Switch to 'TENANT_ID2'.
    firebase.auth().tenantId = 'TENANT_ID2';
    // firebase.auth().currentUser still point to the user.
    // firebase.auth().currentUser.tenantId is 'TENANT_ID1'.
  });

// You could also get the current user from Auth state observer.
firebase.auth().onAuthStateChanged((user) => {
  if (user) {
    // User is signed in.
    // user.tenantId is set to 'TENANT_ID1'.
  } else {
    // No user is signed in.
  }
});

이메일/비밀번호 계정

다음 예시에서는 새 사용자를 등록하는 방법을 보여줍니다.

웹 버전 9

import { createUserWithEmailAndPassword } from "firebase/auth";
auth.tenantId = 'TENANT_ID';

createUserWithEmailAndPassword(auth, email, password)
  .then((userCredential) => {
    // User is signed in.
    // userCredential.user.tenantId is 'TENANT_ID'.
  }).catch((error) => {
    // Handle / display error.
    // ...
  });

웹 버전 8

firebase.auth().tenantId = 'TENANT_ID';

firebase.auth().createUserWithEmailAndPassword(email, password)
  .then((result) => {
    // result.user.tenantId is 'TENANT_ID'.
  }).catch((error) => {
    // Handle error.
  });

기존 사용자를 로그인 처리하려면 다음 단계를 따르세요.

웹 버전 9

import { signInWithEmailAndPassword } from "firebase/auth";
auth.tenantId = 'TENANT_ID';

signInWithEmailAndPassword(auth, email, password)
  .then((userCredential) => {
    // User is signed in.
    // userCredential.user.tenantId is 'TENANT_ID'.
  }).catch((error) => {
    // Handle / display error.
    // ...
  });

웹 버전 8

firebase.auth().tenantId = 'TENANT_ID';

firebase.auth().signInWithEmailAndPassword(email, password)
  .then((result) => {
    // result.user.tenantId is 'TENANT_ID'.
  }).catch((error) => {
    // Handle error.
  });

SAML

SAML 공급업체를 통해 로그인하려면 Google Cloud Console의 공급업체 ID로 SAMLAuthProvider 인스턴스를 인스턴스화합니다.

웹 버전 9

import { SAMLAuthProvider } from "firebase/auth";

const provider = new SAMLAuthProvider("saml.myProvider");

웹 버전 8

const provider = new firebase.auth.SAMLAuthProvider('saml.myProvider');

그런 다음 팝업 또는 리디렉션 흐름을 사용하여 SAML 공급업체에 로그인할 수 있습니다.

  • 팝업

    웹 버전 9

    import { signInWithPopup } from "firebase/auth";
    // Switch to TENANT_ID1.
    auth.tenantId = 'TENANT_ID1';
    
    // Sign-in with popup.
    signInWithPopup(auth, provider)
      .then((userCredential) => {
        // User is signed in.
        const user = userCredential.user;
        // user.tenantId is set to 'TENANT_ID1'.
        // Provider data available from the result.user.getIdToken()
        // or from result.user.providerData
      })
      .catch((error) => {
        // Handle / display error.
        // ...
      });

    웹 버전 8

    // Switch to TENANT_ID1.
    firebase.auth().tenantId = 'TENANT_ID1';
    
    // Sign-in with popup.
    firebase.auth().signInWithPopup(provider)
      .then((result) => {
        // User is signed in.
        // tenant ID is available in result.user.tenantId.
        // Identity provider data is available in result.additionalUserInfo.profile.
      })
      .catch((error) => {
        // Handle error.
      });

  • 리디렉션

    웹 버전 9

    import { signInWithRedirect, getRedirectResult } from "firebase/auth";
    // Switch to TENANT_ID1.
    auth.tenantId = 'TENANT_ID1';
    
    // Sign-in with redirect.
    signInWithRedirect(auth, provider);
    
    // After the user completes sign-in and returns to the app, you can get
    // the sign-in result by calling getRedirectResult. However, if they sign out
    // and sign in again with an IdP, no tenant is used.
    getRedirectResult(auth)
      .then((result) => {
        // User is signed in.
        // The tenant ID available in result.user.tenantId.
        // Provider data available from the result.user.getIdToken()
        // or from result.user.providerData
      })
      .catch((error) => {
        // Handle / display error.
        // ...
      });

    웹 버전 8

    // Switch to TENANT_ID1.
    firebase.auth().tenantId = 'TENANT_ID1';
    
    // Sign-in with redirect.
    firebase.auth().signInWithRedirect(provider);
    
    // After the user completes sign-in and returns to the app, you can get
    // the sign-in result by calling getRedirectResult. However, if they sign out
    // and sign in again with an IdP, no tenant is used.
    firebase.auth().getRedirectResult()
      .then((result) => {
        // User is signed in.
        // The tenant ID available in result.user.tenantId.
        // Identity provider data is available in result.additionalUserInfo.profile.
      })
      .catch((error) => {
        // Handle error.
      });

두 경우 모두 auth 인스턴스에 올바른 테넌트 ID를 설정합니다.

이 인증 과정을 시작하려면 사용자에게 이메일 주소를 제공하도록 요청하는 인터페이스를 표시하고 sendSignInLinkToEmail을 호출하여 인증 링크를 보냅니다. 이메일을 보내기 전에 auth 인스턴스에 올바른 테넌트 ID를 설정하세요.

웹 버전 9

import { sendSignInLinkToEmail } from "firebase/auth";
// Switch to TENANT_ID1
auth.tenantId = 'TENANT_ID1';

sendSignInLinkToEmail(auth, email, actionCodeSettings)
  .then(() => {
    // The link was successfully sent. Inform the user.
    // Save the email locally so you don't need to ask the user for it again
    // if they open the link on the same device.
    window.localStorage.setItem('emailForSignIn', email);
  })
  .catch((error) => {
    // Handle / display error.
    // ...
  });

웹 버전 8

// Switch to TENANT_ID1
firebase.auth().tenantId = 'TENANT_ID1';

firebase.auth().sendSignInLinkToEmail(email, actionCodeSettings)
  .then(() => {
    // The link was successfully sent. Inform the user.
    // Save the email locally so you don't need to ask the user for it again
    // if they open the link on the same device.
    window.localStorage.setItem('emailForSignIn', email);
  })
  .catch((error) => {
    // Some error occurred, you can inspect the code: error.code
  });

방문 페이지에서 로그인을 완료하려면 먼저 이메일 링크에서 테넌트 ID를 파싱하여 auth 인스턴스에 설정합니다. 그런 다음 사용자의 이메일과 일회용 코드가 포함된 실제 이메일 링크를 사용하여 signInWithEmailLink를 호출합니다.

웹 버전 9

import { isSignInWithEmailLink, parseActionCodeURL, signInWithEmailLink } from "firebase/auth";
if (isSignInWithEmailLink(auth, window.location.href)) {
  const actionCodeUrl = parseActionCodeURL(window.location.href);
  if (actionCodeUrl.tenantId) {
    auth.tenantId = actionCodeUrl.tenantId;
  }
  let email = window.localStorage.getItem('emailForSignIn');
  if (!email) {
    // User opened the link on a different device. To prevent session fixation
    // attacks, ask the user to provide the associated email again. For example:
    email = window.prompt('Please provide your email for confirmation');
  }
  // The client SDK will parse the code from the link for you.
  signInWithEmailLink(auth, email, window.location.href)
    .then((result) => {
      // User is signed in.
      // tenant ID available in result.user.tenantId.
      // Clear email from storage.
      window.localStorage.removeItem('emailForSignIn');
    });
}

웹 버전 8

if (firebase.auth().isSignInWithEmailLink(window.location.href)) {
  const actionCodeUrl = firebase.auth.ActionCodeURL.parseLink(window.location.href);
  if (actionCodeUrl.tenantId) {
    firebase.auth().tenantId = actionCodeUrl.tenantId;
  }
  let email = window.localStorage.getItem('emailForSignIn');
  if (!email) {
    // User opened the link on a different device. To prevent session fixation
    // attacks, ask the user to provide the associated email again. For example:
    email = window.prompt('Please provide your email for confirmation');
  }
  firebase.auth().signInWithEmailLink(email, window.location.href)
    .then((result) => {
      // User is signed in.
      // tenant ID available in result.user.tenantId.
    });
}

커스텀 토큰 만들기

멀티 테넌트 인식 커스텀 토큰을 만드는 것은 일반 커스텀 토큰을 만드는 것과 동일합니다. auth 인스턴스에 올바른 테넌트 ID가 설정되어 있으면 최상위 tenant_id 클레임이 결과 JWT에 추가됩니다. 커스텀 토큰을 만들고 사용하는 방법에 대한 자세한 안내는 커스텀 토큰 만들기를 참조하세요.

다음 예시에서는 Admin SDK를 사용하여 커스텀 토큰을 만드는 방법을 보여줍니다.

웹 버전 9

// Ensure you're using a tenant-aware auth instance
const tenantManager = admin.auth().tenantManager();
const tenantAuth = tenantManager.authForTenant('TENANT_ID1');

// Create a custom token in the usual manner
tenantAuth.createCustomToken(uid)
  .then((customToken) => {
    // Send token back to client
  })
  .catch((error) => {
    console.log('Error creating custom token:', error);
  });

웹 버전 8

// Ensure you're using a tenant-aware auth instance
const tenantManager = admin.auth().tenantManager();
const tenantAuth = tenantManager.authForTenant('TENANT_ID1');

// Create a custom token in the usual manner
tenantAuth.createCustomToken(uid)
  .then((customToken) => {
    // Send token back to client
  })
  .catch((error) => {
    console.log('Error creating custom token:', error);
  });

다음 코드는 커스텀 토큰을 사용하여 로그인하는 방법을 보여줍니다.

웹 버전 9

import { signInWithCustomToken } from "firebase/auth";
auth.tenantId = 'TENANT_ID1';

signInWithCustomToken(auth, token)
  .catch((error) => {
    // Handle / display error.
    // ...
  });

웹 버전 8

firebase.auth().tenantId = 'TENANT_ID1';

firebase.auth().signInWithCustomToken(token)
  .catch((error) => {
    // Handle Errors here.
    const errorCode = error.code;
    const errorMessage = error.message;
    // ...
  });

테넌트 ID가 일치하지 않으면 signInWithCustomToken() 메서드가 실패합니다.

멀티 테넌트 사용자 인증 정보 연결

다른 유형의 사용자 인증 정보를 기존 멀티 테넌트 사용자에 연결할 수 있습니다. 예를 들어 사용자가 이전에 테넌트에서 SAML 공급업체를 통해 인증한 경우 기존 계정에 이메일/비밀번호 로그인을 추가하여 두 방법 중 하나를 사용하여 테넌트에 로그인할 수 있습니다.

웹 버전 9

import { signInWithPopup, EmailAuthProvider, linkWithCredential, SAMLAuthProvider, signInWithCredential } from "firebase/auth";
// Switch to TENANT_ID1
auth.tenantId = 'TENANT_ID1';

// Sign-in with popup
signInWithPopup(auth, provider)
  .then((userCredential) => {
    // Existing user with e.g. SAML provider.
    const prevUser = userCredential.user;
    const emailCredential =
      EmailAuthProvider.credential(email, password);
    return linkWithCredential(prevUser, emailCredential)
      .then((linkResult) => {
        // Sign in with the newly linked credential
        const linkCredential = SAMLAuthProvider.credentialFromResult(linkResult);
        return signInWithCredential(auth, linkCredential);
      })
      .then((signInResult) => {
        // Handle sign in of merged user
        // ...
      });
  })
  .catch((error) => {
    // Handle / display error.
    // ...
  });

웹 버전 8

// Switch to TENANT_ID1
firebase.auth().tenantId = 'TENANT_ID1';

// Sign-in with popup
firebase.auth().signInWithPopup(provider)
  .then((result) => {
    // Existing user with e.g. SAML provider.
    const user = result.user;
    const emailCredential =
      firebase.auth.EmailAuthProvider.credential(email, password);
    return user.linkWithCredential(emailCredential);
  })
  .then((linkResult) => {
    // The user can sign in with both SAML and email/password now.
  });

기존 멀티 테넌트 사용자를 연결하거나 다시 인증할 때 auth.tenantId는 무시됩니다. user.tenantId를 사용하여 사용할 테넌트를 지정합니다. 이는 updateProfileupdatePassword와 같은 다른 사용자 관리 API에도 적용됩니다.

account-exists-with-different-credential 오류 처리

Google Cloud Console에서 동일한 이메일을 사용하는 계정 연결 설정을 사용 설정한 경우 사용자가 Google과 같은 다른 공급업체에 이미 있는 이메일로 SAML과 같은 공급업체에 로그인을 시도하면 AuthCredential 객체와 함께 auth/account-exists-with-different-credential 오류가 발생합니다.

사용자가 원하는 공급업체에 로그인하려면 먼저 Google 등 기존 공급업체에 로그인한 다음 이전 AuthCredential(SAML)에 연결해야 합니다.

팝업 또는 리디렉션 흐름을 사용하여 이 오류를 처리할 수 있습니다.

  • 팝업

    웹 버전 9

    import { signInWithPopup, fetchSignInMethodsForEmail, linkWithCredential } from "firebase/auth";
    // Step 1.
    // User tries to sign in to the SAML provider in that tenant.
    auth.tenantId = 'TENANT_ID';
    signInWithPopup(auth, samlProvider)
      .catch((error) => {
        // An error happened.
        if (error.code === 'auth/account-exists-with-different-credential') {
          // Step 2.
          // User's email already exists.
          // The pending SAML credential.
          const pendingCred = error.credential;
          // The credential's tenantId if needed: error.tenantId
          // The provider account's email address.
          const email = error.customData.email;
          // Get sign-in methods for this email.
          fetchSignInMethodsForEmail(email, auth)
            .then((methods) => {
              // Step 3.
              // Ask the user to sign in with existing Google account.
              if (methods[0] == 'google.com') {
                signInWithPopup(auth, googleProvider)
                  .then((result) => {
                    // Step 4
                    // Link the SAML AuthCredential to the existing user.
                    linkWithCredential(result.user, pendingCred)
                      .then((linkResult) => {
                        // SAML account successfully linked to the existing
                        // user.
                        goToApp();
                      });
                  });
              }
            });
        }
      });

    웹 버전 8

    // Step 1.
    // User tries to sign in to the SAML provider in that tenant.
    firebase.auth().tenantId = 'TENANT_ID';
    firebase.auth().signInWithPopup(samlProvider)
      .catch((error) => {
        // An error happened.
        if (error.code === 'auth/account-exists-with-different-credential') {
          // Step 2.
          // User's email already exists.
          // The pending SAML credential.
          const pendingCred = error.credential;
          // The credential's tenantId if needed: error.tenantId
          // The provider account's email address.
          const email = error.email;
          // Get sign-in methods for this email.
          firebase.auth().fetchSignInMethodsForEmail(email)
            .then((methods) => {
              // Step 3.
              // Ask the user to sign in with existing Google account.
              if (methods[0] == 'google.com') {
                firebase.auth().signInWithPopup(googleProvider)
                  .then((result) => {
                    // Step 4
                    // Link the SAML AuthCredential to the existing user.
                    result.user.linkWithCredential(pendingCred)
                      .then((linkResult) => {
                        // SAML account successfully linked to the existing
                        // user.
                        goToApp();
                      });
                  });
              }
            });
        }
      });
  • 리디렉션

    signInWithRedirect를 사용하는 경우 리디렉션 흐름을 완료하면 getRedirectResult에서 auth/account-exists-with-different-credential 오류가 발생합니다.

    오류 객체에는 error.tenantId 속성이 포함되어 있습니다. 리디렉션한 auth 인스턴스의 테넌트 ID는 유지되지 않으므로 오류 객체의 테넌트 ID를 auth 인스턴스로 설정해야 합니다.

    다음 예시는 오류를 처리하는 방법을 보여줍니다.

    웹 버전 9

    import { signInWithRedirect, getRedirectResult, fetchSignInMethodsForEmail, linkWithCredential } from "firebase/auth";
    // Step 1.
    // User tries to sign in to SAML provider.
    auth.tenantId = 'TENANT_ID';
    signInWithRedirect(auth, samlProvider);
    var pendingCred;
    // Redirect back from SAML IDP. auth.tenantId is null after redirecting.
    getRedirectResult(auth).catch((error) => {
      if (error.code === 'auth/account-exists-with-different-credential') {
        // Step 2.
        // User's email already exists.
        const tenantId = error.tenantId;
        // The pending SAML credential.
        pendingCred = error.credential;
        // The provider account's email address.
        const email = error.customData.email;
        // Need to set the tenant ID again as the page was reloaded and the
        // previous setting was reset.
        auth.tenantId = tenantId;
        // Get sign-in methods for this email.
        fetchSignInMethodsForEmail(auth, email)
          .then((methods) => {
            // Step 3.
            // Ask the user to sign in with existing Google account.
            if (methods[0] == 'google.com') {
              signInWithRedirect(auth, googleProvider);
            }
          });
      }
    });
    
    // Redirect back from Google. auth.tenantId is null after redirecting.
    getRedirectResult(auth).then((result) => {
      // Step 4
      // Link the SAML AuthCredential to the existing user.
      // result.user.tenantId is 'TENANT_ID'.
      linkWithCredential(result.user, pendingCred)
        .then((linkResult) => {
          // SAML account successfully linked to the existing
          // user.
          goToApp();
        });
    });

    웹 버전 8

    // Step 1.
    // User tries to sign in to SAML provider.
    firebase.auth().tenantId = 'TENANT_ID';
    firebase.auth().signInWithRedirect(samlProvider);
    var pendingCred;
    // Redirect back from SAML IDP. auth.tenantId is null after redirecting.
    firebase.auth().getRedirectResult().catch((error) => {
      if (error.code === 'auth/account-exists-with-different-credential') {
        // Step 2.
        // User's email already exists.
        const tenantId = error.tenantId;
        // The pending SAML credential.
        pendingCred = error.credential;
        // The provider account's email address.
        const email = error.email;
        // Need to set the tenant ID again as the page was reloaded and the
        // previous setting was reset.
        firebase.auth().tenantId = tenantId;
        // Get sign-in methods for this email.
        firebase.auth().fetchSignInMethodsForEmail(email)
          .then((methods) => {
            // Step 3.
            // Ask the user to sign in with existing Google account.
            if (methods[0] == 'google.com') {
              firebase.auth().signInWithRedirect(googleProvider);
            }
          });
      }
    });
    
    // Redirect back from Google. auth.tenantId is null after redirecting.
    firebase.auth().getRedirectResult().then((result) => {
      // Step 4
      // Link the SAML AuthCredential to the existing user.
      // result.user.tenantId is 'TENANT_ID'.
      result.user.linkWithCredential(pendingCred)
        .then((linkResult) => {
          // SAML account successfully linked to the existing
          // user.
          goToApp();
        });
    });

최종 사용자 계정 만들기 및 삭제 중지

사용자 작업을 통해 계정을 만드는 대신 관리자가 사용자 계정을 만들도록 하는 경우가 있습니다. 이러한 경우 REST API를 통해 사용자 작업을 중지할 수 있습니다.

curl --location --request PATCH 'https://identitytoolkit.googleapis.com/v2/projects/PROJECT_ID/tenants/TENANT_ID?updateMask=client' \
  --header 'Authorization: Bearer AUTH_TOKEN' \
  --header 'Content-Type: application/json' \
  --data-raw '{
    "client": {
        "permissions": {
            "disabled_user_signup": true,
            "disabled_user_deletion": true
        }
    }
}'

다음을 바꿉니다.

  • AUTH_TOKEN: 인증 토큰.
  • PROJECT_ID: 프로젝트 ID입니다.
  • TENANT_ID: 테넌트 ID.

다음 단계