🔒 보안

FIDO2

패스워드 없는 인증 표준

📖 상세 설명

FIDO2는 FIDO Alliance와 W3C가 공동 개발한 패스워드 없는 인증(Passwordless Authentication) 표준입니다. WebAuthn(Web Authentication API)과 CTAP(Client to Authenticator Protocol)의 두 가지 핵심 사양으로 구성됩니다. 사용자는 생체인식(지문, 얼굴), 보안키, PIN 등을 통해 안전하게 인증합니다.

FIDO2의 핵심은 공개키 암호화입니다. 등록 시 사용자 기기에서 공개키/비밀키 쌍이 생성되고, 비밀키는 기기에 안전하게 저장됩니다. 공개키만 서버로 전송되어 저장되므로, 서버가 해킹되어도 비밀키가 유출되지 않습니다. 피싱 공격도 원천 차단됩니다.

인증 흐름은 Challenge-Response 방식입니다. 서버가 랜덤 challenge를 보내면, 클라이언트는 비밀키로 서명하여 응답합니다. 서버는 저장된 공개키로 서명을 검증합니다. 비밀키가 기기를 떠나지 않으므로, Man-in-the-Middle 공격에도 안전합니다.

FIDO2는 모든 주요 브라우저(Chrome, Safari, Firefox, Edge)와 운영체제(Windows Hello, macOS Touch ID, Android)에서 지원됩니다. Passkey, WebAuthn, 보안키(YubiKey 등) 모두 FIDO2를 기반으로 합니다. 기업 환경에서는 MFA의 강력한 두 번째 요소로 활용됩니다.

💻 코드 예제

// FIDO2/WebAuthn 등록 (Registration)
async function registerCredential(userId: string, userName: string) {
  // 서버에서 challenge 및 옵션 가져오기
  const optionsRes = await fetch('/api/webauthn/register/options', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ userId, userName })
  });
  const options = await optionsRes.json();

  // Base64URL → ArrayBuffer 변환
  options.challenge = base64URLToBuffer(options.challenge);
  options.user.id = base64URLToBuffer(options.user.id);

  // 브라우저 WebAuthn API 호출 - 생체인식/보안키 프롬프트
  const credential = await navigator.credentials.create({
    publicKey: {
      ...options,
      authenticatorSelection: {
        authenticatorAttachment: 'platform', // 내장 인증기 (Touch ID 등)
        userVerification: 'required',        // 생체인식 필수
        residentKey: 'required'              // Passkey 지원
      },
      pubKeyCredParams: [
        { type: 'public-key', alg: -7 },    // ES256 (ECDSA)
        { type: 'public-key', alg: -257 }   // RS256 (RSA)
      ]
    }
  });

  // 서버로 credential 전송하여 등록 완료
  await fetch('/api/webauthn/register/verify', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      id: credential.id,
      rawId: bufferToBase64URL(credential.rawId),
      type: credential.type,
      response: {
        clientDataJSON: bufferToBase64URL(credential.response.clientDataJSON),
        attestationObject: bufferToBase64URL(credential.response.attestationObject)
      }
    })
  });
}

// FIDO2/WebAuthn 인증 (Authentication)
async function authenticate() {
  const optionsRes = await fetch('/api/webauthn/authenticate/options');
  const options = await optionsRes.json();
  options.challenge = base64URLToBuffer(options.challenge);

  // 사용자에게 인증 프롬프트 표시
  const assertion = await navigator.credentials.get({
    publicKey: options,
    mediation: 'optional' // Passkey 자동완성 지원
  });

  // 서버에서 서명 검증
  const verifyRes = await fetch('/api/webauthn/authenticate/verify', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      id: assertion.id,
      response: {
        authenticatorData: bufferToBase64URL(assertion.response.authenticatorData),
        clientDataJSON: bufferToBase64URL(assertion.response.clientDataJSON),
        signature: bufferToBase64URL(assertion.response.signature)
      }
    })
  });

  return verifyRes.json();
}

🗣️ 실무 대화 예시

보안팀 미팅에서:

"MFA 피로 공격 때문에 OTP 방식이 뚫렸어요. FIDO2 보안키로 전환하면 피싱도 막고 사용자 경험도 좋아집니다. YubiKey 배포하거나, Windows Hello/Touch ID 같은 플랫폼 인증기만 써도 됩니다."

기술 면접에서:

"FIDO2가 피싱에 강한 이유는요?" - "origin binding 덕분입니다. credential 생성 시 도메인이 바인딩되어서, 공격자 사이트에서는 해당 credential을 사용할 수 없어요. 서버 challenge도 replay attack을 방지합니다."

아키텍처 설계에서:

"Passkey로 패스워드 완전히 없앨 수 있나요?" - "가능해요. 하지만 계정 복구 플로우가 중요합니다. 여러 기기에 Passkey 등록하게 하거나, 백업 코드, 신뢰할 수 있는 연락처 복구 등을 같이 설계해야 해요."

⚠️ 주의사항

🔗 관련 용어

Passkey OAuth 2.1 MFA Token-based Auth Zero Trust

📚 더 배우기

📄 FIDO Alliance - FIDO2 공식 페이지 📄 WebAuthn Guide - 개발자 가이드 📄 MDN - Web Authentication API