비밀번호 없는 로그인의 패스키 생성

패스키를 사용하면 사용자 계정을 더 안전하고 간편하고 쉽게 사용할 수 있습니다.

패스키를 사용하면 보안이 강화되고 로그인이 간소화되며 비밀번호가 대체됩니다. 사용자가 직접 기억하고 입력해야 하는 일반 비밀번호와 달리 패스키는 생체 인식이나 PIN과 같은 기기의 화면 잠금 메커니즘을 사용하며 피싱 위험과 사용자 인증 정보 도용을 줄입니다.

패스키는 Google 비밀번호 관리자 및 iCloud 키체인과 같은 패스키 제공자를 사용하여 여러 기기에서 동기화됩니다.

패스키를 생성하여 비공개 키를 패스키 제공업체에 안전하게 저장하고 필요한 메타데이터와 인증을 위해 서버에 저장된 공개 키를 저장해야 합니다. 비공개 키는 유효한 도메인에서 사용자 인증 후 서명을 발급하여 패스키를 피싱으로부터 보호합니다. 공개 키는 민감한 사용자 인증 정보를 저장하지 않고 서명을 확인하므로 패스키는 사용자 인증 정보 도용에 강합니다.

패스키 생성 작동 방식

사용자가 패스키로 로그인할 수 있도록 하려면 먼저 패스키를 만들고 이를 사용자 계정과 연결한 후 공개 키를 서버에 저장해야 합니다.

다음과 같은 상황에서 사용자에게 패스키를 만들도록 요청할 수 있습니다.

  • 가입 중 또는 가입 후
  • 로그인한 후
  • 다른 기기의 패스키를 사용하여 로그인한 후 ([authenticatorAttachment](https://web.dev/articles/passkey-form-autofill#authenticator-attachment)cross-platform임).
  • 사용자가 패스키를 관리할 수 있는 전용 페이지

패스키를 만들려면 WebAuthn API를 사용합니다.

패스키 등록 흐름의 네 가지 구성요소는 다음과 같습니다.

  • 백엔드: 공개 키를 비롯한 사용자 계정 세부정보를 저장합니다.
  • 프런트엔드: 브라우저와 통신하고 백엔드에서 필요한 데이터를 가져옵니다.
  • 브라우저: JavaScript를 실행하고 WebAuthn API와 상호작용합니다.
  • 패스키 제공업체: 패스키를 생성하고 저장합니다. 일반적으로 Google 비밀번호 관리자와 같은 비밀번호 관리자 또는 보안 키입니다.
패스키 생성 및 등록 절차
패스키를 생성하고 등록하는 프로세스입니다.

패스키를 만들기 전에 시스템이 다음 기본 요건을 충족하는지 확인하세요.

  • 사용자 계정이 상당히 짧은 시간 내에 안전한 방법 (예: 이메일, 전화 인증, ID 제휴)을 통해 인증됩니다.

  • 프런트엔드와 백엔드는 안전하게 통신하여 사용자 인증 정보 데이터를 교환할 수 있습니다.

  • 브라우저에서 WebAuthn 및 패스키 생성을 지원합니다.

다음 섹션에서는 대부분의 항목을 확인하는 방법을 보여드립니다.

시스템이 이 조건을 충족하면 패스키를 만들기 위해 다음 프로세스가 실행됩니다.

  1. 사용자가 작업을 시작하면 (예: 패스키 관리 페이지에서 '패스키 만들기' 버튼을 클릭하거나 등록을 완료한 후) 시스템에서 패스키 생성 프로세스를 트리거합니다.
  2. 프런트엔드는 중복을 방지하기 위해 사용자 정보, 챌린지, 사용자 인증 정보 ID를 비롯한 필요한 사용자 인증 정보 데이터를 백엔드에서 요청합니다.
  3. 프런트엔드는 navigator.credentials.create()를 호출하여 기기의 패스키 제공업체에 백엔드의 정보를 사용하여 패스키를 생성하라는 메시지를 표시합니다. 이 호출은 프로미스를 반환합니다.
  4. 사용자의 기기는 생체 인식 방식, PIN 또는 패턴을 사용하여 사용자를 인증하고 패스키를 생성합니다.
  5. 패스키 제공업체는 패스키를 생성하고 공개 키 사용자 인증 정보를 프런트엔드로 반환하여 약속을 확인합니다.
  6. 프런트엔드는 생성된 공개 키 사용자 인증 정보를 백엔드로 전송합니다.
  7. 백엔드는 향후 인증을 위해 공개 키와 기타 중요한 데이터를 저장합니다.
  8. 백엔드는 패스키 생성을 확인하고 잠재적인 무단 액세스를 감지하기 위해 사용자에게 알림을 보냅니다 (예: 이메일 사용).

이 프로세스는 사용자에게 안전하고 원활한 패스키 등록 프로세스를 보장합니다.

호환성

대부분의 브라우저는 WebAuthn을 지원하지만 약간의 간격이 있습니다. 브라우저 및 OS 호환성 세부정보는 passkeys.dev를 참고하세요.

새 패스키 만들기

새 패스키를 만들려면 프런트엔드에서 다음 프로세스를 따라야 합니다.

  1. 호환성을 확인합니다.
  2. 백엔드에서 정보를 가져옵니다.
  3. WebAuth API를 호출하여 패스키를 만듭니다.
  4. 반환된 공개 키를 백엔드로 전송합니다.
  5. 사용자 인증 정보를 저장합니다.

다음 섹션에서는 이를 수행하는 방법을 보여줍니다.

호환성 확인

'새 패스키 만들기' 버튼을 표시하기 전에 프런트엔드는 다음을 확인해야 합니다.

  • 브라우저가 PublicKeyCredential를 사용하여 WebAuthn을 지원합니다.

Browser Support

  • Chrome: 67.
  • Edge: 18.
  • Firefox: 60.
  • Safari: 13.

Source

  • 기기가 PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()를 사용하여 플랫폼 인증자 (패스키를 생성하고 패스키로 인증할 수 있음)를 지원합니다.

Browser Support

  • Chrome: 67.
  • Edge: 18.
  • Firefox: 60.
  • Safari: 13.

Source

  • 브라우저가 PublicKeyCredenital.isConditionalMediationAvailable()를 사용하여 WebAuthn 조건부 UI를 지원합니다.

Browser Support

  • Chrome: 108.
  • Edge: 108.
  • Firefox: 119.
  • Safari: 16.

Source

다음 코드 스니펫은 패스키 관련 옵션을 표시하기 전에 호환성을 확인하는 방법을 보여줍니다.

// Availability of `window.PublicKeyCredential` means WebAuthn is usable.  
// `isUserVerifyingPlatformAuthenticatorAvailable` means the feature detection is usable.  
// `isConditionalMediationAvailable` means the feature detection is usable.  
if (window.PublicKeyCredential &&  
    PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable &&  
    PublicKeyCredential.isConditionalMediationAvailable) {  
  // Check if user verifying platform authenticator is available.  
  Promise.all([  
    PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(),  
    PublicKeyCredential.isConditionalMediationAvailable(),  
  ]).then(results => {  
    if (results.every(r => r === true)) {  
      // Display "Create a new passkey" button  
    }  
  });  
}  

이 예에서는 모든 조건이 충족되는 경우에만 새 패스키 만들기 버튼이 표시되어야 합니다.

백엔드에서 정보 가져오기

사용자가 버튼을 클릭하면 백엔드에서 필요한 정보를 가져와 navigator.credentials.create()을(를) 호출합니다.

다음 코드 스니펫은 navigator.credentials.create()를 호출하는 데 필요한 정보가 포함된 JSON 객체를 보여줍니다.

// Example `PublicKeyCredentialCreationOptions` contents
{
  challenge: *****,
  rp: {
    name: "Example",
    id: "example.com",
  },
  user: {
    id: *****,
    name: "john78",
    displayName: "John",
  },
  pubKeyCredParams: [{
    alg: -7, type: "public-key"
  },{
    alg: -257, type: "public-key"
  }],
  excludeCredentials: [{
    id: *****,
    type: 'public-key',
    transports: ['internal'],
  }],
  authenticatorSelection: {
    authenticatorAttachment: "platform",
    requireResidentKey: true,
  }
}

객체의 키-값 쌍에는 다음 정보가 포함됩니다.

  • challenge: 이 등록에 관한 ArrayBuffer의 서버 생성 챌린지입니다.
  • rp.id: RP ID (신뢰 당사자 ID), 도메인, 웹사이트에서 도메인 또는 등록 가능한 접미사를 지정할 수 있습니다. 예를 들어 RP의 출처가 https://login.example.com:1337인 경우 RP ID는 login.example.com 또는 example.com일 수 있습니다. RP ID가 example.com으로 지정된 경우 사용자는 login.example.com 또는 example.com의 하위 도메인에서 인증할 수 있습니다. 자세한 내용은 관련 출처 요청을 사용하여 사이트에서 패스키 재사용 허용을 참고하세요.
  • rp.name: RP (신뢰 당사자) 이름입니다. 이는 WebAuthn L3에서 지원 중단되었지만 호환성을 위해 포함되어 있습니다.
  • user.id: 계정 생성 시 생성되는 ArrayBuffer의 고유한 사용자 ID입니다. 수정 가능한 사용자 이름과 달리 영구적이어야 합니다. 사용자 ID는 계정을 식별하지만 개인 식별 정보 (PII)를 포함해서는 안 됩니다. 시스템에 이미 사용자 ID가 있을 수 있지만 필요한 경우 패스키 전용 사용자 ID를 만들어 PII가 포함되지 않도록 합니다.
  • user.name: 사용자가 인식할 수 있는 계정의 고유 식별자(예: 이메일 주소 또는 사용자 이름)입니다. 이것은 계정 선택기에 표시됩니다.
  • user.displayName: 계정의 필수 사용자 친화적인 이름입니다. 고유하지 않아도 되며 사용자가 선택한 이름일 수 있습니다. 사이트에 여기에 포함할 수 있는 적합한 값이 없는 경우 빈 문자열을 전달합니다. 브라우저에 따라 계정 선택기에 표시될 수 있습니다.
  • pubKeyCredParams: RP (신뢰 당사자)에서 지원하는 공개 키 알고리즘을 지정합니다. [{alg: -7, type: "public-key"},{alg: -257, type: "public-key"}]로 설정하는 것이 좋습니다. P-256 및 RSA PKCS#1을 사용하는 ECDSA 지원을 지정하며, 지원을 통해 완전한 적용 범위를 제공합니다.
  • excludeCredentials: 이미 등록된 사용자 인증 정보 ID 목록입니다. 이미 등록된 사용자 인증 정보 ID 목록을 제공하여 동일한 기기가 두 번 등록되는 것을 방지합니다. 제공되는 경우 transports 구성원에는 각 사용자 인증 정보를 등록하는 동안 getTransports()를 호출한 결과가 포함되어야 합니다.
  • authenticatorSelection.authenticatorAttachment: 패스키 생성이 로그인 후 프로모션과 같이 비밀번호에서 업그레이드된 경우 hint: ['client-device']와 함께 "platform"로 설정합니다. "platform"는 RP가 USB 보안 키 삽입과 같은 메시지를 표시하지 않는 플랫폼 인증자 (플랫폼 기기에 삽입된 인증자)를 원함을 나타냅니다. 사용자는 패스키를 만드는 더 간단한 옵션을 사용할 수 있습니다.
  • authenticatorSelection.requireResidentKey: 불리언 true로 설정합니다. 검색 가능한 사용자 인증 정보 (상주 키)는 사용자 정보를 패스키에 저장하고 사용자가 인증 시 계정을 선택할 수 있도록 합니다.
  • authenticatorSelection.userVerification: 기기 화면 잠금을 사용하는 사용자 확인이 "required", "preferred" 또는 "discouraged"인지를 나타냅니다. 기본값은 "preferred"이며 인증자가 사용자 인증을 건너뛸 수 있음을 의미합니다. "preferred"로 설정하거나 속성을 생략합니다.

서버에서 객체를 생성하고 ArrayBuffer를 Base64URL로 인코딩한 후 프런트엔드에서 가져오는 것이 좋습니다. 이렇게 하면 PublicKeyCredential.parseCreationOptionsFromJSON()를 사용하여 페이로드를 디코딩하고 navigator.credentials.create()에 직접 전달할 수 있습니다.

다음 코드 스니펫은 패스키를 만드는 데 필요한 정보를 가져와 디코딩하는 방법을 보여줍니다.

// Fetch an encoded `PubicKeyCredentialCreationOptions` from the server.
const _options = await fetch('/webauthn/registerRequest');

// Deserialize and decode the `PublicKeyCredentialCreationOptions`.
const decoded_options = JSON.parse(_options);
const options = PublicKeyCredential.parseCreationOptionsFromJSON(decoded_options);
...

WebAuthn API를 호출하여 패스키 만들기

navigator.credentials.create()를 호출하여 새 패스키를 만듭니다. API는 모달 대화상자를 표시하는 사용자 상호작용을 기다리는 약속을 반환합니다.

Browser Support

  • Chrome: 60.
  • Edge: 18.
  • Firefox: 60.
  • Safari: 13.

Source

// Invoke WebAuthn to create a passkey.
const credential = await navigator.credentials.create({
  publicKey: options
});

반환된 공개 키 사용자 인증 정보를 백엔드로 전송

사용자가 기기의 화면 잠금을 사용하여 인증되면 패스키가 생성되고 프로미스가 해결되어 프런트엔드에 PublicKeyCredential 객체를 반환합니다.

약속은 다양한 이유로 거부될 수 있습니다. Error 객체의 name 속성을 확인하여 이러한 오류를 처리할 수 있습니다.

  • InvalidStateError: 기기에 패스키가 이미 있습니다. 사용자에게 오류 대화상자가 표시되지 않습니다. 사이트에서 이를 오류로 처리해서는 안 됩니다. 사용자가 로컬 기기를 등록하고 싶어 했으며 기기가 등록되었습니다.
  • NotAllowedError: 사용자가 작업을 취소했습니다.
  • AbortError: 작업이 취소되었습니다.
  • 기타 예외: 예상치 못한 문제가 발생했습니다. 브라우저가 사용자에게 오류 대화상자를 표시합니다.

공개 키 사용자 인증 정보 객체에는 다음 속성이 포함되어 있습니다.

  • id: 생성된 패스키의 Base64URL로 인코딩된 ID입니다. 이 ID는 인증 시 브라우저에서 기기에 일치하는 패스키가 있는지 확인하는 데 도움이 됩니다. 이 값은 백엔드의 데이터베이스에 저장해야 합니다.
  • rawId: 사용자 인증 정보 ID의 ArrayBuffer 버전입니다.
  • response.clientDataJSON: ArrayBuffer로 인코딩된 클라이언트 데이터입니다.
  • response.attestationObject: ArrayBuffer로 인코딩된 증명 객체입니다. 여기에는 RP ID, 플래그, 공개 키와 같은 중요한 정보가 포함되어 있습니다.
  • authenticatorAttachment: 이 사용자 인증 정보가 패스키 지원 기기에서 생성되면 "platform"을 반환합니다.
  • type: 이 필드는 항상 "public-key"로 설정됩니다.

.toJSON() 메서드로 객체를 인코딩하고 JSON.stringify()로 직렬화한 다음 서버로 전송합니다.

...

// Encode and serialize the `PublicKeyCredential`.
const _result = credential.toJSON();
const result = JSON.stringify(_result);

// Encode and send the credential to the server for verification.  
const response = await fetch('/webauthn/registerResponse', {
  method: 'post',
  credentials: 'same-origin',
  body: result
});
...

사용자 인증 정보 저장

백엔드에서 공개 키 사용자 인증 정보를 수신한 후에는 공개 키 사용자 인증 정보를 처리하기 위해 자체 코드를 작성하는 대신 서버 측 라이브러리 또는 솔루션을 사용하는 것이 좋습니다.

그런 다음 사용자 인증 정보에서 가져온 정보를 나중에 사용할 수 있도록 데이터베이스에 저장할 수 있습니다.

다음 목록에는 저장하는 것이 권장되는 속성이 포함되어 있습니다.

  • 사용자 인증 정보 ID: 공개 키 사용자 인증 정보와 함께 반환된 사용자 인증 정보 ID입니다.
  • 사용자 인증 정보 이름: 사용자 인증 정보의 이름입니다. AAGUID를 기반으로 식별할 수 있는 생성된 패스키 제공업체의 이름을 따서 이름을 지정합니다.
  • 사용자 ID: 패스키를 만드는 데 사용된 사용자 ID입니다.
  • 공개 키: 공개 키 사용자 인증 정보와 함께 반환된 공개 키입니다. 패스키 어설션을 확인하는 데 필요합니다.
  • 생성 날짜 및 시간: 패스키 생성 날짜 및 시간을 기록합니다. 패스키를 식별하는 데 유용합니다.
  • 마지막 사용 날짜 및 시간: 사용자가 패스키를 사용하여 로그인한 마지막 날짜 및 시간을 기록합니다. 이는 사용자가 사용했거나 사용하지 않은 패스키를 확인하는 데 유용합니다.
  • AAGUID: 패스키 제공업체의 고유 식별자입니다.
  • 백업 자격 요건 플래그: 기기에서 패스키 동기화가 가능한 경우 true입니다. 이 정보는 사용자가 패스키 관리 페이지에서 동기화 가능한 패스키와 기기 연결 (동기화 불가) 패스키를 식별하는 데 도움이 됩니다.

서버 측 패스키 등록에서 자세한 안내를 따르세요.

등록에 실패하면 신호를 보냅니다.

패스키 등록에 실패하면 사용자에게 혼란이 올 수 있습니다. 패스키 제공업체에 패스키가 있고 사용자가 사용할 수 있지만 연결된 공개 키가 서버 측에 저장되지 않은 경우 패스키를 사용한 로그인 시도가 성공하지 않으며 문제를 해결하기가 어렵습니다. 이 경우 사용자에게 알려야 합니다.

이러한 상태를 방지하려면 Signal API를 사용하여 패스키 제공업체에 알 수 없는 패스키를 신호하면 됩니다. RP는 RP ID 및 사용자 인증 정보 ID를 사용하여 PublicKeyCredential.signalUnknownCredential()를 호출하여 패스키 제공업체에 지정된 사용자 인증 정보가 삭제되었거나 존재하지 않음을 알릴 수 있습니다. 이 신호를 처리하는 방법은 패스키 제공업체에 따라 다르지만 지원되는 경우 연결된 패스키가 삭제될 것으로 예상됩니다.

// Detect authentication failure due to lack of the credential
if (response.status === 404) {
  // Feature detection
  if (PublicKeyCredential.signalUnknownCredential) {
    await PublicKeyCredential.signalUnknownCredential({
      rpId: "example.com",
      credentialId: "vI0qOggiE3OT01ZRWBYz5l4MEgU0c7PmAA" // base64url encoded credential ID
    });
  } else {
    // Encourage the user to delete the passkey from the password manager nevertheless.
    ...
  }
}

Signal API에 대해 자세히 알아보려면 Signal API를 사용하여 패스키를 서버의 사용자 인증 정보와 일치시키기를 참고하세요.

사용자에게 알림 보내기

패스키가 등록될 때 알림 (예: 이메일)을 보내면 사용자가 승인되지 않은 계정 액세스를 감지하는 데 도움이 됩니다. 공격자가 사용자의 알지 못하는 사이에 패스키를 생성하면 비밀번호가 변경된 후에도 패스키를 악용할 수 있습니다. 알림은 사용자에게 경고하고 이를 방지하는 데 도움이 됩니다.

체크리스트

  • 사용자에게 패스키 생성을 허용하기 전에 사용자를 인증합니다 (이메일 또는 보안 방법을 사용하는 것이 좋음).
  • excludeCredentials를 사용하여 동일한 패스키 제공업체에 중복 패스키가 생성되지 않도록 합니다.
  • 패스키 제공업체를 식별하고 사용자의 사용자 인증 정보 이름을 지정하기 위해 AAGUID를 저장합니다.
  • 패스키 등록 시도에 PublicKeyCredential.signalUnknownCredential() 오류가 발생하면 신호를 보냅니다.
  • 사용자의 계정에 패스키를 생성하고 등록한 후 사용자에게 알림을 보냅니다.

리소스

다음 단계: 양식 자동 완성을 통해 패스키로 로그인