첫 번째 WebAuthn 앱 빌드하기

1. 시작하기 전에

Web Authentication API(WebAuthn이라고도 함)를 사용하면 원본 범위의 공개 키 사용자 인증 정보를 만들어 사용자 인증을 실행할 수 있습니다.

API는 보안 키라고도 하는 BLE, NFC, USB 로밍 U2F 또는 FIDO2 인증자뿐 아니라 사용자가 지문 또는 화면 잠금을 사용하여 인증할 수 있는 플랫폼 인증자 사용을 지원합니다.

이 Codelab에서는 지문 센서를 사용하는 간단한 재인증 기능이 지원되는 웹사이트를 빌드합니다. 재인증을 사용하면 웹사이트에 이미 로그인한 사용자가 웹사이트의 중요한 섹션에 들어가려고 하거나 일정 시간이 지난 후 웹사이트를 다시 방문하려고 할 때 다시 인증을 해야 하므로 계정 데이터가 보호됩니다.

기본 요건

  • WebAuthn의 작동 원리에 관한 기본적인 이해
  • 자바스크립트를 활용한 기본적인 프로그래밍 기술

실행할 작업

  • 지문 센서를 사용하는 간단한 재인증 기능이 지원되는 웹사이트 빌드하기

준비물

  • 다음 기기 중 하나가 필요합니다.
    • Android 기기, 생체 인식 센서가 있는 기기 권장
    • Touch ID 또는 Face ID가 지원되는 iPhone 또는 iPad(iOS 14 이상)
    • Touch ID가 지원되는 MacBook Pro 또는 Air(macOS Big Sur 이상)
    • Windows Hello가 설정된 Windows 10 19H1 이상
  • 다음 브라우저 중 하나가 필요합니다.
    • Chrome 67 이상
    • Microsoft Edge 85 이상
    • Safari 14 이상

2. 설정

이 Codelab에서는 glitch라는 서비스를 사용합니다. 이 서비스에서는 자바스크립트로 클라이언트 및 서버 측 코드를 수정하고 즉시 배포할 수 있습니다.

https://glitch.com/edit/#!/webauthn-codelab-start로 이동합니다.

사용 방법 보기

다음 단계에 따라 웹사이트의 초기 상태를 확인합니다.

  1. 62bb7a6aac381af8.png 표시 > 3343769d04c09851.png 새 창에서 열기를 클릭하여 실제 웹사이트를 확인합니다.
  2. 원하는 사용자 이름을 입력하고 다음을 클릭합니다.
  3. 비밀번호를 입력하고 로그인을 클릭합니다.

비밀번호는 무시되지만 여전히 인증되어 있습니다. 홈페이지가 표시됩니다.

  1. 재인증 시도를 클릭하고 두 번째, 세 번째, 네 번째 단계를 반복합니다.
  2. 로그아웃을 클릭합니다.

로그인할 때마다 비밀번호를 입력해야 합니다. 이렇게 하면 재인증해야 웹사이트의 중요한 섹션에 액세스할 수 있는 사용자가 에뮬레이션됩니다.

코드 리믹스

  1. WebAuthn / FIDO2 API Codelab으로 이동합니다.
  2. 프로젝트를 포크하고 새 URL에서 자체 버전을 계속 사용하려면 프로젝트 이름 > 프로젝트 리믹스306122647ce93305.png를 클릭합니다.

8d42bd24f0fd185c.png

3. 지문으로 사용자 인증 정보 등록하기

기기에 내장되어 있으며 사용자의 ID를 확인하는 인증자인 UVPA에 의해 생성된 사용자 인증 정보를 등록해야 합니다. 사용자 기기에 따라 일반적으로 지문 센서의 형태를 띱니다.

이 기능을 /home 페이지에 추가합니다.

260aab9f1a2587a7.png

registerCredential() 함수 만들기

새 사용자 인증 정보를 등록하는 registerCredential() 함수를 만듭니다.

public/client.js

export const registerCredential = async () => {

};

서버 엔드포인트에서 본인 확인 및 기타 옵션 가져오기

사용자에게 새 사용자 인증 정보를 등록하도록 요청하기 전에 WebAuthn에서 전달할 본인 확인 등의 매개변수를 반환하도록 서버에 요청합니다. 다행히 이러한 매개변수로 응답하는 서버 엔드포인트가 이미 있습니다.

다음 코드를 registerCredential()에 추가합니다.

public/client.js

const opts = {
  attestation: 'none',
  authenticatorSelection: {
    authenticatorAttachment: 'platform',
    userVerification: 'required',
    requireResidentKey: false
  }
};

const options = await _fetch('/auth/registerRequest', opts);

서버와 클라이언트 간 프로토콜은 WebAuthn 사양의 일부가 아닙니다. 그러나 이 Codelab은 WebAuthn 사양과 일치하도록 설계되었으며 서버에 전달하는 JSON 객체는 PublicKeyCredentialCreationOptions와 매우 유사하기 때문에 직관적입니다. 다음 표에는 서버에 전달할 수 있는 중요한 매개변수가 표시되어 있으며, 실행하는 작업이 설명되어 있습니다.

매개변수

설명

attestation

증명 전달에 관한 환경설정: none, indirect 또는 direct. 필요하지 않다면 none을 선택합니다.

excludeCredentials

인증자가 중복 항목의 생성을 방지할 수 있는 PublicKeyCredentialDescriptor의 배열.

authenticatorSelection

authenticatorAttachment

사용 가능한 인증자를 필터링합니다. 인증자를 기기에 연결하려면 'platform'을 사용하세요. 로밍 인증자의 경우 'cross-platform'을 사용하세요.

userVerification

인증자 로컬 사용자 확인이 'required', 'preferred' 또는 'discouraged'인지 판단합니다. 지문 또는 화면 잠금 인증을 원한다면 'required'를 사용하세요.

requireResidentKey

생성된 사용자 인증 정보를 향후 계정 선택 도구 UX에 사용할 수 있다면 true를 사용합니다.

이러한 옵션에 대한 자세한 내용은 5.4. 사용자 인증 정보 생성 옵션(사전 PublicKeyCredentialCreationOptions)을 참고하세요.

다음은 서버에서 받는 옵션의 예입니다.

{
  "rp": {
    "name": "WebAuthn Codelab",
    "id": "webauthn-codelab.glitch.me"
  },
  "user": {
    "displayName": "User Name",
    "id": "...",
    "name": "test"
  },
  "challenge": "...",
  "pubKeyCredParams": [
    {
      "type": "public-key",
      "alg": -7
    }, {
      "type": "public-key",
      "alg": -257
    }
  ],
  "timeout": 1800000,
  "attestation": "none",
  "excludeCredentials": [
    {
      "id": "...",
      "type": "public-key",
      "transports": [
        "internal"
      ]
    }
  ],
  "authenticatorSelection": {
    "authenticatorAttachment": "platform",
    "userVerification": "required"
  }
}

사용자 인증 정보 만들기

  1. 이러한 옵션은 HTTP 프로토콜을 거치도록 인코딩된 상태로 제공되므로 일부 매개변수를 다시 바이너리, 구체적으로는 user.id, challenge, excludeCredentials 배열에 포함된 id 인스턴스로 변환합니다.

public/client.js

options.user.id = base64url.decode(options.user.id);
options.challenge = base64url.decode(options.challenge);

if (options.excludeCredentials) {
  for (let cred of options.excludeCredentials) {
    cred.id = base64url.decode(cred.id);
  }
}
  1. navigator.credentials.create() 메서드를 호출하여 새 사용자 인증 정보를 만듭니다.

브라우저는 이 호출을 통해 인증자와 상호작용하며 UVPA를 통해 사용자 ID를 확인하려고 시도합니다.

public/client.js

const cred = await navigator.credentials.create({
  publicKey: options,
});

사용자가 신원을 확인하면 서버에 전송할 수 있는 사용자 인증 정보 객체를 받아 인증자를 등록해야 합니다.

서버 엔드포인트에 사용자 인증 정보 등록하기

다음은 수신해야 하는 사용자 인증 정보 객체의 예입니다.

{
  "id": "...",
  "rawId": "...",
  "type": "public-key",
  "response": {
    "clientDataJSON": "...",
    "attestationObject": "..."
  }
}
  1. 사용자 인증 정보를 등록하는 옵션 객체를 수신할 때와 마찬가지로 사용자 인증 정보의 바이너리 매개변수를 인코딩하여 문자열로 서버로 전송할 수 있습니다.

public/client.js

const credential = {};
credential.id = cred.id;
credential.rawId = base64url.encode(cred.rawId);
credential.type = cred.type;

if (cred.response) {
  const clientDataJSON =
    base64url.encode(cred.response.clientDataJSON);
  const attestationObject =
    base64url.encode(cred.response.attestationObject);
  credential.response = {
    clientDataJSON,
    attestationObject,
  };
}
  1. 사용자가 돌아오면 인증에 사용할 수 있도록 사용자 인증 정보 ID를 로컬에 저장합니다.

public/client.js

localStorage.setItem(`credId`, credential.id);
  1. 객체를 서버로 전송하고 객체가 HTTP code 200을 반환하면 새 사용자 인증 정보가 성공적으로 등록된 것으로 간주합니다.

public/client.js

return await _fetch('/auth/registerResponse' , credential);

이제 전체 registerCredential() 함수가 있습니다.

이 섹션의 최종 코드

public/client.js

...
export const registerCredential = async () => {
  const opts = {
    attestation: 'none',
    authenticatorSelection: {
      authenticatorAttachment: 'platform',
      userVerification: 'required',
      requireResidentKey: false
    }
  };

  const options = await _fetch('/auth/registerRequest', opts);

  options.user.id = base64url.decode(options.user.id);
  options.challenge = base64url.decode(options.challenge);

  if (options.excludeCredentials) {
    for (let cred of options.excludeCredentials) {
      cred.id = base64url.decode(cred.id);
    }
  }

  const cred = await navigator.credentials.create({
    publicKey: options
  });

  const credential = {};
  credential.id =     cred.id;
  credential.rawId =  base64url.encode(cred.rawId);
  credential.type =   cred.type;

  if (cred.response) {
    const clientDataJSON =
      base64url.encode(cred.response.clientDataJSON);
    const attestationObject =
      base64url.encode(cred.response.attestationObject);
    credential.response = {
      clientDataJSON,
      attestationObject
    };
  }

  localStorage.setItem(`credId`, credential.id);

  return await _fetch('/auth/registerResponse' , credential);
};
...

4. 사용자 인증 정보를 등록, 가져오기, 삭제하는 UI 빌드하기

등록된 사용자 인증 정보와 버튼 중 삭제할 목록을 만들어 두는 것이 좋습니다.

9b5b5ae4a7b316bd.png

UI 자리표시자 빌드하기

사용자 인증 정보와 버튼을 나열하여 새 사용자 인증 정보를 등록할 수 있는 UI를 추가합니다. 기능의 사용 가능 여부에 따라 경고 메시지나 버튼에서 hidden 클래스를 삭제하여 새 사용자 인증 정보를 등록합니다. ul#list는 등록된 사용자 인증 정보 목록을 추가하기 위한 자리표시자입니다.

views/home.html

<p id="uvpa_unavailable" class="hidden">
  This device does not support User Verifying Platform Authenticator. You can't register a credential.
</p>
<h3 class="mdc-typography mdc-typography--headline6">
  Your registered credentials:
</h3>
<section>
  <div id="list"></div>
</section>
<mwc-button id="register" class="hidden" icon="fingerprint" raised>Add a credential</mwc-button>

특징 감지 및 UVPA 가용성

UVPA 가용성을 확인하려면 다음 단계를 따르세요.

  1. window.PublicKeyCredential을 확인하여 WebAuthn을 사용할 수 있는지 확인합니다.
  2. PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()을 호출하여 UVPA를 사용할 수 있는지 확인합니다. 사용 가능한 경우 새 사용자 인증 정보를 등록하는 버튼이 표시됩니다. 둘 다 사용할 수 없는 경우 경고 메시지가 표시됩니다.

views/home.html

const register = document.querySelector('#register');

if (window.PublicKeyCredential) {
  PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()
  .then(uvpaa => {
    if (uvpaa) {
      register.classList.remove('hidden');
    } else {
      document
        .querySelector('#uvpa_unavailable')
        .classList.remove('hidden');
    }
  });
} else {
  document
    .querySelector('#uvpa_unavailable')
    .classList.remove('hidden');
}

사용자 인증 정보 목록 가져오기 및 표시하기

  1. 등록된 사용자 인증 정보를 가져와 목록에 표시할 수 있도록 getCredentials() 함수를 만듭니다. 다행히 /auth/getKeys 서버에는 이미 로그인된 사용자의 사용자 인증 정보를 가져올 수 있는 유용한 엔드포인트가 있습니다.

반환된 JSON에는 idpublicKey 같은 사용자 인증 정보가 포함됩니다. HTML을 작성하여 사용자에게 표시할 수 있습니다.

views/home.html

const getCredentials = async () => {
  const res = await _fetch('/auth/getKeys');
  const list = document.querySelector('#list');
  const creds = html`${res.credentials.length > 0 ? res.credentials.map(cred => html`
    <div class="mdc-card credential">
      <span class="mdc-typography mdc-typography--body2">${cred.credId}</span>
      <pre class="public-key">${cred.publicKey}</pre>
      <div class="mdc-card__actions">
        <mwc-button id="${cred.credId}" @click="${removeCredential}" raised>Remove</mwc-button>
      </div>
    </div>`) : html`
    <p>No credentials found.</p>
    `}`;
  render(creds, list);
};
  1. getCredentials()를 호출하여 사용자가 /home 페이지에 방문하자마자 사용 가능한 사용자 인증 정보를 표시합니다.

views/home.html

getCredentials();

사용자 인증 정보 삭제하기

사용자 인증 정보 목록에 각 사용자 인증 정보를 삭제하는 버튼을 추가했습니다. credId 쿼리 매개변수와 함께 /auth/removeKey에 요청을 전송하여 삭제할 수 있습니다.

public/client.js

export const unregisterCredential = async (credId) => {
  localStorage.removeItem('credId');
  return _fetch(`/auth/removeKey?credId=${encodeURIComponent(credId)}`);
};
  1. 기존 import 문에 unregisterCredential을 추가합니다.

views/home.html

import { _fetch, unregisterCredential } from '/client.js';
  1. 사용자가 삭제를 클릭할 때 호출할 함수를 추가합니다.

views/home.html

const removeCredential = async e => {
  try {
    await unregisterCredential(e.target.id);
    getCredentials();
  } catch (e) {
    alert(e);
  }
};

사용자 인증 정보 등록하기

사용자가 사용자 인증 정보 추가를 클릭하면 registerCredential()을 호출하여 새 사용자 인증 정보를 등록할 수 있습니다.

  1. 기존 import 문에 registerCredential을 추가합니다.

views/home.html

import { _fetch, registerCredential, unregisterCredential } from '/client.js';
  1. navigator.credentials.create() 옵션을 사용하여 registerCredential()을 호출합니다.

등록 후 getCredentials()를 호출하여 사용자 인증 정보 목록을 갱신해야 합니다.

views/home.html

register.addEventListener('click', e => {
  registerCredential().then(user => {
    getCredentials();
  }).catch(e => alert(e));
});

이제 새 사용자 인증 정보를 등록하고 그에 관한 정보를 표시할 수 있습니다. 라이브 웹사이트에서 사용해 볼 수 있습니다.

이 섹션의 최종 코드

views/home.html

...
      <p id="uvpa_unavailable" class="hidden">
        This device does not support User Verifying Platform Authenticator. You can't register a credential.
      </p>
      <h3 class="mdc-typography mdc-typography--headline6">
        Your registered credentials:
      </h3>
      <section>
        <div id="list"></div>
        <mwc-fab id="register" class="hidden" icon="add"></mwc-fab>
      </section>
      <mwc-button raised><a href="/reauth">Try reauth</a></mwc-button>
      <mwc-button><a href="/auth/signout">Sign out</a></mwc-button>
    </main>
    <script type="module">
      import { _fetch, registerCredential, unregisterCredential } from '/client.js';
      import { html, render } from 'https://unpkg.com/lit-html@1.0.0/lit-html.js?module';

      const register = document.querySelector('#register');

      if (window.PublicKeyCredential) {
        PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()
        .then(uvpaa => {
          if (uvpaa) {
            register.classList.remove('hidden');
          } else {
            document
              .querySelector('#uvpa_unavailable')
              .classList.remove('hidden');
          }
        });
      } else {
        document
          .querySelector('#uvpa_unavailable')
          .classList.remove('hidden');
      }

      const getCredentials = async () => {
        const res = await _fetch('/auth/getKeys');
        const list = document.querySelector('#list');
        const creds = html`${res.credentials.length > 0 ? res.credentials.map(cred => html`
          <div class="mdc-card credential">
            <span class="mdc-typography mdc-typography--body2">${cred.credId}</span>
            <pre class="public-key">${cred.publicKey}</pre>
            <div class="mdc-card__actions">
              <mwc-button id="${cred.credId}" @click="${removeCredential}" raised>Remove</mwc-button>
            </div>
          </div>`) : html`
          <p>No credentials found.</p>
          `}`;
        render(creds, list);
      };

      getCredentials();

      const removeCredential = async e => {
        try {
          await unregisterCredential(e.target.id);
          getCredentials();
        } catch (e) {
          alert(e);
        }
      };

      register.addEventListener('click', e => {
        registerCredential({
          attestation: 'none',
          authenticatorSelection: {
            authenticatorAttachment: 'platform',
            userVerification: 'required',
            requireResidentKey: false
          }
        })
        .then(user => {
          getCredentials();
        })
        .catch(e => alert(e));
      });
    </script>
...

public/client.js

...
export const unregisterCredential = async (credId) => {
  localStorage.removeItem('credId');
  return _fetch(`/auth/removeKey?credId=${encodeURIComponent(credId)}`);
};
...

5. 지문으로 사용자 인증하기

이제 사용자 인증 정보를 등록하여 사용자 인증 방법으로 사용할 수 있습니다. 이제 웹사이트에 재인증 기능을 추가합니다. 사용자 환경은 다음과 같습니다.

사용자가 /reauth 페이지를 방문하면 생체 인식 인증이 가능한 경우 인증 버튼이 표시됩니다. 지문(UVPA)을 통한 인증은 인증을 탭하고 인증을 완료한 다음 /home 페이지로 이동하면 시작됩니다. 생체 인식 인증을 사용할 수 없거나 생체 인식 인증에 실패하면 기존 비밀번호 양식을 사용하도록 UI가 대체됩니다.

b8770c4e7475b075.png

authenticate() 함수 만들기

지문으로 사용자의 신원을 확인하는 authenticate()라는 함수를 만듭니다. 여기에 자바스크립트 코드를 추가합니다.

public/client.js

export const authenticate = async () => {

};

서버 엔드포인트에서 본인 확인 및 기타 옵션 가져오기

  1. 인증 전에 사용자에게 저장된 사용자 인증 정보 ID가 있는지 확인하고, 있는 경우 쿼리 매개변수로 설정하세요.

다른 옵션과 함께 사용자 인증 정보 ID를 제공하면 서버가 관련 allowCredentials를 제공할 수 있으며, 이렇게 하면 사용자 인증을 안정적으로 실행할 수 있습니다.

public/client.js

const opts = {};

let url = '/auth/signinRequest';
const credId = localStorage.getItem(`credId`);
if (credId) {
  url += `?credId=${encodeURIComponent(credId)}`;
}
  1. 사용자에게 인증을 요청하기 전에 서버에 본인 확인 요청 및 기타 매개변수를 다시 전송하도록 요청하세요. opts를 인수로 _fetch()를 호출하여 POST 요청을 서버에 전송합니다.

public/client.js

const options = await _fetch(url, opts);

다음은 받는 옵션의 예입니다(PublicKeyCredentialRequestOptions에 부합).

{
  "challenge": "...",
  "timeout": 1800000,
  "rpId": "webauthn-codelab.glitch.me",
  "userVerification": "required",
  "allowCredentials": [
    {
      "id": "...",
      "type": "public-key",
      "transports": [
        "internal"
      ]
    }
  ]
}

여기에서 가장 중요한 옵션은 allowCredentials입니다. 서버로부터 옵션을 수신할 때 allowCredentials는 서버 측에서 쿼리 매개변수 ID의 사용자 인증 정보를 찾을 수 있는지에 따라 배열의 단일 객체이거나 빈 배열이어야 합니다.

  1. allowCredentials가 빈 배열인 경우 null을 사용하여 프로미스를 확인합니다. 그러면 UI가 비밀번호를 묻는 요청으로 대체됩니다.
if (options.allowCredentials.length === 0) {
  console.info('No registered credentials found.');
  return Promise.resolve(null);
}

로컬에서 사용자 인증 및 사용자 인증 정보 가져오기

  1. 이러한 옵션은 HTTP 프로토콜을 거치도록 인코딩된 상태로 제공되므로 일부 매개변수를 다시 바이너리, 구체적으로는 challengeallowCredentials 배열에 포함된 id 인스턴스로 변환합니다.

public/client.js

options.challenge = base64url.decode(options.challenge);

for (let cred of options.allowCredentials) {
  cred.id = base64url.decode(cred.id);
}
  1. UVPA를 통해 사용자의 신원을 확인하려면 navigator.credentials.get() 메서드를 호출합니다.

public/client.js

const cred = await navigator.credentials.get({
  publicKey: options
});

사용자가 신원을 확인하면 서버에 전송할 수 있는 사용자 인증 정보 객체를 받아 사용자를 인증해야 합니다.

사용자 인증 정보 확인하기

다음은 수신한 PublicKeyCredential 객체(responseAuthenticatorAssertionResponse임)의 예입니다.

{
  "id": "...",
  "type": "public-key",
  "rawId": "...",
  "response": {
    "clientDataJSON": "...",
    "authenticatorData": "...",
    "signature": "...",
    "userHandle": ""
  }
}
  1. 사용자 인증 정보의 바이너리 매개변수를 인코딩하여 문자열로 서버로 전송할 수 있습니다.

public/client.js

const credential = {};
credential.id = cred.id;
credential.type = cred.type;
credential.rawId = base64url.encode(cred.rawId);

if (cred.response) {
  const clientDataJSON =
    base64url.encode(cred.response.clientDataJSON);
  const authenticatorData =
    base64url.encode(cred.response.authenticatorData);
  const signature =
    base64url.encode(cred.response.signature);
  const userHandle =
    base64url.encode(cred.response.userHandle);
  credential.response = {
    clientDataJSON,
    authenticatorData,
    signature,
    userHandle,
  };
}
  1. 객체를 서버로 전송하고 객체가 HTTP code 200을 반환하면 사용자가 로그인된 것으로 간주합니다.

public/client.js

return await _fetch(`/auth/signinResponse`, credential);

이제 전체 authentication() 함수가 있습니다.

이 섹션의 최종 코드

public/client.js

...
export const authenticate = async () => {
  const opts = {};

  let url = '/auth/signinRequest';
  const credId = localStorage.getItem(`credId`);
  if (credId) {
    url += `?credId=${encodeURIComponent(credId)}`;
  }

  const options = await _fetch(url, opts);

  if (options.allowCredentials.length === 0) {
    console.info('No registered credentials found.');
    return Promise.resolve(null);
  }

  options.challenge = base64url.decode(options.challenge);

  for (let cred of options.allowCredentials) {
    cred.id = base64url.decode(cred.id);
  }

  const cred = await navigator.credentials.get({
    publicKey: options
  });

  const credential = {};
  credential.id = cred.id;
  credential.type = cred.type;
  credential.rawId = base64url.encode(cred.rawId);

  if (cred.response) {
    const clientDataJSON =
      base64url.encode(cred.response.clientDataJSON);
    const authenticatorData =
      base64url.encode(cred.response.authenticatorData);
    const signature =
      base64url.encode(cred.response.signature);
    const userHandle =
      base64url.encode(cred.response.userHandle);
    credential.response = {
      clientDataJSON,
      authenticatorData,
      signature,
      userHandle,
    };
  }

  return await _fetch(`/auth/signinResponse`, credential);
};
...

6. 재인증 환경 사용 설정하기

UI 빌드하기

사용자가 돌아오면 최대한 쉽고 안전하게 재인증할 수 있도록 만들고자 합니다. 생체 인식 인증은 바로 여기에서 중요합니다. 하지만 다음과 같은 경우에는 생체 인식 인증이 작동하지 않을 수 있습니다.

  • UVPA를 사용할 수 없습니다.
  • 사용자가 아직 기기에 사용자 인증 정보를 등록하지 않았습니다.
  • 스토리지가 삭제되고 기기에 더 이상 사용자 인증 정보 ID가 저장되지 않습니다.
  • 사용자의 손가락이 젖어 있거나 마스크를 착용하는 등의 이유로 인해 본인 확인을 할 수 없습니다.

따라서 다른 로그인 옵션을 대체로 제공하는 것이 항상 중요합니다. 이 Codelab에서는 양식 기반 비밀번호 솔루션을 사용합니다.

19da999b0145054.png

  1. 비밀번호 양식 외에 생체 인식 인증을 호출하는 인증 버튼을 표시하도록 UI를 추가합니다.

hidden 클래스를 사용하여 사용자의 상태에 따라 버튼 중 하나를 선택적으로 표시하거나 숨깁니다.

views/reauth.html

<div id="uvpa_available" class="hidden">
  <h2>
    Verify your identity
  </h2>
  <div>
    <mwc-button id="reauth" raised>Authenticate</mwc-button>
  </div>
  <div>
    <mwc-button id="cancel">Sign-in with password</mwc-button>
  </div>
</div>
  1. 양식에 class="hidden"을 추가합니다.

views/reauth.html

<form id="form" method="POST" action="/auth/password" class="hidden">

특징 감지 및 UVPA 가용성

다음 조건 중 하나가 충족되면 사용자는 비밀번호로 로그인해야 합니다.

  • WebAuthn을 사용할 수 없습니다.
  • UVPA를 사용할 수 없습니다.
  • 이 UVPA의 사용자 인증 정보 ID를 검색할 수 없습니다.

인증 버튼을 선택적으로 표시하거나 숨깁니다.

views/reauth.html

if (window.PublicKeyCredential) {
  PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()
  .then(uvpaa => {
    if (uvpaa && localStorage.getItem(`credId`)) {
      document
        .querySelector('#uvpa_available')
        .classList.remove('hidden');
    } else {
      form.classList.remove('hidden');
    }
  });
} else {
  form.classList.remove('hidden');
}

비밀번호 양식으로 대체하기

사용자가 비밀번호로 로그인하도록 선택할 수도 있어야 합니다.

사용자가 비밀번호로 로그인을 클릭하면 비밀번호 양식을 표시하고 인증 버튼을 숨깁니다.

views/reauth.html

const cancel = document.querySelector('#cancel');
cancel.addEventListener('click', e => {
  form.classList.remove('hidden');
  document
    .querySelector('#uvpa_available')
    .classList.add('hidden');
});

c4a82800889f078c.png

생체 인식 인증 호출하기

마지막으로 생체 인식 인증을 사용 설정합니다.

  1. 기존 import 문에 authenticate를 추가합니다.

views/reauth.html

import { _fetch, authenticate } from '/client.js';
  1. 사용자가 인증을 탭하여 생체 인식 인증을 시작하면 authenticate()를 호출합니다.

생체 인식 인증 실패가 비밀번호 양식으로 대체되었는지 확인합니다.

views/reauth.html

const button = document.querySelector('#reauth');
button.addEventListener('click', e => {
  authenticate().then(user => {
    if (user) {
      location.href = '/home';
    } else {
      throw 'User not found.';
    }
  }).catch(e => {
    console.error(e.message || e);
    alert('Authentication failed. Use password to sign-in.');
    form.classList.remove('hidden');
    document.querySelector('#uvpa_available').classList.add('hidden');
  });
});

이 섹션의 최종 코드

views/reauth.html

...
    <main class="content">
      <div id="uvpa_available" class="hidden">
        <h2>
          Verify your identity
        </h2>
        <div>
          <mwc-button id="reauth" raised>Authenticate</mwc-button>
        </div>
        <div>
          <mwc-button id="cancel">Sign-in with password</mwc-button>
        </div>
      </div>
      <form id="form" method="POST" action="/auth/password" class="hidden">
        <h2>
          Enter a password
        </h2>
        <input type="hidden" name="username" value="{{username}}" />
        <div class="mdc-text-field mdc-text-field--filled">
          <span class="mdc-text-field__ripple"></span>
          <label class="mdc-floating-label" id="password-label">password</label>
          <input type="password" class="mdc-text-field__input" aria-labelledby="password-label" name="password" />
          <span class="mdc-line-ripple"></span>
        </div>
        <input type="submit" class="mdc-button mdc-button--raised" value="Sign-In" />
        <p class="instructions">password will be ignored in this demo.</p>
      </form>
    </main>
    <script src="https://unpkg.com/material-components-web@7.0.0/dist/material-components-web.min.js"></script>
    <script type="module">
      new mdc.textField.MDCTextField(document.querySelector('.mdc-text-field'));
      import { _fetch, authenticate } from '/client.js';
      const form = document.querySelector('#form');
      form.addEventListener('submit', e => {
        e.preventDefault();
        const form = new FormData(e.target);
        const cred = {};
        form.forEach((v, k) => cred[k] = v);
        _fetch(e.target.action, cred)
        .then(user => {
          location.href = '/home';
        })
        .catch(e => alert(e));
      });

      if (window.PublicKeyCredential) {
        PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()
        .then(uvpaa => {
          if (uvpaa && localStorage.getItem(`credId`)) {
            document
              .querySelector('#uvpa_available')
              .classList.remove('hidden');
          } else {
            form.classList.remove('hidden');
          }
        });
      } else {
        form.classList.remove('hidden');
      }

      const cancel = document.querySelector('#cancel');
      cancel.addEventListener('click', e => {
        form.classList.remove('hidden');
        document
          .querySelector('#uvpa_available')
          .classList.add('hidden');
      });

      const button = document.querySelector('#reauth');
      button.addEventListener('click', e => {
        authenticate().then(user => {
          if (user) {
            location.href = '/home';
          } else {
            throw 'User not found.';
          }
        }).catch(e => {
          console.error(e.message || e);
          alert('Authentication failed. Use password to sign-in.');
          form.classList.remove('hidden');
          document.querySelector('#uvpa_available').classList.add('hidden');
        });
      });
    </script>
...

7. 축하합니다

이 Codelab을 완료했습니다.

자세히 알아보기

도움을 주신 FIDO Alliance의 유리 에커만 님께 감사 말씀을 드립니다.