การพัฒนาตัวเชื่อมต่อ (Implement connectors)
หลังจากดูโครงสร้างไฟล์ของตัวเชื่อมต่อแล้ว มาทำความเข้าใจแนวคิดหลักและวิธีการพัฒนาตัวเชื่อมต่อกัน
เราจะยกตัวอย่างที่เข้าใจง่ายสำหรับตัวเชื่อมต่อโซเชียลและแบบไม่ใช้รหัสผ่าน (passwordless) เพื่อให้คุณสามารถสร้างตัวเชื่อมต่อของคุณเองได้ด้วยแนวคิดเดียวกันเกือบทั้งหมด
ในส่วนนี้ เราจะไม่ลงลึกในรายละเอียดของพารามิเตอร์เฉพาะ (เช่น config
) เนื่องจากไม่ใช่จุดประสงค์ของคู่มือนี้ นักพัฒนาที่จะสร้างตัวเชื่อมต่อใหม่ควรอ่านเอกสารจากผู้ให้บริการภายนอก ซึ่งจะอธิบายรายละเอียดของพารามิเตอร์ต่าง ๆ ไว้อย่างครบถ้วน
สร้างตัวเชื่อมต่อโซเชียล
ขอยกตัวอย่างตัวเชื่อมต่อ GitHub
โฟลว์การอนุญาตของตัวเชื่อมต่อโซเชียลส่วนใหญ่จะเป็นไปตาม OAuth Authorization Code Flow
สำหรับ ตัวเชื่อมต่อโซเชียล ส่วนใหญ่ การดึงข้อมูลโปรไฟล์ผู้ใช้ด้วยการยืนยันตัวตนของผู้ใช้ปลายทางจะเป็นไปตามขั้นตอนสองขั้น (สมมติว่าทุกขั้นสำเร็จ):
- เริ่มคำขอการยืนยันตัวตนและรับการยืนยันตัวตนของผู้ใช้
- ดึง
accessToken
โดยใช้authCode
ที่ได้รับจากผู้ให้บริการตัวเชื่อมต่อ - ขอข้อมูลโปรไฟล์ผู้ใช้ที่เข้าถึงได้สาธารณะโดยใช้
accessToken
เพื่อให้โฟลว์นี้สมบูรณ์ เราต้องมีเมธอดหลัก 3 ตัวดังนี้
getAuthorizationUri
getAuthorizationUri
สร้าง URL สำหรับเปลี่ยนเส้นทางผู้ใช้ไปยังหน้าที่ต้องการการยืนยันตัวตน
อินเทอร์เฟซนี้ถูกกำหนดเป็น GetAuthorizationUri
ใน @logto/connector-kit
คุณสามารถจัดเก็บข้อมูลสำคัญที่เกี่ยวข้องกับการเข้าสู่ระบบโดยใช้ setSession
(พารามิเตอร์ตัวที่สองของ GetAuthorizationUri
) เพื่อใช้ในเมธอด getUserInfo
พารามิเตอร์ที่จำเป็นมีดังนี้:
authorizationEndpoint
สามารถดูได้จากเอกสาร OAuth ของ GitHub ซึ่งเป็นหน้าที่ผู้ใช้ควรไปเพื่อยืนยันตัวตนconfig
ซึ่งรวมclientId
และclientSecret
ในกรณีของ GitHubstate
สตริงสุ่มเพื่อป้องกัน CSRFredirectUri
หน้าปลายทางหลังจากผู้ใช้ยืนยันตัวตนสำเร็จ
const getAuthorizationUri = async ({ state, redirectUri }) => {
const queryParameters = new URLSearchParams({
client_id: config.clientId, // `config` contains your GitHub application credential
redirect_uri: redirectUri,
state,
});
return `${authorizationEndpoint}?${queryParameters.toString()}`;
};
getAccessToken
getAccessToken
ใช้สำหรับรับ access token ด้วย authorization code ที่ออกหลังจากผู้ใช้ยืนยันตัวตนสำเร็จ
นอกจาก config
ที่กล่าวถึงใน getAuthorizationUri
แล้ว เรายังต้องการ:
- authorization
code
จากพารามิเตอร์ที่ถูกส่งมายังหน้า redirect accessTokenEndpoint
ซึ่งเป็น endpoint สำหรับขอ access token ด้วย authorization code
const getAccessToken = async (config: GithubConfig, code: string) => {
const { clientId: client_id, clientSecret: client_secret } = config;
const httpResponse = await got.post({
url: accessTokenEndpoint,
json: {
client_id,
client_secret,
code,
},
timeout: defaultTimeout,
});
const result = accessTokenResponseGuard.safeParse(qs.parse(httpResponse.body));
if (!result.success) {
throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, result.error);
}
const { access_token: accessToken } = result.data;
assert(accessToken, new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid));
return { accessToken };
};
getUserInfo
getUserInfo
ใช้สำหรับดึงข้อมูลผู้ใช้ด้วย access token ที่ได้จากขั้นตอนก่อนหน้า
อินเทอร์เฟซนี้ถูกกำหนดเป็น GetUserInfo
ใน @logto/connector-kit
สำหรับการเข้าสู่ระบบ คุณสามารถดึงข้อมูลที่จำเป็นโดยใช้ฟังก์ชัน getSession
userInfoEndpoint
คือ endpoint ที่ใช้สำหรับดึงข้อมูลผู้ใช้
คุณควรตรวจสอบเอกสารทางการเพื่อดูว่าข้อมูลผู้ใช้ใดบ้างที่เข้าถึงได้ที่ user info endpoint และขอบเขต (scope) ที่เกี่ยวข้อง
id
ที่ได้รับมอบหมายโดยผู้ให้บริการข้อมูลระบุตัวตน (ในกรณีนี้คือ GitHub) เป็นข้อมูลที่จำเป็น; ข้อมูลอื่นเป็นทางเลือก หากคุณสามารถดึง email
หรือ phone
จากโปรไฟล์ผู้ใช้ได้ โปรดตรวจสอบให้แน่ใจว่าข้อมูลเหล่านั้น “ได้รับการยืนยัน” แล้ว และควรจัดชื่อคีย์ที่ส่งกลับให้ตรงกับฟิลด์ใน Logto user profile
const getUserInfo = async (
data: { code: string; config: GithubConfig },
getSession: GetSession,
{ set: SetStorageValue, get: GetStorageValue }
) => {
const { code, config } = data;
const { accessToken } = await getAccessToken(config, code);
try {
const httpResponse = await got.get(userInfoEndpoint, {
headers: {
authorization: `token ${accessToken}`,
},
timeout: defaultTimeout,
});
const result = userInfoResponseGuard.safeParse(parseJson(httpResponse.body));
if (!result.success) {
throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, result.error);
}
const { id, avatar_url: avatar, email, name } = result.data;
return {
id: String(id),
avatar: conditional(avatar),
email: conditional(email),
name: conditional(name),
};
} catch (error: unknown) {
if (error instanceof HTTPError) {
const { statusCode, body: rawBody } = error.response;
if (statusCode === 401) {
throw new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid);
}
throw new ConnectorError(ConnectorErrorCodes.General, JSON.stringify(rawBody));
}
throw error;
}
};
คุณสามารถดูตัวอย่างการใช้งานเต็มรูปแบบได้ที่ ที่นี่
สำหรับรายละเอียดเพิ่มเติมเกี่ยวกับพารามิเตอร์ที่กำหนดค่าได้ ดูที่ README ของตัวเชื่อมต่อ GitHub หรือเอกสารทางการของ GitHub
ตัวอย่างที่กล่าวถึงนี้อ้างอิงจาก Authorization Code grant type ของโปรโตคอล OAuth ซึ่งใช้ในตัวเชื่อมต่อ GitHub ของ Logto อย่างไรก็ตาม ยังมี grant type อื่น เช่น Implicit grant type ที่สามารถใช้ดึงโปรไฟล์ผู้ใช้ได้เช่นกัน และจะให้ access_token โดยตรงใน response ของการยืนยันตัวตน แม้จะสะดวกกว่า แต่โดยทั่วไปแนะนำให้ใช้ Authorization Code grant type มากกว่า Implicit type เนื่องจากมีความปลอดภัยสูงกว่า
คุณยังสามารถสร้างตัวเชื่อมต่อโดยอิงกับ OIDC หรือโปรโตคอลเปิดอื่น ๆ ได้ ขึ้นอยู่กับกรณีการใช้งานและความเข้ากันได้ของผู้ให้บริการโซเชียลที่คุณต้องการเชื่อมต่อ
สร้างตัวเชื่อมต่อแบบไม่ใช้รหัสผ่าน (Passwordless)
มาดูตัวอย่างการพัฒนาตัวเชื่อมต่อ Aliyun direct mail เพื่อเข้าใจกระบวนการสร้างตัวเชื่อมต่อแบบไม่ใช้รหัสผ่าน
ตัวเชื่อมต่อแบบไม่ใช้รหัสผ่านใช้สำหรับส่งรหัสสุ่มไปยังอีเมลหรือโทรศัพท์ของผู้ใช้ปลายทาง ดังนั้นจึงต้องมีเมธอด sendMessage
sendMessage
เพื่อส่งข้อความ เราต้องตั้งค่า config
และ endpoint
ให้ถูกต้อง
endpoint
คือ endpoint ที่ API ของคุณจะเชื่อมต่อไปconfig
ประกอบด้วยtemplates
(เทมเพลตเนื้อหาสำหรับส่งรหัสผ่านในแต่ละ flow),clientId
และclientSecret
(สำหรับเข้าถึง API)
const sendMessage = async (data, inputConfig) => {
const { to, type, payload } = data;
const config = inputConfig ?? (await getConfig(defaultMetadata.id));
validateConfig<AliyunDmConfig>(config, aliyunDmConfigGuard);
const { accessKeyId, accessKeySecret, accountName, fromAlias, templates } = config;
const template = templates.find((template) => template.usageType === type);
assert(
template,
new ConnectorError(
ConnectorErrorCodes.TemplateNotFound,
`Cannot find template for type: ${type}`
)
);
const parameters = {
AccessKeyId: accessKeyId,
AccountName: accountName,
ReplyToAddress: 'false',
AddressType: '1',
ToAddress: to,
FromAlias: fromAlias,
Subject: template.subject,
HtmlBody:
typeof payload.code === 'string'
? template.content.replace(/{{code}}/g, payload.code)
: template.content,
};
try {
const httpResponse = await request(
endpoint,
{ Action: 'SingleSendMail', ...staticConfigs, ...parameters },
accessKeySecret
);
const result = sendEmailResponseGuard.safeParse(parseJson(httpResponse.body));
if (!result.success) {
throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, result.error);
}
return result.data;
} catch (error: unknown) {
if (error instanceof HTTPError) {
const {
response: { body: rawBody },
} = error;
assert(typeof rawBody === 'string', new ConnectorError(ConnectorErrorCodes.InvalidResponse));
errorHandler(rawBody);
}
throw error;
}
};
const request = async (
url: string,
parameters: PublicParameters & Record<string, string>,
accessKeySecret: string
) => {
const finalParameters: Record<string, string> = {
...parameters,
SignatureNonce: String(Math.random()),
Timestamp: new Date().toISOString(),
};
const signature = getSignature(finalParameters, accessKeySecret, 'POST');
const payload = new URLSearchParams();
for (const [key, value] of Object.entries(finalParameters)) {
payload.append(key, value);
}
payload.append('Signature', signature);
return got.post({
url,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
form: payload,
});
};
คุณสามารถดูตัวอย่างการใช้งานเต็มรูปแบบได้ที่ ที่นี่
สำหรับรายละเอียดเพิ่มเติมเกี่ยวกับพารามิเตอร์ที่กำหนดค่าได้ ดูที่ README ของตัวเชื่อมต่อ Aliyun direct mail หรือเอกสารทางการของ Aliyun direct mail
มีอะไรอีกบ้าง?
เพื่อดูนิยามของเมธอดตัวเชื่อมต่อและเข้าใจการออกแบบอินเทอร์เฟซของตัวเชื่อมต่อ ดูได้ที่ @logto/connector-kit
คุณยังสามารถดูข้อมูลอ้างอิง ConnectorMetadata ได้ที่ "Connectors - ConnectorMetadata" และ "โครงสร้างไฟล์ของตัวเชื่อมต่อ" เพื่อช่วยจัดระเบียบการพัฒนาของคุณ
- สคีมา config ของตัวเชื่อมต่อ (Zod) เป็นสิ่งจำเป็นสำหรับตัวเชื่อมต่อทุกตัว ซึ่งสำคัญมากเพราะเราจะตรวจสอบชนิดข้อมูลก่อนบันทึก
config
ลงฐานข้อมูลและเรียก API ที่ต้องใช้ข้อมูลconfig
- ตัวเชื่อมต่อ SMS และ อีเมล ทุกตัวต้องมีเมธอด
sendMessage
เพื่อเรียก API ส่งข้อความของผู้ให้บริการโดยใช้ config จากฐานข้อมูล นักพัฒนายังสามารถใช้เมธอดนี้ซ้ำเพื่อส่งข้อความทดสอบด้วย config ที่ยังไม่ได้บันทึกขณะตั้งค่าตัวเชื่อมต่อใน Admin Console - ตัวสร้าง URL สำหรับการอนุญาต
getAuthorizationUri
และตัวดึงโปรไฟล์ผู้ใช้getUserInfo
เป็นสิ่งจำเป็นสำหรับ ตัวเชื่อมต่อโซเชียล ทุกตัว (getAccessToken
ถือเป็นขั้นตอนเสริมในgetUserInfo
) - เมธอดของตัวเชื่อมต่อทุกตัวทำงานผ่านการเรียก API ดังนั้นนักพัฒนาตัวเชื่อมต่อควรตรวจสอบเอกสารและจัดการกับ response ที่อาจไม่สำเร็จจาก API
ติดตั้งตัวเชื่อมต่อของคุณเอง
เราถือว่าคุณได้สร้างตัวเชื่อมต่อของคุณเองเสร็จแล้ว ให้ทำตามขั้นตอนต่อไปนี้เพื่อติดตั้งด้วยตนเอง:
- คัดลอกโฟลเดอร์ตัวเชื่อมต่อที่คุณพัฒนาไปยังไดเรกทอรี
/packages/connectors
ของlogto-io/logto
- ติดตั้ง dependencies ของ repository ตัวเชื่อมต่อโดยรัน
pnpm pnpm:devPreinstall && pnpm i
ที่ root path ของโฟลเดอร์ logto - สร้างตัวเชื่อมต่อด้วยคำสั่ง
pnpm connectors build
- ลิงก์ตัวเชื่อมต่อ local โดยใช้
pnpm cli connector link
- รีสตาร์ท Logto instance ด้วย
pnpm dev
ที่ root directory ของlogto-io/logto
แล้วคุณจะพบว่าตัวเชื่อมต่อถูกติดตั้งสำเร็จ
ตอนนี้คุณสามารถทดสอบและลองใช้งานตัวเชื่อมต่อของคุณเพื่อดูว่าทำงานได้ตามที่คาดหวังหรือไม่
หากคุณต้องการเพิ่มตัวเชื่อมต่อที่เผยแพร่แล้วบน NPM หรือเป็นตัวเชื่อมต่อทางการของ Logto สามารถดูได้ที่ การใช้ Logto CLI - จัดการตัวเชื่อมต่อ