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

- 패스키로 인증하는 데 필요한 챌린지와 기타 옵션을 정의합니다. 패스키 인증 호출(웹의
navigator.credentials.get)에 전달할 수 있도록 클라이언트에 전송합니다. 사용자가 패스키 인증을 확인하면 패스키 인증 호출이 처리되고 사용자 인증 정보 (PublicKeyCredential)를 반환합니다. 사용자 인증 정보에는 인증 어설션이 포함됩니다.
- 인증 어설션을 확인합니다.
- 인증 어설션이 유효하면 사용자를 인증합니다.
다음 섹션에서는 각 단계의 세부정보를 자세히 설명합니다.
챌린지 만들기
실제로 챌린지는 ArrayBuffer 객체로 표현되는 임의의 바이트 배열입니다.
// Example challenge, base64URL-encoded
weMLPOSx1VfSnMV6uPwDKbjGdKRMaUDGxeDEUTT5VN8
챌린지가 목적을 달성하려면 다음을 충족해야 합니다.
- 동일한 챌린지가 두 번 이상 사용되지 않도록 합니다. 로그인 시도마다 새로운 질문을 생성합니다. 로그인 시도 후에는 성공 여부와 관계없이 챌린지를 삭제합니다. 일정 시간이 지나면 챌린지도 삭제합니다. 답변에서 동일한 챌린지를 두 번 이상 수락하지 마세요.
- 챌린지가 암호화 방식으로 안전한지 확인합니다. 챌린지는 추측이 사실상 불가능해야 합니다. 암호화 방식으로 안전한 서버 측 챌린지를 만들려면 신뢰할 수 있는 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를 만든 후 고객에게 전송합니다.
challenge 디코딩은 클라이언트 측에서 이루어집니다.예시 코드: 사용자 인증 정보 요청 옵션 만들기
예시에서는 SimpleWebAuthn 라이브러리를 사용합니다. 여기서는 사용자 인증 정보 요청 옵션 생성을 generateAuthenticationOptions 함수에 전달합니다.
import {
generateAuthenticationOptions,
} 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 객체를 반환합니다.
navigator.credentials.get는 PublicKeyCredential를 반환합니다.response는 AuthenticatorAssertionResponse입니다. RP에서 패스키로 인증을 시도하는 데 필요한 항목을 만들라는 클라이언트의 지시에 대한 패스키 제공업체의 응답을 나타냅니다. 여기에는 다음이 포함됩니다.
response.authenticatorData및response.clientDataJSON(예: 패스키 등록 단계)response.signature: 이러한 값에 대한 서명이 포함됩니다.
PublicKeyCredential 객체를 서버로 전송합니다.
서버에서 다음을 수행합니다.
- 어설션을 확인하고 사용자를 인증하는 데 필요한 정보를 수집합니다.
- 인증 옵션을 생성할 때 세션에 저장한 예상 챌린지를 가져옵니다.
- 예상 출처 및 RP ID를 가져옵니다.
- 데이터베이스에서 사용자가 누구인지 찾습니다. 검색 가능한 사용자 인증 정보의 경우 인증 요청을 하는 사용자가 누구인지 알 수 없습니다. 이를 확인하는 방법에는 두 가지가 있습니다.
- 옵션 1:
PublicKeyCredential객체의response.userHandle를 사용합니다. 사용자 테이블에서userHandle와 일치하는passkey_user_id를 찾습니다. - 옵션 2:
PublicKeyCredential객체에 있는 사용자 인증 정보id를 사용합니다. 공개 키 사용자 인증 정보 테이블에서PublicKeyCredential객체에 있는 사용자 인증 정보id와 일치하는 사용자 인증 정보id를 찾습니다. 그런 다음 Users 테이블에 대한 외래 키passkey_user_id를 사용하여 해당 사용자를 찾습니다.
- 옵션 1:
- 수신한 인증 어설션과 일치하는 공개 키 사용자 인증 정보 정보를 데이터베이스에서 찾습니다. 이렇게 하려면 공개 키 사용자 인증 정보 테이블에서
PublicKeyCredential객체에 있는 사용자 인증 정보id와 일치하는 사용자 인증 정보id를 찾습니다.
인증 어설션을 확인합니다. 이 확인 단계를 FIDO 서버 측 라이브러리에 넘깁니다. 이 라이브러리는 일반적으로 이 목적을 위한 유틸리티 함수를 제공합니다. SimpleWebAuthn은 예를 들어
verifyAuthenticationResponse를 제공합니다. 부록: 인증 응답 확인에서 내부적으로 어떤 일이 일어나는지 알아보세요.확인 성공 여부와 관계없이 챌린지를 삭제하여 재생 공격을 방지합니다.
사용자를 로그인 처리합니다. 확인에 성공하면 세션 정보를 업데이트하여 사용자를 로그인 상태로 표시합니다. 프런트엔드에서 새로 로그인한 사용자와 연결된 정보를 사용할 수 있도록
user객체를 클라이언트에 반환할 수도 있습니다.
예시 코드: 사용자 인증 및 로그인
예시에서는 SimpleWebAuthn 라이브러리를 사용합니다. 여기서는 인증 응답의 확인을 verifyAuthenticationResponse 함수에 전달합니다.
import {
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.');
}
// Prepare credential information for verification
const credential = {
id: cred.id,
publicKey: isoBase64URL.toBuffer(cred.publicKey),
transports: cred.transports,
};
// Verify the credential
const { verified, authenticationInfo } = await verifyAuthenticationResponse({
response,
expectedChallenge,
expectedOrigin,
expectedRPID,
credential,
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로 지정한 요구사항을 따랐는지 확인합니다. 사용자 인증이 필요한 경우
authenticatorData의uv(사용자 인증됨) 플래그가true인지 확인합니다. 패스키에는 사용자 현재 상태가 항상 필요하므로authenticatorData의up(사용자 현재 상태) 플래그가true인지 확인합니다. - 서명을 확인합니다. 서명을 확인하려면 다음이 필요합니다.
- 서명된 챌린지인 서명:
response.signature - 서명을 확인하는 공개 키입니다.
- 서명된 원본 데이터입니다. 서명을 확인할 데이터입니다.
- 서명을 만드는 데 사용된 암호화 알고리즘입니다.
- 서명된 챌린지인 서명:
이 단계에 관한 자세한 내용은 SimpleWebAuthn의 verifyAuthenticationResponse 소스 코드를 확인하거나 사양의 전체 인증 목록을 살펴보세요.