Files
2nd/10_Wiki/Topic_Programming/Coding/Security_2FA_TOTP_WebAuthn.md
T
Antigravity Agent 9148c358d0 docs(10_Wiki): 위키 전체 재구성 — Topic_* 폴더를 4개 카테고리로 통합 + 대규모 중복 제거
Topic_Agent/Topic_Blog/Topics/Topics_Biz/Topics_Meeting/Topics_Rag의 마크다운 지식 문서를
Topic_General/Topic_Programming/Topic_Graphic/Topic_Business 4개 카테고리로 재분류.

- 중복 제거: frontmatter의 status:duplicate/merged + duplicate_of/redirect_to 필드로
  자기 자신을 중복으로 선언한 리다이렉트 stub 1032개 제거, 완전 동일 내용 파일 472개 제거,
  동일 파일명·다른 내용 충돌 시 더 큰(완전한) 버전만 유지(162개 제거) — 총 1639개 중복 제거.
- 분류: 폴더 단위로 명확한 항목(AI_and_ML/Coding/Architecture 등 → Programming,
  Comfyui/Visual_Effects → Graphic, Topics_Biz/Topics_Meeting/사업 등 → Business,
  Poetic_Blog_Writing/창의성/Game_Design 등 → General)은 폴더 우선순위로,
  나머지 혼재 폴더(Topic_Agent/Topic_Blog/Topics 루트/Thinking & Reasoning/Other/UI_UX_Assets)는
  title/tags 키워드 스코어링으로 파일 단위 분류(불명확한 경우 General로 폴백).
  원본 폴더명은 "From_*" 서브폴더로 보존해 추적 가능성 유지.
- 최종 배치: Programming 2784 / General 1608 / Graphic 285 / Business 249 = 4926개 문서.
- 에이전트 운영 상태(.astra/.agent/.obsidian/sessions/memory/_company/docs/lessons/_shared/src)는
  지식 콘텐츠가 아니므로 재분류 대상에서 제외하고 원위치 유지.
- Topics/Topic_email(상위 보호 폴더 Topic_email과 파일명 100% 중복) 삭제 — 보호 폴더 자체는 미변경.
- 완전히 비게 된 Topic_Agent/Topic_Blog/Topics_Biz/Topics_Rag 폴더 제거.
2026-07-05 00:33:48 +09:00

6.1 KiB

id, title, category, status, source_trust_level, verification_status, created_at, updated_at, tags, tech_stack, applied_in, aliases
id title category status source_trust_level verification_status created_at updated_at tags tech_stack applied_in aliases
security-2fa-totp-webauthn 2FA — TOTP / WebAuthn / Passkey Coding draft B conceptual 2026-05-09 2026-05-09
security
2fa
totp
webauthn
passkey
vibe-coding
language applicable_to
TS / WebAuthn
Backend
Frontend
2FA
TOTP
Google Authenticator
WebAuthn
passkey
FIDO2

2FA / Passkeys

SMS 2FA = SIM swap 위험. TOTP (Authenticator) = 무료 + 안전, WebAuthn / Passkey = 진짜 phishing-resistant. iOS/Android Passkey 가 미래.

📖 핵심 개념

  • TOTP: 6자리 30초 코드 (RFC 6238). HMAC + 시간.
  • WebAuthn: 공개키 인증. 서버 = 비밀 X.
  • Passkey: WebAuthn 의 동기화 + UX 개선 — Apple/Google 계정에 sync.
  • Recovery codes: 백업 — 1회용.

💻 코드 패턴

TOTP (otplib)

import { authenticator } from 'otplib';

// Setup
const secret = authenticator.generateSecret(); // base32
const otpauth = authenticator.keyuri(user.email, 'Acme', secret);
// QR: otpauth → qrcode 라이브러리

await db.user2fa.upsert({ userId: user.id, secret, enabled: false });

// Verify (활성화)
function verify(code: string, secret: string) {
  return authenticator.check(code, secret);
}

// 정상 검증 후
await db.user2fa.update(userId, { enabled: true });

Login flow with TOTP

// Step 1: password
const u = await verifyPassword(email, password);
if (!u) return 401;

if (u.twoFactorEnabled) {
  // 임시 token (TOTP 만 검증 가능, 30초 유효)
  return { needs2FA: true, partialToken: signPartial({ userId: u.id }) };
}
return loginSuccess(u);

// Step 2: TOTP
app.post('/login/2fa', async (req, res) => {
  const partial = verifyPartial(req.body.partialToken);
  const { secret } = await db.user2fa.find(partial.userId);
  if (!authenticator.check(req.body.code, secret)) return res.status(401).end();
  return loginSuccess({ id: partial.userId });
});

Recovery codes

import { randomBytes } from 'node:crypto';

function generateRecoveryCodes(n = 10): string[] {
  return Array.from({ length: n }, () => randomBytes(8).toString('hex'));
}

const codes = generateRecoveryCodes();
const hashed = await Promise.all(codes.map(c => argon2.hash(c)));
await db.recoveryCodes.insertMany(userId, hashed);
// codes 한 번만 사용자에게 표시

WebAuthn / Passkey (server side)

import { generateRegistrationOptions, verifyRegistrationResponse,
         generateAuthenticationOptions, verifyAuthenticationResponse }
  from '@simplewebauthn/server';

// Registration
app.post('/webauthn/register/options', authRequired, async (req, res) => {
  const opts = await generateRegistrationOptions({
    rpName: 'Acme',
    rpID: 'acme.com',
    userID: Buffer.from(req.user.id),
    userName: req.user.email,
    attestationType: 'none',
    authenticatorSelection: {
      residentKey: 'preferred', // passkey 만들기
      userVerification: 'preferred',
    },
  });
  await redis.set(`webauthn:reg:${req.user.id}`, opts.challenge, 'EX', 300);
  res.json(opts);
});

app.post('/webauthn/register/verify', authRequired, async (req, res) => {
  const expectedChallenge = await redis.get(`webauthn:reg:${req.user.id}`);
  const v = await verifyRegistrationResponse({
    response: req.body, expectedChallenge: expectedChallenge!,
    expectedOrigin: 'https://acme.com', expectedRPID: 'acme.com',
  });
  if (!v.verified) return res.status(400).end();

  await db.passkeys.insert({
    userId: req.user.id,
    credentialId: v.registrationInfo!.credentialID,
    publicKey: v.registrationInfo!.credentialPublicKey,
    counter: v.registrationInfo!.counter,
  });
  res.json({ ok: true });
});
// Authentication
app.post('/webauthn/login/options', async (req, res) => {
  const opts = await generateAuthenticationOptions({
    rpID: 'acme.com',
    userVerification: 'preferred',
  });
  await redis.set(`webauthn:auth:${opts.challenge}`, '1', 'EX', 300);
  res.json(opts);
});

app.post('/webauthn/login/verify', async (req, res) => {
  // ... credentialId 로 passkey lookup
  const passkey = await db.passkeys.findByCredentialId(req.body.id);
  const v = await verifyAuthenticationResponse({
    response: req.body,
    expectedChallenge: ..., expectedOrigin, expectedRPID,
    authenticator: { credentialID: passkey.credentialId, credentialPublicKey: passkey.publicKey, counter: passkey.counter },
  });
  if (!v.verified) return res.status(401).end();
  await db.passkeys.update(passkey.id, { counter: v.authenticationInfo.newCounter });
  res.json(loginSuccess(passkey.userId));
});

Client (browser)

import { startRegistration, startAuthentication } from '@simplewebauthn/browser';

const opts = await fetch('/webauthn/register/options').then(r => r.json());
const att = await startRegistration(opts); // 시스템 dialog
await fetch('/webauthn/register/verify', { method: 'POST', body: JSON.stringify(att) });

Conditional UI (auto-fill passkey)

<input type="text" name="username" autocomplete="username webauthn" />
const att = await startAuthentication({ ...opts, useBrowserAutofill: true });

🤔 의사결정 기준

사용자 / 환경 추천
일반 사용자 TOTP + recovery codes
보안 강 TOTP + WebAuthn
미래 / UX 최고 Passkey (Apple/Google)
Enterprise SSO SAML / OIDC + MFA on IdP
SMS 비추 (SIM swap) — 다른 방법 없을 때만
Hardware token YubiKey + WebAuthn

안티패턴

  • SMS only: SIM swap.
  • TOTP secret 평문 저장: 암호화 저장 또는 KMS.
  • Recovery codes 무제한 사용: 1회용 필수.
  • Passkey 만 — fallback X: device 잃으면 lock-out. recovery 같이.
  • Counter 검증 안 함 (WebAuthn): clone 방어 못 함.
  • Challenge 재사용: replay 가능.
  • TOTP window 큼 (5+): 30초 코드 무의미.
  • 2FA 우회 endpoint: 모두 enforce.

🤖 LLM 활용 힌트

  • TOTP = otplib + recovery codes.
  • Passkey = @simplewebauthn (server + browser).
  • 두 개 모두 지원 + recovery.

🔗 관련 문서