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

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

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

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

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

비밀번호를 사용자가 없는 상태에서 단일 로그인 버튼처럼 간단한 인증 과정이 진행되는 것이 가장 좋습니다. 사용자가 버튼을 탭하면 계정 선택기 대화상자가 팝업되며 사용자는 계정을 선택하고 화면을 잠금 해제하여 인증 및 로그인할 수 있습니다.

하지만 비밀번호에서 패스키 기반 인증으로 전환하는 것은 어려울 수 있습니다. 사용자가 패스키로 전환하더라도 비밀번호를 사용하는 사용자는 계속 있으므로 웹사이트에서 두 유형의 사용자를 모두 수용해야 합니다. 사용자는 패스키로 전환한 사이트를 기억해서는 안 되므로 사용자에게 어떤 방법을 먼저 사용할지 선택하도록 요청하면 UX가 나빠질 수 있습니다.

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

조건부 UI

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

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

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

사용 방법

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

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

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

기본 요건

조건부 WebAuthn UI는 iOS 16, iPadOS 16, macOS Ventura의 Safari에서 공개적으로 지원됩니다. Android, macOS, Windows 11 22H2의 Chrome에서도 사용할 수 있습니다.

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

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

양식 입력란에 주석 달기

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

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

기능 감지

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

  • 브라우저에서 WebAuthn을 지원합니다.
  • 브라우저에서 WebAuthn 조건부 UI를 지원합니다.
// 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 서버에서 챌린지 가져오기

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

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

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

자료