패스키 조건부 생성 (로그인 후 프로모션)

이 가이드에서는 비밀번호 기반 로그인에 성공한 직후 사용자의 패스키를 자동으로 등록하여 마찰을 최소화하고 패스키 채택을 늘리는 방법을 자세히 설명합니다.

적절한 트리거 순간

자동 패스키 생성(조건부 생성 또는 자동 로그인 후 프로모션이라고도 함)은 비밀번호를 포함한 전체 로그인이 성공한 직후에만 트리거되어야 합니다(MUST).

  • 비밀번호 없는 흐름 (예: 매직 링크, SMS OTP, ID 제휴)에 조건부 생성을 시도하지 마세요.
  • 다중 인증이 필요한 경우 조건부 생성을 시작하기 전에 모든 인증이 완료될 때까지 기다려야 합니다(MUST).
  • 생성 엔드포인트에 요청하기 전에 유효하고 인증된 사용자 세션이 활성 상태인지 확인합니다.

구현 단계

1. 이전 자동 완성 작업 중단

로그인 페이지에서 양식 자동 완성 (조건부 UI/Get)을 사용하는 경우 브라우저 충돌을 방지하기 위해 활성 사용자 인증 정보 가져오기 호출을 중단해야 합니다.

  • navigator.credentials.create()를 호출하기 전에 대기 중인 navigator.credentials.get() 자동 완성 요청에 연결된 AbortController에서 abortController.abort()를 호출합니다.

2. 기능 감지

PublicKeyCredential.getClientCapabilities()로 conditionalCreate를 확인하여 조건부 생성을 사용할 수 있는지 확인합니다.

const capabilities = await PublicKeyCredential.getClientCapabilities();
if (capabilities.conditionalCreate) {
  // Conditional create is available
}

3. 조건부 생성으로 패스키 만들기

  • navigator.credentials.create() 옵션 내에서 mediation: 'conditional'를 전달합니다. 이렇게 하면 브라우저가 방해가 되는 모달 대화상자를 표시하지 않고 백그라운드에서 또는 상황에 맞게 패스키 생성 흐름을 자동으로 처리합니다.
  • 중복 키가 등록되지 않도록 사용자의 기존 패스키 사용자 인증 정보 ID로 excludeCredentials를 채웁니다.

4. 자동 오류 처리

  • 패스키 생성 프롬프트 (navigator.credentials.create)를 try/catch 블록으로 래핑합니다. 사용자에게 오류 UI를 렌더링하지 않고 일반적인 사용자 대상 예외(InvalidStateError, NotAllowedError, AbortError)를 포착하고 자동으로 무시해야 합니다(MUST).

5. 서버 측 사용자 현재 상태 확인

  • 서버 측 확인 엔드포인트는 조건부 생성 트리거에 의해 생성된 사용자 인증 정보를 확인할 때만 사용자 참여(UP) 요구사항(requireUserPresence: false)을 완화해야 합니다(MUST). 표준 명시적 생성의 경우 엄격한 현재 상태 확인이 활성 상태로 유지되어야 합니다.

6. 서버 인증 실패를 원활하게 처리

  • navigator.credentials.create()가 성공했지만 서버 인증 가져오기에서 잘못된 응답을 반환하는 경우 (예: 서명 확인 실패) PublicKeyCredential.signalUnknownCredential()를 호출하여 분리된 사용자 인증 정보가 패스키 제공업체에 남아 있지 않도록 합니다.

코드 예시

// optionsFetch and registerVerifyFetch are app-defined server endpoint requests
import { optionsFetch, registerVerifyFetch } from './api.js';

async function triggerConditionalCreate(loginAbortController) {
  const capabilities = await PublicKeyCredential.getClientCapabilities();
  if (capabilities.conditionalCreate !== true) {
    return; // Platform does not support conditional creation
  }

  // 1. Abort any active autofill conditional-get controllers to clear the WebAuthn pipeline
  loginAbortController.abort();

  // 2. Fetch creation options signaling the backend that this is a conditional request
  const creationOptionsJSON = await optionsFetch({ conditional: true });
  const publicKey = PublicKeyCredential.parseCreationOptionsFromJSON(creationOptionsJSON);

  let credential;
  try {
    // 3. Invoke silent credentials creation prompt
    credential = await navigator.credentials.create({ 
      publicKey,
      mediation: 'conditional' // Silent background creation mediation
    });
  } catch (e) {
    // 4. Silently swallow common WebAuthn browser exceptions
    if (['InvalidStateError', 'NotAllowedError', 'AbortError'].includes(e.name)) {
      return; 
    }
    console.error('Unexpected conditional create error:', e);
    return;
  }

  // 5. Server verification step using dedicated Try/Catch block
  let encodedResponse = credential.toJSON();
  try {
    const response = await registerVerifyFetch(encodedResponse);
    if (!response.ok) {
      // If the server verification fails, clean up using Signal API
      if (PublicKeyCredential.signalUnknownCredential) {
        await PublicKeyCredential.signalUnknownCredential({
          rpId, // RP ID must match the one defined on the server
          credentialId: encodedResponse.id
        });
      }
    }
  } catch (serverErr) {
    console.error('Verification network failure:', serverErr);
    if (PublicKeyCredential.signalUnknownCredential) {
      await PublicKeyCredential.signalUnknownCredential({
        rpId, // RP ID must match the one defined on the server
        credentialId: encodedResponse.id
      });
    }
  }
}

대체 전략

패스키 기능 감지 대체

항상 'webauthn-polyfills'를 설치하고 컨텍스트에서 가져옵니다. PublicKeyCredential가 지원되는 한 PublicKeyCredential.getClientCapabilities도 지원됩니다.

import 'webauthn-polyfills';

신호 API 동기화 대체

WebAuthn Signal API (webauthn-signals)는 비밀번호 관리자를 서버 사용자 인증 정보 상태와 동기화하는 데 사용되는 점진적 최적화입니다.

  • 대체 환경: if (PublicKeyCredential.signalUnknownCredential)를 통해 제한됩니다. 지원되지 않는 경우 백그라운드 인증 동기화는 브라우저 예외를 발생시키지 않고 정상적으로 우회됩니다.

간편한 JSON 직렬화 대체

항상 'webauthn-polyfills'를 설치하고 컨텍스트에서 가져옵니다. PublicKeyCredential가 지원되는 한 PublicKeyCredential.parseCreationOptionsFromJSON 및 PublicKeyCredential.prototype.toJSON도 지원됩니다.

import 'webauthn-polyfills';