mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-08 19:13:25 -08:00
[Server/Client] Enforce and complete the forced password change at login (#7680)
checkUserPassword ignored force_password_change on the challenge-response path, so a reset password stayed valid. Returning PasswordChangeRequired was not enough on its own: the session was dropped and the client told the user to ask the administrator for the temporary password they had just used. The pending session now keeps its identity without registering a user session or announcing a join, may only ping and change its password, and the client completes the change in-session before reconnecting. Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
parent
ef4574d2b9
commit
50a2ae9cc8
16 changed files with 575 additions and 28 deletions
|
|
@ -131,6 +131,16 @@ AuthenticationResult Server::loginUser(Server_ProtocolHandler *session,
|
|||
return authState;
|
||||
}
|
||||
|
||||
if (authState == PasswordChangeRequired) {
|
||||
// The credentials were right, but the account must choose a new password before it may
|
||||
// use the server. Give the session an identity so it can complete that change, without
|
||||
// registering a user session or announcing the user as logged in.
|
||||
ServerInfo_User pendingData = databaseInterface->getUserData(name, true);
|
||||
pendingData.set_address(session->getAddress().toStdString());
|
||||
session->setUserInfo(pendingData);
|
||||
return authState;
|
||||
}
|
||||
|
||||
ServerInfo_User data = databaseInterface->getUserData(name, true);
|
||||
data.set_address(session->getAddress().toStdString());
|
||||
name = QString::fromStdString(data.name()); // Compensate for case indifference
|
||||
|
|
@ -296,7 +306,9 @@ void Server::removeClient(Server_ProtocolHandler *client)
|
|||
QWriteLocker locker(&clientsLock);
|
||||
clients.removeAt(clientIndex);
|
||||
ServerInfo_User *data = client->getUserInfo();
|
||||
if (data) {
|
||||
// A session that never completed login (for example one that still owes a password change)
|
||||
// has no session id: it was never announced as joined, so it must not be announced as leaving.
|
||||
if (data && data->has_session_id()) {
|
||||
Event_UserLeft event;
|
||||
event.set_name(data->name());
|
||||
SessionEvent *se = Server_ProtocolHandler::prepareSessionEvent(event);
|
||||
|
|
@ -311,12 +323,10 @@ void Server::removeClient(Server_ProtocolHandler *client)
|
|||
users.remove(QString::fromStdString(data->name()));
|
||||
qDebug() << "Server::removeClient: name=" << QString::fromStdString(data->name());
|
||||
|
||||
if (data->has_session_id()) {
|
||||
const qint64 sessionId = data->session_id();
|
||||
usersBySessionId.remove(sessionId);
|
||||
emit endSession(sessionId);
|
||||
qDebug() << "closed session id:" << sessionId;
|
||||
}
|
||||
const qint64 sessionId = data->session_id();
|
||||
usersBySessionId.remove(sessionId);
|
||||
emit endSession(sessionId);
|
||||
qDebug() << "closed session id:" << sessionId;
|
||||
}
|
||||
qDebug() << "Server::removeClient: removed" << (void *)client << ";" << clients.size() << "clients; "
|
||||
<< users.size() << "users left";
|
||||
|
|
@ -330,7 +340,9 @@ QList<QString> Server::getOnlineModeratorList() const
|
|||
ServerInfo_User *data = client->getUserInfo();
|
||||
|
||||
//! \todo This line should be updated in the event there is any type of new user level created.
|
||||
if (data &&
|
||||
// A session that still owes a forced password change has user info but no session id:
|
||||
// it never joined, so it must not be reported as an online moderator either.
|
||||
if (data && data->has_session_id() &&
|
||||
(data->user_level() & ServerInfo_User::IsModerator || data->user_level() & ServerInfo_User::IsAdmin)) {
|
||||
results << QString::fromStdString(data->name()).simplified();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,8 +64,9 @@ Server_ProtocolHandler::Server_ProtocolHandler(Server *_server,
|
|||
Server_DatabaseInterface *_databaseInterface,
|
||||
QObject *parent)
|
||||
: QObject(parent), Server_AbstractUserInterface(_server), deleted(false), databaseInterface(_databaseInterface),
|
||||
authState(NotLoggedIn), usingRealPassword(false), acceptsUserListChanges(false), acceptsRoomListChanges(false),
|
||||
idleClientWarningSent(false), timeRunning(0), lastDataReceived(0), lastActionReceived(0)
|
||||
authState(NotLoggedIn), usingRealPassword(false), passwordChangePending(false), acceptsUserListChanges(false),
|
||||
acceptsRoomListChanges(false), idleClientWarningSent(false), timeRunning(0), lastDataReceived(0),
|
||||
lastActionReceived(0)
|
||||
|
||||
{
|
||||
connect(server, &Server::pingClockTimeout, this, &Server_ProtocolHandler::pingClockTimeout);
|
||||
|
|
@ -212,7 +213,10 @@ Response::ResponseCode Server_ProtocolHandler::processSessionCommandContainer(co
|
|||
logDebugMessage(getSafeDebugString(sc));
|
||||
}
|
||||
const auto commandType = static_cast<SessionCommand::SessionCommandType>(num);
|
||||
if (authState == NotLoggedIn && !isPreAuthSessionCommand(commandType)) {
|
||||
if (passwordChangePending && commandType != SessionCommand::PING &&
|
||||
commandType != SessionCommand::ACCOUNT_PASSWORD) {
|
||||
resp = Response::RespFunctionNotAllowed;
|
||||
} else if (authState == NotLoggedIn && !isPreAuthSessionCommand(commandType)) {
|
||||
resp = Response::RespLoginNeeded;
|
||||
} else {
|
||||
switch (commandType) {
|
||||
|
|
@ -489,6 +493,19 @@ void Server_ProtocolHandler::processCommandContainer(const CommandContainer &con
|
|||
|
||||
lastDataReceived = timeRunning;
|
||||
|
||||
// A session that still owes a forced password change may only reach the session family,
|
||||
// where processSessionCommandContainer narrows it further to ping and the change itself.
|
||||
// Every other family is rejected here, including a container that also carries a session
|
||||
// command: the dispatch below prefers game/room over session, so testing only for the
|
||||
// absence of a session command would let such a container through.
|
||||
if (passwordChangePending &&
|
||||
(cont.game_command_size() || cont.room_command_size() || cont.moderator_command_size() ||
|
||||
cont.admin_command_size() || cont.developer_command_size())) {
|
||||
sendResponseContainer(ResponseContainer(cont.has_cmd_id() ? cont.cmd_id() : -1),
|
||||
Response::RespFunctionNotAllowed);
|
||||
return;
|
||||
}
|
||||
|
||||
ResponseContainer responseContainer(cont.has_cmd_id() ? cont.cmd_id() : -1);
|
||||
Response::ResponseCode finalResponseCode;
|
||||
|
||||
|
|
@ -696,6 +713,11 @@ Response::ResponseCode Server_ProtocolHandler::cmdLogin(const Command_Login &cmd
|
|||
server->recordFailedLogin(getAddress(), userName);
|
||||
return Response::RespAccountNotActivated;
|
||||
case PasswordChangeRequired:
|
||||
// The credentials were right, but the account must choose a new password first.
|
||||
// Keep the session authenticated just far enough for the client to do that:
|
||||
// processCommandContainer refuses everything except the change itself.
|
||||
authState = PasswordRight;
|
||||
passwordChangePending = true;
|
||||
return Response::RespPasswordChangeRequired;
|
||||
default:
|
||||
server->clearFailedLogins(getAddress(), userName);
|
||||
|
|
|
|||
|
|
@ -46,6 +46,12 @@ protected:
|
|||
Server_DatabaseInterface *databaseInterface;
|
||||
AuthenticationResult authState;
|
||||
bool usingRealPassword;
|
||||
/**
|
||||
* @brief True when the credentials were right but the account must choose a new password
|
||||
* before it may use the server. Only ping, the password change and disconnecting
|
||||
* are served while this is set.
|
||||
*/
|
||||
bool passwordChangePending;
|
||||
bool acceptsUserListChanges;
|
||||
bool acceptsRoomListChanges;
|
||||
bool idleClientWarningSent;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue