Mengarahkan pengguna yang tidak diautentikasi ke halaman login khusus saat mereka mengklik link "Login", memulai checkout, menyimpan item ke favorit, atau mencapai penghalang konten berbayar artikel akan menyebabkan hilangnya konteks dan meningkatkan rasio pengguna yang berhenti. Mode UI langsung (uiMode: 'immediate' di navigator.credentials.get()) memungkinkan login cepat kontekstual dengan menampilkan pemilih akun browser terpadu segera saat kunci sandi lokal (publicKey) atau sandi tersimpan (password: true) ada di perangkat.
Tidak seperti dialog WebAuthn modal standar—yang selalu menampilkan UI browser dan beralih ke kode QR lintas perangkat atau kunci keamanan eksternal—mode Immediate UI akan gagal dengan cepat dan menampilkan NotAllowedError jika tidak ada kredensial yang memenuhi syarat yang tersedia secara lokal di perangkat. Hal ini memungkinkan aplikasi Anda mengautentikasi pengguna yang kembali di tempat dengan sekali ketuk, sekaligus mentransisikan pengguna tanpa kredensial lokal dengan lancar ke halaman login standar, checkout tanpa login, atau formulir login inline.
Pola dan anti-pola UX yang direkomendasikan
Picu mode UI Langsung di dalam pengendali peristiwa yang dimulai pengguna (memerlukan aktivasi pengguna sementara) pada saat niat pengguna jelas tepat sebelum keluar dari halaman saat ini. Jika autentikasi langsung berhasil, segera selesaikan intent pengguna (baik dengan mengupdate halaman saat ini atau langsung melanjutkan ke tujuan yang diautentikasi). Jika gagal dengan NotAllowedError, tampilkan alur login atau pendaftaran dengan jalur untuk menyelesaikan intent pengguna setelah selesai.
Kasus penggunaan potensial:
- Intersepsi link atau tombol login header: Mengintersepsi klik pada link "Login" global (
event.preventDefault()) untuk segera mengautentikasi pengguna yang kembali, atau melanjutkan navigasi ke halaman login khusus diNotAllowedError. - Login dinamis sebelum checkout: Autentikasi pembeli yang kembali dengan sekali ketuk saat mereka mengklik "Lanjutkan ke checkout" sehingga mereka langsung diarahkan ke langkah pengiriman dan pembayaran, atau beralih ke layar login standar atau checkout tanpa login jika terjadi kegagalan.
- Login inline untuk tindakan mikro (Favorit, Suka, Bookmark, Simpan): Lakukan autentikasi di tempat saat pengguna yang logout mengklik tombol tindakan mikro dan segera menyelesaikan tindakan (misalnya, mengalihkan
aria-pressed="true"), atau buka modal login pengganti atau halaman login dengan parameter URL kembali. - Penghalang konten berbayar terbatas dalam artikel: Mengautentikasi pembaca di tempat saat mereka mengklik "Lanjutkan membaca dengan akun Anda" di banner penghalang konten berbayar, atau meluaskan formulir login atau pendaftaran inline saat penggantian.
Anti-pola yang harus dihindari:
- JANGAN gunakan mode UI Langsung di halaman login khusus: Jangan pernah melampirkan
uiMode: 'immediate'ke tombol "Login" atau "Login dengan kunci sandi" utama di halaman login tujuan akhir Anda. Pengguna akan diarahkan ke halaman login khusus saat mereka tidak memiliki kredensial lokal di perangkat saat ini, atau saat mereka ingin menggunakan kunci sandi lintas perangkat (memindai kode QR dengan ponsel) atau kunci keamanan hardware eksternal. KarenauiMode: 'immediate'menyembunyikan opsi kode QR dan kunci keamanan lintas perangkat dan langsung menampilkanNotAllowedErrorjika tidak ada kredensial lokal, menggunakannya di halaman login khusus akan menjebak pengguna dalam jalan buntu, yaitu mengklik tombol tidak akan menghasilkan apa pun. Di halaman login khusus, gunakan UI Bersyarat (mediation: 'conditional'denganautocomplete="username webauthn") pada input formulir yang dipasangkan dengan tombolnavigator.credentials.get({ publicKey })modal standar (lihat Autentikasi Kunci Sandi). - JANGAN gunakan mode UI Langsung untuk autentikasi ulang peningkatan: Jangan pernah menggunakan
uiMode: 'immediate'untuk mengautentikasi ulang pengguna yang sudah login sebelum operasi sensitif (seperti mengubah sandi, memperbarui alamat pengiriman, mengedit metode pembayaran tersimpan, atau menambahkan kunci sandi baru). Autentikasi ulang harus membatasi perintah ke kredensial pengguna yang login melalui daftarallowCredentialsyang tidak kosong, yang ditolak oleh mode UI Langsung denganNotAllowedError, dan menghilangkanallowCredentialsakan memungkinkan pengguna secara tidak sengaja memilih akun yang berbeda dari akun yang saat ini login. Untuk autentikasi ulang peningkatan, ikuti Autentikasi Ulang Kunci Sandi.
Batasan privasi dan keamanan
Browser menerapkan aturan privasi dan keamanan yang ketat saat uiMode: 'immediate' diminta:
- Aktivasi pengguna sementara diperlukan: Anda HARUS memanggil
navigator.credentials.get({ uiMode: 'immediate' })di dalam pengendali peristiwa yang dimulai pengguna (seperti pemrosesclickpada elemen<button>atau<a>) untuk mencegah pemeriksaan kredensial secara diam-diam. Jangan pernah memanggilnya secara otomatis saat pemuatan halaman (DOMContentLoaded). Memanggil API tidak akan menggunakan aktivasi sementara. - Jangan teruskan daftar
allowCredentialsyang tidak kosong: JANGAN isipublicKey.allowCredentialsjikauiMode: 'immediate'ditentukan. Browser akan langsung menolak permintaan mode UI Langsung dengan daftarallowCredentialsyang tidak kosong dengan menampilkanNotAllowedErrorsehingga pihak tepercaya tidak dapat menyelidiki ID kredensial tertentu untuk menyimpulkan kunjungan sebelumnya atau melacak pengguna di seluruh sesi. - Jangan teruskan
AbortSignal(signal): JANGAN tetapkan propertisignaldinavigator.credentials.get()saatuiMode: 'immediate'ditentukan, karena browser tidak mengizinkan penutupan dialog login langsung secara terprogram. Jika halaman Anda juga menjalankan permintaan UI Bersyarat (mediation: 'conditional') di latar belakang, panggilabort()padaAbortControllerUI Bersyarat tersebut sebelum memanggil mode UI Langsung, dan hapussignaldari permintaan langsung itu sendiri. - Sesi penjelajahan rahasia dan samaran: Permintaan yang dibuat di jendela penjelajahan rahasia atau samaran selalu ditolak dengan
NotAllowedErrorsehingga sesi pribadi tetap tidak dapat dibedakan dari sesi tanpa kredensial lokal.
Penyiapan sisi server
Endpoint pembuatan opsi
Ekspos endpoint yang menghasilkan opsi permintaan WebAuthn yang dapat ditemukan untuk pengunjung yang tidak diautentikasi:
- Buat tantangan yang terikat sesi: Buat buffer tantangan acak secara kriptografis, simpan di sesi server, dan enkode sebagai Base64URL.
- Mempertahankan preferensi verifikasi pengguna: Tetapkan
userVerification: 'preferred'dan simpanexpectedUserVerification: 'preferred'dalam sesi. - Hilangkan
allowCredentials: Jangan sertakan daftarallowCredentialsyang tidak kosong dalam opsi untuk mode UI Langsung, karena browser menolak daftar yang diizinkan yang tidak kosong denganNotAllowedError.
// Server-side options generation example (/api/login/options)
router.post('/api/login/options', (req, res) => {
const options = {
challenge: serverGeneratedBase64UrlChallenge, // High-entropy random challenge stored in session
rpId: 'example.com',
userVerification: 'preferred',
};
req.session.challenge = options.challenge;
req.session.expectedUserVerification = 'preferred';
return res.json(options);
});
Endpoint verifikasi kredensial ganda
Karena navigator.credentials.get({ password: true, publicKey, uiMode: 'immediate' }) dapat diselesaikan dengan kunci sandi (PublicKeyCredential) atau sandi tersimpan (PasswordCredential), berikan pengendali verifikasi untuk kedua jenis kredensial:
- Verifikasi pernyataan WebAuthn (
/api/login/verify): Memverifikasi pernyataan terhadap verifikasi sesi dan kunci publik yang disimpan. Jika ID kredensial tidak ditemukan dalam database, tampilkan status HTTP404agar klien dapat memberi tahu pengelola sandi melaluiPublicKeyCredential.signalUnknownCredential(). - Verifikasi sandi (
/api/login/password): Verifikasiusername(cred.id) danpassword(cred.password) terhadap database pengguna dan buat sesi yang diautentikasi.
Implementasi sisi klien
Permintaan kredensial terpadu
- Minta kunci sandi dan sandi: Dekode opsi server menggunakan
PublicKeyCredential.parseRequestOptionsFromJSON()dan panggilnavigator.credentials.get()di dalam pengendali klik yang dimulai pengguna denganpassword: true,publicKey, danuiMode: 'immediate'(tanpa meneruskansignal). - Verifikasi cabang oleh
cred.type:- Jika
cred.type === 'public-key', serialisasi pernyataan dengancred.toJSON()dan POST ke endpoint verifikasi WebAuthn Anda. Saat server merespons dengan HTTP404, panggilPublicKeyCredential.signalUnknownCredential({ rpId: publicKey.rpId, credentialId: encoded.id }). - Jika
cred.type === 'password', POST{ username: cred.id, password: cred.password }ke endpoint verifikasi sandi Anda.
- Jika
- Menangani
NotAllowedErrorsaat tidak ada kredensial lokal: TangkapNotAllowedError(langsung ditampilkan saat tidak ada kunci sandi lokal atau sandi tersimpan di perangkat, atau saat pengguna menutup pemilih) dan beralih ke alur login standar Anda (seperti membuka dialog login inline atau membuka/signindengan URL kembali).
let isAuthenticating = false;
async function authenticateWithImmediateUi() {
const optionsResponse = await fetch('/api/login/options', { method: 'POST' });
if (!optionsResponse.ok) {
throw new Error('Failed to fetch authentication options');
}
const optionsJSON = await optionsResponse.json();
const publicKey = PublicKeyCredential.parseRequestOptionsFromJSON(optionsJSON);
// MANDATORY: Request both password and publicKey with uiMode: 'immediate' and NO signal parameter
const cred = await navigator.credentials.get({
password: true,
publicKey,
uiMode: 'immediate',
});
if (!cred) {
throw new DOMException('No credential returned', 'NotAllowedError');
}
if (cred.type === 'public-key') {
const encoded = cred.toJSON();
const verifyResponse = await fetch('/api/login/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(encoded),
});
if (verifyResponse.status === 404) {
await PublicKeyCredential.signalUnknownCredential({
rpId: publicKey.rpId || window.location.hostname,
credentialId: encoded.id, // Base64URL-encoded credential ID string
});
throw new Error('Passkey credential is no longer recognized by the server');
}
if (!verifyResponse.ok) {
throw new Error('Passkey verification failed');
}
return await verifyResponse.json();
}
if (cred.type === 'password') {
const verifyResponse = await fetch('/api/login/password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username: cred.id,
password: cred.password,
}),
});
if (!verifyResponse.ok) {
throw new Error('Password verification failed');
}
return await verifyResponse.json();
}
throw new Error(`Unsupported credential type: ${cred.type}`);
}
// Example: Intercepting an inline micro-action (Favorite button) or Header Sign-in click
const favoriteButton = document.getElementById('favorite-btn');
favoriteButton.addEventListener('click', async (event) => {
event.preventDefault();
if (isAuthenticating) return;
// If already signed in, toggle the micro-action state immediately
if (document.body.dataset.signedIn === 'true') {
const nextPressed = favoriteButton.getAttribute('aria-pressed') !== 'true';
favoriteButton.setAttribute('aria-pressed', String(nextPressed));
return;
}
isAuthenticating = true;
try {
const user = await authenticateWithImmediateUi();
// Complete the user's intended action immediately
document.body.dataset.signedIn = 'true';
favoriteButton.setAttribute('aria-pressed', 'true');
updateHeaderUserState(user);
} catch (err) {
if (err.name === 'NotAllowedError') {
// No local credentials available or user dismissed dialog:
// transition smoothly to fallback sign-in UI or redirect with return URL
showFallbackSignInUi({ returnAction: 'favorite' });
return;
}
console.error('Authentication error:', err);
} finally {
isAuthenticating = false;
}
});
Strategi penggantian
Penggantian mode UI langsung
- Pengalaman progressive enhancement: Perlakukan
uiMode: 'immediate'sebagai progressive enhancement. Jangan menyimpulkan dukungan dari nama atau versi browser; satu-satunya sinyal dukungan yang andal adalah pemeriksaan runtimecapabilities.immediateGetmelaluiPublicKeyCredential.getClientCapabilities(). WAJIB: Selalu periksacapabilities.immediateGetsebelum memanggilnavigator.credentials.get({ uiMode: 'immediate' })—browser yang tidak mengenaliuiMode: 'immediate'akan mengabaikan kunci kamus yang tidak diketahui dan membuka dialog kode QR dan kunci sandi modal pemblokiran. Jikacapabilities.immediateGetbernilai salah, atau jikanavigator.credentials.get()menolak denganNotAllowedError, kembali ke pengalaman login standar aplikasi Anda (seperti membuka modal login inline atau membuka halaman login khusus dengan parameter URL kembali), sehingga pengguna di browser yang tidak didukung akan melihat alur login yang ada tanpa perubahan:
async function isImmediateUiSupported() {
if (!window.PublicKeyCredential || !PublicKeyCredential.getClientCapabilities) {
return false;
}
try {
const capabilities = await PublicKeyCredential.getClientCapabilities();
return Boolean(capabilities?.immediateGet);
} catch {
return false;
}
}
async function authenticateWithImmediateUi() {
// MANDATORY: Check immediateGet support so unsupported browsers do not open a modal QR dialog
const supported = await isImmediateUiSupported();
if (!supported) {
// Throw NotAllowedError so the caller's single catch path treats "unsupported" like "no local credentials"
throw new DOMException('Immediate UI mode is not supported', 'NotAllowedError');
}
// Proceed with fetching options and calling navigator.credentials.get({ password: true, publicKey, uiMode: 'immediate' })
}
Penggantian deteksi kemampuan klien
Selalu instal 'webauthn-polyfills' dan impor di modul klien Anda agar PublicKeyCredential.getClientCapabilities() tersedia setiap kali PublicKeyCredential didukung:
import 'webauthn-polyfills';
Penggantian kredensial sandi
Meneruskan password: true bersama publicKey di navigator.credentials.get() memungkinkan browser pendukung menyertakan sandi tersimpan dari pengelola sandi browser di pemilih akun langsung terpadu.
- Pengalaman penggantian: Browser yang tidak mendukung
PasswordCredentialakan mengabaikan opsipassword: trueatau beralih melalui pemeriksaan fiturcapabilities.immediateGetke formulir login standar, tempat<input autocomplete="username webauthn">dan<input type="password" autocomplete="current-password">menyediakan isi otomatis sandi native.
Penggantian serialisasi JSON yang mudah
Selalu instal 'webauthn-polyfills' dan impor dalam paket klien agar PublicKeyCredential.parseRequestOptionsFromJSON() dan PublicKeyCredential.prototype.toJSON() berfungsi secara konsisten di seluruh browser yang mendukung PublicKeyCredential:
import 'webauthn-polyfills';
Penggantian sinkronisasi Signal API
WebAuthn Signal API (webauthn-signals) adalah progressive enhancement yang menghapus kunci sandi yang tidak berlaku dari pengelola sandi pengguna saat server merespons dengan HTTP 404.
- Pengalaman pengganti: Panggilan gerbang di balik
if (PublicKeyCredential.signalUnknownCredential). Jika tidak didukung, lewati panggilan sinyal dengan baik dan tampilkan pesan yang jelas yang memandu pengguna untuk login dengan metode alternatif.