บทความนี้จะพาทำ 2FA แบบใช้แอป Authenticator (TOTP) บน Next.js + Amazon Cognito ครับ สามารถใช้กับ Google Authenticator, Microsoft Authenticator, หรือ Authy ได้เลย
💡 โค้ดเต็มอยู่บน GitHub — บทความนี้เน้นอธิบาย concepts และ key decisions ครับ ไม่ได้วางโค้ดทั้งหมด
2FA (Two-Factor Authentication) = ใช้ “สองอย่าง” ยืนยันตัวตน:
1. สิ่งที่รู้ — Email + Password
2. สิ่งที่ถืออยู่ — โค้ด 6 หลักจากแอป Authenticator (เปลี่ยนทุก 30 วินาที)
ต่อให้รหัสผ่านหลุดจากฟิชชิ่ง คนร้ายก็ยังเข้าไม่ได้ เพราะไม่มีโค้ดจากมือถือคุณ
📖 วิธีตั้งค่าละเอียด: ดู วิธีเปิดใช้ MFA และ SMS บน Amazon Cognito
Environment Variables
# .env.local
NEXT_PUBLIC_COGNITO_USER_POOL_ID=us-east-1_xxxxxxxxx
NEXT_PUBLIC_COGNITO_CLIENT_ID=xxxxxxxxxxxxxxxxxxxxxxxxxx
NEXT_PUBLIC_APP_NAME=YourAppNameทำไมต้องมี NEXT_PUBLIC: เพราะ Cognito SDK ทำงานฝั่ง client
APP_NAME: จะโชว์ในแอป Authenticator ให้รู้ว่าโค้ดนี้สำหรับระบบไหน
npm install amazon-cognito-identity-js qrcode.react
npm install --save-dev @types/qrcode
src/
├── config/
│ └── cognito.ts # User Pool configuration
├── services/
│ └── auth.ts # ฟังก์ชัน MFA ทั้งหมด
├── components/
│ ├── LoginForm.tsx # ฟอร์มล็อกอิน
│ ├── MfaVerificationForm.tsx # ฟอร์มกรอกโค้ด MFA
│ ├── AuthenticatorSetup.tsx # สร้าง QR Code
│ └── VerifyAuthenticator.tsx # ยืนยัน + เปิดใช้งาน
├── types/
│ └── cognito-errors.ts # Error type guards
└── pages/
├── login.tsx # Orchestrate login flow
└── setup-2fa.tsx # Setup 2FA flow
สร้าง User Pool connection:
// config/cognito.ts
import { CognitoUserPool } from "amazon-cognito-identity-js";
export const userPool = new CognitoUserPool({
UserPoolId: process.env.NEXT_PUBLIC_COGNITO_USER_POOL_ID || "",
ClientId: process.env.NEXT_PUBLIC_COGNITO_CLIENT_ID || "",
});
Concept หลัก: Cognito callbacks บอกสถานะ authentication
cognitoUser.authenticateUser(authDetails, {
onSuccess: (result) => {
// ไม่มี MFA หรือ trusted device
return result.getIdToken().getJwtToken();
},
totpRequired: (challengeName, params) => {
// ต้องใช้ Authenticator app
return { email, password, challengeName, params };
},
mfaRequired: (challengeName, params) => {
// ต้องใช้ SMS
return { email, password, challengeName, params };
},
onFailure: (err) => reject(err),
});💡 ทำไมเก็บ email + password ใน Challenge?
เพราะตอนส่ง MFA code ต้อง re-authenticate กับ Cognito อีกรอบ
Type Definition:
export interface MfaChallenge {
email: string;
password: string;
challengeName: "SMS_MFA" | "SOFTWARE_TOKEN_MFA";
challengeParameters: {
CODE_DELIVERY_DESTINATION?: string;
};
}ะ
Page-level orchestration (ไม่ใช่ component-level):
// pages/login.tsx
const [mfaChallenge, setMfaChallenge] = useState<MfaChallenge | null>(null);
const handleLogin = async (email: string, password: string) => {
const result = await signIn(email, password);
if (typeof result === "string") {
// Got token directly
saveToken(result);
router.push("/home");
} else {
// Got MFA challenge
setMfaChallenge(result);
}
};
// Conditional rendering
{
!mfaChallenge ? (
<LoginForm onLoginSuccess={handleLogin} />
) : (
<MfaVerificationForm
mfaChallenge={mfaChallenge}
onSuccess={() => router.push("/home")}
/>
);
}⚠️ Common Mistake: อย่าใส่ logic routing ใน component — ใช้ callback props แทน
Concept: สร้าง `otpauth://` URI สำหรับ Authenticator apps
ขั้นตอน:
const secret = await setupAuthenticatorApp(); // Get from Cognito
const otpauthUri = `otpauth://totp/${encodeURIComponent(
issuer
)}:${encodeURIComponent(email)}?secret=${secret}&issuer=${encodeURIComponent(
issuer
)}`;
// Render QR
<QRCodeSVG value={otpauthUri} size={200} />;
URI Structure:
otpauth://totp/MyApp:user@example.com?secret=SECRETKEY&issuer=MyApp
│ │ │ │
│ └─ Issuer:Account └─ TOTP secret └─ App name
└─ Protocol (TOTP)
2 ขั้นตอนสำคัญ
// 1. ยืนยันโค้ดแรกจาก Authenticator
await verifyAuthenticatorCode(code, "DeviceName");
// 2. ตั้งให้ TOTP เป็นวิธี MFA หลัก
await setPreferredMfaMethod("SOFTWARE_TOKEN_MFA");
Cognito callbacks
cognitoUser.verifySoftwareToken(code, deviceName, {
onSuccess: () => {
/* ยืนยันสำเร็จ */
},
onFailure: (err) => {
/* โค้ดผิด */
},
});
cognitoUser.setUserMfaPreference(smsSettings, totpSettings, callback);
Best Practice: ใช้ type guards แยกประเภท error
// types/cognito-errors.ts
type CognitoAuthErrorCode =
| "UserNotConfirmedException"
| "NotAuthorizedException"
| "UserNotFoundException"
| "CodeMismatchException"
| "ExpiredCodeException"
| "InvalidParameterException"
| "UsernameExistsException";
export type CognitoAuthError = {
message: string;
code: CognitoAuthErrorCode;
};
export const isCognitoAuthError = (error: unknown): error is CognitoAuthError =>
typeof error === "object" &&
error !== null &&
"code" in error &&
"message" in error;
export const isCognitoCodeMismatchError = (error: unknown): boolean => {
return isCognitoAuthError(error) && error.code === "CodeMismatchException";
};
export const isExpiredCodeError = (error: unknown): boolean => {
return isCognitoAuthError(error) && error.code === "ExpiredCodeException";
};
export const isNotAuthorizedError = (error: unknown): boolean => {
return isCognitoAuthError(error) && error.code === "NotAuthorizedException";
};
ใช้งาน:
try {
await submitMfaCode(email, password, code, challengeName);
} catch (error) {
if (isCognitoCodeMismatchError(error)) {
setError("รหัสไม่ถูกต้อง กรุณาลองใหม่");
} else if (isExpiredCodeError(error)) {
setError("โค้ดนี้ถูกใช้ไปแล้ว รอโค้ดใหม่จากแอป");
} else {
setError("เกิดข้อผิดพลาด");
}
}
2. พิมพ์ email ที่ต้องการเปิด 2FA แล้วกดปุ่ม Generate QR Code
3. สแกน QR code ด้วย Google Authenticator / Microsoft Authenticator
4. สังเกตชื่อที่แสดงบน Authenticator จะตรงตาม env variable NEXT_PUBLIC_APP_NAME
5. กดปุ่ม Continue to Verification
6. กรอกโค้ดแรกผ่าน → กดปุ่ม Verify and Enable 2FA
7. จะได้ข้อความว่า Authenticator setup successful! 2FA is now enabled. Redirecting…
1. Login ด้วย email ที่เพิ่งเปิด 2FA
2. กรอกโค้ดที่แสดงบน Authenticator app
3. หน้าเว็บจะแสดงว่าบัญชีนี้ถูกปกป้องด้วย 2FA
อาการ: Error “Software token MFA is already enabled”
วิธีแก้:
1. ไปที่ Amazon Cognito
2. เลือก user pool และเปิดหน้า Users (ใต้ User management แถบด้านข้าง)
3. ค้นหาบัญชีที่ต้องการ
4. คลิ๊กที่บัญชี จะเห็นว่าสถานะเป็น active
5. กดปุ่ม Actions ขวาบน > Update MFA configuration
6. เลือก MFA inactive
7. กดปุ่ม Save changes
สาเหตุ: Device tracking จำเครื่องไว้แล้ว
วิธีแก้:
สาเหตุที่เป็นไปได้:
วิธีแก้:
ใช้ HttpOnly Cookies (ตั้งฝั่ง server)
// ✅ API Route (server-side only)
// pages/api/auth/set-token.ts
export default function handler(req, res) {
res.setHeader(
"Set-Cookie",
`authToken=${token}; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=3600`
);
res.status(200).json({ success: true });
}⚠️ HttpOnly cookies ไม่สามารถตั้งจาก client-side JavaScript ได้ — ต้องทำผ่าน API Route หรือ server response เท่านั้น
// next.config.ts
export default {
async headers() {
return [
{
source: "/:path*",
headers: [
{
key: "Strict-Transport-Security",
value: "max-age=63072000; includeSubDomains; preload",
},
],
},
];
},
};
เพิ่ม 2FA ด้วย Authenticator ทำให้ระบบคุณปลอดภัยขึ้นเยอะมากครับ ต่อให้รหัสผ่านหลุด ผู้โจมตีก็ยังต้องผ่านด่านโค้ดที่เปลี่ยนทุก 30 วินาทีอยู่ดี
ใจความหลัก:
ขอให้สนุกกับการทำ 2FA ครับ! 🔐
สำหรับใครที่กำลังมองหาทีมงานมืออาชีพในการพัฒนาซอฟต์แวร์หรือแอปพลิเคชันเพื่อนำไปใช้งานในองค์กร ที่ PALO IT เรามีทีมผู้เชี่ยวชาญด้าน Software Development และ Application Development พร้อมช่วยคุณตั้งแต่เริ่มต้นจนสามารถใช้งานได้จริง!
บริการของเราครอบคลุม:
ทักไปที่เพจ Facebook: PALO IT Thailand ได้เลยครับ 🎉