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

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

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

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

이상적으로는 비밀번호 사용자가 없고 인증 흐름이 싱글 사인온 버튼처럼 간단할 수 있습니다. 사용자가 버튼을 탭하면 계정 선택기 대화상자가 표시되며, 사용자가 계정을 선택하고 화면 잠금을 해제하여 인증하고 로그인할 수 있습니다.

하지만 비밀번호 기반 인증에서 패스키 기반 인증으로 전환하는 것은 쉽지 않을 수 있습니다. 사용자가 패스키로 전환해도 비밀번호를 사용하는 사용자는 계속 존재하며 웹사이트는 두 유형의 사용자를 모두 수용해야 합니다. 사용자는 패스키로 전환한 사이트를 기억할 수 없으므로 사용자에게 어떤 방법을 사용할지 미리 선택하도록 요청하는 것은 좋지 않은 UX입니다.

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

조건부 UI

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

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

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

작동 방식

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

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

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

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

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

양식 입력란에 주석 추가

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

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

기능 감지

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

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

브라우저 지원

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

소스

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

브라우저 지원

  • Chrome: 108
  • Edge: 108
  • Firefox: 119.
  • Safari: 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 저장소에서 오픈소스 라이브러리를 확인할 수 있습니다.

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

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

리소스