Add passkey support

This commit is contained in:
Johannes Zellner
2026-02-12 21:10:51 +01:00
parent 3e09bef613
commit 5724ca73b4
16 changed files with 992 additions and 69 deletions

View File

@@ -27,6 +27,7 @@ const assert = require('node:assert'),
marked = require('marked'),
middleware = require('./middleware'),
oidcClients = require('./oidcclients.js'),
passkeys = require('./passkeys.js'),
path = require('node:path'),
paths = require('./paths.js'),
http = require('node:http'),
@@ -383,13 +384,50 @@ async function interactionLogin(req, res, next) {
if (!req.body.username || typeof req.body.username !== 'string') return next(new HttpError(400, 'A username must be non-empty string'));
if (!req.body.password || typeof req.body.password !== 'string') return next(new HttpError(400, 'A password must be non-empty string'));
if ('totpToken' in req.body && typeof req.body.totpToken !== 'string') return next(new HttpError(400, 'totpToken must be a string' ));
if ('totpToken' in req.body && typeof req.body.totpToken !== 'string') return next(new HttpError(400, 'totpToken must be a string'));
if ('passkeyResponse' in req.body && typeof req.body.passkeyResponse !== 'object') return next(new HttpError(400, 'passkeyResponse must be an object'));
const { username, password, totpToken } = req.body;
const { username, password, totpToken, passkeyResponse } = req.body;
const verifyFunc = username.indexOf('@') === -1 ? users.verifyWithUsername : users.verifyWithEmail;
const [verifyError, user] = await safe(verifyFunc(username, password, users.AP_WEBADMIN, { totpToken, skipTotpCheck: false }));
// First verify password, skip 2FA check initially to determine what 2FA methods are available
const [verifyError, user] = await safe(verifyFunc(username, password, users.AP_WEBADMIN, { totpToken, passkeyResponse, skipTotpCheck: !totpToken && !passkeyResponse }));
// Handle passkey verification if provided
if (!verifyError && user && !user.ghost && passkeyResponse && !totpToken) {
const userPasskeys = await passkeys.list(user.id);
if (userPasskeys.length > 0) {
const [passkeyError] = await safe(passkeys.verifyAuthentication(user, passkeyResponse));
if (passkeyError) {
debug(`interactionLogin: passkey verification failed for ${username}: ${passkeyError.message}`);
return next(new HttpError(401, 'Invalid passkey'));
}
debug(`interactionLogin: passkey verified for ${username}`);
}
}
// If password verified but 2FA is required and not provided, return challenge
if (!verifyError && user && !user.ghost && !totpToken && !passkeyResponse) {
const userPasskeys = await passkeys.list(user.id);
const has2FA = user.twoFactorAuthenticationEnabled || userPasskeys.length > 0;
if (has2FA) {
// Generate passkey options if user has passkeys
let passkeyOptions = null;
if (userPasskeys.length > 0) {
const [optionsError, options] = await safe(passkeys.generateAuthenticationOptions(user));
if (!optionsError) passkeyOptions = options;
}
return res.status(200).send({
twoFactorRequired: true,
totpRequired: user.twoFactorAuthenticationEnabled,
passkeyOptions
});
}
}
if (verifyError && verifyError.reason === BoxError.INVALID_CREDENTIALS) return next(new HttpError(401, verifyError.message));
if (verifyError && verifyError.reason === BoxError.NOT_FOUND) return next(new HttpError(401, 'Username and password does not match'));
if (verifyError) return next(new HttpError(500, verifyError));