Files
Lauri Ojansivu d7abe9910a Password policy for new passwords, and compulsory two-factor authentication.
New passwords: PASSWORD_MIN_LENGTH (default 8) and PASSWORD_CHECK_PWNED
(off by default), in Admin Panel / People / Login and on every platform.
The browser sends a password to Meteor hashed, so the server checks every
password that reaches it as text - Admin Panel Set password and Add user,
POST /users/register - and the sign-up, Change Password, reset and
enrolment forms check in the browser. Signing in is never checked, so an
existing shorter password keeps working until it is changed. A breached
password is found with Have I Been Pwned's k-anonymity range API: only 5
hex digits of the SHA-1 leave WeKan, and an unreachable service never
refuses a password.

REQUIRE_2FA: off (default), admins or all. It covers accounts that sign in
with a WeKan password; LDAP, OAuth2, SAML, CAS and Sandstorm accounts sign
in through an identity provider and are not asked. Such an account can
still sign in, sees only the 2FA set-up until it has 2FA, and until then
the server refuses every method and publication but signing in and
enrolling, HTTP routes do not accept its login and the REST API gives it
no token. Once required, 2FA cannot be switched off.

The new interface strings are pending translation.

Thanks to xet7 !
2026-10-31 00:07:58 +02:00

72 lines
3.0 KiB
JavaScript

import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base';
// Shared by DDP and HTTP: possession of a valid token is insufficient when
// an administrator has disabled its owner.
export function allowActiveUser(user, source, req) {
if (!user) return false;
if (!user.loginDisabled) return true;
try {
require('/server/lib/securityLog').record({
key: 'authn.inactive', action: 'blocked', source, req,
userId: user._id, detail: 'Disabled account authentication refused',
});
} catch (_) { /* Logging must never change the authentication decision. */ }
return false;
}
// REQUIRE_2FA (models/lib/twoFactorRequirement.js): an account that has not
// set up the two-factor authentication it must have is not signed in over
// HTTP - files, exports, imports, the REST API. Not recorded in Admin Panel /
// Problems: an account between signing in and enrolling is ordinary use.
export function missingRequiredTwoFactor(user) {
const { requireTwoFactorMode, missingTwoFactor } = require('/models/lib/twoFactorRequirement');
const { authEnv } = require('/server/lib/authConfig');
return missingTwoFactor(user, requireTwoFactorMode(authEnv));
}
export async function activeUserById(id, source, req) {
if (typeof id !== 'string' || !id) return null;
const user = await Meteor.users.findOneAsync(id);
return allowActiveUser(user, source, req) && !missingRequiredTwoFactor(user) ? user : null;
}
export async function activeUserByToken(token, source, req) {
if (typeof token !== 'string' || !token) return null;
const user = await Meteor.users.findOneAsync({
'services.resume.loginTokens.hashedToken': Accounts._hashLoginToken(token),
});
return allowActiveUser(user, source, req) && !missingRequiredTwoFactor(user) ? user : null;
}
// Test the status in the same database operation that mints the token: a
// concurrent disable must not be followed by reinserting usable credentials.
export async function insertActiveLoginToken(userId, stampedToken) {
const result = await Meteor.users.rawCollection().updateOne(
{ _id: userId, loginDisabled: { $in: [false, null, ''] } },
{ $push: { 'services.resume.loginTokens': {
hashedToken: Accounts._hashLoginToken(stampedToken.token),
when: stampedToken.when,
} } },
);
if (!result.matchedCount) {
await activeUserById(userId, 'token-issuance');
throw new Meteor.Error(403, 'Invalid credentials');
}
}
export function revokeDisabledTokensModifier(modifier) {
if (modifier.$set?.loginDisabled) {
modifier.$set['services.resume.loginTokens'] = [];
// Avoid conflicting modifiers when a caller also touches the token array.
if (modifier.$push) delete modifier.$push['services.resume.loginTokens'];
if (modifier.$addToSet) delete modifier.$addToSet['services.resume.loginTokens'];
}
}
export async function revokeDisabledTokens(id) {
await Meteor.users.rawCollection().updateOne(
{ _id: id, loginDisabled: true },
{ $set: { 'services.resume.loginTokens': [] } },
);
}