이 가이드에서는 사용자가 계정에 패스키를 등록하여 피싱에 강하고 안전한 비밀번호 없는 로그인 대안을 제공하는 방법을 자세히 설명합니다.
데이터베이스 요구사항
패스키 등록을 지원하려면 데이터베이스 사용자 인증 정보 테이블에 다음 필드를 저장해야 합니다.
export interface StoredPasskeyCredential {
id: string; // Base64URL-encoded credential ID (unique lookup key)
passkeyUserId: string; // Associated application user ID
credentialPublicKey: string; // Base64URL-encoded public key used to verify assertion signatures
credentialType: "public-key";
credentialDeviceType: "singleDevice" | "multiDevice"; // Helps distinguish device-bound vs cloud-synced passkeys
credentialBackedUp: boolean; // Boolean backup state reported by the authenticator
aaguid: string; // Authenticator Attestation GUID
providerIcon?: string; // Provider icon derived from the AAGUID registry (dark or light theme URLs)
name: string; // Provider name derived from AAGUID registry
transports: string[]; // Array of transport names (e.g. 'internal', 'hybrid') necessary for exclusion options
lastUsedAt?: number; // Optional epoch timestamp of last sign-in
registeredAt: number; // Registration epoch timestamp
counter: number; // Authenticator sign-in signature counter used to prevent replay attacks
}
서버 측
옵션 생성
WebAuthn 생성 매개변수를 생성하는 엔드포인트를 만듭니다. 암호화를 직접 구현하는 대신 카테고리 표준에 따라 검증된 라이브러리를 사용합니다.
- 사전 정의된 RP ID 사용: 사전 정의된 적절한 RP ID를 상수 문자열로 사용합니다.
- 보안 챌린지 만들기: 서버에서 엔트로피가 높고 암호화 방식으로 안전한 무작위 버퍼를 생성하고, 이를 사용자의 세션에 안전하게 저장하고, 옵션 전송을 위해 Base64URL로 인코딩합니다.
- 중복 패스키 방지: 사용자의 기존 사전 등록 사용자 인증 정보 ID를
excludeCredentials옵션 배열에 매핑합니다. 이렇게 하면 인증자가 동일한 패스키 제공업체 계정에 중복 사용자 인증 정보를 등록하지 못합니다. - 검색 가능한 사용자 인증 정보 적용:
authenticatorSelection옵션에서requireResidentKey: true및residentKey: "required"를 설정하여 검색 가능한 사용자 인증 정보를 요청합니다. 이는 검색 가능한 로그인에 필요합니다. - 사용자 인증 구성:
userVerification: "preferred"또는userVerification: "required"를 지정합니다. 많은 규정 준수 사용 사례 (예: 금융, 의료)에서는 생성 시 사용자 인증을 시행하기 위해'required'가 필요합니다. - 연결 범위 결정:
- 프로모션 흐름: 표준 비밀번호 로그인 또는 가입 후 프로모션 직후에 패스키 생성을 제안하는 경우
authenticatorAttachment: "platform"를 설정하여 플랫폼 인증자를 적용하고 외부 보안 키 프롬프트를 우회합니다. - 관리 흐름: 플랫폼 인증자 외에 외부 보안 키가 지원되는 전용 설정 또는 보안 패널에서 호출되는 경우
authenticatorAttachment속성을 완전히 생략합니다. - 도움말:
promotion: boolean요청 플래그를 수락하여 단일 엔드포인트로 두 흐름을 조건부로 처리합니다.
- 프로모션 흐름: 표준 비밀번호 로그인 또는 가입 후 프로모션 직후에 패스키 생성을 제안하는 경우
// Options generation example
const options = {
challenge: serverGeneratedBase64UrlChallenge, // Cryptographically random challenge
rp: { id: "example.com", name: "Secure Application" },
user: {
id: userBase64UrlId, // Unique base64url string identifying the account
name: "user@example.com",
displayName: "Jane Doe",
},
pubKeyCredParams: [
{
type: "public-key",
alg: -7,
},
{
type: "public-key",
alg: -257,
},
],
excludeCredentials: userExistingCredentials.map((cred) => ({
type: "public-key",
id: cred.id,
transports: cred.transports,
})),
authenticatorSelection: {
residentKey: "required",
requireResidentKey: true,
userVerification: "preferred",
...(isPromotionFlow && { authenticatorAttachment: "platform" }),
},
};
인증
- 챌린지 확인: 예상되는 세션 바인딩 챌린지에 대해 챌린지를 안전하게 확인합니다.
- 사용자 현재 상태 확인:
- 파싱된 인증자 데이터에 반환된 사용자 현재 상태 (UP) 플래그가
true인지 확인하여 생성 시점에 사용자가 실제로 있는지 확인합니다.
- 파싱된 인증자 데이터에 반환된 사용자 현재 상태 (UP) 플래그가
- 'preferred'에 대한 완화된 인증:
- 생성 옵션이
userVerification: "preferred"를 지정한 경우 서버 측 확인 호출은requireUserVerification: false로 구성되어야 합니다(MUST). 그렇지 않으면 사용자 인증 없이 등록하는 인증자 (예: 화면 잠금 사용 중지됨)가 허위 서버 인증 실패를 트리거합니다.
- 생성 옵션이
클라이언트 측 로직
- 페이지 로드 시 UI 제어:
- 페이지 로드 시
PublicKeyCredential.getClientCapabilities()를 호출하고conditionalGet또는passkeyPlatformAuthenticator를 사용할 수 없는 경우 '패스키 만들기' 버튼을 사용 중지합니다.
- 페이지 로드 시
- 생성 호출 및 직렬화:
PublicKeyCredential.parseCreationOptionsFromJSON()를 사용하여 서버 옵션을 디코딩하고 결과 구성을navigator.credentials.create()에 전달합니다.credential.toJSON()를 호출하여 확인 엔드포인트를 가져오기 전에AuthenticatorAttestationResponse를 유효한 JSON 직렬화 가능 객체로 인코딩합니다.
- WebAuthn 예외 처리:
InvalidStateError: 일치하는 패스키가 이미 있습니다(excludeCredentials에 의해 일치됨).NotAllowedError: 사용자가 인증 패스키 대화상자를 취소했거나 시간이 초과되었습니다.AbortError: 작업이 취소되었습니다.SecurityError: 보안 출처 (HTTPS) 또는 RP ID 불일치 오류 (구성 문제)
- Signal API의 Try/Catch 분리:
- try/catch 블록에서 서버 인증
fetch()호출을 래핑합니다. 서버 인증 가져오기가 실패하면 (상태response.ok === false또는 네트워크 예외 발생)signalUnknownCredential()를 호출합니다.
- try/catch 블록에서 서버 인증
// optionsFetch and registerVerifyFetch are app-defined HTTP methods
import { optionsFetch, registerVerifyFetch } from "./api.js";
async function registerPasskey(isPromotion = false) {
// Verify passkey capability and conditional UI are available
const capabilities = await PublicKeyCredential.getClientCapabilities();
if (
!capabilities.passkeyPlatformAuthenticator ||
!capabilities.conditionalGet
) {
// Hide "Create passkey" buttons and fall back to password flows instead
showStandardPasswordFallbackUI();
return;
}
const creationOptionsJSON = await optionsFetch({ promotion: isPromotion });
const publicKey =
PublicKeyCredential.parseCreationOptionsFromJSON(creationOptionsJSON);
let credential;
try {
// passkey prompt execution
credential = await navigator.credentials.create({ publicKey });
} catch (err) {
if (err.name === "InvalidStateError") {
console.log("A passkey already exists for this account.");
alert("A passkey already exists for this account.");
} else if (err.name === "SecurityError") {
console.error("Configuration RP ID or Secure Context error.");
alert("Configuration RP ID or Secure Context error.");
} else if (err.name === "NotAllowedError") {
console.log("User cancelled the passkey dialog.");
} else if (err.name === "AbortError") {
console.log("The creation operation has been aborted.");
}
return; // Safe API exit, do not signal unknown for standard WebAuthn cancels
}
// Server Verification phase (Segregated Try/Catch)
let encodedResponse = credential.toJSON();
try {
const response = await registerVerifyFetch(encodedResponse);
if (!response.ok) {
// Server verification failed to verify/authenticate the credential (orphaned)
await PublicKeyCredential.signalUnknownCredential({
rpId, // RP ID must match the one defined on the server
credentialId: encodedResponse.id, // Base64URL-encoded credential ID
});
}
} catch (serverErr) {
console.error("Server verification network failure:", serverErr);
await publickeycredential.signalunknowncredential({
rpId, // RP ID must match the one defined on the server
credentialid: encodedresponse.id, // base64url-encoded credential id
});
}
}
대체 전략
신호 API 동기화 대체
WebAuthn Signal API (webauthn-signals)는 비밀번호 관리자를 서버 사용자 인증 정보 상태와 동기화하는 데 사용되는 점진적 최적화입니다.
- 대체 환경: 브라우저에서
PublicKeyCredential.signalUnknownCredential를 지원하지 않는 경우 기능 감지 게이팅(if (PublicKeyCredential.signalUnknownCredential))을 통해 호출을 안전하게 우회해야 하며(MUST), 서버 측 확인은 관리자 업데이트를 트리거하지 않고 실패를 기록하기만 합니다.
간편한 JSON 직렬화 대체
항상 'webauthn-polyfills'를 설치하고 컨텍스트에서 가져옵니다.
PublicKeyCredential가 지원되는 한 PublicKeyCredential.parseCreationOptionsFromJSON 및 PublicKeyCredential.prototype.toJSON도 지원됩니다.
import 'webauthn-polyfills';