eunsoolib
인증·신원

web-identity

Passkeys·FedCM·Digital Credentials·DBSC 등 Web Identity API 통합

Google I/O 2025에서 발표된 최신 웹 인증/신원확인 API들을 통합하는 TypeScript 라이브러리입니다.

다루는 API

모듈APIChrome 버전설명
CredentialManagerCredential Manager136+ (flag)비밀번호 + 패스키 + FedCM 통합 로그인 UI
PasskeysWebAuthn108+ / 136+패스키 등록/인증, 자동생성, Signal API
FedCMFederated Credential Management132+제휴 인증 (active/passive 모드)
DigitalCredentialsDigital CredentialsOT디지털 지갑 기반 신원 확인
DBSCDevice Bound Session Credentials실험기기 바인딩 세션

설치

pnpm add @cbcruk/web-identity

사용법

import { WebIdentity, DigitalCredentials, Passkeys } from '@cbcruk/web-identity'

const identity = new WebIdentity('example.com')

// 지원 기능 감지 — 브라우저가 지원을 확인해 주지 않는 기능은 false
const support = await identity.detectFeatures()
console.log(support)
// {
//   credentialManager: true,
//   webauthn: true,
//   conditionalMediation: true,
//   immediateMediation: false, // getClientCapabilities()의 immediateGet
//   passkeyConditionalCreate: true, // getClientCapabilities()의 conditionalCreate
//   signalAPI: true,
//   fedcm: true,
//   ...
// }

1. 통합 로그인 (비밀번호 + 패스키)

하나의 브라우저 프롬프트에서 비밀번호와 패스키를 동시에 제공:

const result = await identity.signIn({
  password: true,
  publicKey: {
    challenge: serverChallenge,
    rpId: 'example.com',
  },
  mediation: 'immediate', // 이 기기에서 사용 가능한 것만
})

if (!result) {
  // 이 기기에 저장된 자격증명 없음 → 기존 로그인 UI 표시
  showLoginForm()
} else if (result.type === 'password') {
  // 비밀번호로 로그인
  await loginWithPassword(result.credential)
} else if (result.type === 'publicKey') {
  // 패스키로 로그인
  const serialized = Passkeys.serialize(result.credential)
  await verifyPasskeyOnServer(serialized)
}

2. 패스키 등록

const credential = await identity.registerPasskey({
  rp: { id: 'example.com', name: 'My App' },
  user: {
    id: new TextEncoder().encode(userId),
    name: 'alice@example.com',
    displayName: 'Alice',
  },
  challenge: serverChallenge,
})

// 서버로 전송
const data = Passkeys.serialize(credential)
await fetch('/api/passkey/register', {
  method: 'POST',
  body: JSON.stringify(data),
})

3. 패스키 자동 업그레이드 (Chrome 136+)

비밀번호 로그인 후, 사용자 방해 없이 패스키를 자동 생성:

// 비밀번호 로그인 성공 후
await identity.upgradeToPasskey({
  rp: { id: 'example.com', name: 'My App' },
  user: {
    id: new TextEncoder().encode(userId),
    name: 'alice@example.com',
    displayName: 'Alice',
  },
  challenge: serverChallenge,
})
// → 성공 시 패스키 생성됨 (UI 표시 없음)
// → 미지원 환경이거나 브라우저가 거절(NotAllowedError)하면 null 반환
// → 그 밖의 에러(InvalidStateError 등)는 WebIdentityError로 던짐

지원 여부는 PublicKeyCredential.getClientCapabilities()conditionalCreate로 먼저 확인하므로, 미지원 브라우저에서 일반 등록 UI가 뜨지 않습니다. timeout은 그대로 전달됩니다(생략 시 브라우저 기본값).

identity.passkeys.create({ ...options, conditional: true })도 조건부 생성을 요청하지만, null 대신 에러를 던집니다(미지원이면 NOT_SUPPORTED, 생성되지 않으면 NOT_ALLOWED).

4. Autofill을 통한 패스키 로그인 (Conditional UI)

// 로그인 페이지 로드 시 호출
// <input autocomplete="username webauthn"> 이 필요
const passkey = await identity.passkeys.authenticateConditional({
  challenge: serverChallenge,
  rpId: 'example.com',
})

if (passkey) {
  const data = Passkeys.serialize(passkey)
  await verifyOnServer(data)
}

5. Signal API로 패스키 정리 (Chrome 132+)

서버에서 패스키를 삭제했을 때 비밀번호 관리자에 알림:

// 개별 패스키 삭제 알림
await identity.passkeys.signalUnknownCredential({
  rpId: 'example.com',
  credentialId: deletedCredentialId,
})

// 전체 유효 패스키 목록 동기화
await identity.passkeys.signalAllAcceptedCredentials({
  rpId: 'example.com',
  userId: userIdBuffer,
  allAcceptedCredentialIds: [credId1, credId2], // 서버의 최신 목록
})

6. FedCM 제휴 로그인

import { FedCM } from '@cbcruk/web-identity'

// Active 모드: 버튼 클릭 후 프롬프트 표시
const credential = await identity.signInWithFederation([
  FedCM.provider(
    'https://accounts.google.com/.well-known/fedcm.json',
    'your-client-id',
    { nonce: serverNonce },
  ),
])

if (credential) {
  const token = (credential as any).token
  await verifyTokenOnServer(token)
}

멀티 IdP 지원 (Chrome 136+):

const credential = await identity.fedcm.signInMultiProvider(
  [
    FedCM.provider('https://google.com/.well-known/fedcm.json', 'client-1'),
    FedCM.provider('https://github.com/.well-known/fedcm.json', 'client-2'),
  ],
  { mode: 'active' },
)

7. 디지털 신원 확인

import { DigitalCredentials } from '@cbcruk/web-identity'

// 연령 확인 (18세 이상인지만 확인, 생년월일 미공개)
const cred = await identity.verifyIdentity([
  DigitalCredentials.mdocProvider('org.iso.18013.5.1.mDL', [
    DigitalCredentials.Fields.ageOver18,
  ]),
])

// 운전면허 정보 요청
const license = await identity.verifyIdentity([
  DigitalCredentials.mdocProvider('org.iso.18013.5.1.mDL', [
    DigitalCredentials.Fields.givenName,
    DigitalCredentials.Fields.familyName,
    DigitalCredentials.Fields.portrait,
    DigitalCredentials.Fields.documentNumber,
  ]),
])

API

구조

WebIdentity (facade)
├── CredentialManager    ← navigator.credentials.get/create/store
│   ├── password         ← PasswordCredential
│   ├── publicKey        ← PublicKeyCredential (passkey)
│   ├── identity         ← IdentityCredential (FedCM)
│   └── digital          ← DigitalCredential
├── Passkeys             ← WebAuthn 전용 API
│   ├── create/authenticate
│   ├── conditionalCreate (자동 업그레이드)
│   ├── Signal API       ← 비밀번호 관리자 동기화
│   └── serialize        ← 서버 전송용 직렬화
├── FedCM                ← 제휴 인증
│   ├── active/passive mode
│   ├── multi-provider
│   └── disconnect
├── DigitalCredentials   ← 디지털 지갑
│   ├── request (선별적 공개)
│   ├── issue (프로비저닝)
│   └── Fields (mDL 필드 상수)
└── DBSC                 ← 기기 바인딩 세션

기능 감지

확인할 수 있는 신호가 없으면 true로 추정하지 않고 false를 반환합니다. 비동기로만 알 수 있는 기능은 비동기 메서드를 사용하세요.

메서드판단 근거
Passkeys.isConditionalCreateAvailable()getClientCapabilities()conditionalCreate (비동기)
Passkeys.isImmediateMediationAvailable()getClientCapabilities()immediateGet (비동기)
Passkeys.isConditionalMediationSupported()PublicKeyCredential.isConditionalMediationAvailable()
CredentialManager.isMediationAvailable(m)위 비동기 감지를 mediation 종류별로 사용
CredentialManager.isMediationSupported(m)동기. 'conditional'·'immediate'는 항상 false
FedCM.isActiveModeSupported()identity.mode 옵션을 브라우저가 읽는지 프로브 (동기)
passkeys.isConditionalCreateSupported()deprecated — 동기 신호가 없어 항상 false

signalAllAcceptedCredentials의 옵션 타입은 SignalAllAcceptedCredentialsOptions입니다. 기존 이름 SignalAllKnownCredentialsOptions는 deprecated 별칭으로 남아 있습니다.

에러 처리

모든 에러는 WebIdentityError로 래핑됩니다:

import { WebIdentityError } from '@cbcruk/web-identity';

try {
  const result = await identity.signIn({ ... });
} catch (error) {
  if (error instanceof WebIdentityError) {
    switch (error.code) {
      case 'NOT_SUPPORTED':
        // 브라우저가 해당 API를 지원하지 않음
        break;
      case 'NOT_ALLOWED':
        // 사용자가 거부하거나 자격증명 없음
        break;
      case 'ABORTED':
        // AbortController로 취소됨
        break;
      case 'SECURITY_ERROR':
        // HTTPS가 아니거나 보안 컨텍스트 아님
        break;
    }
  }
}

유틸리티

import { toBase64URL, fromBase64URL } from '@cbcruk/web-identity'

// ArrayBuffer ↔ Base64URL 변환
const encoded = toBase64URL(credential.rawId)
const decoded = fromBase64URL(encoded)

// 챌린지 생성 (⚠️ 프로덕션에서는 서버에서 생성할 것)
const challenge = Passkeys.generateChallenge()

브라우저 호환성

기능ChromeSafariFirefox
Credential Manager (기본)
mediation: 'immediate'136+ (flag)
Passkeys (WebAuthn)108+16+119+
Conditional UI (autofill)108+16+
Conditional Create136+
Signal API132+
FedCM108+
FedCM Active Mode132+
Digital CredentialsOT
DBSC실험

예제 & E2E 테스트

src/examples/에 패스키 서버 검증 코드클라이언트 흐름 예제가 있습니다.

  • passkey-server.ts — 프레임워크 비의존 서버 검증(crypto.subtle만 사용, Node·브라우저 공용). 챌린지 발급, 등록 시 공개키만 저장, 인증 시 ECDSA 서명을 공개키로 검증(ASN.1 DER → raw 변환, rpId 해시·origin·challenge 검사 포함).
  • passkey-client.ts — 이 패키지의 Passkeys로 패스키를 만들고 Passkeys.serialize로 서버 전송용 JSON 생성.

실제 인증기 없이 Playwright 가상 인증기(CDP WebAuthn) 로 등록→인증 전체 왕복을 검증하는 e2e가 있습니다 (passkey.e2e.browser.test.ts) — 가상 인증기가 만든 진짜 ES256 서명을 서버가 WebCrypto로 검증하고, 챌린지 재사용(replay) 거부까지 확인합니다.

# 브라우저(Chromium) 프로젝트에서 실행
pnpm exec vitest run --project browser packages/web-identity

참고 자료

On this page