서버 측 패스키 인증

개요

다음은 패스키 인증과 관련된 주요 단계에 대한 간략한 개요입니다.

패스키 인증 흐름

  • 패스키로 인증하는 데 필요한 본인 확인 요청 및 기타 옵션을 정의합니다. 패스키 인증 호출 (웹의 경우 navigator.credentials.get)에 전달할 수 있도록 클라이언트로 전송합니다. 사용자가 패스키 인증을 확인하면 패스키 인증 호출이 처리되고 사용자 인증 정보 (PublicKeyCredential)가 반환됩니다. 사용자 인증 정보에는 인증 어설션이 포함되어 있습니다.
를 통해 개인정보처리방침을 정의할 수 있습니다.
  • 인증 어설션을 확인합니다.
  • 인증 어설션이 유효하면 사용자를 인증합니다.

다음 섹션에서는 각 단계를 자세히 살펴봅니다.

<ph type="x-smartling-placeholder">

챌린지 만들기

실제로 챌린지는 ArrayBuffer 객체로 표시되는 임의의 바이트 배열입니다.

// Example challenge, base64URL-encoded
weMLPOSx1VfSnMV6uPwDKbjGdKRMaUDGxeDEUTT5VN8

챌린지의 목적을 달성하려면 다음을 수행해야 합니다.

  1. 동일한 로그인 질문이 두 번 이상 사용되지 않도록 합니다. 로그인을 시도할 때마다 새로운 본인 확인 요청 생성 성공 여부와 관계없이 로그인을 시도할 때마다 본인 확인 요청을 삭제합니다. 일정 시간이 지난 후 챌린지를 삭제하세요. 응답에서 동일한 챌린지를 두 번 이상 수락하지 마세요.
  2. 챌린지가 암호화 방식으로 안전한지 확인합니다. 챌린지는 사실상 추측이 불가능해야 합니다. 서버 측에서 암호학적으로 안전한 챌린지를 만들려면 신뢰할 수 있는 FIDO 서버 측 라이브러리를 사용하는 것이 가장 좋습니다. 대신 자체 문제를 만드는 경우 기술 스택에서 제공되는 내장 암호화 기능을 사용하거나 암호화 사용 사례를 위해 설계된 라이브러리를 찾아보세요. Node.js의 iso-crypto 또는 Python의 secrets를 예로 들 수 있습니다. 사양에 따라 챌린지가 16바이트 이상이어야 안전한 것으로 간주됩니다.

챌린지를 만들었다면 나중에 확인할 수 있도록 사용자 세션에 저장합니다.

사용자 인증 정보 요청 만들기 옵션

사용자 인증 정보 요청 옵션을 publicKeyCredentialRequestOptions 객체로 만듭니다.

이렇게 하려면 FIDO 서버 측 라이브러리를 사용하세요. 일반적으로 이러한 옵션을 생성할 수 있는 유틸리티 함수를 제공합니다. 예를 들어 SimpleWebAuthn은 generateAuthenticationOptions를 제공합니다.

publicKeyCredentialRequestOptions에는 패스키 인증에 필요한 모든 정보가 포함되어야 합니다. publicKeyCredentialRequestOptions 객체 생성을 담당하는 FIDO 서버 측 라이브러리의 함수에 이 정보를 전달합니다.

publicKeyCredentialRequestOptions의 일부 필드는 상수가 될 수 있습니다. 다른 속성은 서버에서 동적으로 정의되어야 합니다.

  • rpId: 사용자 인증 정보가 연결될 것으로 예상되는 RP ID입니다(예: example.com). 여기에 입력한 RP ID가 사용자 인증 정보와 연결된 RP ID와 일치하는 경우에만 인증에 성공합니다. RP ID를 채우려면 사용자 인증 정보 등록 중에 publicKeyCredentialCreationOptions에서 설정한 RP ID와 동일한 값을 사용합니다.
  • challenge: 인증 요청 시 사용자가 패스키를 보유하고 있음을 증명하기 위해 패스키 제공업체가 서명하는 데이터입니다. 챌린지 만들기에서 세부정보를 검토하세요.
  • allowCredentials: 이 인증에 허용되는 사용자 인증 정보의 배열입니다. 사용자가 브라우저에 표시된 목록에서 사용 가능한 패스키를 선택할 수 있도록 빈 배열을 전달합니다. 자세한 내용은 RP 서버에서 챌린지 가져오기검색 가능한 사용자 인증 정보 심층 분석을 검토하세요.
  • userVerification: 기기 화면 잠금을 사용한 사용자 확인이 '필수', '선호됨'인지를 나타냅니다. 또는 '권장되지 않음'입니다. RP 서버에서 챌린지 가져오기를 검토합니다.
  • timeout: 사용자가 인증을 완료하는 데 걸릴 수 있는 시간 (밀리초)입니다. 충분히 넉넉해야 하며 challenge의 전체 기간보다 짧아야 합니다. 권장되는 기본값은 5분이지만 권장 범위 이내인 최대 10분까지 늘릴 수 있습니다. 일반적으로 시간이 조금 더 오래 걸리는 하이브리드 워크플로를 사용할 것으로 예상되는 경우 시간 제한을 길게 설정하는 것이 좋습니다. 작업이 타임아웃되면 NotAllowedError이 발생합니다.

publicKeyCredentialRequestOptions를 만든 후 클라이언트에 전송합니다.

<ph type="x-smartling-placeholder">
</ph> 서버에서 전송한 publicKeyCredentialCreationOptions 클래스의 생성자
서버에서 전송하는 옵션입니다. challenge 디코딩은 클라이언트 측에서 이루어집니다.

예시 코드: 사용자 인증 정보 요청 옵션 만들기

이 예에서는 SimpleWebAuthn 라이브러리를 사용합니다. 이제 사용자 인증 정보 요청 옵션 생성을 generateAuthenticationOptions 함수에 넘깁니다.

import {
  generateRegistrationOptions,
  verifyRegistrationResponse,
  generateAuthenticationOptions,
  verifyAuthenticationResponse
} from '@simplewebauthn/server';

router.post('/signinRequest', csrfCheck, async (req, res) => {

  // Ensure you nest calls in try/catch blocks.
  // If something fails, throw an error with a descriptive error message.
  // Return that message with an appropriate error code to the client.
  try {
    // Use the generateAuthenticationOptions function from SimpleWebAuthn
    const options = await generateAuthenticationOptions({
      rpID: process.env.HOSTNAME,
      allowCredentials: [],
    });
    // Save the challenge in the user session
    req.session.challenge = options.challenge;

    return res.json(options);
  } catch (e) {
    console.error(e);
    return res.status(400).json({ error: e.message });
  }
});

사용자 인증 및 로그인

navigator.credentials.get가 클라이언트에서 성공적으로 확인되면 PublicKeyCredential 객체가 반환됩니다.

<ph type="x-smartling-placeholder">
</ph> 서버에서 전송한 PublicKeyCredential 객체
navigator.credentials.getPublicKeyCredential를 반환합니다.

responseAuthenticatorAssertionResponse입니다. RP에서 패스키로 인증하고 인증하는 데 필요한 것을 생성하라는 클라이언트의 안내에 대한 패스키 제공업체의 응답을 나타냅니다. 여기에는 다음이 포함됩니다.

  • response.authenticatorDataresponse.clientDataJSON(예: 패스키 등록 단계)
  • response.signature: 이러한 값에 대한 서명이 포함되어 있습니다.

PublicKeyCredential 객체를 서버로 전송합니다.

서버에서 다음을 수행합니다.

<ph type="x-smartling-placeholder">
</ph> 데이터베이스 스키마
추천 데이터베이스 스키마 서버 측 패스키 등록에서 이 설계에 관해 자세히 알아보세요.
  • 어설션을 확인하고 사용자를 인증하는 데 필요한 정보를 수집합니다. <ph type="x-smartling-placeholder">
      </ph>
    • 인증 옵션을 생성할 때 세션에 저장한 예상 본인 확인 질문을 가져옵니다.
    • 예상 출처 및 RP ID를 가져옵니다.
    • 데이터베이스에서 사용자가 누구인지 찾습니다. 검색 가능한 사용자 인증 정보의 경우 인증을 요청하는 사용자가 누구인지 알 수 없습니다. 이를 확인하려면 다음 두 가지 옵션이 있습니다. <ph type="x-smartling-placeholder">
        </ph>
      • 옵션 1: PublicKeyCredential 객체의 response.userHandle 사용 사용자 테이블에서 userHandle과 일치하는 passkey_user_id를 찾습니다.
      • 옵션 2: PublicKeyCredential 객체에 있는 사용자 인증 정보 id를 사용합니다. 공개 키 사용자 인증 정보 테이블에서 PublicKeyCredential 객체에 있는 사용자 인증 정보 id와 일치하는 사용자 인증 정보 id를 찾습니다. 그런 다음 Users 테이블의 외래 키 passkey_user_id를 사용하여 해당 사용자를 찾습니다.
    • 수신한 인증 어설션과 일치하는 공개 키 사용자 인증 정보를 데이터베이스에서 찾습니다. 이렇게 하려면 공개 키 사용자 인증 정보 테이블에서 PublicKeyCredential 객체에 있는 사용자 인증 정보 id와 일치하는 사용자 인증 정보 id를 찾습니다.
  • 인증 어설션을 확인합니다. 이 확인 단계를 FIDO 서버 측 라이브러리에 넘겨주세요. 서버 측 라이브러리는 일반적으로 이러한 용도를 위한 유틸리티 기능을 제공합니다. 예를 들어 SimpleWebAuthn은 verifyAuthenticationResponse를 제공합니다. 부록: 인증 응답 확인에서 자세히 알아보세요.

  • 재전송 공격을 방지하기 위해 인증 성공 여부에 관계없이 챌린지를 삭제합니다.

  • 사용자 로그인 처리 인증에 성공하면 세션 정보를 업데이트하여 사용자를 로그인 상태로 표시합니다. 프런트엔드가 새로 로그인한 사용자와 관련된 정보를 사용할 수 있도록 user 객체를 클라이언트에 반환할 수도 있습니다.

예시 코드: 사용자 인증 및 로그인

이 예에서는 SimpleWebAuthn 라이브러리를 사용합니다. 여기에서는 verifyAuthenticationResponse 함수에 인증 응답 확인을 넘깁니다.

import {
  generateRegistrationOptions,
  verifyRegistrationResponse,
  generateAuthenticationOptions,
  verifyAuthenticationResponse
} from '@simplewebauthn/server';
import { isoBase64URL } from '@simplewebauthn/server/helpers';

router.post('/signinResponse', csrfCheck, async (req, res) => {
  const response = req.body;
  const expectedChallenge = req.session.challenge;
  const expectedOrigin = getOrigin(req.get('User-Agent'));
  const expectedRPID = process.env.HOSTNAME;

  // Ensure you nest verification function calls in try/catch blocks.
  // If something fails, throw an error with a descriptive error message.
  // Return that message with an appropriate error code to the client.
  try {
    // Find the credential stored to the database by the credential ID
    const cred = Credentials.findById(response.id);
    if (!cred) {
      throw new Error('Credential not found.');
    }
    // Find the user - Here alternatively we could look up the user directly
    // in the Users table via userHandle
    const user = Users.findByPasskeyUserId(cred.passkey_user_id);
    if (!user) {
      throw new Error('User not found.');
    }
    // Base64URL decode some values
    const authenticator = {
      credentialPublicKey: isoBase64URL.toBuffer(cred.publicKey),
      credentialID: isoBase64URL.toBuffer(cred.id),
      transports: cred.transports,
    };

    // Verify the credential
    const { verified, authenticationInfo } = await verifyAuthenticationResponse({
      response,
      expectedChallenge,
      expectedOrigin,
      expectedRPID,
      authenticator,
      requireUserVerification: false,
    });

    if (!verified) {
      throw new Error('User verification failed.');
    }

    // Kill the challenge for this session.
    delete req.session.challenge;

    req.session.username = user.username;
    req.session['signed-in'] = 'yes';

    return res.json(user);
  } catch (e) {
    delete req.session.challenge;

    console.error(e);
    return res.status(400).json({ error: e.message });
  }
});

부록: 인증 응답 확인

인증 응답 확인은 다음 검사로 이루어집니다.

  • RP ID가 사이트와 일치하는지 확인합니다.
  • 요청의 출처가 사이트의 로그인 출처와 일치하는지 확인합니다. Android 앱의 경우 출처 확인을 검토합니다.
  • 기기에서 사용자가 제공한 보안문자를 입력할 수 있는지 확인합니다.
  • 인증 중에 사용자가 관리자가 RP로 요구하는 요구사항을 준수했는지 확인합니다. 사용자 확인이 필요한 경우 authenticatorDatauv (사용자 인증됨) 플래그가 true인지 확인합니다. 패스키에는 사용자 정보가 항상 필요하므로 authenticatorDataup (사용자 있음) 플래그가 true인지 확인합니다.
  • 서명을 확인합니다. 서명을 확인하려면 다음이 필요합니다. <ph type="x-smartling-placeholder">
      </ph>
    • 서명된 챌린지인 서명: response.signature
    • 서명을 확인할 때 사용하는 공개 키입니다.
    • 원래의 서명된 데이터입니다. 서명을 확인할 데이터입니다.
    • 서명을 만드는 데 사용된 암호화 알고리즘입니다.
를 통해 개인정보처리방침을 정의할 수 있습니다.

이 단계에 대해 자세히 알아보려면 SimpleWebAuthn의 verifyAuthenticationResponse 소스 코드를 확인하거나 사양에서 전체 인증 목록을 살펴보세요.