개요
다음은 패스키 등록과 관련된 주요 단계의 대략적인 개요입니다.
- 패스키를 만드는 옵션을 정의합니다. 웹의 WebAuthn API 호출
navigator.credentials.create
및 Android의credentialManager.createCredential
와 같은 비밀번호 생성 호출에 전달할 수 있도록 클라이언트에 전송합니다. 사용자가 패스키 생성을 확인하면 패스키 생성 호출이 해결되고 사용자 인증 정보PublicKeyCredential
가 반환됩니다. - 사용자 인증 정보를 확인하고 서버에 저장합니다.
다음 섹션에서는 각 단계의 세부사항을 자세히 설명합니다.
사용자 인증 정보 생성 옵션 만들기
서버에서 가장 먼저 해야 할 일은 PublicKeyCredentialCreationOptions
객체를 만드는 것입니다.
이렇게 하려면 FIDO 서버 측 라이브러리를 사용하세요. 일반적으로 이러한 옵션을 만들 수 있는 유틸리티 함수를 제공합니다. SimpleWebAuthn은 예를 들어 generateRegistrationOptions
을 제공합니다.
PublicKeyCredentialCreationOptions
에는 패스키 생성에 필요한 모든 항목(사용자, RP에 관한 정보, 생성 중인 사용자 인증 정보의 속성에 관한 구성)이 포함되어야 합니다. 이 모든 항목을 정의한 후에는 PublicKeyCredentialCreationOptions
객체를 생성하는 역할을 하는 FIDO 서버 측 라이브러리의 함수에 필요에 따라 전달합니다.
PublicKeyCredentialCreationOptions
의 일부 필드는 상수일 수 있습니다. 다른 항목은 서버에서 동적으로 정의해야 합니다.
rpId
: 서버에서 RP ID를 채우려면example.com
과 같은 웹 애플리케이션의 호스트 이름을 제공하는 서버 측 함수나 변수를 사용하세요.user.name
및user.displayName
: 이러한 필드를 채우려면 로그인한 사용자의 세션 정보 (또는 사용자가 가입 시 패스키를 만드는 경우 새 사용자 계정 정보)를 사용하세요.user.name
는 일반적으로 이메일 주소이며 RP에 고유합니다.user.displayName
은 사용자 친화적인 이름입니다. 일부 플랫폼에서는displayName
를 사용하지 않습니다.user.id
: 계정 생성 시 생성되는 무작위의 고유한 문자열입니다. 수정 가능한 사용자 이름과 달리 영구적이어야 합니다. 사용자 ID는 계정을 식별하지만 개인 식별 정보 (PII)를 포함해서는 안 됩니다. 시스템에 사용자 ID가 이미 있을 수 있지만 필요한 경우 패스키 전용으로 만들어 개인 식별 정보가 포함되지 않도록 합니다.excludeCredentials
: 패스키 제공업체에서 패스키가 중복되지 않도록 기존 사용자 인증 정보의 ID 목록입니다. 이 필드를 채우려면 데이터베이스에서 이 사용자의 기존 사용자 인증 정보를 조회하세요. 이미 있는 경우 새 패스키 생성 방지에서 세부정보를 검토하세요.challenge
: 사용자 인증 정보 등록의 경우 증명(더 고급 기술로, 패스키 제공업체의 ID와 내보내는 데이터를 확인하는 데 사용됨)을 사용하지 않는 한 챌린지는 관련이 없습니다. 하지만 증명을 사용하지 않더라도 챌린지는 필수 필드입니다. 인증을 위한 보안 챌린지를 만드는 방법은 서버 측 패스키 인증을 참고하세요.
인코딩 및 디코딩

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 = await 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 });
}
});
공개 키 저장

navigator.credentials.create
은 PublicKeyCredential
객체를 반환합니다.클라이언트에서 navigator.credentials.create
가 성공적으로 처리되면 패스키가 성공적으로 생성된 것입니다. PublicKeyCredential
객체가 반환됩니다.
PublicKeyCredential
객체에는 패스키를 생성하라는 클라이언트의 요청에 대한 패스키 제공업체의 응답을 나타내는 AuthenticatorAttestationResponse
객체가 포함됩니다. 여기에는 RP가 나중에 사용자를 인증하는 데 필요한 새 사용자 인증 정보에 관한 정보가 포함됩니다. 부록: AuthenticatorAttestationResponse
에서 AuthenticatorAttestationResponse
에 대해 자세히 알아보세요.
PublicKeyCredential
객체를 서버로 전송합니다. 코드를 받으면 인증하세요.
이 확인 단계를 FIDO 서버 측 라이브러리에 넘겨줍니다. 일반적으로 이러한 목적으로 유틸리티 함수를 제공합니다. SimpleWebAuthn은 예를 들어 verifyRegistrationResponse
을 제공합니다. 부록: 등록 응답 확인에서 내부적으로 어떤 일이 일어나는지 알아보세요.
인증이 완료되면 사용자가 나중에 해당 사용자 인증 정보와 연결된 패스키로 인증할 수 있도록 데이터베이스에 사용자 인증 정보 정보를 저장합니다.
패스키와 연결된 공개 키 사용자 인증 정보를 위한 전용 테이블을 사용합니다. 사용자는 비밀번호를 하나만 가질 수 있지만 패스키는 여러 개 가질 수 있습니다(예: Apple iCloud 키체인을 통해 동기화된 패스키와 Google 비밀번호 관리자를 통해 동기화된 패스키).
다음은 사용자 인증 정보 정보를 저장하는 데 사용할 수 있는 스키마의 예입니다.
- Users 테이블:
user_id
: 기본 사용자 ID입니다. 사용자의 무작위 고유 영구 ID입니다. 이를 Users 테이블의 기본 키로 사용합니다.username
. 사용자 정의 사용자 이름으로, 수정 가능합니다.passkey_user_id
: 등록 옵션에서user.id
로 표시되는 패스키 전용 PII 없는 사용자 ID입니다. 나중에 사용자가 인증을 시도하면 인증자는userHandle
의 인증 응답에서 이passkey_user_id
를 사용할 수 있도록 합니다.passkey_user_id
을 기본 키로 설정하지 않는 것이 좋습니다. 기본 키는 광범위하게 사용되므로 시스템에서 사실상 PII가 되는 경향이 있습니다.
- 공개 키 사용자 인증 정보 표:
id
: 사용자 인증 정보 ID입니다. 공개 키 사용자 인증 정보 테이블의 기본 키로 사용합니다.public_key
: 사용자 인증 정보의 공개 키입니다.passkey_user_id
: Users 테이블과의 링크를 설정하기 위해 외래 키로 사용합니다.backed_up
: 패스키가 패스키 제공업체에 의해 동기화되면 백업됩니다.backed_up
패스키를 보유한 사용자를 위해 향후 비밀번호 삭제를 고려하려는 경우 백업 상태를 저장하는 것이 유용합니다.authenticatorData
의 BE 플래그를 검사하거나 일반적으로 이 정보에 쉽게 액세스할 수 있도록 제공되는 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 {
aaguid,
credentialPublicKey,
credentialID,
credentialBackedUp
} = registrationInfo;
// Name the credential based on AAGUID
const name =
aaguid === undefined ||
aaguid === '000000-0000-0000-0000-00000000' ?
req.useragent?.platform : aaguids[aaguid].name;
const base64CredentialID = isoBase64URL.fromBuffer(credentialID);
const base64PublicKey = isoBase64URL.fromBuffer(credentialPublicKey);
// Existing, signed-in user
const { user } = res.locals;
// Save the credential
await Credentials.update({
id: base64CredentialID,
passkey_user_id: user.passkey_user_id,
publicKey: base64PublicKey,
name,
aaguid,
transports: response.response.transports,
backed_up: credentialBackedUp,
registered_at: new Date().getTime()
});
// 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 버전으로, 웹에서는 브라우저에 표시되는 데이터입니다. 클라이언트가 Android 앱인 경우 RP 출처, 챌린지,androidPackageName
가 포함됩니다. RP로서clientDataJSON
를 읽으면create
요청 시 브라우저에 표시된 정보에 액세스할 수 있습니다.response.attestationObject
에는 다음 두 가지 정보가 포함됩니다.- 증명을 사용하지 않는 한 관련이 없는
attestationStatement
authenticatorData
은 패스키 제공업체에 표시되는 데이터입니다. RP로서authenticatorData
를 읽으면 패스키 제공자가 확인하고create
요청 시 반환하는 데이터에 액세스할 수 있습니다.
- 증명을 사용하지 않는 한 관련이 없는
authenticatorData
에는 새로 생성된 패스키와 연결된 공개 키 사용자 인증 정보에 관한 필수 정보가 포함되어 있습니다.
- 공개 키 사용자 인증 정보 자체와 공개 키 사용자 인증 정보의 고유한 사용자 인증 정보 ID
- 사용자 인증 정보와 연결된 RP ID입니다.
- 패스키가 생성될 때 사용자 상태를 설명하는 플래그입니다. 사용자가 실제로 있었는지, 사용자가 성공적으로 인증되었는지 여부를 나타냅니다 (userVerification 심층 분석 참고).
- AAGUID는 Google 비밀번호 관리자와 같은 패스키 제공업체의 식별자입니다. AAGUID를 기반으로 패스키 제공자를 식별하고 패스키 관리 페이지에 이름을 표시할 수 있습니다. (AAGUID로 패스키 제공자 확인 참고)
authenticatorData
는 attestationObject
내에 중첩되어 있지만 증명 사용 여부와 관계없이 패스키 구현에 필요한 정보를 포함합니다. authenticatorData
는 인코딩되어 있으며 바이너리 형식으로 인코딩된 필드를 포함합니다. 일반적으로 서버 측 라이브러리에서 파싱 및 디코딩을 처리합니다. 서버 측 라이브러리를 사용하지 않는 경우 getAuthenticatorData()
클라이언트 측을 활용하여 서버 측에서 파싱 및 디코딩 작업을 줄이는 것이 좋습니다.
부록: 등록 응답 확인
등록 응답 확인은 다음과 같은 검사로 구성됩니다.
- RP ID가 사이트와 일치하는지 확인합니다.
- 요청의 출처가 사이트의 예상 출처 (기본 사이트 URL, Android 앱)인지 확인합니다.
- 사용자 확인이 필요한 경우 사용자 확인 플래그
authenticatorData.uv
가true
인지 확인합니다. - 사용자 존재 플래그
authenticatorData.up
는 일반적으로true
일 것으로 예상되지만, 사용자 인증 정보가 조건부로 생성된 경우false
일 것으로 예상됩니다. - 클라이언트가 제공한 챌린지를 제공할 수 있는지 확인합니다. 증명을 사용하지 않는 경우 이 검사는 중요하지 않습니다. 하지만 이 검사를 구현하는 것이 좋습니다. 나중에 증명을 사용하기로 결정할 경우 코드가 준비되어 있기 때문입니다.
- 사용자에게 아직 사용자 인증 정보 ID가 등록되지 않았는지 확인합니다.
- 인증서 생성을 위해 패스키 제공업체에서 사용하는 알고리즘이 나열된 알고리즘인지 확인합니다 (
publicKeyCredentialCreationOptions.pubKeyCredParams
의 각alg
필드에 나열됨. 일반적으로 서버 측 라이브러리 내에 정의되며 사용자에게는 표시되지 않음). 이렇게 하면 사용자가 허용하도록 선택한 알고리즘으로만 등록할 수 있습니다.
자세한 내용은 SimpleWebAuthn의 verifyRegistrationResponse
소스 코드를 확인하거나 사양의 전체 확인 목록을 참고하세요.