서버 측 패스키 등록

개요

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

패스키 등록 흐름

  • 패스키 생성 옵션을 정의합니다. 패스키 생성 호출에 전달할 수 있도록 클라이언트로 전송합니다. WebAuthn API는 웹에서 navigator.credentials.create를, Android에서는 credentialManager.createCredential를 호출합니다. 사용자가 패스키 생성을 확인하면 패스키 생성 호출이 처리되고 사용자 인증 정보 PublicKeyCredential가 반환됩니다.
  • 사용자 인증 정보를 확인하고 서버에 저장합니다.

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

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

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

서버에서 수행해야 하는 첫 번째 단계는 PublicKeyCredentialCreationOptions 객체를 만드는 것입니다.

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

PublicKeyCredentialCreationOptions에는 사용자 정보, RP에 관한 정보, 생성 중인 사용자 인증 정보의 속성 구성 등 패스키 생성에 필요한 모든 것이 포함되어야 합니다. 모두 정의했으면 필요에 따라 PublicKeyCredentialCreationOptions 객체 생성을 담당하는 FIDO 서버 측 라이브러리의 함수에 전달합니다.

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

  • rpId: 서버에 RP ID를 채우려면 웹 애플리케이션의 호스트 이름을 제공하는 서버 측 함수 또는 변수(예: example.com)를 사용합니다.
  • user.nameuser.displayName: 이 필드를 채우려면 로그인한 사용자의 세션 정보 (또는 사용자가 가입 시 패스키를 만드는 경우 신규 사용자 계정 정보)를 사용합니다. user.name는 일반적으로 이메일 주소이며 RP마다 고유합니다. user.displayName은 사용자 친화적인 이름입니다. 일부 플랫폼에서는 displayName를 사용하지 않습니다.
  • user.id: 계정 생성 시 생성되는 임의의 고유한 문자열입니다. 이 이름은 수정할 수 있는 사용자 이름과 달리 영구적이어야 합니다. 사용자 ID는 계정을 식별하지만 개인 식별 정보 (PII)를 포함해서는 안 됩니다. 시스템에 이미 사용자 ID가 있을 수 있지만 필요한 경우 패스키용으로 특별히 사용자 ID를 만들어 개인 식별 정보가 삭제되지 않도록 하세요.
  • excludeCredentials: 기존 사용자 인증 정보 목록입니다. 패스키 제공업체의 패스키 중복을 방지하기 위한 ID 이 필드를 채우려면 데이터베이스에서 이 사용자의 기존 사용자 인증 정보를 조회합니다. 패스키가 이미 있는 경우 새 패스키를 생성하지 않음에서 세부정보를 검토하세요.
  • challenge: 사용자 인증 정보 등록의 경우 패스키 제공업체의 ID 및 생성하는 데이터를 확인하는 고급 기법인 증명을 사용하지 않는 한 챌린지는 관련이 없습니다. 그러나 증명을 사용하지 않더라도 챌린지는 여전히 필수 필드입니다. 이 경우 편의상 이 챌린지를 단일 0로 설정할 수 있습니다. 인증을 위한 보안 본인 확인 요청을 만드는 방법은 서버 측 패스키 인증에서 확인할 수 있습니다.

인코딩 및 디코딩

<ph type="x-smartling-placeholder">
</ph> 서버에서 전송한 PublicKeyCredentialCreationOptions
서버에서 PublicKeyCredentialCreationOptions을(를) 전송했습니다. challenge, user.id, excludeCredentials.credentials는 서버 측에서 base64URL로 인코딩되어야 합니다. 그래야 PublicKeyCredentialCreationOptions가 HTTPS를 통해 전송될 수 있습니다.

PublicKeyCredentialCreationOptions에는 ArrayBuffer 필드가 포함되어 있으므로 JSON.stringify()에서 지원되지 않습니다. 즉, 현재 HTTPS를 통해 PublicKeyCredentialCreationOptions를 게재하려면 일부 필드가 base64URL를 사용하여 서버에서 수동으로 인코딩한 다음 클라이언트에서 디코딩되어야 합니다.

  • 서버에서는 일반적으로 FIDO 서버 측 라이브러리에서 인코딩과 디코딩을 처리합니다.
  • 클라이언트에서는 현재 인코딩과 디코딩을 수동으로 수행해야 합니다. 향후 더 쉬워질 것입니다. 옵션을 JSON으로 PublicKeyCredentialCreationOptions로 변환하는 메서드를 사용할 수 있습니다. Chrome에서 구현 상태를 확인하세요.

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

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

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

router.post('/registerRequest', csrfCheck, sessionCheck, async (req, res) => {
  const { user } = res.locals;
  // 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 {
    // `excludeCredentials` prevents users from re-registering existing
    // credentials for a given passkey provider
    const excludeCredentials = [];
    const credentials = Credentials.findByUserId(user.id);
    if (credentials.length > 0) {
      for (const cred of credentials) {
        excludeCredentials.push({
          id: isoBase64URL.toBuffer(cred.id),
          type: 'public-key',
          transports: cred.transports,
        });
      }
    }

    // Generate registration options for WebAuthn create
    const options = generateRegistrationOptions({
      rpName: process.env.RP_NAME,
      rpID: process.env.HOSTNAME,
      userID: user.id,
      userName: user.username,
      userDisplayName: user.displayName || '',
      attestationType: 'none',
      excludeCredentials,
      authenticatorSelection: {
        authenticatorAttachment: 'platform',
        requireResidentKey: true
      },
    });

    // Keep the challenge in the session
    req.session.challenge = options.challenge;

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

공개 키 저장

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

클라이언트에서 navigator.credentials.create가 성공적으로 확인되면 패스키가 성공적으로 생성된 것입니다. PublicKeyCredential 객체가 반환됩니다.

PublicKeyCredential 객체에는 패스키 생성에 대한 클라이언트의 안내에 대한 패스키 제공업체의 응답을 나타내는 AuthenticatorAttestationResponse 객체가 포함됩니다. 여기에는 나중에 사용자를 인증하기 위해 RP로 필요한 새 사용자 인증 정보에 관한 정보가 포함되어 있습니다. 부록: AuthenticatorAttestationResponse에서 AuthenticatorAttestationResponse에 대해 자세히 알아보세요.

PublicKeyCredential 객체를 서버로 전송합니다. 이메일을 받으면 인증합니다.

이 인증 단계를 FIDO 서버 측 라이브러리에 전달합니다. 일반적으로 이러한 용도로 사용할 유틸리티 기능을 제공합니다. 예를 들어 SimpleWebAuthn은 verifyRegistrationResponse를 제공합니다. 부록: 등록 응답 확인에서 자세히 알아보세요.

인증에 성공하면 사용자가 나중에 해당 사용자 인증 정보와 연결된 패스키로 인증할 수 있도록 사용자 인증 정보 정보를 데이터베이스에 저장합니다.

패스키와 연결된 공개 키 사용자 인증 정보에 대한 전용 표를 사용합니다. 사용자는 하나의 비밀번호만 가질 수 있지만 여러 개의 패스키를 가질 수 있습니다. 예를 들어 Apple iCloud 키체인을 통해 동기화된 패스키와 Google 비밀번호 관리자를 통해 동기화된 패스키가 하나씩 있습니다.

다음은 사용자 인증 정보 정보를 저장하는 데 사용할 수 있는 스키마의 예입니다.

패스키용 데이터베이스 스키마

  • Users 테이블에서는 다음 명령어를 사용합니다. <ph type="x-smartling-placeholder">
      </ph>
    • user_id: 기본 사용자 ID입니다. 사용자의 고유한 임의의 영구 ID입니다. 이 키를 Users 테이블의 기본 키로 사용합니다.
    • username 사용자가 정의하고 수정할 수 있는 사용자 이름입니다.
    • passkey_user_id: 패스키별 PII가 없는 사용자 ID로, 등록 옵션user.id로 표시됩니다. 나중에 사용자가 인증을 시도하면 인증자는 userHandle의 인증 응답에서 이 passkey_user_id를 사용할 수 있도록 합니다. passkey_user_id를 기본 키로 설정하지 않는 것이 좋습니다. 기본 키는 광범위하게 사용되기 때문에 시스템에서 사실상의 PII가 되는 경향이 있습니다.
  • 공개 키 사용자 인증 정보 테이블: <ph type="x-smartling-placeholder">
      </ph>
    • id: 사용자 인증 정보 ID입니다. 이 키를 공개 키 사용자 인증 정보 테이블의 기본 키로 사용합니다.
    • public_key: 사용자 인증 정보의 공개 키입니다.
    • passkey_user_id: 이 키를 외래 키로 사용하여 Users 테이블과의 링크를 설정합니다.
    • backed_up: 패스키 제공업체가 동기화하면 패스키가 백업됩니다. 백업 상태를 저장하면 향후 backed_up개의 패스키를 보유한 사용자가 비밀번호를 저장하지 않도록 하려는 경우에 유용합니다. authenticatorData의 플래그를 검사하거나 이 정보에 쉽게 액세스할 수 있도록 일반적으로 제공되는 FIDO 서버 측 라이브러리 기능을 사용하여 패스키가 백업되었는지 확인할 수 있습니다. 백업 자격 요건을 저장하면 잠재적인 사용자 문의를 해결하는 데 도움이 될 수 있습니다.
    • name: (선택사항) 사용자가 사용자 인증 정보 커스텀 이름을 지정할 수 있도록 사용자 인증 정보의 표시 이름입니다.
    • transports: 전송의 배열입니다. 전송을 저장하면 인증 사용자 환경에 유용합니다. 전송을 사용할 수 있으면 브라우저는 적절하게 동작하고 패스키 제공업체가 클라이언트와 통신하는 데 사용하는 전송(특히 allowCredentials가 비어 있지 않은 재인증 사용 사례)과 일치하는 UI를 표시할 수 있습니다.

패스키 제공업체, 사용자 인증 정보 생성 시간, 마지막으로 사용한 시간과 같은 항목 등 사용자 환경을 위해 정보를 저장하면 도움이 될 수 있습니다. 패스키 사용자 인터페이스 디자인에서 자세히 알아보세요.

코드 예시: 사용자 인증 정보 저장

이 예에서는 SimpleWebAuthn 라이브러리를 사용합니다. 이제 등록 응답 확인을 verifyRegistrationResponse 함수에 넘깁니다.

import { isoBase64URL } from '@simplewebauthn/server/helpers';


router.post('/registerResponse', csrfCheck, sessionCheck, async (req, res) => {
  const expectedChallenge = req.session.challenge;
  const expectedOrigin = getOrigin(req.get('User-Agent'));
  const expectedRPID = process.env.HOSTNAME;
  const response = req.body;
  // This sample code is for registering a passkey for an existing,
  // signed-in user

  // 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 {
    // Verify the credential
    const { verified, registrationInfo } = await verifyRegistrationResponse({
      response,
      expectedChallenge,
      expectedOrigin,
      expectedRPID,
      requireUserVerification: false,
    });

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

    const { credentialPublicKey, credentialID } = registrationInfo;

    // Existing, signed-in user
    const { user } = res.locals;
    
    // Save the credential
    await Credentials.update({
      id: base64CredentialID,
      publicKey: base64PublicKey,
      // Optional: set the platform as a default name for the credential
      // (example: "Pixel 7")
      name: req.useragent.platform, 
      transports: response.response.transports,
      passkey_user_id: user.passkey_user_id,
      backed_up: registrationInfo.credentialBackedUp
    });

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

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

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

부록: AuthenticatorAttestationResponse

AuthenticatorAttestationResponse에는 두 가지 중요한 객체가 있습니다.

  • response.clientDataJSON클라이언트 데이터의 JSON 버전으로, 웹에서는 브라우저에 표시되는 데이터입니다. 여기에는 RP 출처와 챌린지, 클라이언트가 Android 앱인 경우 androidPackageName가 포함됩니다. RP로서 clientDataJSON를 읽으면 create 요청 시 브라우저에 표시된 정보에 액세스할 수 있습니다.
  • response.attestationObject에는 다음 두 가지 정보가 포함됩니다. <ph type="x-smartling-placeholder">
      </ph>
    • attestationStatement: 증명을 사용하지 않는 한 관련이 없습니다.
    • authenticatorData는 패스키 제공업체에 표시되는 데이터입니다. RP로서 authenticatorData를 읽으면 패스키 제공업체가 보고 create 요청 시 반환된 데이터에 액세스할 수 있습니다.

authenticatorData에는 새로 생성된 패스키와 연결된 공개 키 사용자 인증 정보에 관한 필수 정보가 포함되어 있습니다.

  • 공개 키 사용자 인증 정보 자체와 이에 대한 고유한 사용자 인증 정보 ID
  • 사용자 인증 정보와 연결된 RP ID입니다.
  • 패스키가 생성되었을 때 사용자 상태, 즉 사용자가 실제로 존재했는지, 사용자가 성공적으로 인증되었는지를 설명하는 플래그입니다 (userVerification 참고).
  • AAGUID: 패스키 제공업체를 식별합니다. 특히 사용자가 여러 패스키 제공업체에서 서비스에 등록된 패스키를 보유한 경우, 패스키 제공업체를 표시하면 사용자에게 유용할 수 있습니다.

authenticatorDataattestationObject 내에 중첩되어 있지만 포함된 정보는 증명 사용 여부와 관계없이 패스키 구현에 필요합니다. authenticatorData는 인코딩되며 바이너리 형식으로 인코딩된 필드를 포함합니다. 서버 측 라이브러리는 일반적으로 파싱 및 디코딩을 처리합니다. 서버 측 라이브러리를 사용하지 않는다면 getAuthenticatorData() 클라이언트 측을 활용하여 서버 측에서 파싱 및 디코딩 작업을 할 필요가 없도록 하세요.

부록: 등록 응답 확인

내부적으로 등록 응답 확인은 다음 검사로 구성됩니다.

  • RP ID가 사이트와 일치하는지 확인합니다.
  • 요청의 출처가 사이트의 예상 출처 (기본 사이트 URL, Android 앱)인지 확인합니다.
  • 사용자 확인이 필요한 경우 사용자 확인 플래그 authenticatorData.uvtrue인지 확인합니다. 패스키에는 사용자 정보가 항상 필요하므로 사용자 정보 플래그 authenticatorData.uptrue인지 확인합니다.
  • 귀사가 제시한 챌린지를 고객이 충분히 설명했는지 확인합니다. 증명을 사용하지 않는다면 이 검사는 중요하지 않습니다. 그러나 이 검사를 구현하는 것이 좋습니다. 이렇게 하면 향후 증명을 사용하기로 결정할 때 코드가 준비될 수 있습니다.
  • 사용자 인증 정보 ID가 아직 다른 사용자에게 등록되지 않았는지 확인합니다.
  • 사용자 인증 정보를 만들기 위해 패스키 제공업체에서 사용한 알고리즘이 내가 나열한 알고리즘인지 확인합니다 (publicKeyCredentialCreationOptions.pubKeyCredParams의 각 alg 필드에 있음). 이 알고리즘은 일반적으로 서버 측 라이브러리 내에 정의되며 나에게는 표시되지 않습니다. 이렇게 하면 허용된 알고리즘으로만 사용자가 등록할 수 있습니다.

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

다음 단계

서버 측 패스키 인증