Node.js와 Twilio Verify API를 이용해 TOTP 기반 패스워드리스 인증을 단계별로 구현하는 방법을 다룬다. 인증자 앱(Authy 등)이 30초마다 생성하는 일회성 코드로 기존 비밀번호를 완전히 대체하는 3단계 흐름을 Express.js 서버에 직접 붙인다.
핵심 포인트- TOTP(Time-based One-Time Password)는 RFC 6238 기반으로 서버와 시간 동기화만으로 코드를 검증해 별도 네트워크 전송이 없다.
- 구현은 Factor 등록 → QR 스캔/초기 검증 → 이후 로그인 챌린지 검증의 3단계로 나뉜다.
- 핵심 의존성은 Express, EJS, express-session, node-qrcode, Twilio Node.js SDK이며 Twilio Verify API가 TOTP 로직을 추상화한다.
- express-session으로 요청 간 userId, factorSid, challengeSid 상태를 유지하며 멀티 스텝 흐름을 관리한다.
- Factor는 활성화(초기 코드 검증) 전까지 `unverified` 상태라 이후 챌린지가 실패하므로 첫 코드 검증 단계를 반드시 거쳐야 한다.
상세 정리- 프로젝트 초기화: Express, EJS, express-session, qrcode, twilio SDK를 설치하고 .env에 TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_VERIFY_SERVICE_SID를 설정한다.
- Factor 생성 엔드포인트: POST /register에서 `client.verify.v2.services(sid).entities(username).newFactors.create({ friendlyName, factorType: "totp" })`를 호출해 factor를 생성한다.
- QR 코드 렌더링: 응답의 `binding.uri`를 `qrcode.toDataURL()`로 base64 이미지로 변환해 EJS 템플릿에 렌더링한다. 사용자는 Authy나 Google Authenticator로 스캔한다.
- Factor 활성화(초기 검증): 첫 로그인에서 사용자가 코드를 입력하면 `factors(factorSid).update({ authPayload: code })`로 factor를 verified 상태로 전환한다. 이 단계가 없으면 이후 모든 챌린지가 실패한다.
- 로그인 챌린지 생성: POST /login에서 `entities(username).challenges.create({ factorSid, expirationDate })`로 챌린지를 생성하고 challengeSid를 세션에 저장한다.
- 챌린지 검증: POST /verify에서 `challenges(challengeSid).update({ authPayload: code })`를 호출해 `challenge.status === "approved"` 여부로 인증을 확정한다.
- 세션 관리: express-session의 req.session에 userId, factorSid, challengeSid를 단계별로 저장하고, 각 엔드포인트에서 미들웨어로 세션 유효성을 체크한다.
- 한계: 사용자당 factor 1개라 멀티 디바이스 지원이나 factor 교체는 별도 로직이 필요하다.
왜 읽나패스워드 없는 인증을 빠르게 프로토타이핑하거나 기존 Node.js/Express 앱에 TOTP 2FA를 추가하려는 백엔드 개발자에게 Twilio Verify API의 전체 흐름과 세션 관리 패턴을 한눈에 파악할 수 있는 실용 레퍼런스.