PALO IT Blog

เพิ่มความปลอดภัยด้วย Two-Factor Authentication (2FA) บน Next.js + Amazon Cognito

เขียนโดย Jack Vatcharat Rattananun - 08/01/26

บทความนี้จะพาทำ 2FA แบบใช้แอป Authenticator (TOTP) บน Next.js + Amazon Cognito ครับ สามารถใช้กับ Google Authenticator, Microsoft Authenticator, หรือ Authy ได้เลย

สิ่งที่จะได้

  • เข้าใจ flow การทำงานของ MFA Challenge
  • รู้วิธีสร้าง QR Code สำหรับ Authenticator
  • ตั้งค่า Cognito ให้รองรับ TOTP
  • จัดการ error และ security best practices

💡 โค้ดเต็มอยู่บน GitHub — บทความนี้เน้นอธิบาย concepts และ key decisions ครับ ไม่ได้วางโค้ดทั้งหมด

หน้าเว็บที่แสดงว่า login ด้วย 2FA สำเร็จ

2FA คืออะไร สำคัญยังไง

2FA (Two-Factor Authentication) = ใช้ “สองอย่าง” ยืนยันตัวตน:

1. สิ่งที่รู้ — Email + Password

2. สิ่งที่ถืออยู่ — โค้ด 6 หลักจากแอป Authenticator (เปลี่ยนทุก 30 วินาที)

ทำไมต้องมี?

ต่อให้รหัสผ่านหลุดจากฟิชชิ่ง คนร้ายก็ยังเข้าไม่ได้ เพราะไม่มีโค้ดจากมือถือคุณ

ต้องเตรียมอะไรใน AWS Cognito

สิ่งที่ต้องมีใน AWS

  • Cognito User Pool ตั้ง MFA เป็น “Optional”
  • App Client เปิด Software Token MFA (TOTP)
  • User Pool ID + App Client ID

📖 วิธีตั้งค่าละเอียด: ดู วิธีเปิดใช้ 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 ให้รู้ว่าโค้ดนี้สำหรับระบบไหน

 

Dependencies

npm install amazon-cognito-identity-js qrcode.react
npm install --save-dev @types/qrcode

 

เข้าใจ Flow การล็อกอินแบบมี 2FA


Flow Diagram

Sequence Diagram

สถาปัตยกรรมและไฟล์สำคัญ

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

 

ฟังก์ชันหลักใน services/auth.ts

ขั้นตอนการทำ

1. Cognito Configuration

สร้าง 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 || "",
});

 

2. MFA Challenge Detection

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;
};
}ะ

ดูโค้ดเต็ม signIn()

3. Component Integration Pattern

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 แทน

ดูโค้ดเต็ม Components

4. QR Code Generation

Concept: สร้าง `otpauth://` URI สำหรับ Authenticator apps

ขั้นตอน:

  1. ขอ secretCode จาก Cognito
  2. สร้าง URI format ตามมาตรฐาน
  3. แปลงเป็น QR Code ด้วย qrcode.react
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)

ดูโค้ดเต็ม QR Generation

5. Verification & Activation

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);

 

6. Error Handling

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("เกิดข้อผิดพลาด");
}
}

 

ดูโค้ดเต็ม Error Handling

ทดสอบว่า 2FA ทำงานถูกต้อง

  1. เข้าเพจตั้งค่า 2FA โดยกดปุ่ม Set Up Authenticator App

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…

ล็อกอินด้วยบัญชีที่มี 2FA

1. Login ด้วย email ที่เพิ่งเปิด 2FA

2. กรอกโค้ดที่แสดงบน Authenticator app

3. หน้าเว็บจะแสดงว่าบัญชีนี้ถูกปกป้องด้วย 2FA

ปัญหาที่มักเจอ + วิธีแก้

MFA เปิดอยู่แล้ว ไม่สามารถ setup ใหม่ได้

อาการ: 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

เว็บไม่ถาม MFA แม้เปิดใช้แล้ว

สาเหตุ: Device tracking จำเครื่องไว้แล้ว

วิธีแก้:

  • ตัวเลือก 1: ปิด “Trust remembered devices” ใน Cognito Console
  • ตัวเลือก 2: Forget device ของ user นั้น (Actions → Forget device)

โค้ดถูกแต่ยัง error “Code mismatch”

สาเหตุที่เป็นไปได้:

  • เวลาเครื่องไม่ตรง (TOTP ใช้ time-based)
  • ใช้โค้ดเดิมซ้ำ (แต่ละโค้ดใช้ได้ครั้งเดียว)
  • ตั้งค่า Cognito ไม่ถูก (ไม่ได้เปิด TOTP)

วิธีแก้:

  • ตรวจเวลาบนมือถือให้ตรง (sync กับ network time)
  • รอโค้ดใหม่ (ประมาณ 30 วินาที)
  • ตรวจสอบ User Pool settings

Tips ความปลอดภัยตอนขึ้น Production

1. เก็บ Token ให้ปลอดภัย

ใช้ 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 เท่านั้น

 

2. HTTPS Only

// next.config.ts
export default {
async headers() {
return [
{
source: "/:path*",
headers: [
{
key: "Strict-Transport-Security",
value: "max-age=63072000; includeSubDomains; preload",
},
],
},
];
},
};

 

แหล่งอ้างอิง

สรุป

เพิ่ม 2FA ด้วย Authenticator ทำให้ระบบคุณปลอดภัยขึ้นเยอะมากครับ ต่อให้รหัสผ่านหลุด ผู้โจมตีก็ยังต้องผ่านด่านโค้ดที่เปลี่ยนทุก 30 วินาทีอยู่ดี

ใจความหลัก:

  • ความปลอดภัยเพิ่ม ไม่ยุ่งยากเกินไป
  • ผู้ใช้ตั้งค่าง่ายด้วย QR Code สแกนจบ
  • ปล่อยให้ Cognito ดูแลงานยากๆ (โทเคน / session / scale)
  • ถ้าเก็บ token ดี ก็พร้อมขึ้น production ได้เลยครับ

ขอให้สนุกกับการทำ 2FA ครับ! 🔐

สำหรับใครที่กำลังมองหาทีมงานมืออาชีพในการพัฒนาซอฟต์แวร์หรือแอปพลิเคชันเพื่อนำไปใช้งานในองค์กร ที่ PALO IT เรามีทีมผู้เชี่ยวชาญด้าน Software Development และ Application Development พร้อมช่วยคุณตั้งแต่เริ่มต้นจนสามารถใช้งานได้จริง!

บริการของเราครอบคลุม:

  • ออกแบบและพัฒนาซอฟต์แวร์ (Software Development) ตามความต้องการเฉพาะของธุรกิจคุณ
  • สร้างแอปพลิเคชัน (Application Development) ให้ตอบโจทย์การใช้งานขององค์กรในทุกมิติ
  • ปรับแต่งและดูแลระบบให้มีประสิทธิภาพสูง รองรับการขยายตัวของธุรกิจในอนาคต
  • ทดสอบและประเมินผลอย่างมืออาชีพ เพื่อให้มั่นใจว่าระบบของคุณใช้งานได้จริงและมีประสิทธิภาพ
  • ไม่ว่าคุณจะมีไอเดียใหม่ๆ หรือมีระบบอยู่แล้วที่อยากต่อยอด PALO IT พร้อมเป็น partner ที่จะช่วยให้คุณก้าวไปได้ไกลยิ่งขึ้น

ทักไปที่เพจ Facebook: PALO IT Thailand ได้เลยครับ 🎉