Passkey

Passkey는 WebAuthn 및 FIDO2 표준을 지원하는 웹 브라우저에서 암호화 키 쌍을 사용하는 안전한 비밀번호 없는 인증 방법입니다. 고유한 키 쌍으로 비밀번호를 대체합니다: 사용자의 기기에 저장되는 개인 키와 웹사이트와 공유되는 공개 키. 사용자는 생체 인식, PIN 또는 보안 키를 사용하여 로그인할 수 있으며, 기존 비밀번호 없이 피싱에 강한 강력한 인증을 제공합니다.

passkey 플러그인 구현은 SimpleWebAuthn을 기반으로 합니다.

설치

auth 구성에 플러그인 추가

passkey 플러그인을 auth 구성에 추가하려면 플러그인을 import하고 auth 인스턴스의 plugins 옵션에 전달해야 합니다.

옵션

rpID: 웹사이트의 고유 식별자입니다. 로컬 개발의 경우 'localhost'도 괜찮습니다

rpName: 웹사이트의 사람이 읽을 수 있는 제목입니다

origin: 등록 및 인증이 발생해야 하는 URL입니다. http://localhosthttp://localhost:PORT도 유효합니다. 후행 /를 포함하지 마세요

authenticatorSelection: WebAuthn 인증기 선택 기준을 사용자 정의할 수 있습니다. 기본 설정을 사용하려면 지정하지 마세요.

  • authenticatorAttachment: 인증기 유형을 지정합니다
    • platform: 인증기가 플랫폼에 연결됨 (예: 지문 인식기)
    • cross-platform: 인증기가 플랫폼에 연결되지 않음 (예: 보안 키)
    • 기본값: 설정되지 않음 (플랫폼 및 cross-platform 모두 허용되며, 플랫폼 선호)
  • residentKey: 자격 증명 저장 동작을 결정합니다.
    • required: 사용자가 인증기에 자격 증명을 저장해야 함 (최고 보안)
    • preferred: 자격 증명 저장을 권장하지만 필수는 아님
    • discouraged: 자격 증명 저장이 필요하지 않음 (가장 빠른 경험)
    • 기본값: preferred
  • userVerification: 인증 중 생체 인식/PIN 검증을 제어합니다:
    • required: 사용자가 신원을 검증해야 함 (최고 보안)
    • preferred: 검증을 권장하지만 필수는 아님
    • discouraged: 검증이 필요하지 않음 (가장 빠른 경험)
    • 기본값: preferred
auth.ts
import { betterAuth } from "better-auth"
import { passkey } from "better-auth/plugins/passkey"

export const auth = betterAuth({
    plugins: [ 
        passkey(), 
    ], 
})

데이터베이스 마이그레이션

마이그레이션을 실행하거나 스키마를 생성하여 필요한 필드와 테이블을 데이터베이스에 추가합니다.

npx @better-auth/cli migrate
npx @better-auth/cli generate

필드를 수동으로 추가하려면 Schema 섹션을 참조하세요.

클라이언트 플러그인 추가

auth-client.ts
import { createAuthClient } from "better-auth/client"
import { passkeyClient } from "better-auth/client/plugins"

export const authClient = createAuthClient({
    plugins: [ 
        passkeyClient() 
    ] 
})

사용법

Passkey 추가/등록

passkey를 추가하거나 등록하려면 사용자가 인증되었는지 확인한 다음 클라이언트에서 제공하는 passkey.addPasskey 함수를 호출합니다.

POST
/passkey/add-passkey
const { data, error } = await authClient.passkey.addPasskey({    name: "example-passkey-name",    authenticatorAttachment: "cross-platform",});
PropDescriptionType
name?
등록 중인 인증기 계정에 레이블을 지정하는 선택적 이름입니다. 제공되지 않으면 기본적으로 사용자의 이메일 주소 또는 사용자 ID로 설정됩니다
string
authenticatorAttachment?
등록하려는 인증기 유형을 지정할 수도 있습니다. 기본 동작은 플랫폼 및 cross-platform passkey를 모두 허용합니다
"platform" | "cross-platform"

fetch 옵션에서 throw: true를 설정하는 것은 등록 및 로그인 passkey 응답에는 영향을 미치지 않습니다 - 항상 오류 객체를 포함하는 data 객체를 반환합니다.

Passkey로 로그인

passkey로 로그인하려면 signIn.passkey 메서드를 사용할 수 있습니다. 이렇게 하면 사용자에게 passkey로 로그인하라는 메시지가 표시됩니다.

POST
/sign-in/passkey
const { data, error } = await authClient.signIn.passkey({    autoFill: true,});
PropDescriptionType
autoFill?
브라우저 자동 완성, 즉 조건부 UI. 자세히 읽기: https://simplewebauthn.dev/docs/packages/browser#browser-autofill-aka-conditional-ui
boolean

사용 예제

// 인증 후 리디렉션 포함
await authClient.signIn.passkey({
    autoFill: true,
    fetchOptions: {
        onSuccess(context) {
            // 성공적인 인증 후 대시보드로 리디렉션
            window.location.href = "/dashboard";
        },
        onError(context) {
            // 인증 오류 처리
            console.error("인증 실패:", context.error.message);
        }
    }
});

Passkey 목록

passkey.listUserPasskeys를 호출하여 인증된 사용자의 모든 passkey를 나열할 수 있습니다:

GET
/passkey/list-user-passkeys
const { data: passkeys, error } = await authClient.passkey.listUserPasskeys();

Passkey 삭제

passkey.delete를 호출하고 passkey ID를 제공하여 passkey를 삭제할 수 있습니다.

POST
/passkey/delete-passkey
const { data, error } = await authClient.passkey.deletePasskey({    id: "some-passkey-id", // required});
PropDescriptionType
id
삭제할 passkey의 ID입니다.
string

Passkey 이름 업데이트

POST
/passkey/update-passkey
const { data, error } = await authClient.passkey.updatePasskey({    id: "passkey의 id", // required    name: "my-new-passkey-name", // required});
PropDescriptionType
id
업데이트하려는 passkey의 ID입니다.
string
name
passkey가 업데이트될 새 이름입니다.
string

조건부 UI

플러그인은 조건부 UI를 지원하므로 사용자가 이미 passkey를 등록한 경우 브라우저가 passkey를 자동으로 채울 수 있습니다.

조건부 UI가 작동하려면 두 가지 요구 사항이 있습니다:

입력 필드 업데이트

입력 필드에 값이 webauthnautocomplete 속성을 추가합니다. 여러 입력 필드에 이 속성을 추가할 수 있지만 조건부 UI가 작동하려면 최소한 하나는 필요합니다.

webauthn 값은 autocomplete 속성의 마지막 항목이어야 합니다.

<label for="name">사용자 이름:</label>
<input type="text" name="name" autocomplete="username webauthn">
<label for="password">비밀번호:</label>
<input type="password" name="password" autocomplete="current-password webauthn">

Passkey 미리 로드

컴포넌트가 마운트될 때 autoFill 옵션을 true로 설정하여 authClient.signIn.passkey 메서드를 호출하여 사용자의 passkey를 미리 로드할 수 있습니다.

불필요한 호출을 방지하기 위해 브라우저가 조건부 UI를 지원하는지 확인하는 검사도 추가합니다.

useEffect(() => {
   if (!PublicKeyCredential.isConditionalMediationAvailable ||
       !PublicKeyCredential.isConditionalMediationAvailable()) {
     return;
   }

  void authClient.signIn.passkey({ autoFill: true })
}, [])

브라우저에 따라 passkey를 자동으로 채우라는 프롬프트가 나타납니다. 사용자에게 여러 passkey가 있는 경우 사용하려는 passkey를 선택할 수 있습니다.

일부 브라우저는 자동 완성 프롬프트가 나타나기 전에 사용자가 먼저 입력 필드와 상호 작용해야 합니다.

디버깅

passkey 구현을 테스트하려면 에뮬레이트된 인증기를 사용할 수 있습니다. 이렇게 하면 물리적 기기를 소유하지 않고도 등록 및 로그인 프로세스를 테스트할 수 있습니다.

Schema

플러그인은 passkey 데이터를 저장하기 위해 데이터베이스에 새 테이블이 필요합니다.

테이블 이름: passkey

Field NameTypeKeyDescription
idstring각 passkey의 고유 식별자
namestringpasskey의 이름
publicKeystring-passkey의 공개 키
userIdstring사용자의 ID
credentialIDstring-등록된 자격 증명의 고유 식별자
counternumber-passkey의 카운터
deviceTypestring-passkey를 등록하는 데 사용된 기기 유형
backedUpboolean-passkey가 백업되었는지 여부
transportsstring-passkey를 등록하는 데 사용된 전송
createdAtDate-passkey가 생성된 시간
aaguidstring인증기 유형을 나타내는 인증기의 Attestation GUID

옵션

rpID: 웹사이트의 고유 식별자입니다. 로컬 개발의 경우 'localhost'도 괜찮습니다.

rpName: 웹사이트의 사람이 읽을 수 있는 제목입니다.

origin: 등록 및 인증이 발생해야 하는 URL입니다. http://localhosthttp://localhost:PORT도 유효합니다. 후행 /를 포함하지 마세요.

authenticatorSelection: WebAuthn 인증기 선택 기준을 사용자 정의할 수 있습니다. 지정하지 않으면 residentKeyuserVerification에 대해 preferred 설정으로 플랫폼 및 cross-platform 인증기가 모두 허용됩니다.

aaguid: (선택 사항) Authenticator Attestation GUID입니다. 이것은 passkey provider(기기 또는 인증기 유형)의 고유 식별자이며 등록 또는 인증 중에 사용된 passkey 기기 유형을 식별하는 데 사용할 수 있습니다.