การสวมรอยผู้ใช้ (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 ให้ใส่ข้อมูลรับรองใน header Authorization:
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('Access token สำหรับสวมรอย Alex:', accessToken);
} catch (error) {
console.error('สวมรอยไม่สำเร็จ:', error);
}
}
// เรียกใช้งานการสวมรอย
void performImpersonation();
- subject token มีอายุสั้นและใช้ได้เพียงครั้งเดียว
- access token สำหรับการสวมรอยจะไม่มี refresh token หาก token หมดอายุ 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) ซึ่งมีประโยชน์สำหรับการตรวจสอบและติดตามการสวมรอย
การปรับแต่ง token claims
Logto อนุญาตให้คุณ ปรับแต่ง token claims สำหรับ access token ที่ใช้ในการสวมรอย ซึ่งมีประโยชน์สำหรับการเพิ่มข้อมูลบริบทหรือ metadata เพิ่มเติม เช่น เหตุผลในการสวมรอย หรือหมายเลขตั๋วสนับสนุนที่เกี่ยวข้อง
เมื่อเซิร์ฟเวอร์ของ TechCorp ขอ subject token จาก Logto Management API สามารถส่ง context object ได้ดังนี้:
{
"userId": "alex123",
"context": {
"ticketId": "TECH-1234",
"reason": "Resource access issue",
"supportEngineerId": "sarah789"
}
}
context นี้สามารถนำไปใช้ในฟังก์ชัน getCustomJwtClaims() เพื่อเพิ่ม claim เฉพาะลงใน 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"
}
// ... claim มาตรฐานอื่น ๆ
}
ด้วยการปรับแต่ง access token claims แบบนี้ TechCorp สามารถใส่ข้อมูลสำคัญเกี่ยวกับบริบทของการสวมรอย ช่วยให้ตรวจสอบและเข้าใจการดำเนินการสวมรอยในระบบได้ง่ายขึ้น
โปรดระวังเมื่อเพิ่ม custom claim ลงใน token ของคุณ หลีกเลี่ยงการใส่ข้อมูลสำคัญที่อาจก่อให้เกิดความเสี่ยงด้านความปลอดภัยหาก token ถูกดักจับหรือรั่วไหล JWT จะถูกเซ็นชื่อแต่ไม่ถูกเข้ารหัส ดังนั้น claim ทั้งหมดจะมองเห็นได้สำหรับผู้ที่เข้าถึง token
แหล่งข้อมูลที่เกี่ยวข้อง
การสวมรอยในโลกไซเบอร์และการจัดการเอกลักษณ์คืออะไร? เอเจนต์ AI ใช้งานได้อย่างไร?