이 가이드에서는 Signal API를 사용하여 서버와 사용자의 비밀번호 관리자 간에 저장된 사용자 인증 정보와 사용자 프로필 세부정보를 동기화하면서 사용자가 등록된 패스키를 보고, 이름을 바꾸고, 삭제할 수 있도록 하는 방법을 자세히 설명합니다.
서버 측 작업
백엔드 데이터베이스 레이어와 엔드포인트는 등록된 사용자 인증 정보에 대한 일반적인 CRUD 작업과 사용자 프로필 세부정보 업데이트를 지원해야 합니다(MUST). 프레임워크별 라이브러리와 분리된 서버는 다음을 위해 엔드포인트를 노출합니다.
- 모든 사용자 인증 정보 나열: 로그인한 사용자의 ID와 일치하는 모든
StoredPasskeyCredential레코드를 가져옵니다. - 사용자 인증 정보 이름 업데이트: 특정 사용자 인증 정보 ID의 새 맞춤 닉네임을 수락하고 서버에서 업데이트를 유지합니다.
- 사용자 인증 정보 삭제: 데이터베이스에서 특정 사용자 인증 정보 ID를 삭제합니다.
- 사용자 계정 세부정보 업데이트: 클라이언트가 패스키 제공업체와 동기화할 수 있도록 로그인한 사용자의 프로필에 대해 업데이트된
name(사용자 이름) 및displayName값을 허용합니다.
// Node.js routing example for credential CRUD and user profile rename
router.get('/api/credentials', checkUserAuthenticated, async (req, res) => {
const list = await db.findCredentialsByUserId(req.user.id);
return res.json(list);
});
router.put('/api/credential/:id', checkUserAuthenticated, async (req, res) => {
const { id } = req.params;
const { name } = req.body;
const cred = await db.findCredentialById(id);
if (!cred || cred.passkeyUserId !== req.user.id) {
return res.status(404).json({ error: 'Credential not found.' });
}
cred.name = name;
await db.saveCredential(cred);
return res.json(cred);
});
router.delete('/api/credential/:id', checkUserAuthenticated, async (req, res) => {
const { id } = req.params;
const cred = await db.findCredentialById(id);
if (!cred || cred.passkeyUserId !== req.user.id) {
return res.status(404).json({ error: 'Credential not found.' });
}
await db.deleteCredential(id);
return res.json({ success: true });
});
router.put('/api/user', checkUserAuthenticated, async (req, res) => {
const { name, displayName } = req.body;
const updatedUser = await db.updateUser(req.user.id, { name, displayName });
return res.json(updatedUser);
});
클라이언트 측 관리 UI
사용자가 등록된 인증 옵션과 계정 프로필 세부정보를 쉽게 감사하고 관리할 수 있는 전용 설정 패널을 렌더링합니다.
- 저장된 목록 표시: 엔드포인트에서 목록을 가져와 개별 사용자 인증 정보 행을 렌더링합니다. 응답이 비어 있으면 유용한 빈 상태 메시지 (예: '패스키를 찾을 수 없음')를 렌더링합니다.
- AAGUID 메타데이터 매핑: 각 패스키에 대해 로컬 레지스트리에서
aaguid속성을 조회하여 제공업체 세부정보를 렌더링합니다. 자세한 내용은 AAGUID에서 패스키 제공업체 확인 섹션을 참고하세요. - 항목별 UI 요구사항: 목록 컨테이너 내부의 모든 행은 다음을 렌더링해야 합니다(MUST).
- 제공업체 아이콘: AAGUID에서 파생된 이미지 또는 데이터 URI입니다.
- 제공업체/맞춤 이름: AAGUID에서 파생된 이름 또는 사용자가 이름을 바꾼 문자열입니다.
- 등록일: 클라이언트 표시를 위해 인간이 읽을 수 있는 날짜로 형식이 지정된 데이터베이스에 유지되는 원시 에포크 타임스탬프
registeredAt입니다. - 마지막 사용 날짜: 클라이언트 표시를 위해 사람이 읽을 수 있는 날짜 (있는 경우)로 형식이 지정된 데이터베이스 지속 원시 에포크 타임스탬프
lastUsedAt입니다. - 이름 바꾸기 버튼: 이름 바꾸기 작업을 트리거하여 서버에서 사용자 인증 정보의 맞춤 닉네임을 업데이트합니다 (
PUT /api/credential/:id). 참고: 개별 패스키의 닉네임 이름을 바꾸는 것은 서버 측 라벨 변경이며signalCurrentUserDetails()를 호출하지 않습니다. - 삭제 버튼: 사용자 인증 정보 (
DELETE /api/credential/:id) 삭제를 트리거하고signalAllAcceptedCredentials()를 통해 나머지 사용자 인증 정보 ID를 동기화합니다.
- 사용자 계정 이름 바꾸기 UI:
- 사용자가 계정 사용자 이름 (
name) 또는displayName(PUT /api/user)을 업데이트할 수 있는 컨트롤을 제공하고 업데이트된 사용자 세부정보를PublicKeyCredential.signalCurrentUserDetails()를 통해 비밀번호 관리자와 즉시 동기화합니다.
- 사용자가 계정 사용자 이름 (
- 조건부 '패스키 만들기' 버튼:
- 관리 페이지에 눈에 잘 띄는 '패스키 생성' 등록 트리거 버튼을 제공합니다. 이 UI 요소를 렌더링하기 전에 페이지는
PublicKeyCredential.getClientCapabilities()를 사용하여 기능을 감지하여 플랫폼 인증자가 지원되는지 확인해야 합니다(MUST). 패스키가 지원되지 않는 경우 이 버튼을 숨기고 대신 표준 MFA 등록을 유도합니다. navigator.credentials.create()호출에서authenticatorSelection.authenticatorAttachment를 생략하여 보안 키를 등록할 수 있습니다.
- 관리 페이지에 눈에 잘 띄는 '패스키 생성' 등록 트리거 버튼을 제공합니다. 이 UI 요소를 렌더링하기 전에 페이지는
신호 API 동기화
Signal API를 사용하면 애플리케이션이 사용자 인증 정보 상태를 비밀번호 관리자에 전달하여 사용자의 동기화된 저장소와 백엔드 데이터베이스를 동기화할 수 있습니다.
- 특성 감지 및 매개변수 인코딩 규칙:
- Signal API 메서드를 호출하기 전에 항상 기능 감지 (
if (PublicKeyCredential.signalAllAcceptedCredentials)및if (PublicKeyCredential.signalCurrentUserDetails))를 실행하세요. - Signal API 메서드(
signalAllAcceptedCredentials,signalCurrentUserDetails)에 전달되는 모든userId및 사용자 인증 정보 ID 매개변수는 Base64URL로 인코딩된 문자열이어야 합니다(MUST).
- Signal API 메서드를 호출하기 전에 항상 기능 감지 (
- 페이지 로드 동기화 시작:
- 애플리케이션은 관리 페이지가 로드될 때(예:
DOMContentLoaded이벤트 리스너, 모듈 초기화 또는useEffect와 같은 프레임워크 구성요소 마운트 후크 내에서)signalAllAcceptedCredentials()를 자동으로 호출해야 합니다(MUST).
- 애플리케이션은 관리 페이지가 로드될 때(예:
- 관리 업데이트 동기화:
- 애플리케이션은 사용자 인증 정보 삭제 클릭 핸들러 가져오기 후 즉시
signalAllAcceptedCredentials()를 호출해야 합니다(MUST). - 애플리케이션은 사용자 계정 이름 바꾸기(
name또는displayName) 클릭 핸들러 가져오기 후 즉시signalCurrentUserDetails()를 호출해야 합니다(MUST).
- 애플리케이션은 사용자 인증 정보 삭제 클릭 핸들러 가져오기 후 즉시
// Client-side management synchronization ES module
import { listFetch, renameCredentialFetch, updateUserFetch, deleteFetch } from './api.js';
// Base64URL-encoded User ID string (illustration only)
const base64UrlUserId = "M2YPl-KGnA8";
async function syncAcceptedCredentials(currentCredentialsList) {
if (!window.PublicKeyCredential || !PublicKeyCredential.signalAllAcceptedCredentials) return;
try {
const credentialIds = currentCredentialsList.map(c => c.id); // Array of Base64URL credential ID strings
await PublicKeyCredential.signalAllAcceptedCredentials({
rpId, // RP ID must match the one defined on the server
userId: base64UrlUserId, // User ID Base64URL-encoded string
allAcceptedCredentialIds: credentialIds
});
} catch (e) {
console.error('SignalAllAcceptedCredentials sync failure:', e);
}
}
async function loadManagementPanel() {
const response = await listFetch();
const list = await response.json();
renderUI(list);
// Sync accepted credentials on initial load
await syncAcceptedCredentials(list);
}
async function performDelete(credentialId) {
const response = await deleteFetch(credentialId);
if (response.ok) {
const updatedResponse = await listFetch();
const updatedList = await updatedResponse.json();
renderUI(updatedList);
// Sync remaining accepted credentials after deletion
await syncAcceptedCredentials(updatedList);
}
}
// Renaming a passkey's nickname updates the server record only
async function performCredentialRename(credentialId, newCredentialName) {
const response = await renameCredentialFetch(credentialId, { name: newCredentialName });
if (response.ok) {
const updatedResponse = await listFetch();
renderUI(await updatedResponse.json());
}
}
// Renaming the user's account username or displayName triggers signalCurrentUserDetails
async function performUserRename(rpId, userId, updatedName, updatedDisplayName) {
const response = await updateUserFetch({ name: updatedName, displayName: updatedDisplayName });
if (response.ok && window.PublicKeyCredential && PublicKeyCredential.signalCurrentUserDetails) {
try {
await PublicKeyCredential.signalCurrentUserDetails({
rpId, // RP ID must match the one defined on the server
userId, // Base64URL-encoded user ID
name: updatedName, // Updated username
displayName: updatedDisplayName // Updated display name
});
} catch (e) {
console.error('SignalCurrentUserDetails sync failure:', e);
}
}
}
window.addEventListener('DOMContentLoaded', loadManagementPanel);
AAGUID에서 패스키 제공업체 확인
AAGUID (인증자 증명 전역 고유 식별자)는 특정 인스턴스가 아닌 인증자 모델을 나타내는 128비트 식별자입니다. 패스키 등록 중에 인증자 데이터에 포함되며, 어떤 패스키 제공업체 (예: Google 비밀번호 관리자, iCloud 키체인, 1Password)가 사용자 인증 정보를 생성했는지 확인하는 데 사용할 수 있습니다.
AAGUID는 사용자의 패스키 관리를 지원하는 데만 사용해야 합니다. 암호화 증명이 없는 한 수정할 수 있으며, 현재 플랫폼 패스키는 이를 지원하지 않습니다.
1. AAGUID 레지스트리
커뮤니티에서 관리하는 AAGUID와 제공업체 이름 및 아이콘의 JSON 매핑은 다음에서 확인할 수 있습니다.
https://raw.githubusercontent.com/passkeydeveloper/passkey-authenticator-aaguids/refs/heads/main/combined_aaguid.json
각 항목에는 다음 스키마가 있습니다.
{
"<aaguid-uuid>": {
"name": "Provider Name",
"icon_light": "data:image/png;base64,...",
"icon_dark": "data:image/png;base64,..."
}
}
2. 등록 후 AAGUID 사용
등록 응답을 확인한 후 등록 결과에서 aaguid를 읽고 레지스트리에서 조회하여 사용자 인증 정보의 name 및 providerIcon를 채웁니다.
레지스트리에서 AAGUID를 조회하기 전에 '00000000-0000-0000-0000-000000000000'와 같은지 확인합니다. 이 경우 레지스트리 조회를 건너뛰고 name를 대체 (예: 사용자 에이전트의 기기 이름 또는 '알 수 없는 패스키 제공업체')로 설정하고 providerIcon를 undefined로 설정합니다. 0이 아닌 AAGUID의 레지스트리만 조회합니다.
import aaguids from './aaguids.json' with { type: 'json' };
const { aaguid } = registrationInfo;
if (aaguid === '00000000-0000-0000-0000-000000000000') {
// use the device name as the passkey provider based on
// the information derived from the user agent string,
// or just say "Unknown passkey provider"
} else {
const provider = aaguids[aaguid];
const credential = {
// ...other fields
aaguid,
name: provider?.name || 'Unknown passkey provider',
providerIcon: provider?.icon_light,
};
}
대체 전략
패스키 기능 감지 대체
항상 'webauthn-polyfills'를 설치하고 컨텍스트에서 가져옵니다.
PublicKeyCredential가 지원되는 한 PublicKeyCredential.getClientCapabilities도 지원됩니다.
import 'webauthn-polyfills';
신호 API 동기화 대체
브라우저가 PublicKeyCredential.parseRequestOptionsFromJSON를 지원하지 않는 경우 'webauthn-polyfills'를 사용합니다.
<script type="module">
if (!PublicKeyCredential.parseRequestOptionsFromJSON) {
await import('https://unpkg.com/webauthn-polyfills');
}
</script>
이렇게 하면 PublicKeyCredential.prototype.toJSON에 대한 지원도 추가됩니다.