WebAuthn으로 여는 passwordless 시대: 생체 인증의 실전 구현 예제
Web Authentication API(WebAuthn)를 활용해 지문이나 얼굴 인식 같은 생체 인증을 구현하는 방법을 Node.js와 브라우저에서 탐구합니다.
들어가며
웹 애플리케이션에서 비밀번호는 여전히 가장 흔한 인증 수단이지만, 피싱 공격과 입력 불편함으로 인해 대안이 절실합니다. Web Authentication API(WebAuthn)는 W3C와 FIDO Alliance가 표준화한 API로, 하드웨어 기반의 생체 인증(지문, 얼굴 인식 등)을 통해 비밀번호 없는 로그인 경험을 제공합니다. 이 포스트에서는 2025년 현재 WebAuthn의 최신 버전(Level 3)을 중심으로 Node.js 백엔드와 브라우저 프론트엔드를 연동한 구현 과정을 단계적으로 살펴보겠습니다. 개발자들이 쉽게 적용할 수 있도록 실전 예시를 중점으로 다루며, 보안 고려사항도 함께 논의하겠습니다.
WebAuthn의 기본 개념과 배경
WebAuthn 정의와 배경 지식
WebAuthn은 브라우저와 운영 체제에서 지원하는 표준 API로, Public Key Cryptography(PKC)를 기반으로 한 인증을 실현합니다. 2019년 처음 출시된 이후, 2025년 현재 Level 3 버전에서 다중 디바이스 지원과 향상된 프라이버시 기능이 추가되어 모바일부터 데스크톱까지 광범위하게 적용되고 있습니다. 이를 통해 사용자는 하드웨어 토큰(YubiKey)이나 내장 생체 센서(예: Touch ID, Windows Hello)를 활용해 안전한 인증을 수행할 수 있습니다. 전통적인 비밀번호와 달리 서버에 민감한 데이터를 저장하지 않기 때문에, 데이터 유출 위험이 크게 줄어듭니다. 이 API는 HTTPS 환경에서만 동작하며, Chrome, Firefox, Safari 등 주요 브라우저에서 지원됩니다.
WebAuthn의 인증 흐름
WebAuthn의 인증은 크게 등록(Registration)과 인증(Authentication) 두 단계로 나뉩니다. 등록 단계에서 사용자의 생체 데이터는 디바이스에만 저장되며, 공개키 쌍이 생성되어 서버에 공개키만 전달됩니다. 인증 시 사용자가 생체를 확인하면, 디바이스가 서명된 챌린지를 생성해 서버가 검증합니다. 이 과정에서 서버는 챌린지(랜덤 값)를 생성하고, 디바이스가 이를 서명한 후 반환받아 유효성을 확인합니다. 2025년 트렌드로, WebAuthn은 제로 트러스트 아키텍처와 결합되어 클라우드 기반 아이덴티티 관리(IDaaS)에서 필수 요소로 자리 잡고 있습니다. 이 흐름을 이해하면, 기존 OAuth나 JWT와의 통합도 수월해집니다.
WebAuthn의 장점과 단점
WebAuthn의 주요 장점은 피싱 저항성으로, 도메인 바인딩(domain-bound) 인증으로 인해 중간자 공격을 방지합니다. 또한, 사용자 경험(UX)이 우수해 비밀번호 입력 없이 1초 이내 로그인이 가능하며, GDPR 같은 프라이버시 규정 준수에 적합합니다. 반면, 단점으로는 하드웨어 의존성(생체 센서가 없는 디바이스에서 제한됨)과 초기 설정 복잡성, 그리고 레거시 브라우저 호환성 문제가 있습니다. 2025년 기준으로 글로벌 채택률이 80%를 넘었으나, 개발 시 폴백(fallback) 전략(비밀번호 옵션 제공)이 필요합니다. 이러한 균형을 통해 WebAuthn은 보안과 편의성의 최적점을 이룹니다.
Node.js와 TypeScript를 활용한 구현 예시
서버 측 등록 및 인증 로직 구현
WebAuthn 서버 구현은 Express.js와 TypeScript를 사용해 간단히 구성할 수 있습니다. 먼저, crypto
모듈과 @simplewebauthn/server
라이브러리를 활용합니다. 2025년 최신 버전(10.x)에서 지원하는 generateRegistrationOptions
와 verifyRegistrationResponse
함수를 사용해 등록을 처리합니다. 서버는 챌린지를 생성하고, 사용자 공개키를 데이터베이스(예: PostgreSQL)에 저장합니다.
// server.ts (Express + TypeScript)
import express from 'express';
import { generateRegistrationOptions, verifyRegistrationResponse } from '@simplewebauthn/server';
import { ISOBufferToString, StringToISOBuffer } from '@simplewebauthn/server/helper';
const app = express();
app.use(express.json());
// 등록 옵션 생성 (클라이언트에게 전달)
app.post('/auth/register/start', (req, res) => {
const { userName } = req.body;
const options = generateRegistrationOptions({
rpName: 'My App',
rpID: 'localhost', // 프로덕션에서는 도메인 사용
userID: new TextEncoder().encode(userName),
userName,
attestationType: 'direct', // 생체 인증 우선
challenge: new Uint8Array(32), // 32바이트 랜덤 챌린지
supportedAlgorithmList: [-7, -257], // ES256, RS256
});
// 세션에 챌린지 저장 (예: Redis)
res.json(options);
});
// 등록 응답 검증
app.post('/auth/register/finish', async (req, res) => {
const { response } = req.body;
const verification = await verifyRegistrationResponse({
response: {
...response,
challenge: StringToISOBuffer(response.challenge),
user: {
...response.user,
id: StringToISOBuffer(response.user.id),
},
},
expectedChallenge: '이전 챌린지 값', // 세션에서 불러옴
expectedOrigin: 'http://localhost:3000',
expectedRPID: 'localhost',
});
if (verification.verified) {
// 공개키를 DB에 저장
console.log('등록 성공');
res.json({ success: true });
} else {
res.status(400).json({ error: '등록 실패' });
}
});
app.listen(3000, () => console.log('Server running on port 3000'));
이 코드에서 챌린지는 세션 스토어(예: Express-session)에 저장해야 재생 공격을 방지할 수 있습니다. TypeScript 타입 안전성을 위해 @simplewebauthn/types
를 추가로 import합니다.
클라이언트 측 생체 인증 호출
브라우저에서 navigator.credentials
API를 사용해 등록과 인증을 처리합니다. Vanilla JavaScript나 React에서 쉽게 적용 가능하며, 여기서는 TypeScript로 작성합니다. 2025년 브라우저는 PublicKeyCredentialCreationOptions
를 완벽히 지원합니다.
// client.ts (브라우저 측)
async function register() {
// 서버로부터 옵션 가져오기
const response = await fetch('/auth/register/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userName: 'testuser' }),
});
const options = await response.json();
// PublicKeyCredential 생성 (생체 인증 호출)
const credential = await navigator.credentials.create({
publicKey: {
...options.publicKey,
challenge: new Uint8Array(options.publicKey.challenge),
user: {
...options.publicKey.user,
id: new Uint8Array(options.publicKey.user.id),
},
},
} as PublicKeyCredentialCreationOptions);
// 서버로 응답 전송
await fetch('/auth/register/finish', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ response: credential.response }),
});
}
// 인증 함수 (로그인 시)
async function authenticate() {
const response = await fetch('/auth/login/start'); // 유사한 챌린지 생성 엔드포인트
const options = await response.json();
const credential = await navigator.credentials.get({
publicKey: {
...options.publicKey,
challenge: new Uint8Array(options.publicKey.challenge),
allowCredentials: [], // 서버 저장 credential ID 목록
timeout: 60000,
},
} as PublicKeyCredentialRequestOptions);
// 서버 검증 요청
await fetch('/auth/login/finish', {
method: 'POST',
body: JSON.stringify({ response: credential.response }),
});
}
이 예시에서 브라우저는 사용자의 생체 센서를 자동 호출하며, 성공 시 서명된 데이터를 반환합니다. 에러 핸들링(예: NotAllowedError
)을 추가해 UX를 개선하세요. 테스트 시 Chrome DevTools의 WebAuthn 플래그를 활성화하면 시뮬레이션 가능합니다.
구현 시 보안 및 최적화 팁
생체 인증 구현에서 보안은 최우선입니다. 챌린지는 항상 랜덤하게 생성하고, TTL(Time To Live)을 5분으로 제한하세요. 서버 측에서는 attestation
을 검증해 디바이스 신뢰성을 확인합니다. 2025년 트렌드로, WebAuthn을 Passkeys(Apple/Google의 표준)와 연동하면 크로스 플랫폼 호환성이 높아집니다. 단, 개발 환경에서는 rpID
를 localhost
로 설정하고, 프로덕션에서 attestationType: 'indirect'
을 사용해 프라이버시를 강화하세요. 성능 측면에서, 등록 후 공개키를 캐싱하지 말고 매번 검증하며, JWT 토큰으로 세션을 연계합니다.
WebAuthn의 고급 적용과 미래 전망
다중 디바이스와 Passkeys 통합
2025년 WebAuthn Level 3은 다중 디바이스 전환(예: 스마트폰에서 PC로 키 이동)을 지원하며, iCloud Keychain이나 Google Password Manager와의 연동으로 사용자 편의가 극대화됩니다. 구현 시 residentKey: 'required'
옵션을 사용해 디바이스에 키를 영구 저장하면, 별도 키 관리 없이 반복 로그인이 가능합니다. 이는 모바일 웹 앱(PWA)에서 특히 유용하며, 예를 들어 e-commerce 사이트에서 카트 복원과 결합할 수 있습니다. 그러나, 키 동기화 시 클라우드 보안(예: end-to-end 암호화)을 확보해야 합니다.
잠재적 도전과 대안 전략
생체 인증의 한계로, 센서 미지원 사용자(약 20%)를 위한 폴백이 필수입니다. OTP나 이메일 링크를 병행하며, A/B 테스트로 전환율을 측정하세요. 미래 전망으로는, WebAuthn이 Web3 지갑 인증과 결합되어 블록체인 DApp에서 표준이 될 것으로 보입니다. 개발자들은 FIDO2 인증서를 취득해 표준 준수를 보장하는 것이 좋습니다.
마무리
WebAuthn은 비밀번호의 시대를 마감하고 생체 인증의 새로운 패러다임을 열고 있습니다. 이 포스트에서 다룬 Node.js 구현 예시를 통해, 보안과 UX를 동시에 달성할 수 있음을 확인할 수 있습니다. 2025년 개발자로서 WebAuthn을 도입하면 시스템의 신뢰성을 높일 수 있으며, 점진적 마이그레이션(기존 시스템과 병행)으로 리스크를 최소화하세요. 실제 프로젝트에서 시도해보고, 피드백을 공유해주세요.
참고
- W3C Web Authentication API Level 3 사양: https://www.w3.org/TR/webauthn-3/
- FIDO Alliance 문서: https://fidoalliance.org/specs/ (2025년 업데이트 버전 참조)
- SimpleWebAuthn 라이브러리 공식 가이드: https://github.com/MasterKale/SimpleWebAuthn
- MDN Web Docs - Web Authentication API: https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API