passkey: implement passwordless login

This commit is contained in:
Girish Ramakrishnan
2026-03-16 20:04:36 +05:30
parent d0745d1914
commit bc5737b9b0
6 changed files with 159 additions and 5 deletions
+48 -3
View File
@@ -312,6 +312,8 @@ async function renderInteractionPage(req, res) {
if (prompt.name === 'login') {
const data = {
submitUrl: `${ROUTE_PREFIX}/interaction/${uid}/login`,
passkeyAuthOptionsUrl: `${ROUTE_PREFIX}/interaction/${uid}/passkey_auth_options`,
passkeyLoginUrl: `${ROUTE_PREFIX}/interaction/${uid}/passkey_login`,
iconUrl: '/api/v1/cloudron/avatar',
name: client.name || await branding.getCloudronName(),
footer: marked.parse(await branding.renderFooter()),
@@ -449,6 +451,47 @@ async function interactionLogin(req, res, next) {
res.status(200).send({ redirectTo });
}
async function interactionPasskeyAuthOptions(req, res, next) {
const [detailsError, details] = await safe(gOidcProvider.interactionDetails(req, res));
if (detailsError) return next(new HttpError(detailsError.statusCode, detailsError.error_description));
const { uid } = details;
const [error, options] = await safe(passkeys.getDiscoverableAuthOptions(uid));
if (error) return next(new HttpError(500, error));
res.status(200).send(options);
}
async function interactionPasskeyLogin(req, res, next) {
const [detailsError, details] = await safe(gOidcProvider.interactionDetails(req, res));
if (detailsError) return next(new HttpError(detailsError.statusCode, detailsError.error_description));
if (!req.body.passkeyResponse || typeof req.body.passkeyResponse !== 'object') return next(new HttpError(400, 'passkeyResponse must be an object'));
const { uid } = details;
const [verifyError, result] = await safe(passkeys.verifyDiscoverableAuth(uid, req.body.passkeyResponse));
if (verifyError) {
trace(`interactionPasskeyLogin: passkey verification failed: ${verifyError.message}`);
return next(new HttpError(401, 'Passkey verification failed'));
}
const user = await users.get(result.userId);
if (!user) return next(new HttpError(401, 'User not found'));
const interactionResult = {
login: {
accountId: user.username,
},
};
const [interactionFinishError, redirectTo] = await safe(gOidcProvider.interactionResult(req, res, interactionResult));
if (interactionFinishError) return next(new HttpError(500, interactionFinishError));
res.status(200).send({ redirectTo });
}
async function interactionConfirm(req, res, next) {
const [detailsError, interactionDetails] = await safe(gOidcProvider.interactionDetails(req, res));
if (detailsError) return next(new HttpError(detailsError.statusCode, detailsError.error_description));
@@ -772,9 +815,11 @@ async function start() {
}
app.get (`${ROUTE_PREFIX}/interaction/:uid`, setNoCache, renderInteractionPage);
app.post(`${ROUTE_PREFIX}/interaction/:uid/login`, setNoCache, json, interactionLogin);
app.post(`${ROUTE_PREFIX}/interaction/:uid/confirm`, setNoCache, json, interactionConfirm);
app.get (`${ROUTE_PREFIX}/interaction/:uid/abort`, setNoCache, interactionAbort);
app.post(`${ROUTE_PREFIX}/interaction/:uid/login`, setNoCache, json, interactionLogin);
app.post(`${ROUTE_PREFIX}/interaction/:uid/passkey_auth_options`, setNoCache, json, interactionPasskeyAuthOptions);
app.post(`${ROUTE_PREFIX}/interaction/:uid/passkey_login`, setNoCache, json, interactionPasskeyLogin);
app.post(`${ROUTE_PREFIX}/interaction/:uid/confirm`, setNoCache, json, interactionConfirm);
app.get (`${ROUTE_PREFIX}/interaction/:uid/abort`, setNoCache, interactionAbort);
// cloudflare access has a bug that it cannot handle OKP key type. https://github.com/sebadob/rauthy/issues/1229#issuecomment-3610993452
app.get (`${ROUTE_PREFIX}/jwks_rsaonly`, setNoCache, async function (req, res) {