사용자 가장 (User impersonation)
TechCorp의 지원 엔지니어 Sarah가 Alex라는 고객으로부터 중요한 리소스에 접근할 수 없다는 긴급 티켓을 받았다고 상상해 보세요. 문제를 효율적으로 진단하고 해결하기 위해 Sarah는 시스템에서 Alex가 보는 것과 똑같이 볼 필요가 있습니다. 이럴 때 Logto의 사용자 가장 (User impersonation) 기능이 유용하게 사용됩니다.
사용자 가장 (User impersonation)은 Sarah와 같은 인가 (Authorization)된 사용자가 시스템 내에서 Alex와 같은 다른 사용자를 대신하여 임시로 행동할 수 있도록 해줍니다. 이 강력한 기능은 문제 해결, 고객 지원, 관리 작업 수행에 매우 유용합니다.
어떻게 동작하나요?
가장 (Impersonation) 프로세스는 세 가지 주요 단계로 이루어집니다:
- Sarah가 TechCorp의 백엔드 서버를 통해 가장 (Impersonation)을 요청합니다.
- TechCorp의 서버가 Logto의 Management API에서 subject 토큰을 획득합니다.
- Sarah의 애플리케이션이 이 subject 토큰을 액세스 토큰으로 교환합니다.
Sarah가 이 기능을 사용하여 Alex를 도울 수 있는 방법을 살펴보겠습니다.
1단계: 가장 (Impersonation) 요청하기
먼저, Sarah의 지원 애플리케이션이 TechCorp의 백엔드 서버에 가장 (Impersonation)을 요청해야 합니다.
요청 (Sarah의 애플리케이션 → TechCorp의 서버)
POST /api/request-impersonation HTTP/1.1
Host: api.techcorp.com
Authorization: Bearer <Sarah's_access_token>
Content-Type: application/json
{
"userId": "alex123",
"reason": "리소스 접근 문제 조사",
"ticketId": "TECH-1234"
}
이 API에서 백엔드는 Sarah가 Alex를 가장할 수 있는 적절한 권한 (Permission)이 있는지 인가 (Authorization) 검사를 수행해야 합니다.
2단계: subject 토큰 획득하기
Sarah의 요청을 검증한 후, TechCorp의 서버는 Logto의 Management API에 subject 토큰을 요청합니다.
요청 (TechCorp의 서버 → Logto Management API)
POST /api/subject-tokens HTTP/1.1
Host: techcorp.logto.app
Authorization: Bearer <TechCorp_m2m_access_token>
Content-Type: application/json
{
"userId": "alex123",
"context": {
"ticketId": "TECH-1234",
"reason": "리소스 접근 문제",
"supportEngineerId": "sarah789"
}
}
응답 (Logto → TechCorp의 서버)
{
"subjectToken": "sub_7h32jf8sK3j2",
"expiresIn": 600
}
TechCorp의 서버는 이 subject 토큰을 Sarah의 애플리케이션에 반환해야 합니다.
응답 (TechCorp의 서버 → Sarah의 애플리케이션)
{
"subjectToken": "sub_7h32jf8sK3j2",
"expiresIn": 600
}
3단계: subject 토큰을 액세스 토큰으로 교환하기
이제 Sarah의 애플리케이션은 이 subject 토큰을 Alex를 대표하는 액세스 토큰으로 교환하며, 토큰이 사용될 리소스를 명시합니다.
요청 (Sarah의 애플리케이션 → Logto 토큰 엔드포인트)
POST /oidc/token HTTP/1.1
Host: techcorp.logto.app
Content-Type: application/x-www-form-urlencoded
grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&client_id=techcorp_support_app
&scope=resource:read
&subject_token=alx_7h32jf8sK3j2
&subject_token_type=urn:ietf:params:oauth:token-type:access_token
&resource=https://api.techcorp.com/customer-data
응답 (Logto → Sarah의 애플리케이션)
{
"access_token": "eyJhbG...<truncated>",
"issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "resource:read"
}
반환된 access_token
은 지정된 리소스에 바인딩되어 있으므로 TechCorp의 고객 데이터 API에서만 사용할 수 있습니다.
참고: 전통적인 웹 애플리케이션의 경우, 토큰 요청 헤더에 client_id
와 client_secret
을 포함해야 401 invalid_client 오류를 방지할 수 있습니다.
Node.js 예시:
Authorization: `Basic ${Buffer.from(`${client_id}:${client_secret}`, 'utf8').toString('base64')}`
사용 예시
Sarah가 Node.js 지원 애플리케이션에서 이 기능을 사용하는 방법은 다음과 같습니다:
interface ImpersonationResponse {
subjectToken: string;
expiresIn: number;
}
interface TokenExchangeResponse {
access_token: string;
issued_token_type: string;
token_type: string;
expires_in: number;
scope: string;
}
async function impersonateUser(
userId: string,
clientId: string,
ticketId: string,
resource: string
): Promise<string> {
try {
// 1, 2단계: 가장 (Impersonation) 요청 및 subject 토큰 획득
const impersonationResponse = await fetch(
'https://api.techcorp.com/api/request-impersonation',
{
method: 'POST',
headers: {
Authorization: "Bearer <Sarah's_access_token>",
'Content-Type': 'application/json',
},
body: JSON.stringify({
userId,
reason: '리소스 접근 문제 조사',
ticketId,
}),
}
);
if (!impersonationResponse.ok) {
throw new Error(`HTTP 오류 발생. 상태: ${impersonationResponse.status}`);
}
const { subjectToken } = (await impersonationResponse.json()) as ImpersonationResponse;
// 3단계: subject 토큰을 액세스 토큰으로 교환
const tokenExchangeBody = new URLSearchParams({
grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange',
client_id: clientId,
scope: 'openid profile resource.read',
subject_token: subjectToken,
subject_token_type: 'urn:ietf:params:oauth:token-type:access_token',
resource: resource,
});
const tokenExchangeResponse = await fetch('https://techcorp.logto.app/oidc/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: tokenExchangeBody,
});
if (!tokenExchangeResponse.ok) {
throw new Error(`HTTP 오류! 상태: ${tokenExchangeResponse.status}`);
}
const tokenData = (await tokenExchangeResponse.json()) as TokenExchangeResponse;
return tokenData.access_token;
} catch (error) {
console.error('가장 (Impersonation) 실패:', error);
throw error;
}
}
// Sarah가 이 함수를 사용해 Alex를 가장합니다
async function performImpersonation(): Promise<void> {
try {
const accessToken = await impersonateUser(
'alex123',
'techcorp_support_app',
'TECH-1234',
'https://api.techcorp.com/customer-data'
);
console.log('Alex를 위한 가장 (Impersonation) 액세스 토큰:', accessToken);
} catch (error) {
console.error('가장 (Impersonation) 수행 실패:', error);
}
}
// 가장 (Impersonation) 실행
void performImpersonation()
- subject 토큰은 단기 유효하며 1회용입니다.
- 가장 (Impersonation) 액세스 토큰에는 리프레시 토큰 (Refresh token)이 포함되지 않습니다. 만약 토큰이 만료되기 전에 Alex의 문제가 해결되지 않았다면 Sarah는 이 과정을 반복해야 합니다.
- TechCorp의 백엔드 서버는 Sarah와 같은 인가 (Authorization)된 지원 인력만 가장 (Impersonation)을 요청할 수 있도록 적절한 인가 (Authorization) 검사를 구현해야 합니다.
act
클레임 (Claim)
가장 (Impersonation)을 위한 토큰 교환 플로우를 사용할 때, 발급된 액세스 토큰에는 추가적으로 act
(actor) 클레임이 포함될 수 있습니다. 이 클레임은 "행동 주체"의 아이덴티티를 나타냅니다. 즉, 예시에서는 Sarah가 Alex를 대신해 행동하고 있음을 의미합니다.
act
클레임을 포함하려면, Sarah의 애플리케이션이 토큰 교환 요청에 actor_token
을 제공해야 합니다. 이 토큰은 openid
스코프가 포함된 Sarah의 유효한 액세스 토큰이어야 합니다. 요청 예시는 다음과 같습니다:
POST /oidc/token HTTP/1.1
Host: techcorp.logto.app
Content-Type: application/x-www-form-urlencoded
grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&client_id=techcorp_support_app
&scope=resource:read
&subject_token=alx_7h32jf8sK3j2
&subject_token_type=urn:ietf:params:oauth:token-type:access_token
&actor_token=sarah_access_token
&actor_token_type=urn:ietf:params:oauth:token-type:access_token
&resource=https://api.techcorp.com/customer-data
actor_token
이 제공되면, 결과 액세스 토큰에는 다음과 같은 act
클레임이 포함됩니다:
{
"aud": "https://api.techcorp.com",
"iss": "https://techcorp.logto.app",
"exp": 1443904177,
"sub": "alex123",
"act": {
"sub": "sarah789"
}
}
이 act
클레임은 Sarah (sarah789)가 Alex (alex123)를 대신해 행동하고 있음을 명확히 나타냅니다. act
클레임은 가장 (Impersonation) 행위의 감사 및 추적에 유용합니다.
토큰 클레임 (Claim) 커스터마이징
Logto는 가장 (Impersonation) 토큰에 대해 토큰 클레임 (Claim) 커스터마이징을 지원합니다. 이를 통해 가장 (Impersonation) 과정에 추가적인 컨텍스트나 메타데이터(예: 가장 사유, 지원 티켓 등)를 포함할 수 있습니다.
TechCorp의 서버가 Logto Management API에 subject 토큰을 요청할 때, context
객체를 포함할 수 있습니다:
{
"userId": "alex123",
"context": {
"ticketId": "TECH-1234",
"reason": "리소스 접근 문제",
"supportEngineerId": "sarah789"
}
}
이 context는 getCustomJwtClaims()
함수에서 최종 액세스 토큰에 특정 클레임을 추가하는 데 사용할 수 있습니다. 구현 예시는 다음과 같습니다:
const getCustomJwtClaims = async ({ token, context, environmentVariables }) => {
if (context.grant?.type === 'urn:ietf:params:oauth:grant-type:token-exchange') {
const { ticketId, reason, supportEngineerId } = context.grant.subjectTokenContext;
return {
impersonation_context: {
ticket_id: ticketId,
reason: reason,
support_engineer: supportEngineerId,
},
};
}
return {};
};
Sarah가 받는 최종 액세스 토큰은 다음과 같이 보일 수 있습니다:
{
"sub": "alex123",
"aud": "https://api.techcorp.com/customer-data",
"impersonation_context": {
"ticket_id": "TECH-1234",
"reason": "리소스 접근 문제",
"support_engineer": "sarah789"
}
// ... 기타 표준 클레임
}
이처럼 액세스 토큰의 클레임을 커스터마이징하면, TechCorp는 가장 (Impersonation) 컨텍스트에 대한 유용한 정보를 포함시켜 시스템 내에서 가장 (Impersonation) 활동을 감사하고 이해하기 쉽게 만들 수 있습니다.
토큰에 커스텀 클레임을 추가할 때는 주의하세요. 토큰이 탈취되거나 유출될 경우 보안 위험이 될 수 있는 민감한 정보를 포함하지 마세요. JWT는 서명되어 있지만 암호화되어 있지 않으므로, 토큰에 접근할 수 있는 누구나 클레임을 볼 수 있습니다.
관련 리소스
사이버보안 및 아이덴티티 관리에서 가장 (Impersonation)이란 무엇인가요? AI 에이전트가 이를 어떻게 사용할 수 있나요?