본문으로 건너뛰기
Kreath Archive
TechProjectsBooksAbout
TechProjectsBooksAbout
TechProjectsBooksAbout
© 2026 Kreath. All rights reserved.
홈TechProjectsBooksAbout
//
  1. 홈
  2. 테크
  3. 3장: Passkeys와 WebAuthn - 비밀번호 없는 미래
2026년 6월 15일·아키텍처·

3장: Passkeys와 WebAuthn - 비밀번호 없는 미래

WebAuthn 프로토콜과 FIDO2 표준의 구조, 패스키의 등록 및 인증 플로우, 플랫폼 인증기와 크로스 디바이스 인증의 동작 원리를 설명합니다.

16분674자1개 섹션
securityprotocoldesign-patternsinfrastructureperformance
공유
modern-auth3 / 10
12345678910
이전2장: OAuth 2.1과 OIDC - 현대 인증의 기반다음4장: Keycloak과 Auth0 - 아이덴티티 플랫폼 비교

Passkeys와 WebAuthn - 비밀번호 없는 미래

비밀번호는 인터넷 보안의 가장 오래된 적입니다. 사용자는 기억하기 쉬운 약한 비밀번호를 여러 사이트에 재사용하고, 공격자는 피싱과 크리덴셜 스터핑(Credential Stuffing)으로 이를 악용합니다. 패스키(Passkeys)는 이 근본적 문제를 해결하기 위해 등장한 기술로, 비밀번호를 공개키 암호학 기반의 자격 증명으로 완전히 대체합니다.

FIDO2와 WebAuthn의 관계

먼저 용어 관계를 명확히 정리합니다. 패스키 생태계는 여러 표준과 사양이 중첩되어 혼동이 발생하기 쉽습니다.

용어정의
FIDO2FIDO Alliance가 정의한 인증 프레임워크. WebAuthn + CTAP2의 조합
WebAuthnW3C 표준. 브라우저/플랫폼이 제공하는 JavaScript API
CTAP2Client to Authenticator Protocol. 클라이언트와 외부 인증기(보안 키) 간 통신 규약
패스키(Passkeys)동기화 가능한(Syncable) FIDO 자격 증명. Apple, Google, Microsoft가 공동 추진
Info

패스키는 기술적으로 "동기화 가능한 WebAuthn 자격 증명"입니다. 기존 FIDO2 보안 키(YubiKey 등)는 디바이스에 바인딩되어 동기화가 불가능했지만, 패스키는 iCloud Keychain, Google Password Manager 등을 통해 디바이스 간 동기화됩니다.

WebAuthn 프로토콜 아키텍처

WebAuthn의 핵심 참여자는 세 가지입니다.

  • 신뢰 당사자(Relying Party, RP): 인증을 요청하는 웹 서비스
  • 클라이언트: 브라우저 또는 OS. WebAuthn API를 제공하며, RP와 인증기 사이를 중개
  • 인증기(Authenticator): 자격 증명을 생성하고 서명하는 주체. 플랫폼 인증기(지문, Face ID)와 로밍 인증기(보안 키)로 구분

인증기의 유형

유형설명예시
플랫폼 인증기디바이스에 내장Touch ID, Face ID, Windows Hello
로밍 인증기외부 디바이스YubiKey, Titan Security Key
하이브리드다른 디바이스를 인증기로 사용스마트폰으로 PC 로그인 (QR 기반)

등록(Registration) 플로우

패스키 등록은 사용자가 서비스에 새로운 공개키 자격 증명을 생성하는 과정입니다.

서버 측 등록 구현

server/passkey-registration.ts
typescript
import {
  generateRegistrationOptions,
  verifyRegistrationResponse,
} from "@simplewebauthn/server";
 
// 1단계: 등록 옵션 생성
async function createRegistrationOptions(userId: string) {
  const user = await getUserById(userId);
  const existingCredentials = await getCredentialsByUserId(userId);
 
  const options = await generateRegistrationOptions({
    rpName: "My Service",
    rpID: "example.com",
    userName: user.email,
    userDisplayName: user.displayName,
    // 이미 등록된 인증기 제외
    excludeCredentials: existingCredentials.map((cred) => ({
      id: cred.credentialId,
      type: "public-key",
    })),
    authenticatorSelection: {
      // 플랫폼 인증기 우선 (패스키)
      authenticatorAttachment: "platform",
      // 사용자 검증 필수 (생체 인증 등)
      userVerification: "required",
      // Discoverable Credential 필수 (사용자 이름 없는 인증 가능)
      residentKey: "required",
    },
    attestationType: "none",
  });
 
  // challenge를 세션에 저장
  await storeChallenge(userId, options.challenge);
 
  return options;
}
 
// 2단계: 등록 응답 검증
async function verifyRegistration(
  userId: string,
  registrationResponse: RegistrationResponseJSON,
) {
  const expectedChallenge = await getStoredChallenge(userId);
 
  const verification = await verifyRegistrationResponse({
    response: registrationResponse,
    expectedChallenge,
    expectedOrigin: "https://example.com",
    expectedRPID: "example.com",
  });
 
  if (verification.verified && verification.registrationInfo) {
    // 공개키와 자격 증명 정보를 DB에 저장
    await saveCredential({
      userId,
      credentialId: verification.registrationInfo.credential.id,
      publicKey: verification.registrationInfo.credential.publicKey,
      counter: verification.registrationInfo.credential.counter,
      transports:
        registrationResponse.response.transports ?? [],
    });
  }
 
  return verification.verified;
}

클라이언트 측 등록 구현

client/passkey-registration.ts
typescript
import { startRegistration } from "@simplewebauthn/browser";
 
async function registerPasskey(): Promise<boolean> {
  try {
    // 서버에서 등록 옵션 가져오기
    const optionsResponse = await fetch("/api/auth/passkey/register/options", {
      method: "POST",
    });
    const options = await optionsResponse.json();
 
    // WebAuthn API 호출 (브라우저가 인증기와 통신)
    const registrationResponse = await startRegistration({
      optionsJSON: options,
    });
 
    // 서버에 결과 전송
    const verifyResponse = await fetch("/api/auth/passkey/register/verify", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(registrationResponse),
    });
 
    const result = await verifyResponse.json();
    return result.verified;
  } catch (error) {
    if (error instanceof Error && error.name === "NotAllowedError") {
      console.error("사용자가 인증을 취소했습니다.");
    }
    return false;
  }
}

인증(Authentication) 플로우

등록 이후의 인증 과정은 더 간단합니다. 서버가 챌린지를 보내면, 인증기가 개인키로 서명하고, 서버가 저장된 공개키로 검증합니다.

server/passkey-authentication.ts
typescript
import {
  generateAuthenticationOptions,
  verifyAuthenticationResponse,
} from "@simplewebauthn/server";
 
// 인증 옵션 생성
async function createAuthenticationOptions() {
  const options = await generateAuthenticationOptions({
    rpID: "example.com",
    userVerification: "required",
    // allowCredentials를 비워두면 Discoverable Credential 사용
    // (사용자가 패스키 목록에서 선택)
  });
 
  await storeChallenge("auth", options.challenge);
  return options;
}
 
// 인증 응답 검증
async function verifyAuthentication(
  authResponse: AuthenticationResponseJSON,
) {
  const expectedChallenge = await getStoredChallenge("auth");
 
  // credentialId로 저장된 자격 증명 조회
  const credential = await getCredentialById(authResponse.id);
  if (!credential) {
    throw new Error("등록되지 않은 자격 증명입니다.");
  }
 
  const verification = await verifyAuthenticationResponse({
    response: authResponse,
    expectedChallenge,
    expectedOrigin: "https://example.com",
    expectedRPID: "example.com",
    credential: {
      id: credential.credentialId,
      publicKey: credential.publicKey,
      counter: credential.counter,
    },
  });
 
  if (verification.verified) {
    // 카운터 업데이트 (복제 감지)
    await updateCredentialCounter(
      credential.credentialId,
      verification.authenticationInfo.newCounter,
    );
  }
 
  return {
    verified: verification.verified,
    userId: credential.userId,
  };
}
Warning

카운터(counter) 검증은 인증기 복제 감지에 중요합니다. 인증기가 사용될 때마다 카운터가 증가하므로, 서버에 저장된 카운터보다 작은 값이 들어오면 복제된 인증기를 의심해야 합니다. 다만 패스키(동기화 자격 증명)에서는 카운터가 항상 0일 수 있으므로, 이 경우에는 카운터 검증을 완화해야 합니다.

크로스 디바이스 인증

패스키의 강력한 기능 중 하나는 하이브리드 인증(Hybrid Authentication)입니다. 예를 들어, PC의 브라우저에서 로그인할 때 스마트폰의 패스키를 사용할 수 있습니다.

하이브리드 인증은 내부적으로 Bluetooth Low Energy(BLE)를 사용하여 두 디바이스의 물리적 근접성을 확인합니다. 이는 원격 피싱 공격을 방지하는 핵심 메커니즘입니다. 공격자가 QR 코드 이미지를 피해자에게 전달해도, BLE 근접 확인을 통과할 수 없습니다.

패스키 동기화 인프라

패스키가 기존 FIDO2 보안 키와 구별되는 가장 큰 특징은 디바이스 간 동기화입니다.

Info

패스키의 개인키는 각 플랫폼의 안전한 동기화 인프라를 통해 E2E 암호화되어 전송됩니다. Apple의 경우 iCloud Keychain이 HSM(Hardware Security Module)으로 보호된 키로 E2E 암호화를 수행합니다. 서비스 제공자(RP)는 개인키에 접근할 수 없습니다.

패스키의 보안 특성

패스키가 비밀번호보다 근본적으로 안전한 이유를 정리합니다.

공격 유형비밀번호패스키
피싱취약 (가짜 사이트에 입력)면역 (RP ID 바인딩)
크리덴셜 스터핑취약 (재사용된 비밀번호)면역 (사이트별 고유 키 쌍)
서버 침해해시 탈취 후 크래킹 가능공개키만 저장 (무의미)
중간자 공격가능 (SSL Strip 등)면역 (origin 바인딩)
소셜 엔지니어링가능 (비밀번호 유출)불가능 (개인키 추출 불가)

패스키의 피싱 방지가 가능한 이유는 오리진 바인딩(Origin Binding)에 있습니다. 인증기가 서명을 생성할 때 RP의 오리진 정보가 포함되므로, example.com에서 생성된 자격 증명은 examp1e.com(피싱 사이트)에서 사용할 수 없습니다.

도입 현황과 과제

2026년 현재, 패스키 지원은 빠르게 확산되고 있습니다.

  • 플랫폼 지원: iOS 16+, Android 9+, macOS Ventura+, Windows 10+ 에서 지원
  • 브라우저 지원: Chrome, Safari, Edge, Firefox 모두 지원
  • 주요 서비스 도입: Google, Apple, Microsoft, GitHub, Amazon, PayPal 등

하지만 여전히 해결해야 할 과제가 있습니다.

  • 계정 복구: 모든 디바이스를 분실한 경우의 복구 절차가 필요합니다
  • 엔터프라이즈 관리: 조직에서 직원의 패스키를 관리하는 표준이 아직 성숙하지 않습니다
  • 크로스 플랫폼 동기화: Apple과 Google 생태계 간 패스키 이동이 제한적입니다
Tip

패스키를 도입할 때는 기존 인증 수단(비밀번호 + MFA)을 즉시 제거하지 말고, 점진적으로 전환하는 것이 좋습니다. 패스키를 우선 옵션으로 제시하되, 사용할 수 없는 환경을 위한 폴백을 유지합니다.

마치며

패스키와 WebAuthn은 비밀번호의 근본적 한계를 공개키 암호학으로 해결하는 기술입니다. 피싱에 면역이고, 사용자 경험도 비밀번호보다 나으며, 서버에 비밀을 저장하지 않습니다. 인증의 미래가 비밀번호 없는 세상을 향한다면, 패스키는 그 방향의 가장 현실적인 구현입니다.

다음 장에서는 이러한 인증 프로토콜들을 실제로 구현하고 관리하는 아이덴티티 플랫폼(Identity Platform)을 비교합니다. Keycloak, Auth0, Zitadel의 아키텍처와 트레이드오프를 살펴보겠습니다.

이 글이 도움이 되셨나요?

관련 글

아키텍처

4장: Keycloak과 Auth0 - 아이덴티티 플랫폼 비교

Keycloak, Auth0, Zitadel의 아키텍처와 기능을 비교하고, 셀프 호스팅과 SaaS 간의 트레이드오프, 렐름/테넌트 설계, 페더레이션 전략을 정리합니다.

2026년 6월 18일·17분
아키텍처

2장: OAuth 2.1과 OIDC - 현대 인증의 기반

OAuth 2.1이 OAuth 2.0의 모범 사례를 어떻게 통합했는지, OIDC의 인증 레이어 구조, PKCE와 DPoP의 동작 원리를 코드와 다이어그램으로 설명합니다.

2026년 6월 12일·16분
아키텍처

5장: RBAC에서 ReBAC로 - 권한 모델의 진화

RBAC의 한계를 분석하고, ABAC과 ReBAC(관계 기반 접근 제어)의 개념, Google Zanzibar 모델, OpenFGA와 SpiceDB의 구현 방식을 비교합니다.

2026년 6월 20일·14분
이전 글2장: OAuth 2.1과 OIDC - 현대 인증의 기반
다음 글4장: Keycloak과 Auth0 - 아이덴티티 플랫폼 비교

댓글

목차

약 16분 남음
  • Passkeys와 WebAuthn - 비밀번호 없는 미래
    • FIDO2와 WebAuthn의 관계
    • WebAuthn 프로토콜 아키텍처
      • 인증기의 유형
    • 등록(Registration) 플로우
      • 서버 측 등록 구현
      • 클라이언트 측 등록 구현
    • 인증(Authentication) 플로우
    • 크로스 디바이스 인증
    • 패스키 동기화 인프라
    • 패스키의 보안 특성
    • 도입 현황과 과제
    • 마치며