개요
다음은 패스키 인증과 관련된 주요 단계에 대한 간략한 개요입니다.
- 패스키로 인증하는 데 필요한 본인 확인 질문과 기타 옵션을 정의합니다. 패스키 인증 호출 (웹의
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
를 만든 후 클라이언트로 전송합니다.
코드 예시: 사용자 인증 정보 요청 옵션 만들기
이 예에서는 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
객체를 반환합니다.
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 {
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로 지정된 요구사항을 준수했는지 확인합니다. 사용자 확인이 필요한 경우
authenticatorData
의uv
(사용자 확인됨) 플래그가true
인지 확인합니다. 패스키에는 사용자 존재가 항상 필요하므로authenticatorData
의up
(사용자 존재) 플래그가true
인지 확인합니다. - 서명을 확인합니다. 서명을 확인하려면 다음이 필요합니다.
- 서명된 챌린지인 서명:
response.signature
- 서명을 확인할 공개 키입니다.
- 원래 서명된 데이터입니다. 서명을 확인할 데이터입니다.
- 서명을 만드는 데 사용된 암호화 알고리즘입니다.
- 서명된 챌린지인 서명:
이 단계에 대해 자세히 알아보려면 SimpleWebAuthn의 verifyAuthenticationResponse
소스 코드를 확인하거나 사양에서 전체 인증 목록을 살펴보세요.