양식 자동 완성을 통한 패스키 로그인

기존 비밀번호 사용자를 계속 수용하면서 패스키를 활용하는 로그인 환경을 만듭니다.

패스키는 비밀번호를 대체하며 웹의 사용자 계정을 안전하고 단순하며 사용하기 쉽게 만들어 줍니다. 그러나 비밀번호 기반에서 패스키 기반 인증으로 전환하면 사용자 환경이 복잡해질 수 있습니다. 양식 자동 완성을 사용하여 패스키를 추천하면 통합 환경을 만드는 데 도움이 될 수 있습니다.

패스키로 로그인할 때 양식 자동 완성을 사용해야 하는 이유는 무엇인가요?

패스키를 사용하면 사용자가 지문, 얼굴 또는 기기 PIN을 사용하여 웹사이트에 로그인할 수 있습니다.

비밀번호 사용자가 없는 것이 이상적이며 인증 흐름은 단일 로그인 버튼만큼 간단합니다. 사용자가 버튼을 탭하면 계정 선택기 대화상자가 팝업되고, 사용자는 계정을 선택하고, 화면을 잠금 해제하여 인증 및 로그인할 수 있습니다.

그러나 비밀번호에서 패스키 기반 인증으로 전환하는 과정은 어려울 수 있습니다. 사용자가 패스키로 전환해도 비밀번호를 사용하는 사용자는 여전히 존재하며 웹사이트에서 두 유형의 사용자를 모두 수용해야 합니다. 사용자가 패스키로 전환한 사이트를 기억하지 못할 수 있으므로 사용자에게 어떤 방법을 미리 사용할지 묻는 것은 좋지 않은 UX입니다.

패스키도 새로운 기술입니다. 웹사이트에는 이러한 기능을 설명하고 사용자가 편안하게 사용할 수 있도록 하기가 어려울 수 있습니다. 익숙한 사용자 환경을 활용하여 비밀번호 자동 완성을 사용하면 두 가지 문제를 모두 해결할 수 있습니다.

조건부 UI

패스키 사용자와 비밀번호 사용자 모두를 위한 효율적인 사용자 환경을 빌드하려면 자동 완성 추천에 패스키를 포함할 수 있습니다. 이를 조건부 UI라고 하며 WebAuthn 표준의 일부입니다.

사용자가 사용자 이름 입력란을 탭하자마자 자동 완성 추천 대화상자가 팝업되어 비밀번호 자동 완성 추천과 함께 저장된 패스키를 강조표시합니다. 그러면 사용자는 계정을 선택하고 기기 화면 잠금을 사용하여 로그인할 수 있습니다.

이렇게 하면 사용자는 아무것도 변경되지 않은 것처럼 기존 양식으로 웹사이트에 로그인할 수 있지만 패스키가 있는 경우 패스키의 추가 보안 이점을 누리게 됩니다.

작동 원리

패스키로 인증하려면 WebAuthn API를 사용합니다.

패스키 인증 흐름의 네 가지 구성요소는 사용자:

  • 백엔드: 공개 키 및 패스키에 대한 기타 메타데이터를 저장하는 계정 데이터베이스를 보유하는 백엔드 서버입니다.
  • 프런트엔드: 브라우저와 통신하고 가져오기 요청을 백엔드로 보내는 프런트엔드입니다.
  • 브라우저: 자바스크립트를 실행하는 사용자의 브라우저입니다.
  • 인증자: 패스키를 생성하고 저장하는 사용자의 인증자입니다. 브라우저와 동일한 기기(예: Windows Hello 사용 시) 또는 다른 기기(예: 휴대전화)에 있을 수 있습니다.
패스키 인증 다이어그램
  1. 사용자가 프런트엔드에 도착하자마자 패스키로 인증하기 위해 백엔드에 본인 확인 요청을 요청하고 navigator.credentials.get()를 호출하여 패스키로 인증을 시작합니다. 그러면 Promise이 반환됩니다.
  2. 사용자가 커서를 로그인 필드에 두면 브라우저에 패스키를 포함한 비밀번호 자동 완성 대화상자가 표시됩니다. 사용자가 패스키를 선택하면 인증 대화상자가 표시됩니다.
  3. 사용자가 기기의 화면 잠금을 사용하여 ID를 확인하면 프로미스가 해결되고 공개 키 사용자 인증 정보가 프런트엔드로 반환됩니다.
  4. 프런트엔드는 공개 키 사용자 인증 정보를 백엔드로 전송합니다. 백엔드는 데이터베이스에 있는 일치하는 계정의 공개 키를 기준으로 서명을 확인합니다. 성공하면 사용자가 로그인됩니다.

양식 자동 완성을 통해 패스키로 인증

사용자가 로그인하려는 경우 조건부 WebAuthn get 호출을 실행하여 패스키가 자동 완성 추천에 포함될 수 있음을 나타낼 수 있습니다. WebAuthnnavigator.credentials.get() API에 대한 조건부 호출은 UI를 표시하지 않고 사용자가 자동 완성 제안에서 로그인할 계정을 선택할 때까지 대기 상태로 유지됩니다. 사용자가 패스키를 선택하면 로그인 양식을 작성하는 대신 브라우저가 사용자 인증 정보로 프라미스를 해결합니다. 그런 다음 사용자가 로그인하도록 페이지에서 담당합니다

양식 입력란에 주석 달기

필요한 경우 사용자 이름 input 필드에 autocomplete 속성을 추가합니다. 패스키를 추천하도록 usernamewebauthn를 토큰으로 추가합니다.

<input type="text" name="username" autocomplete="username webauthn" ...>

특성 감지

조건부 WebAuthn API 호출을 호출하기 전에 다음 사항을 확인합니다.

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

브라우저 지원

  • 67
  • 18
  • 60
  • 13

소스

  • 브라우저는 PublicKeyCredenital.isConditionalMediationAvailable()를 통해 WebAuthn 조건부 UI를 지원합니다.

브라우저 지원

  • 108
  • 108
  • 119
  • 16

소스

// Availability of `window.PublicKeyCredential` means WebAuthn is usable.  
if (window.PublicKeyCredential &&  
    PublicKeyCredential.​​isConditionalMediationAvailable) {  
  // Check if conditional mediation is available.  
  const isCMA = await PublicKeyCredential.​​isConditionalMediationAvailable();  
  if (isCMA) {  
    // Call WebAuthn authentication  
  }  
}  

RP 서버에서 챌린지 가져오기

navigator.credentials.get()를 호출하는 데 필요한 챌린지를 RP 서버에서 가져옵니다.

  • challenge: ArrayBuffer의 서버 생성 챌린지입니다. 이는 재생 공격을 방지하는 데 필요합니다. 로그인을 시도할 때마다 새 본인 확인 요청을 생성하고 일정 시간이 지난 후 또는 로그인 시도가 검증에 실패한 후 무시합니다. CSRF 토큰이라고 생각하면 됩니다.
  • allowCredentials: 이 인증에 허용되는 사용자 인증 정보의 배열입니다. 빈 배열을 전달하여 사용자가 브라우저에 표시된 목록에서 사용 가능한 패스키를 선택할 수 있도록 합니다.
  • userVerification: 기기 화면 잠금을 사용한 사용자 확인이 "required"인지, "preferred"인지, "discouraged"인지를 나타냅니다. 기본값은 "preferred"이며, 이 경우 인증자가 사용자 확인을 건너뛸 수 있습니다. "preferred"로 설정하거나 속성을 생략합니다.

conditional 플래그로 WebAuthn API를 호출하여 사용자를 인증합니다.

navigator.credentials.get()를 호출하여 사용자 인증 대기를 시작합니다.

// To abort a WebAuthn call, instantiate an `AbortController`.
const abortController = new AbortController();

const publicKeyCredentialRequestOptions = {
  // Server generated challenge
  challenge: ****,
  // The same RP ID as used during registration
  rpId: 'example.com',
};

const credential = await navigator.credentials.get({
  publicKey: publicKeyCredentialRequestOptions,
  signal: abortController.signal,
  // Specify 'conditional' to activate conditional UI
  mediation: 'conditional'
});
  • rpId: RP ID는 도메인이며 웹사이트는 도메인 또는 등록 가능한 서픽스를 지정할 수 있습니다. 이 값은 패스키 생성 시 사용된 rp.id와 일치해야 합니다.

요청을 조건부로 만들려면 mediation: 'conditional'를 지정해야 합니다.

반환된 공개 키 사용자 인증 정보를 RP 서버로 전송합니다.

사용자가 계정을 선택하고 기기의 화면 잠금을 사용하는 데 동의하면 PublicKeyCredential 객체를 RP 프런트엔드에 반환하는 프로미스가 해결됩니다.

프로미스는 여러 가지 이유로 거부될 수 있습니다. Error 객체의 name 속성에 따라 적절히 오류를 처리해야 합니다.

  • NotAllowedError: 사용자가 작업을 취소했습니다.
  • 기타 예외: 예상치 못한 오류가 발생했습니다. 브라우저가 사용자에게 오류 대화상자를 표시합니다.

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

  • id: 인증된 패스키 사용자 인증 정보의 base64url로 인코딩된 ID입니다.
  • rawId: 사용자 인증 정보 ID의 ArrayBuffer 버전입니다.
  • response.clientDataJSON: 클라이언트 데이터의 ArrayBuffer. 이 필드에는 챌린지 및 RP 서버가 확인해야 하는 출처와 같은 정보가 포함됩니다.
  • response.authenticatorData: 인증자 데이터의 ArrayBuffer입니다. 이 필드에는 RP ID와 같은 정보가 포함됩니다.
  • response.signature: 서명의 ArrayBuffer. 이 값은 사용자 인증 정보의 핵심이며 서버에서 확인되어야 합니다.
  • response.userHandle: 생성 시 설정된 사용자 ID가 포함된 ArrayBuffer입니다. 서버가 사용하는 ID 값을 선택해야 하거나 백엔드가 사용자 인증 정보 ID에 대한 색인을 만들지 않으려는 경우 사용자 인증 정보 ID 대신 이 값을 사용할 수 있습니다.
  • authenticatorAttachment: 이 사용자 인증 정보를 로컬 기기에서 받은 경우 platform를 반환합니다. 그 외의 경우에는 cross-platform, 특히 사용자가 휴대전화로 로그인할 때를 나타냅니다. 사용자가 휴대전화로 로그인해야 하는 경우 로컬 기기에서 패스키를 생성하라는 메시지를 표시하는 것이 좋습니다.
  • type: 이 필드는 항상 "public-key"로 설정됩니다.

라이브러리를 사용하여 RP 서버에서 공개 키 사용자 인증 정보 객체를 처리하는 경우, base64url로 부분적으로 인코딩한 후 전체 객체를 서버로 전송하는 것이 좋습니다.

서명 확인

서버에서 공개 키 사용자 인증 정보를 수신하면 이를 FIDO 라이브러리에 전달하여 객체를 처리합니다.

id 속성을 사용하여 일치하는 사용자 인증 정보 ID를 조회합니다. 사용자 계정을 확인해야 하는 경우 사용자 인증 정보를 만들 때 지정한 user.iduserHandle 속성을 사용합니다. 저장된 공개 키로 사용자 인증 정보의 signature를 확인할 수 있는지 확인합니다. 이렇게 하려면 코드를 직접 작성하는 대신 서버 측 라이브러리나 솔루션을 사용하는 것이 좋습니다. 오픈소스 라이브러리는 awesome-webauth GitHub 저장소에서 찾을 수 있습니다.

일치하는 공개 키로 사용자 인증 정보가 확인되면 사용자를 로그인 처리합니다.

서버 측 패스키 인증에서 자세한 안내를 따릅니다.

리소스