ข้ามไปยังเนื้อหาหลัก

เพิ่มการยืนยันตัวตนให้กับแอปเว็บแบบดั้งเดิมของคุณ (Add authentication to your traditional web application)

บันทึก:

คู่มือนี้ถือว่าคุณได้สร้างแอปพลิเคชันประเภท "Traditional web" ใน Admin Console แล้ว

แอปของคุณอาจทำงานฝั่งเซิร์ฟเวอร์แทนที่จะทำงานในเบราว์เซอร์ โดยใช้เฟรมเวิร์กอย่าง Django, Laravel ฯลฯ ซึ่งเรียกว่าเว็บแอปแบบดั้งเดิม หากคุณไม่พบ SDK ที่เหมาะสมในหน้านี้ คุณอาจต้องเชื่อมต่อเองแบบแมนนวล

บทความนี้จะแนะนำวิธีดำเนินการทีละขั้นตอน โดยจะยกตัวอย่าง Express บน Node.js

เคล็ดลับ:

บทความนี้ไม่ได้จำกัดเฉพาะ Express หากคุณใช้เฟรมเวิร์กอื่นหรือภาษาอื่น ๆ สามารถแทนที่ @logto/js ด้วย core SDK ของภาษานั้น ๆ และปรับบางขั้นตอนให้เหมาะสม

รับซอร์สโค้ดตัวอย่าง

คุณสามารถเข้าไปที่ GitHub เพื่อดูโค้ดตัวอย่างฉบับสมบูรณ์สำหรับคู่มือนี้

เริ่มต้นโปรเจกต์ Express

ด้วย express-generator คุณสามารถเริ่มโปรเจกต์ Express ได้อย่างรวดเร็ว

mkdir express-logto
cd express-logto
npx express-generator

ติดตั้ง dependencies

แอปตัวอย่างนี้จะใช้ dependencies 4 ตัวดังนี้:

  1. @logto/js: Core SDK ของ Logto สำหรับ JavaScript
  2. node-fetch: โค้ดขนาดเล็กสำหรับ API ที่เข้ากันได้กับ window.fetch บน Node.js
  3. express-session: มิดเดิลแวร์สำหรับ session เราจะใช้ session เพื่อเก็บโทเค็นของผู้ใช้
  4. js-base64: ตัวแปลงรหัส Base64
npm i @logto/js node-fetch@v2 express-session js-base64

ใช้งาน session

เมื่อผู้ใช้ลงชื่อเข้าใช้ พวกเขาจะได้รับชุดโทเค็น (โทเค็นการเข้าถึง (Access token), โทเค็น ID (ID token), โทเค็นรีเฟรช (Refresh token)) และข้อมูลการโต้ตอบต่าง ๆ โดย session เป็นที่เก็บข้อมูลเหล่านี้ได้อย่างดี

เราได้ติดตั้ง express-session ในขั้นตอนก่อนหน้าแล้ว ดังนั้นให้เพิ่มโค้ดต่อไปนี้เพื่อเซ็ตอัป:

// app.js

const session = require('express-session');

app.use(
session({
secret: 'keyboard cat', // เปลี่ยนเป็น secret key ของคุณเอง
cookie: { maxAge: 86400 },
})
);

สร้างฟังก์ชันสำหรับยืนยันตัวตนผู้ใช้

เคล็ดลับ:

ตัวอย่างโค้ดต่อไปนี้ สมมติว่าแอปพลิเคชันรันอยู่ที่ http://localhost:3000

ในขั้นตอนนี้ เราต้องสร้างฟังก์ชันสำหรับยืนยันตัวตนดังนี้:

  1. getSignInUrl: สร้างและคืน URL แบบสมบูรณ์ของ Logto Authorization Server เพื่อเปลี่ยนเส้นทางผู้ใช้ไป
  2. handleSignIn: แยกวิเคราะห์ callback URL หลังจากกระบวนการยืนยันตัวตนเสร็จสิ้น ดึง query parameter ชื่อ code และดึงโทเค็น (โทเค็นการเข้าถึง, โทเค็นรีเฟรช, โทเค็น ID) เพื่อจบขั้นตอนการลงชื่อเข้าใช้
  3. refreshTokens: ขอแลกเปลี่ยนโทเค็นการเข้าถึงใหม่โดยใช้โทเค็นรีเฟรช
เคล็ดลับ:

คุณสามารถค้นหาและคัดลอก "App Secret" ได้จากหน้ารายละเอียดแอปพลิเคชันใน Admin Console:

App Secret
// logto.js

const {
withReservedScopes,
fetchOidcConfig,
discoveryPath,
generateSignInUri,
verifyAndParseCodeFromCallbackUri,
fetchTokenByAuthorizationCode,
fetchTokenByRefreshToken,
} = require('@logto/js');
const fetch = require('node-fetch');
const { randomFillSync, createHash } = require('crypto');
const { fromUint8Array } = require('js-base64');

const config = {
endpoint: 'https://logto.dev',
appId: 'foo',
appSecret: '<your-app-secret-copied-from-console>',
redirectUri: 'http://localhost:3000/callback', // คุณอาจต้องเปลี่ยนเป็น address ของแอปใน production
scopes: withReservedScopes().split(' '),
};

const requester = (input, init) => {
const { appId, appSecret } = config;

return fetch(input, {
...init,
headers: {
...init?.headers,
authorization: `Basic ${Buffer.from(`${appId}:${appSecret}`, 'utf8').toString('base64')}`,
},
});
};

const generateRandomString = (length = 64) => {
return fromUint8Array(randomFillSync(new Uint8Array(length)), true);
};

const generateCodeChallenge = async (codeVerifier) => {
const encodedCodeVerifier = new TextEncoder().encode(codeVerifier);
const hash = createHash('sha256');
hash.update(encodedCodeVerifier);
const codeChallenge = hash.digest();
return fromUint8Array(codeChallenge, true);
};

const getOidcConfig = async () => {
return fetchOidcConfig(new URL(discoveryPath, config.endpoint).toString(), requester);
};

exports.getSignInUrl = async () => {
const { authorizationEndpoint } = await getOidcConfig();
const codeVerifier = generateRandomString();
const codeChallenge = await generateCodeChallenge(codeVerifier);
const state = generateRandomString();

const { redirectUri, scopes, appId: clientId } = config;

const signInUri = generateSignInUri({
authorizationEndpoint,
clientId,
redirectUri: redirectUri,
codeChallenge,
state,
scopes,
});

return { redirectUri, codeVerifier, state, signInUri };
};

exports.handleSignIn = async (signInSession, callbackUri) => {
const { redirectUri, state, codeVerifier } = signInSession;
const code = verifyAndParseCodeFromCallbackUri(callbackUri, redirectUri, state);

const { appId: clientId } = config;
const { tokenEndpoint } = await getOidcConfig();
const codeTokenResponse = await fetchTokenByAuthorizationCode(
{
clientId,
tokenEndpoint,
redirectUri,
codeVerifier,
code,
},
requester
);

return codeTokenResponse;
};

exports.refreshTokens = async (refreshToken) => {
const { appId: clientId, scopes } = config;
const { tokenEndpoint } = await getOidcConfig();
const tokenResponse = await fetchTokenByRefreshToken(
{
clientId,
tokenEndpoint,
refreshToken,
scopes,
},
requester
);

return tokenResponse;
};

ลงชื่อเข้าใช้

ก่อนที่เราจะลงลึกในรายละเอียด นี่คือภาพรวมประสบการณ์ของผู้ใช้ปลายทาง กระบวนการลงชื่อเข้าใช้สามารถสรุปได้ดังนี้:

  1. แอปของคุณเรียกใช้งานเมธอดลงชื่อเข้าใช้
  2. ผู้ใช้จะถูกเปลี่ยนเส้นทางไปยังหน้าลงชื่อเข้าใช้ของ Logto สำหรับแอปเนทีฟ ระบบจะเปิดเบราว์เซอร์ของระบบ
  3. ผู้ใช้ลงชื่อเข้าใช้และถูกเปลี่ยนเส้นทางกลับไปยังแอปของคุณ (ตามที่กำหนดไว้ใน redirect URI)

เกี่ยวกับการลงชื่อเข้าใช้แบบเปลี่ยนเส้นทาง (redirect-based sign-in)

  1. กระบวนการยืนยันตัวตนนี้เป็นไปตามโปรโตคอล OpenID Connect (OIDC) และ Logto บังคับใช้มาตรการรักษาความปลอดภัยอย่างเข้มงวดเพื่อปกป้องการลงชื่อเข้าใช้ของผู้ใช้
  2. หากคุณมีหลายแอป คุณสามารถใช้ผู้ให้บริการข้อมูลระบุตัวตน (Logto) เดียวกันได้ เมื่อผู้ใช้ลงชื่อเข้าใช้แอปหนึ่งแล้ว Logto จะดำเนินการลงชื่อเข้าใช้โดยอัตโนมัติเมื่อผู้ใช้เข้าถึงแอปอื่น

หากต้องการเรียนรู้เพิ่มเติมเกี่ยวกับเหตุผลและประโยชน์ของการลงชื่อเข้าใช้แบบเปลี่ยนเส้นทาง โปรดดูที่ อธิบายประสบการณ์การลงชื่อเข้าใช้ของ Logto


ตั้งค่า Redirect URI

ไปที่หน้ารายละเอียดแอปพลิเคชันของ Logto Console เพิ่ม Redirect URI http://localhost:3000/callback แล้วคลิก "บันทึกการเปลี่ยนแปลง" (Save changes)

Redirect URI ใน Logto Console

สร้าง route สำหรับลงชื่อเข้าใช้

บันทึก:

ก่อนเรียก getSignInUrl() โปรดตรวจสอบให้แน่ใจว่าคุณได้กำหนดค่า Redirect URI ใน Admin Console อย่างถูกต้องแล้ว

สร้าง route ใน Express สำหรับลงชื่อเข้าใช้:

const { getSignInUrl } = require('./logto');

app.get('/sign-in', async (req, res) => {
const { redirectUri, codeVerifier, state, signInUri } = await getSignInUrl();
req.session.signIn = { codeVerifier, state, redirectUri };
res.redirect(signInUri);
});

และ route สำหรับจัดการ callback:

app.get('/callback', async (req, res) => {
if (!req.session.signIn) {
res.send('Bad request.');
return;
}

const response = await handleSignIn(
req.session.signIn,
`${req.protocol}://${req.get('host')}${req.originalUrl}`
);
req.session.tokens = {
...response,
expiresAt: response.expiresIn + Date.now(),
idToken: decodeIdToken(response.idToken),
};
req.session.signIn = null;

res.redirect('/');
});

ลงชื่อออก

TODO: ลิงก์ไปยังบท "session & cookies" ในส่วนอ้างอิงผู้ใช้

คุณสามารถลบโทเค็นใน session เพื่อให้ผู้ใช้ลงชื่อออกจากแอปนี้ และอ่านข้อมูลเพิ่มเติมเกี่ยวกับ "sign out" ได้ที่ลิงก์นี้

app.get('/sign-out', (req, res) => {
req.session.tokens = null;
res.send('Sign out successful');
});

เข้าถึงทรัพยากรที่ต้องได้รับอนุญาต

สร้างมิดเดิลแวร์ชื่อ withAuth เพื่อแนบอ็อบเจกต์ auth เข้ากับ req และตรวจสอบว่าผู้ใช้ลงชื่อเข้าใช้หรือไม่

// auth.js

const { decodeIdToken } = require('@logto/js');
const { refreshTokens } = require('./logto');

const withAuth =
({ requireAuth } = { requireAuth: true }) =>
async (req, res, next) => {
if (requireAuth && !req.session.tokens) {
res.redirect('/sign-in');
return;
}

if (req.session.tokens) {
if (req.session.tokens.expiresAt >= Date.now()) {
// โทเค็นการเข้าถึงหมดอายุ รีเฟรชเพื่อรับโทเค็นใหม่
try {
const response = await refreshTokens(req.session.tokens.refreshToken);
req.session.tokens = {
...response,
expiresAt: response.expiresIn + Date.now(),
idToken: decodeIdToken(response.idToken),
};
} catch {
// แลกเปลี่ยนโทเค็นล้มเหลว เปลี่ยนเส้นทางไปลงชื่อเข้าใช้ใหม่
res.redirect('/sign-in');
return;
}
}

req.auth = req.session.tokens.idToken.sub;
}

next();
};

module.exports = withAuth;

สร้างหน้า index แสดงลิงก์ลงชื่อเข้าใช้สำหรับผู้เยี่ยมชม และลิงก์ไปโปรไฟล์สำหรับผู้ใช้ที่ลงชื่อเข้าใช้แล้ว:

router.get('/', withAuth({ requireAuth: false }), function (req, res, next) {
res.render('index', { auth: Boolean(req.auth) });
});
extends layout

block content
h1 Hello logto
if auth
p: a(href="/user") ไปที่โปรไฟล์
else
p: a(href="/sign-in") คลิกที่นี่เพื่อเข้าสู่ระบบ

สร้างหน้า user เพื่อแสดง userId (subject):

app.get('/user', withAuth(), (req, res, next) => {
res.render('user', { userId: req.auth });
});
extends layout

block content
h1 Hello logto
p userId: #{userId}

อ่านเพิ่มเติม

กระบวนการสำหรับผู้ใช้ปลายทาง: กระบวนการยืนยันตัวตน, กระบวนการบัญชี, และกระบวนการองค์กร ตั้งค่าตัวเชื่อมต่อ การอนุญาต (Authorization)

ยืนยันตัวตนผู้ใช้ใน GPT actions: สร้างผู้ช่วยจัดการตารางส่วนตัว