การสวมรอยผู้ใช้ (User impersonation)
ลองจินตนาการว่า Sarah วิศวกรฝ่ายสนับสนุนที่ TechCorp ได้รับทิกเก็ตเร่งด่วนจาก Alex ลูกค้าที่ไม่สามารถเข้าถึงทรัพยากรสำคัญได้ เพื่อวินิจฉัยและแก้ไขปัญหาอย่างมีประสิทธิภาพ Sarah จำเป็นต้องเห็นสิ่งเดียวกับที่ Alex เห็นในระบบ นี่คือจุดที่ฟีเจอร์การสวมรอยผู้ใช้ของ Logto มีประโยชน์อย่างยิ่ง
การสวมรอยผู้ใช้ช่วยให้ผู้ใช้ที่ได้รับอนุญาต เช่น Sarah สามารถดำเนินการในระบบแทนผู้ใช้อื่นชั่วคราว เช่น Alex ฟีเจอร์นี้มีประโยชน์มากสำหรับการแก้ไขปัญหา การให้บริการลูกค้า และการดำเนินงานด้านผู้ดูแลระบบ
ทำงานอย่างไร?
กระบวนการสวมรอยประกอบด้วย 3 ขั้นตอนหลัก:
- Sarah ขอสิทธิ์สวมรอยผ่านเซิร์ฟเวอร์ backend ของ TechCorp
- เซิร์ฟเวอร์ของ TechCorp ขอ subject token จาก Logto Management API
- แอปของ Sarah แลก subject token นี้เป็น access token
มาดูกันว่า Sarah จะใช้ฟีเจอร์นี้ช่วย Alex ได้อย่างไร
ขั้นตอนที่ 1: ขอสิทธิ์สวมรอย
ก่อนอื่น แอปสนับสนุนของ Sarah ต้องขอสิทธิ์สวมรอยจากเซิร์ฟเวอร์ backend ของ TechCorp
Request (แอปของ 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 นี้ backend ควรตรวจสอบการอนุญาตอย่างเหมาะสมเพื่อให้แน่ใจว่า Sarah มีสิทธิ์สวมรอย Alex
ขั้นตอนที่ 2: ขอ subject token
เมื่อเซิร์ฟเวอร์ของ TechCorp ตรวจสอบคำขอของ Sarah แล้ว จะเรียก Management API ของ Logto เพื่อขอ subject token
Request (เซิร์ฟเวอร์ของ 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"
}
}
Response (Logto ไปยังเซิร์ฟเวอร์ของ TechCorp)
{
"subjectToken": "sub_7h32jf8sK3j2",
"expiresIn": 600
}
จากนั้นเซิร์ฟเวอร์ของ TechCorp ควรส่ง subject token นี้กลับไปยังแอปของ Sarah
Response (เซิร์ฟเวอร์ของ TechCorp ไปยังแอปของ Sarah)
{
"subjectToken": "sub_7h32jf8sK3j2",
"expiresIn": 600
}
ขั้นตอนที่ 3: แลก subject token เป็น access token
ก่อนใช้งาน token exchange grant คุณต้องเปิดใช้งานสำหรับแอปพลิเคชันของคุณก่อน:
- ไปที่ Console > Applications และเลือกแอปพลิเคชันของคุณ
- ในหน้าการตั้งค่าแอปพลิเคชัน ให้ค้นหาส่วน "Token exchange"
- เปิดสวิตช์ "Allow token exchange"
Token exchange ถูกปิดใช้งานโดยค่าเริ่มต้นเพื่อเหตุผลด้านความปลอดภัย หากคุณไม่เปิดใช้งาน คุณจะได้รับข้อผิดพลาดว่า "token exchange is not allowed for this application"
ตอนนี้แอปของ Sarah จะแลก subject token นี้เป็น access token ที่แทนตัว Alex โดยระบุ resource ที่จะใช้ token นี้
Request (แอปของ Sarah ไปยัง Logto token endpoint)
สำหรับเว็บแอปแบบดั้งเดิมหรือแอป machine-to-machine ที่มี app secret ให้ใส่ข้อมูลรับรองใน Authorization header:
POST /oidc/token HTTP/1.1
Host: techcorp.logto.app
Content-Type: application/x-www-form-urlencoded
Authorization: Basic <base64(client_id:client_secret)>
grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&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
สำหรับ single-page application (SPA) หรือ native application ที่ไม่มี app secret ให้ใส่ client_id ใน request body:
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
Response (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 ที่ได้รับจะถูกผูกกับ resource ที่ระบุไว้ ทำให้สามารถใช้ได้เฉพาะกับ API ข้อมูลลูกค้าของ TechCorp เท่านั้น
ตัวอย่างการใช้งาน
ตัวอย่างการใช้งานในแอป Node.js ฝ่ายสนับสนุนของ Sarah:
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,
clientSecret?: string // จำเป็นสำหรับเว็บแอปแบบดั้งเดิมหรือแอป machine-to-machine
): 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. สถานะ: ${impersonationResponse.status}`);
}
const { subjectToken } = (await impersonationResponse.json()) as ImpersonationResponse;
// ขั้นตอนที่ 3: แลก subject token เป็น access token
// สำหรับเว็บแอปแบบดั้งเดิมหรือ M2M ใช้ Basic auth พร้อม client secret
// สำหรับ SPA หรือ native app ให้ใส่ client_id ใน body
const headers: Record<string, string> = {
'Content-Type': 'application/x-www-form-urlencoded',
};
const tokenExchangeBody = new URLSearchParams({
grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange',
scope: 'openid profile resource.read',
subject_token: subjectToken,
subject_token_type: 'urn:ietf:params:oauth:token-type:access_token',
resource: resource,
});
if (clientSecret) {
// Confidential client: ใช้ Basic auth
headers['Authorization'] =
`Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`;
} else {
// Public client: ใส่ client_id ใน body
tokenExchangeBody.append('client_id', clientId);
}
const tokenExchangeResponse = await fetch('https://techcorp.logto.app/oidc/token', {
method: 'POST',
headers,
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('การสวมรอยล้มเหลว:', error);
throw error;
}
}
// Sarah ใช้ฟังก์ชันนี้เพื่อสวมรอย Alex
async function performImpersonation(): Promise<void> {
try {
// สำหรับเว็บแอปแบบดั้งเดิมหรือ M2M ให้ส่ง client secret
const accessToken = await impersonateUser(
'alex123',
'techcorp_support_app',
'TECH-1234',
'https://api.techcorp.com/customer-data',
'your-client-secret' // ไม่ต้องใส่สำหรับ SPA หรือ native app
);
console.log('Impersonation access token สำหรับ Alex:', accessToken);
} catch (error) {
console.error('สวมรอยไม่สำเร็จ:', error);
}
}
// เรียกใช้งานการสวมรอย
void performImpersonation();
- subject token มีอายุสั้นและใช้ได้ครั้งเดียว
- access token สำหรับการสวมรอยจะไม่มี refresh token หาก token หมดอายุก่อนที่ Sarah จะแก้ปัญหาให้ Alex เสร็จ Sarah ต้องดำเนินการขั้นตอนนี้ใหม่
- เซิร์ฟเวอร์ backend ของ TechCorp ต้องตรวจสอบการอนุญาตอย่างเหมาะสมเพื่อให้แน่ใจว่าเฉพาะเจ้าหน้าที่สนับสนุนที่ได้รับอนุญาต เช่น Sarah เท่านั้นที่สามารถขอสวมรอยได้
act claim
เมื่อใช้ token exchange flow สำหรับการสวมรอย access token ที่ออกให้สามารถมี act (actor) claim เพิ่มเติมได้ claim นี้แสดงตัวตนของ “ผู้ดำเนินการ” — ในตัวอย่างนี้คือ Sarah ผู้ที่กำลังสวมรอย
เพื่อให้มี act claim แอปของ Sarah ต้องส่ง actor_token ในคำขอ token exchange โดย token นี้ควรเป็น access token ที่ถูกต้องของ Sarah พร้อมขอบเขต openid ตัวอย่างการใส่ในคำขอ:
สำหรับเว็บแอปแบบดั้งเดิมหรือแอป machine-to-machine:
POST /oidc/token HTTP/1.1
Host: techcorp.logto.app
Content-Type: application/x-www-form-urlencoded
Authorization: Basic <base64(client_id:client_secret)>
grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&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
สำหรับ SPA หรือ native app ให้ใส่ client_id ใน body แทน:
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 มีประโยชน์สำหรับการตรวจสอบย้อนหลังและติดตามการสวมรอย
การปรับแต่ง token claims
Logto อนุญาตให้คุณ ปรับแต่ง token claims สำหรับโทเค็นการสวมรอย ซึ่งมีประโยชน์สำหรับการเพิ่มข้อมูลบริบทหรือเมตาดาต้าเพิ่มเติม เช่น เหตุผลในการสวมรอยหรือหมายเลขทิกเก็ตสนับสนุน
เมื่อเซิร์ฟเวอร์ของ TechCorp ขอ subject token จาก Logto Management API สามารถส่ง context object ได้ดังนี้:
{
"userId": "alex123",
"context": {
"ticketId": "TECH-1234",
"reason": "Resource access issue",
"supportEngineerId": "sarah789"
}
}
context นี้สามารถนำไปใช้ในฟังก์ชัน getCustomJwtClaims() เพื่อเพิ่ม claims เฉพาะลงใน access token สุดท้าย ตัวอย่างเช่น:
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 {};
};
access token ที่ Sarah ได้รับอาจมีลักษณะดังนี้:
{
"sub": "alex123",
"aud": "https://api.techcorp.com/customer-data",
"impersonation_context": {
"ticket_id": "TECH-1234",
"reason": "Resource access issue",
"support_engineer": "sarah789"
}
// ... claims มาตรฐานอื่น ๆ
}
ด้วยการปรับแต่ง claims ของ access token แบบนี้ TechCorp สามารถใส่ข้อมูลสำคัญเกี่ยวกับบริบทการสวมรอย ช่วยให้ตรวจสอบและเข้าใจการสวมรอยในระบบได้ง่ายขึ้น
โปรดระมัดระวังเมื่อเพิ่ม custom claims ลงในโทเค็น หลีกเลี่ยงการใส่ข้อมูลสำคัญที่อาจก่อให้เกิดความเสี่ยงด้านความปลอดภัยหากโทเค็นถูกดักจับหรือรั่วไหล JWT จะถูกเซ็นชื่อแต่ไม่ถูกเข้ารหัส ดังนั้น claims จะมองเห็นได้สำหรับผู้ที่เข้าถึงโทเค็น
แหล่งข้อมูลที่เกี่ยวข้อง
การสวมรอยในโลกไซเบอร์และการจัดการข้อมูลระบุตัวตนคืออะไร? เอเจนต์ AI ใช้งานได้อย่างไร?