用户模拟 (User impersonation)
想象一下,TechCorp 的支持工程师 Sarah 收到了一条来自客户 Alex 的紧急工单,Alex 无法访问关键资源。为了高效地诊断和解决问题,Sarah 需要看到系统中 Alex 所看到的一切。这时,Logto 的用户模拟 (User impersonation) 功能就派上用场了。
用户模拟 (User impersonation) 允许像 Sarah 这样的授权用户在系统中临时以其他用户(如 Alex)的身份进行操作。这一强大的功能对于故障排查、客户支持和执行管理任务非常有价值。
工作原理?
模拟流程包含三个主要步骤:
- Sarah 通过 TechCorp 的后端服务器请求模拟
- TechCorp 的服务器从 Logto 的 Management API 获取 subject token
- Sarah 的应用用这个 subject token 换取访问令牌 (Access token)
让我们一起看看 Sarah 如何利用这个功能帮助 Alex。
步骤 1:请求模拟
首先,Sarah 的支持应用需要向 TechCorp 的后端服务器请求模拟。
请求(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": "Investigating resource access issue",
"ticketId": "TECH-1234"
}
在这个 API 中,后端应进行适当的授权 (Authorization) 检查,确保 Sarah 拥有模拟 Alex 的必要权限。
步骤 2:获取 subject token
TechCorp 的服务器在验证 Sarah 的请求后,会调用 Logto 的 Management API 获取 subject token。
请求(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": "Resource access issue",
"supportEngineerId": "sarah789"
}
}
响应(Logto 到 TechCorp 的服务器)
{
"subjectToken": "sub_7h32jf8sK3j2",
"expiresIn": 600
}
TechCorp 的服务器随后应将此 subject token 返回给 Sarah 的应用。
响应(TechCorp 的服务器到 Sarah 的应用)
{
"subjectToken": "sub_7h32jf8sK3j2",
"expiresIn": 600
}
步骤 3:用 subject token 换取访问令牌 (Access token)
现在,Sarah 的应用用这个 subject token 换取代表 Alex 的访问令牌 (Access token),并指定该令牌将被用于哪个资源。
请求(Sarah 的应用到 Logto token endpoint)
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。
注意:对于传统 Web 应用,在令牌请求的 header 中包含 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:请求模拟并获取 subject token
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: 'Investigating resource access issue',
ticketId,
}),
}
);
if (!impersonationResponse.ok) {
throw new Error(`HTTP error occurred. Status: ${impersonationResponse.status}`);
}
const { subjectToken } = (await impersonationResponse.json()) as ImpersonationResponse;
// 步骤 3:用 subject token 换取访问令牌 (Access token)
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 error! status: ${tokenExchangeResponse.status}`);
}
const tokenData = (await tokenExchangeResponse.json()) as TokenExchangeResponse;
return tokenData.access_token;
} catch (error) {
console.error('Impersonation failed:', 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('Impersonation access token for Alex:', accessToken);
} catch (error) {
console.error('Failed to perform impersonation:', error);
}
}
// 执行模拟
void performImpersonation()
- subject token 是短时且一次性使用的。
- 模拟访问令牌 (Access token) 不会携带 刷新令牌 (Refresh token)。如果令牌在 Sarah 解决 Alex 问题前过期,她需要重复此流程。
- TechCorp 的后端服务器必须实现适当的授权 (Authorization) 检查,确保只有像 Sarah 这样的授权支持人员才能请求模拟。
act
声明 (Claim)
在使用令牌交换流程进行模拟时,签发的访问令牌 (Access token) 可以包含额外的 act
(actor)声明 (Claim)。该声明 (Claim) 表示“执行方”的身份——在我们的例子中,就是执行模拟操作的 Sarah。
要包含 act
声明 (Claim),Sarah 的应用需要在令牌交换请求中提供一个 actor_token
。该令牌应为带有 openid
权限 (Scope) 的 Sarah 的有效访问令牌 (Access token)。以下是如何在令牌交换请求中包含它:
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
,最终的访问令牌 (Access token) 会包含如下的 act
声明 (Claim):
{
"aud": "https://api.techcorp.com",
"iss": "https://techcorp.logto.app",
"exp": 1443904177,
"sub": "alex123",
"act": {
"sub": "sarah789"
}
}
这个 act
声明 (Claim) 清楚地表明 Sarah(sarah789)正在以 Alex(alex123)的身份操作。act
声明 (Claim) 对于审计和追踪模拟操作非常有用。
自定义令牌声明 (Claims)
Logto 允许你为模拟令牌 自定义令牌声明 (Claims)。这对于在模拟过程中添加额外的上下文或元数据(如模拟原因或关联的支持工单)非常有用。
当 TechCorp 的服务器向 Logto Management API 请求 subject token 时,可以包含一个 context
对象:
{
"userId": "alex123",
"context": {
"ticketId": "TECH-1234",
"reason": "Resource access issue",
"supportEngineerId": "sarah789"
}
}
这个 context 可以在 getCustomJwtClaims()
函数中用于向最终访问令牌 (Access token) 添加特定声明 (Claims)。以下是实现示例:
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 获得的最终访问令牌 (Access token) 可能如下所示:
{
"sub": "alex123",
"aud": "https://api.techcorp.com/customer-data",
"impersonation_context": {
"ticket_id": "TECH-1234",
"reason": "Resource access issue",
"support_engineer": "sarah789"
}
// ... 其他标准声明 (Claims)
}
通过这种方式自定义访问令牌 (Access token) 声明 (Claims),TechCorp 可以在令牌中包含有关模拟上下文的有价值信息,使其更易于审计和理解系统中的模拟活动。
在为令牌添加自定义声明 (Claims) 时要谨慎。避免包含敏感信息,以防令牌被截获或泄露带来安全风险。JWT 虽然已签名但未加密,任何获得令牌的人都能看到声明 (Claims) 内容。
相关资源
什么是网络安全和身份管理中的模拟 (Impersonation)?AI agent 如何使用它?