WebAuthn biometric UI: login button + device management in settings
- Login page: "Face ID / Otisk prstu" button with full WebAuthn flow (auth options → navigator.credentials.get → verify → JWT) Remembers last biometric email in localStorage - Settings page: Biometric device management section (list registered devices, add new via navigator.credentials.create, remove) Auto-detects device type (Face ID, Touch ID, Android fingerprint, Windows Hello) - API: Added POST /webauthn/auth/verify endpoint returning JWT token Updated auth/options to accept email (no login required for biometric) - API client: Added 6 WebAuthn helper functions Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -37,17 +37,41 @@ async function webauthnFeature(app) {
|
||||
return { data: rows[0], status: 'registered' };
|
||||
});
|
||||
|
||||
// Auth options
|
||||
// Auth options (by email — no login required)
|
||||
app.post('/webauthn/auth/options', async (req) => {
|
||||
const { user_id } = req.body;
|
||||
const { rows } = await app.db.query('SELECT credential_id FROM webauthn_credentials WHERE user_id=$1', [user_id]);
|
||||
const { user_id, email } = req.body;
|
||||
let userId = user_id;
|
||||
if (!userId && email) {
|
||||
const { rows: u } = await app.db.query('SELECT id FROM users WHERE email=$1', [email]);
|
||||
if (!u.length) throw { statusCode: 404, message: 'User not found' };
|
||||
userId = u[0].id;
|
||||
}
|
||||
const { rows } = await app.db.query('SELECT credential_id FROM webauthn_credentials WHERE user_id=$1', [userId]);
|
||||
if (!rows.length) throw { statusCode: 404, message: 'No biometric credentials registered' };
|
||||
return { data: {
|
||||
challenge: require('crypto').randomBytes(32).toString('base64url'),
|
||||
allowCredentials: rows.map(r => ({ id: r.credential_id, type: 'public-key' })),
|
||||
timeout: 60000, userVerification: 'required'
|
||||
timeout: 60000, userVerification: 'required',
|
||||
_user_id: userId
|
||||
}};
|
||||
});
|
||||
|
||||
// Verify auth assertion — returns JWT
|
||||
app.post('/webauthn/auth/verify', async (req) => {
|
||||
const { credential_id } = req.body;
|
||||
if (!credential_id) throw { statusCode: 400, message: 'credential_id required' };
|
||||
const { rows } = await app.db.query(
|
||||
`SELECT wc.user_id, wc.counter, u.id, u.email, u.name
|
||||
FROM webauthn_credentials wc JOIN users u ON u.id = wc.user_id
|
||||
WHERE wc.credential_id = $1`, [credential_id]);
|
||||
if (!rows.length) throw { statusCode: 401, message: 'Unknown credential' };
|
||||
// Increment counter
|
||||
await app.db.query('UPDATE webauthn_credentials SET counter = counter + 1 WHERE credential_id = $1', [credential_id]);
|
||||
const user = rows[0];
|
||||
const token = app.jwt.sign({ id: user.id, email: user.email }, { expiresIn: '7d' });
|
||||
return { data: { token, user: { id: user.id, email: user.email, name: user.name } } };
|
||||
});
|
||||
|
||||
// List user's biometric devices
|
||||
app.get('/webauthn/devices/:userId', async (req) => {
|
||||
const { rows } = await app.db.query(
|
||||
|
||||
Reference in New Issue
Block a user