개요
다음은 패스키 인증과 관련된 주요 단계에 대한 간략한 개요입니다.
- 패스키로 인증하는 데 필요한 본인 확인 요청 및 기타 옵션을 정의합니다. 패스키 인증 호출 (웹의 경우
navigator.credentials.get
)에 전달할 수 있도록 클라이언트로 전송합니다. 사용자가 패스키 인증을 확인하면 패스키 인증 호출이 처리되고 사용자 인증 정보 (PublicKeyCredential
)가 반환됩니다. 사용자 인증 정보에는 인증 어설션이 포함되어 있습니다.
- 인증 어설션을 확인합니다.
- 인증 어설션이 유효하면 사용자를 인증합니다.
다음 섹션에서는 각 단계를 자세히 살펴봅니다.
<ph type="x-smartling-placeholder">챌린지 만들기
실제로 챌린지는 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
객체를 서버로 전송합니다.
서버에서 다음을 수행합니다.
<ph type="x-smartling-placeholder">- 어설션을 확인하고 사용자를 인증하는 데 필요한 정보를 수집합니다.
<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
를 사용하여 해당 사용자를 찾습니다.
- 옵션 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
인지 확인합니다. - 서명을 확인합니다. 서명을 확인하려면 다음이 필요합니다.
<ph type="x-smartling-placeholder">
- </ph>
- 서명된 챌린지인 서명:
response.signature
- 서명을 확인할 때 사용하는 공개 키입니다.
- 원래의 서명된 데이터입니다. 서명을 확인할 데이터입니다.
- 서명을 만드는 데 사용된 암호화 알고리즘입니다.
- 서명된 챌린지인 서명:
이 단계에 대해 자세히 알아보려면 SimpleWebAuthn의 verifyAuthenticationResponse
소스 코드를 확인하거나 사양에서 전체 인증 목록을 살펴보세요.