import utils from "../services/utils.js"; import optionService from "../services/options.js"; import myScryptService from "../services/encryption/my_scrypt.js"; import log from "../services/log.js"; import passwordService from "../services/encryption/password.js"; import assetPath, { assetUrlFragment } from "../services/asset_path.js"; import appPath from "../services/app_path.js"; import ValidationError from "../errors/validation_error.js"; import type { Request, Response } from 'express'; import totp from '../services/totp.js'; import recoveryCodeService from '../services/encryption/recovery_codes.js'; import openID from '../services/open_id.js'; import openIDEncryption from '../services/encryption/open_id_encryption.js'; import { getCurrentLocale } from "../services/i18n.js"; import userManagement from "../services/user_management.js"; import sql from "../services/sql.js"; function loginPage(req: Request, res: Response) { // Login page is triggered twice. Once here, and another time (see sendLoginError) if the password is failed. res.render('login', { wrongPassword: false, wrongTotp: false, totpEnabled: totp.isTotpEnabled(), ssoEnabled: openID.isOpenIDEnabled(), ssoIssuerName: openID.getSSOIssuerName(), ssoIssuerIcon: openID.getSSOIssuerIcon(), assetPath: assetPath, assetPathFragment: assetUrlFragment, appPath: appPath, currentLocale: getCurrentLocale() }); } function setPasswordPage(req: Request, res: Response) { res.render("set_password", { error: false, assetPath, appPath, currentLocale: getCurrentLocale() }); } function setPassword(req: Request, res: Response) { if (passwordService.isPasswordSet()) { throw new ValidationError("Password has been already set"); } let { password1, password2 } = req.body; password1 = password1.trim(); password2 = password2.trim(); let error; if (password1 !== password2) { error = "Entered passwords don't match."; } else if (password1.length < 4) { error = "Password must be at least 4 characters long."; } if (error) { res.render("set_password", { error, assetPath, appPath, currentLocale: getCurrentLocale() }); return; } passwordService.setPassword(password1); res.redirect("login"); } /** * @swagger * /login: * post: * tags: * - auth * summary: Log in using password * description: This will give you a Trilium session, which is required for some other API endpoints. `totpToken` is only required if the user configured TOTP authentication. * operationId: login-normal * externalDocs: * description: HMAC calculation * url: https://github.com/TriliumNext/Trilium/blob/v0.91.6/src/services/utils.ts#L62-L66 * requestBody: * content: * application/x-www-form-urlencoded: * schema: * type: object * required: * - password * properties: * password: * type: string * totpToken: * type: string * responses: * '200': * description: Successful operation * '401': * description: Password / TOTP mismatch */ function login(req: Request, res: Response) { if (openID.isOpenIDEnabled()) { res.oidc.login({ returnTo: '/', authorizationParams: { prompt: 'consent', access_type: 'offline' } }); return; } const submittedPassword = req.body.password; const submittedTotpToken = req.body.totpToken; const submittedUsername = req.body.username; // New field for multi-user mode if (totp.isTotpEnabled()) { if (!verifyTOTP(submittedTotpToken)) { sendLoginError(req, res, 'totp'); return; } } // Check if multi-user mode is enabled const multiUserMode = isMultiUserEnabled(); let authenticatedUser: any = null; if (multiUserMode) { if (submittedUsername) { // Multi-user authentication when username is provided authenticatedUser = verifyMultiUserCredentials(submittedUsername, submittedPassword); if (!authenticatedUser) { sendLoginError(req, res, 'credentials'); return; } } else { // Backward-compatible fallback: allow legacy password-only login if (!verifyPassword(submittedPassword)) { sendLoginError(req, res, 'password'); return; } } } else { // Legacy single-user authentication if (!verifyPassword(submittedPassword)) { sendLoginError(req, res, 'password'); return; } } const rememberMe = req.body.rememberMe; req.session.regenerate(() => { if (!rememberMe) { // unset default maxAge set by sessionParser // Cookie becomes non-persistent and expires // after current browser session (e.g. when browser is closed) req.session.cookie.maxAge = undefined; } req.session.lastAuthState = { totpEnabled: totp.isTotpEnabled(), ssoEnabled: openID.isOpenIDEnabled() }; req.session.loggedIn = true; // Store user information in session for multi-user mode if (authenticatedUser) { req.session.userId = authenticatedUser.tmpID; // Store tmpID from user_data table req.session.username = authenticatedUser.username; req.session.isAdmin = authenticatedUser.role === 'admin'; } res.redirect('.'); }); } function verifyTOTP(submittedTotpToken: string) { if (totp.validateTOTP(submittedTotpToken)) return true; const recoveryCodeValidates = recoveryCodeService.verifyRecoveryCode(submittedTotpToken); return recoveryCodeValidates; } function verifyPassword(submittedPassword: string) { const hashed_password = utils.fromBase64(optionService.getOption("passwordVerificationHash")); const guess_hashed = myScryptService.getVerificationHash(submittedPassword); return guess_hashed.equals(hashed_password); } /** * Check if multi-user mode is enabled (user_data table has users) */ function isMultiUserEnabled(): boolean { try { const count = sql.getValue(`SELECT COUNT(*) as count FROM user_data WHERE isSetup = 'true'`) as number; return count > 0; } catch (e) { return false; } } /** * Authenticate using multi-user credentials (username + password) */ function verifyMultiUserCredentials(username: string, password: string) { return userManagement.validateCredentials(username, password); } function sendLoginError(req: Request, res: Response, errorType: 'password' | 'totp' | 'credentials' = 'password') { // note that logged IP address is usually meaningless since the traffic should come from a reverse proxy if (totp.isTotpEnabled()) { log.info(`WARNING: Wrong ${errorType} from ${req.ip}, rejecting.`); } else { log.info(`WARNING: Wrong password from ${req.ip}, rejecting.`); } res.status(401).render('login', { wrongPassword: errorType === 'password', wrongTotp: errorType === 'totp', totpEnabled: totp.isTotpEnabled(), ssoEnabled: openID.isOpenIDEnabled(), assetPath: assetPath, assetPathFragment: assetUrlFragment, appPath: appPath, currentLocale: getCurrentLocale() }); } function logout(req: Request, res: Response) { req.session.regenerate(() => { req.session.loggedIn = false; if (openID.isOpenIDEnabled() && openIDEncryption.isSubjectIdentifierSaved()) { res.oidc.logout({ returnTo: '/' }); } res.redirect('login'); }); } export default { loginPage, setPasswordPage, setPassword, login, logout };