feat: Add two-factor authentication via TOTP

Adds TOTP setup with QR code, login challenge, recovery codes and
trusted devices that let a browser skip the second factor for 30
days. Admins can reset another user's second factor by confirming
with their own password.
This commit is contained in:
Daniel Hiller
2026-08-07 20:11:55 +02:00
parent 36aa732fec
commit 2e4904f77d
74 changed files with 4173 additions and 4 deletions
@@ -3,7 +3,7 @@
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
const { AccessTokenSteps } = require('../../../constants');
const { AccessTokenSteps, TRUST_DEVICE_COOKIE_NAME } = require('../../../constants');
const Errors = {
ADMIN_LOGIN_REQUIRED_TO_INITIALIZE_INSTANCE: {
@@ -39,6 +39,7 @@ module.exports = {
exits: {
adminLoginRequiredToInitializeInstance: {},
termsAcceptanceRequired: {},
totpVerificationRequired: {},
},
async fn(inputs) {
@@ -91,6 +92,53 @@ module.exports = {
};
}
if (inputs.user.isTotpEnabled) {
const trustCookie =
inputs.request.cookies && inputs.request.cookies[TRUST_DEVICE_COOKIE_NAME];
const isDeviceTrusted = trustCookie
? await sails.helpers.trustedDevices.checkToken.with({
userId: inputs.user.id,
plainToken: trustCookie,
})
: false;
if (!isDeviceTrusted) {
const { token: pendingToken, payload: pendingTokenPayload } =
sails.helpers.utils.createJwtToken(
AccessTokenSteps.VERIFY_TOTP,
undefined,
PENDING_TOKEN_EXPIRES_IN,
);
const session = await sails.helpers.sessions.createOne.with({
values: {
pendingToken,
userId: inputs.user.id,
remoteAddress: inputs.remoteAddress,
userAgent: inputs.request.headers['user-agent'],
},
withHttpOnlyToken: inputs.withHttpOnlyToken,
});
if (session.httpOnlyToken && !inputs.request.isSocket) {
sails.helpers.utils.setHttpOnlyTokenCookie(
session.httpOnlyToken,
pendingTokenPayload,
inputs.response,
);
}
throw {
totpVerificationRequired: {
pendingToken,
message: 'TOTP verification required',
step: AccessTokenSteps.VERIFY_TOTP,
},
};
}
}
const { token: accessToken, payload: accessTokenPayload } = sails.helpers.utils.createJwtToken(
inputs.user.id,
);