remove naked password from redux layer

This commit is contained in:
seavor 2026-04-12 15:55:00 -05:00
parent 559a3ff1f4
commit 98ce317ee1
8 changed files with 151 additions and 107 deletions

View file

@ -8,7 +8,7 @@ import { SessionPersistence } from '../../persistence';
import { disconnect, login, updateStatus } from './'; import { disconnect, login, updateStatus } from './';
export function activate(options: WebSocketConnectOptions, passwordSalt?: string): void { export function activate(options: WebSocketConnectOptions, password?: string, passwordSalt?: string): void {
const { userName, token } = options as unknown as AccountActivationParams; const { userName, token } = options as unknown as AccountActivationParams;
BackendService.sendSessionCommand('Command_Activate', { BackendService.sendSessionCommand('Command_Activate', {
@ -19,7 +19,7 @@ export function activate(options: WebSocketConnectOptions, passwordSalt?: string
onResponseCode: { onResponseCode: {
[ProtoController.root.Response.ResponseCode.RespActivationAccepted]: () => { [ProtoController.root.Response.ResponseCode.RespActivationAccepted]: () => {
SessionPersistence.accountActivationSuccess(); SessionPersistence.accountActivationSuccess();
login(options, passwordSalt); login(options, password, passwordSalt);
}, },
}, },
onError: () => { onError: () => {

View file

@ -8,8 +8,8 @@ import { hashPassword } from '../../utils';
import { disconnect, updateStatus } from '.'; import { disconnect, updateStatus } from '.';
export function forgotPasswordReset(options: WebSocketConnectOptions, passwordSalt?: string): void { export function forgotPasswordReset(options: WebSocketConnectOptions, newPassword?: string, passwordSalt?: string): void {
const { userName, token, newPassword } = options as unknown as ForgotPasswordResetParams; const { userName, token } = options as unknown as ForgotPasswordResetParams;
const params: any = { const params: any = {
...webClient.clientConfig, ...webClient.clientConfig,

View file

@ -12,8 +12,8 @@ import {
updateStatus, updateStatus,
} from './'; } from './';
export function login(options: WebSocketConnectOptions, passwordSalt?: string): void { export function login(options: WebSocketConnectOptions, password?: string, passwordSalt?: string): void {
const { userName, password, hashedPassword } = options; const { userName, hashedPassword } = options;
const loginConfig: any = { const loginConfig: any = {
...webClient.clientConfig, ...webClient.clientConfig,
@ -71,7 +71,10 @@ export function login(options: WebSocketConnectOptions, passwordSalt?: string):
onLoginError('Login failed: server error'), onLoginError('Login failed: server error'),
[ResponseCode.RespAccountNotActivated]: () => [ResponseCode.RespAccountNotActivated]: () =>
onLoginError('Login failed: account not activated', onLoginError('Login failed: account not activated',
() => SessionPersistence.accountAwaitingActivation(options) () => {
const { password: _p, newPassword: _np, ...safeOptions } = options;
SessionPersistence.accountAwaitingActivation(safeOptions);
}
), ),
}, },
onError: (responseCode) => onError: (responseCode) =>

View file

@ -9,8 +9,8 @@ import { hashPassword } from '../../utils';
import { login, disconnect, updateStatus } from './'; import { login, disconnect, updateStatus } from './';
export function register(options: WebSocketConnectOptions, passwordSalt?: string): void { export function register(options: WebSocketConnectOptions, password?: string, passwordSalt?: string): void {
const { userName, password, email, country, realName } = options as ServerRegisterParams; const { userName, email, country, realName } = options as ServerRegisterParams;
const params: any = { const params: any = {
...webClient.clientConfig, ...webClient.clientConfig,
@ -37,12 +37,13 @@ export function register(options: WebSocketConnectOptions, passwordSalt?: string
BackendService.sendSessionCommand('Command_Register', params, { BackendService.sendSessionCommand('Command_Register', params, {
onResponseCode: { onResponseCode: {
[ResponseCode.RespRegistrationAccepted]: () => { [ResponseCode.RespRegistrationAccepted]: () => {
login(options, passwordSalt); login(options, password, passwordSalt);
SessionPersistence.registrationSuccess(); SessionPersistence.registrationSuccess();
}, },
[ResponseCode.RespRegistrationAcceptedNeedsActivation]: () => { [ResponseCode.RespRegistrationAcceptedNeedsActivation]: () => {
updateStatus(StatusEnum.DISCONNECTED, 'Registration accepted, awaiting activation'); updateStatus(StatusEnum.DISCONNECTED, 'Registration accepted, awaiting activation');
SessionPersistence.accountAwaitingActivation(options); const { password: _p, newPassword: _np, ...safeOptions } = options;
SessionPersistence.accountAwaitingActivation(safeOptions);
disconnect(); disconnect();
}, },
[ResponseCode.RespUserAlreadyExists]: () => onRegistrationError( [ResponseCode.RespUserAlreadyExists]: () => onRegistrationError(

View file

@ -14,7 +14,7 @@ import {
updateStatus updateStatus
} from './'; } from './';
export function requestPasswordSalt(options: WebSocketConnectOptions): void { export function requestPasswordSalt(options: WebSocketConnectOptions, password?: string, newPassword?: string): void {
const { userName } = options as RequestPasswordSaltParams; const { userName } = options as RequestPasswordSaltParams;
const onFailure = () => { const onFailure = () => {
@ -41,13 +41,13 @@ export function requestPasswordSalt(options: WebSocketConnectOptions): void {
switch (options.reason) { switch (options.reason) {
case WebSocketConnectReason.ACTIVATE_ACCOUNT: case WebSocketConnectReason.ACTIVATE_ACCOUNT:
activate(options, passwordSalt); activate(options, password, passwordSalt);
break; break;
case WebSocketConnectReason.PASSWORD_RESET: case WebSocketConnectReason.PASSWORD_RESET:
forgotPasswordReset(options, passwordSalt); forgotPasswordReset(options, newPassword, passwordSalt);
break; break;
default: default:
login(options, passwordSalt); login(options, password, passwordSalt);
} }
}, },
onResponseCode: { onResponseCode: {

View file

@ -124,7 +124,7 @@ describe('login', () => {
const { login } = jest.requireActual('./login'); const { login } = jest.requireActual('./login');
it('sends Command_Login with plain password when no salt', () => { it('sends Command_Login with plain password when no salt', () => {
login({ userName: 'alice', password: 'pw' } as any); login({ userName: 'alice' } as any, 'pw');
expect(BackendService.sendSessionCommand).toHaveBeenCalledWith( expect(BackendService.sendSessionCommand).toHaveBeenCalledWith(
'Command_Login', 'Command_Login',
expect.objectContaining({ userName: 'alice', password: 'pw' }), expect.objectContaining({ userName: 'alice', password: 'pw' }),
@ -133,7 +133,7 @@ describe('login', () => {
}); });
it('sends Command_Login with hashedPassword when salt is given', () => { it('sends Command_Login with hashedPassword when salt is given', () => {
login({ userName: 'alice', password: 'pw' } as any, 'salt'); login({ userName: 'alice' } as any, 'pw', 'salt');
expect(BackendService.sendSessionCommand).toHaveBeenCalledWith( expect(BackendService.sendSessionCommand).toHaveBeenCalledWith(
'Command_Login', 'Command_Login',
expect.objectContaining({ hashedPassword: 'hashed_pw' }), expect.objectContaining({ hashedPassword: 'hashed_pw' }),
@ -142,7 +142,7 @@ describe('login', () => {
}); });
it('uses options.hashedPassword if provided', () => { it('uses options.hashedPassword if provided', () => {
login({ userName: 'alice', password: 'pw', hashedPassword: 'pre_hashed' } as any, 'salt'); login({ userName: 'alice', hashedPassword: 'pre_hashed' } as any, 'pw', 'salt');
expect(BackendService.sendSessionCommand).toHaveBeenCalledWith( expect(BackendService.sendSessionCommand).toHaveBeenCalledWith(
'Command_Login', 'Command_Login',
expect.objectContaining({ hashedPassword: 'pre_hashed' }), expect.objectContaining({ hashedPassword: 'pre_hashed' }),
@ -151,7 +151,7 @@ describe('login', () => {
}); });
it('onSuccess dispatches buddy/ignore/user and calls listUsers/listRooms', () => { it('onSuccess dispatches buddy/ignore/user and calls listUsers/listRooms', () => {
login({ userName: 'alice', password: 'pw' } as any); login({ userName: 'alice' } as any, 'pw');
const loginResp = { buddyList: [], ignoreList: [], userInfo: { name: 'alice' } }; const loginResp = { buddyList: [], ignoreList: [], userInfo: { name: 'alice' } };
invokeOnSuccess(loginResp, { responseCode: 0, '.Response_Login.ext': loginResp }); invokeOnSuccess(loginResp, { responseCode: 0, '.Response_Login.ext': loginResp });
expect(SessionPersistence.updateBuddyList).toHaveBeenCalledWith([]); expect(SessionPersistence.updateBuddyList).toHaveBeenCalledWith([]);
@ -164,7 +164,7 @@ describe('login', () => {
}); });
it('onSuccess does NOT pass plaintext password to loginSuccessful', () => { it('onSuccess does NOT pass plaintext password to loginSuccessful', () => {
login({ userName: 'alice', password: 'secret' } as any); login({ userName: 'alice' } as any, 'secret');
const loginResp = { buddyList: [], ignoreList: [], userInfo: { name: 'alice' } }; const loginResp = { buddyList: [], ignoreList: [], userInfo: { name: 'alice' } };
invokeOnSuccess(loginResp, { responseCode: 0, '.Response_Login.ext': loginResp }); invokeOnSuccess(loginResp, { responseCode: 0, '.Response_Login.ext': loginResp });
const calledWith = (SessionPersistence.loginSuccessful as jest.Mock).mock.calls[0][0]; const calledWith = (SessionPersistence.loginSuccessful as jest.Mock).mock.calls[0][0];
@ -172,7 +172,7 @@ describe('login', () => {
}); });
it('onSuccess passes hashedPassword to loginSuccessful when salt is used', () => { it('onSuccess passes hashedPassword to loginSuccessful when salt is used', () => {
login({ userName: 'alice', password: 'pw' } as any, 'salt'); login({ userName: 'alice' } as any, 'pw', 'salt');
const loginResp = { buddyList: [], ignoreList: [], userInfo: { name: 'alice' } }; const loginResp = { buddyList: [], ignoreList: [], userInfo: { name: 'alice' } };
invokeOnSuccess(loginResp, { responseCode: 0, '.Response_Login.ext': loginResp }); invokeOnSuccess(loginResp, { responseCode: 0, '.Response_Login.ext': loginResp });
const calledWith = (SessionPersistence.loginSuccessful as jest.Mock).mock.calls[0][0]; const calledWith = (SessionPersistence.loginSuccessful as jest.Mock).mock.calls[0][0];
@ -180,63 +180,65 @@ describe('login', () => {
}); });
it('onResponseCode RespClientUpdateRequired calls onLoginError', () => { it('onResponseCode RespClientUpdateRequired calls onLoginError', () => {
login({ userName: 'alice', password: 'pw' } as any); login({ userName: 'alice' } as any, 'pw');
invokeResponseCode(1); invokeResponseCode(1);
expect(SessionPersistence.loginFailed).toHaveBeenCalled(); expect(SessionPersistence.loginFailed).toHaveBeenCalled();
expect(SessionIndexMocks.disconnect).toHaveBeenCalled(); expect(SessionIndexMocks.disconnect).toHaveBeenCalled();
}); });
it('onResponseCode RespWrongPassword', () => { it('onResponseCode RespWrongPassword', () => {
login({ userName: 'alice', password: 'pw' } as any); login({ userName: 'alice' } as any, 'pw');
invokeResponseCode(2); invokeResponseCode(2);
expect(SessionPersistence.loginFailed).toHaveBeenCalled(); expect(SessionPersistence.loginFailed).toHaveBeenCalled();
}); });
it('onResponseCode RespUsernameInvalid', () => { it('onResponseCode RespUsernameInvalid', () => {
login({ userName: 'alice', password: 'pw' } as any); login({ userName: 'alice' } as any, 'pw');
invokeResponseCode(3); invokeResponseCode(3);
expect(SessionPersistence.loginFailed).toHaveBeenCalled(); expect(SessionPersistence.loginFailed).toHaveBeenCalled();
}); });
it('onResponseCode RespWouldOverwriteOldSession', () => { it('onResponseCode RespWouldOverwriteOldSession', () => {
login({ userName: 'alice', password: 'pw' } as any); login({ userName: 'alice' } as any, 'pw');
invokeResponseCode(4); invokeResponseCode(4);
expect(SessionPersistence.loginFailed).toHaveBeenCalled(); expect(SessionPersistence.loginFailed).toHaveBeenCalled();
}); });
it('onResponseCode RespUserIsBanned', () => { it('onResponseCode RespUserIsBanned', () => {
login({ userName: 'alice', password: 'pw' } as any); login({ userName: 'alice' } as any, 'pw');
invokeResponseCode(5); invokeResponseCode(5);
expect(SessionPersistence.loginFailed).toHaveBeenCalled(); expect(SessionPersistence.loginFailed).toHaveBeenCalled();
}); });
it('onResponseCode RespRegistrationRequired', () => { it('onResponseCode RespRegistrationRequired', () => {
login({ userName: 'alice', password: 'pw' } as any); login({ userName: 'alice' } as any, 'pw');
invokeResponseCode(6); invokeResponseCode(6);
expect(SessionPersistence.loginFailed).toHaveBeenCalled(); expect(SessionPersistence.loginFailed).toHaveBeenCalled();
}); });
it('onResponseCode RespClientIdRequired', () => { it('onResponseCode RespClientIdRequired', () => {
login({ userName: 'alice', password: 'pw' } as any); login({ userName: 'alice' } as any, 'pw');
invokeResponseCode(7); invokeResponseCode(7);
expect(SessionPersistence.loginFailed).toHaveBeenCalled(); expect(SessionPersistence.loginFailed).toHaveBeenCalled();
}); });
it('onResponseCode RespContextError', () => { it('onResponseCode RespContextError', () => {
login({ userName: 'alice', password: 'pw' } as any); login({ userName: 'alice' } as any, 'pw');
invokeResponseCode(8); invokeResponseCode(8);
expect(SessionPersistence.loginFailed).toHaveBeenCalled(); expect(SessionPersistence.loginFailed).toHaveBeenCalled();
}); });
it('onResponseCode RespAccountNotActivated calls accountAwaitingActivation', () => { it('onResponseCode RespAccountNotActivated calls accountAwaitingActivation without password in options', () => {
login({ userName: 'alice', password: 'pw' } as any); login({ userName: 'alice', password: 'leaked' } as any, 'pw');
invokeResponseCode(9); invokeResponseCode(9);
expect(SessionPersistence.accountAwaitingActivation).toHaveBeenCalled(); expect(SessionPersistence.accountAwaitingActivation).toHaveBeenCalledWith(
expect.not.objectContaining({ password: expect.anything() })
);
expect(SessionPersistence.loginFailed).toHaveBeenCalled(); expect(SessionPersistence.loginFailed).toHaveBeenCalled();
}); });
it('onError calls onLoginError with unknown error message', () => { it('onError calls onLoginError with unknown error message', () => {
login({ userName: 'alice', password: 'pw' } as any); login({ userName: 'alice' } as any, 'pw');
invokeOnError(999); invokeOnError(999);
expect(SessionPersistence.loginFailed).toHaveBeenCalled(); expect(SessionPersistence.loginFailed).toHaveBeenCalled();
}); });
@ -249,7 +251,7 @@ describe('register', () => {
const { register } = jest.requireActual('./register'); const { register } = jest.requireActual('./register');
it('sends Command_Register with plain password when no salt', () => { it('sends Command_Register with plain password when no salt', () => {
register({ userName: 'alice', password: 'pw', email: 'a@b.com', country: 'US', realName: 'Al' } as any); register({ userName: 'alice', email: 'a@b.com', country: 'US', realName: 'Al' } as any, 'pw');
expect(BackendService.sendSessionCommand).toHaveBeenCalledWith( expect(BackendService.sendSessionCommand).toHaveBeenCalledWith(
'Command_Register', 'Command_Register',
expect.objectContaining({ userName: 'alice', password: 'pw' }), expect.objectContaining({ userName: 'alice', password: 'pw' }),
@ -258,7 +260,7 @@ describe('register', () => {
}); });
it('uses hashedPassword when salt is provided', () => { it('uses hashedPassword when salt is provided', () => {
register({ userName: 'alice', password: 'pw' } as any, 'salt'); register({ userName: 'alice' } as any, 'pw', 'salt');
expect(BackendService.sendSessionCommand).toHaveBeenCalledWith( expect(BackendService.sendSessionCommand).toHaveBeenCalledWith(
'Command_Register', 'Command_Register',
expect.objectContaining({ hashedPassword: 'hashed_pw' }), expect.objectContaining({ hashedPassword: 'hashed_pw' }),
@ -267,76 +269,78 @@ describe('register', () => {
}); });
it('RespRegistrationAccepted calls login without salt and registrationSuccess', () => { it('RespRegistrationAccepted calls login without salt and registrationSuccess', () => {
register({ userName: 'alice', password: 'pw' } as any); register({ userName: 'alice' } as any, 'pw');
invokeResponseCode(10); invokeResponseCode(10);
expect(SessionIndexMocks.login).toHaveBeenCalledWith(expect.any(Object), undefined); expect(SessionIndexMocks.login).toHaveBeenCalledWith(expect.any(Object), 'pw', undefined);
expect(SessionPersistence.registrationSuccess).toHaveBeenCalled(); expect(SessionPersistence.registrationSuccess).toHaveBeenCalled();
}); });
it('RespRegistrationAccepted forwards salt to login', () => { it('RespRegistrationAccepted forwards salt to login', () => {
register({ userName: 'alice', password: 'pw' } as any, 'mySalt'); register({ userName: 'alice' } as any, 'pw', 'mySalt');
invokeResponseCode(10); invokeResponseCode(10);
expect(SessionIndexMocks.login).toHaveBeenCalledWith(expect.any(Object), 'mySalt'); expect(SessionIndexMocks.login).toHaveBeenCalledWith(expect.any(Object), 'pw', 'mySalt');
expect(SessionPersistence.registrationSuccess).toHaveBeenCalled(); expect(SessionPersistence.registrationSuccess).toHaveBeenCalled();
}); });
it('RespRegistrationAcceptedNeedsActivation calls accountAwaitingActivation', () => { it('RespRegistrationAcceptedNeedsActivation calls accountAwaitingActivation without password in options', () => {
register({ userName: 'alice', password: 'pw' } as any); register({ userName: 'alice', password: 'leaked' } as any, 'pw');
invokeResponseCode(11); invokeResponseCode(11);
expect(SessionPersistence.accountAwaitingActivation).toHaveBeenCalled(); expect(SessionPersistence.accountAwaitingActivation).toHaveBeenCalledWith(
expect.not.objectContaining({ password: expect.anything() })
);
expect(SessionIndexMocks.disconnect).toHaveBeenCalled(); expect(SessionIndexMocks.disconnect).toHaveBeenCalled();
}); });
it('RespUserAlreadyExists calls registrationUserNameError', () => { it('RespUserAlreadyExists calls registrationUserNameError', () => {
register({ userName: 'alice', password: 'pw' } as any); register({ userName: 'alice' } as any, 'pw');
invokeResponseCode(12); invokeResponseCode(12);
expect(SessionPersistence.registrationUserNameError).toHaveBeenCalled(); expect(SessionPersistence.registrationUserNameError).toHaveBeenCalled();
}); });
it('RespUsernameInvalid calls registrationUserNameError', () => { it('RespUsernameInvalid calls registrationUserNameError', () => {
register({ userName: 'alice', password: 'pw' } as any); register({ userName: 'alice' } as any, 'pw');
invokeResponseCode(3); invokeResponseCode(3);
expect(SessionPersistence.registrationUserNameError).toHaveBeenCalled(); expect(SessionPersistence.registrationUserNameError).toHaveBeenCalled();
}); });
it('RespPasswordTooShort calls registrationPasswordError', () => { it('RespPasswordTooShort calls registrationPasswordError', () => {
register({ userName: 'alice', password: 'pw' } as any); register({ userName: 'alice' } as any, 'pw');
invokeResponseCode(13); invokeResponseCode(13);
expect(SessionPersistence.registrationPasswordError).toHaveBeenCalled(); expect(SessionPersistence.registrationPasswordError).toHaveBeenCalled();
}); });
it('RespEmailRequiredToRegister calls registrationRequiresEmail', () => { it('RespEmailRequiredToRegister calls registrationRequiresEmail', () => {
register({ userName: 'alice', password: 'pw' } as any); register({ userName: 'alice' } as any, 'pw');
invokeResponseCode(14); invokeResponseCode(14);
expect(SessionPersistence.registrationRequiresEmail).toHaveBeenCalled(); expect(SessionPersistence.registrationRequiresEmail).toHaveBeenCalled();
}); });
it('RespEmailBlackListed calls registrationEmailError', () => { it('RespEmailBlackListed calls registrationEmailError', () => {
register({ userName: 'alice', password: 'pw' } as any); register({ userName: 'alice' } as any, 'pw');
invokeResponseCode(15); invokeResponseCode(15);
expect(SessionPersistence.registrationEmailError).toHaveBeenCalled(); expect(SessionPersistence.registrationEmailError).toHaveBeenCalled();
}); });
it('RespTooManyRequests calls registrationEmailError', () => { it('RespTooManyRequests calls registrationEmailError', () => {
register({ userName: 'alice', password: 'pw' } as any); register({ userName: 'alice' } as any, 'pw');
invokeResponseCode(16); invokeResponseCode(16);
expect(SessionPersistence.registrationEmailError).toHaveBeenCalled(); expect(SessionPersistence.registrationEmailError).toHaveBeenCalled();
}); });
it('RespRegistrationDisabled calls registrationFailed', () => { it('RespRegistrationDisabled calls registrationFailed', () => {
register({ userName: 'alice', password: 'pw' } as any); register({ userName: 'alice' } as any, 'pw');
invokeResponseCode(17); invokeResponseCode(17);
expect(SessionPersistence.registrationFailed).toHaveBeenCalled(); expect(SessionPersistence.registrationFailed).toHaveBeenCalled();
}); });
it('RespUserIsBanned calls registrationFailed with raw.reasonStr and raw.endTime', () => { it('RespUserIsBanned calls registrationFailed with raw.reasonStr and raw.endTime', () => {
register({ userName: 'alice', password: 'pw' } as any); register({ userName: 'alice' } as any, 'pw');
invokeResponseCode(5, { reasonStr: 'bad user', endTime: 9999 }); invokeResponseCode(5, { reasonStr: 'bad user', endTime: 9999 });
expect(SessionPersistence.registrationFailed).toHaveBeenCalledWith('bad user', 9999); expect(SessionPersistence.registrationFailed).toHaveBeenCalledWith('bad user', 9999);
}); });
it('onError calls registrationFailed', () => { it('onError calls registrationFailed', () => {
register({ userName: 'alice', password: 'pw' } as any); register({ userName: 'alice' } as any, 'pw');
invokeOnError(); invokeOnError();
expect(SessionPersistence.registrationFailed).toHaveBeenCalled(); expect(SessionPersistence.registrationFailed).toHaveBeenCalled();
}); });
@ -348,16 +352,25 @@ describe('register', () => {
describe('activate', () => { describe('activate', () => {
const { activate } = jest.requireActual('./activate'); const { activate } = jest.requireActual('./activate');
it('sends Command_Activate', () => { it('sends Command_Activate with userName and token, not password', () => {
activate({ userName: 'alice', token: 'tok' } as any); activate({ userName: 'alice', token: 'tok' } as any, 'pw');
expect(BackendService.sendSessionCommand).toHaveBeenCalledWith('Command_Activate', expect.any(Object), expect.any(Object)); expect(BackendService.sendSessionCommand).toHaveBeenCalledWith(
'Command_Activate',
expect.objectContaining({ userName: 'alice', token: 'tok' }),
expect.any(Object)
);
expect(BackendService.sendSessionCommand).toHaveBeenCalledWith(
'Command_Activate',
expect.not.objectContaining({ password: expect.anything() }),
expect.any(Object)
);
}); });
it('RespActivationAccepted calls accountActivationSuccess and login with salt', () => { it('RespActivationAccepted calls accountActivationSuccess and forwards password+salt to login', () => {
activate({ userName: 'alice', token: 'tok' } as any, 'salt'); activate({ userName: 'alice', token: 'tok' } as any, 'pw', 'salt');
invokeResponseCode(18); invokeResponseCode(18);
expect(SessionPersistence.accountActivationSuccess).toHaveBeenCalled(); expect(SessionPersistence.accountActivationSuccess).toHaveBeenCalled();
expect(SessionIndexMocks.login).toHaveBeenCalledWith(expect.any(Object), 'salt'); expect(SessionIndexMocks.login).toHaveBeenCalledWith(expect.any(Object), 'pw', 'salt');
}); });
it('onError calls accountActivationFailed and disconnect', () => { it('onError calls accountActivationFailed and disconnect', () => {
@ -438,7 +451,7 @@ describe('forgotPasswordReset', () => {
const { forgotPasswordReset } = jest.requireActual('./forgotPasswordReset'); const { forgotPasswordReset } = jest.requireActual('./forgotPasswordReset');
it('sends Command_ForgotPasswordReset with plain newPassword when no salt', () => { it('sends Command_ForgotPasswordReset with plain newPassword when no salt', () => {
forgotPasswordReset({ userName: 'alice', token: 'tok', newPassword: 'newpw' } as any); forgotPasswordReset({ userName: 'alice', token: 'tok' } as any, 'newpw');
expect(BackendService.sendSessionCommand).toHaveBeenCalledWith( expect(BackendService.sendSessionCommand).toHaveBeenCalledWith(
'Command_ForgotPasswordReset', 'Command_ForgotPasswordReset',
expect.objectContaining({ newPassword: 'newpw' }), expect.objectContaining({ newPassword: 'newpw' }),
@ -447,7 +460,7 @@ describe('forgotPasswordReset', () => {
}); });
it('sends hashed new password when salt provided', () => { it('sends hashed new password when salt provided', () => {
forgotPasswordReset({ userName: 'alice', token: 'tok', newPassword: 'newpw' } as any, 'salt'); forgotPasswordReset({ userName: 'alice', token: 'tok' } as any, 'newpw', 'salt');
expect(BackendService.sendSessionCommand).toHaveBeenCalledWith( expect(BackendService.sendSessionCommand).toHaveBeenCalledWith(
'Command_ForgotPasswordReset', 'Command_ForgotPasswordReset',
expect.objectContaining({ hashedNewPassword: 'hashed_pw' }), expect.objectContaining({ hashedNewPassword: 'hashed_pw' }),
@ -456,14 +469,14 @@ describe('forgotPasswordReset', () => {
}); });
it('onSuccess calls resetPasswordSuccess and disconnect', () => { it('onSuccess calls resetPasswordSuccess and disconnect', () => {
forgotPasswordReset({ userName: 'alice', token: 'tok', newPassword: 'newpw' } as any); forgotPasswordReset({ userName: 'alice', token: 'tok' } as any, 'newpw');
invokeOnSuccess(); invokeOnSuccess();
expect(SessionPersistence.resetPasswordSuccess).toHaveBeenCalled(); expect(SessionPersistence.resetPasswordSuccess).toHaveBeenCalled();
expect(SessionIndexMocks.disconnect).toHaveBeenCalled(); expect(SessionIndexMocks.disconnect).toHaveBeenCalled();
}); });
it('onError calls resetPasswordFailed and disconnect', () => { it('onError calls resetPasswordFailed and disconnect', () => {
forgotPasswordReset({ userName: 'alice', token: 'tok', newPassword: 'newpw' } as any); forgotPasswordReset({ userName: 'alice', token: 'tok' } as any, 'newpw');
invokeOnError(); invokeOnError();
expect(SessionPersistence.resetPasswordFailed).toHaveBeenCalled(); expect(SessionPersistence.resetPasswordFailed).toHaveBeenCalled();
expect(SessionIndexMocks.disconnect).toHaveBeenCalled(); expect(SessionIndexMocks.disconnect).toHaveBeenCalled();
@ -477,53 +490,53 @@ describe('requestPasswordSalt', () => {
const { requestPasswordSalt } = jest.requireActual('./requestPasswordSalt'); const { requestPasswordSalt } = jest.requireActual('./requestPasswordSalt');
it('sends Command_RequestPasswordSalt', () => { it('sends Command_RequestPasswordSalt', () => {
requestPasswordSalt({ userName: 'alice', reason: WebSocketConnectReason.LOGIN } as any); requestPasswordSalt({ userName: 'alice', reason: WebSocketConnectReason.LOGIN } as any, 'pw');
expect(BackendService.sendSessionCommand).toHaveBeenCalledWith('Command_RequestPasswordSalt', expect.any(Object), expect.any(Object)); expect(BackendService.sendSessionCommand).toHaveBeenCalledWith('Command_RequestPasswordSalt', expect.any(Object), expect.any(Object));
}); });
it('onSuccess with LOGIN reason calls login', () => { it('onSuccess with LOGIN reason forwards password+salt to login', () => {
requestPasswordSalt({ userName: 'alice', reason: WebSocketConnectReason.LOGIN } as any); requestPasswordSalt({ userName: 'alice', reason: WebSocketConnectReason.LOGIN } as any, 'pw');
const resp = { passwordSalt: 'salt123' }; const resp = { passwordSalt: 'salt123' };
invokeOnSuccess(resp, { responseCode: 0, '.Response_PasswordSalt.ext': resp }); invokeOnSuccess(resp, { responseCode: 0, '.Response_PasswordSalt.ext': resp });
expect(SessionIndexMocks.login).toHaveBeenCalledWith(expect.any(Object), 'salt123'); expect(SessionIndexMocks.login).toHaveBeenCalledWith(expect.any(Object), 'pw', 'salt123');
}); });
it('onSuccess with ACTIVATE_ACCOUNT reason calls activate', () => { it('onSuccess with ACTIVATE_ACCOUNT reason forwards password+salt to activate', () => {
requestPasswordSalt({ userName: 'alice', reason: WebSocketConnectReason.ACTIVATE_ACCOUNT } as any); requestPasswordSalt({ userName: 'alice', reason: WebSocketConnectReason.ACTIVATE_ACCOUNT } as any, 'pw');
const resp = { passwordSalt: 'salt123' }; const resp = { passwordSalt: 'salt123' };
invokeOnSuccess(resp, { responseCode: 0, '.Response_PasswordSalt.ext': resp }); invokeOnSuccess(resp, { responseCode: 0, '.Response_PasswordSalt.ext': resp });
expect(SessionIndexMocks.activate).toHaveBeenCalledWith(expect.any(Object), 'salt123'); expect(SessionIndexMocks.activate).toHaveBeenCalledWith(expect.any(Object), 'pw', 'salt123');
}); });
it('onSuccess with PASSWORD_RESET reason calls forgotPasswordReset', () => { it('onSuccess with PASSWORD_RESET reason forwards newPassword+salt to forgotPasswordReset', () => {
requestPasswordSalt({ userName: 'alice', reason: WebSocketConnectReason.PASSWORD_RESET } as any); requestPasswordSalt({ userName: 'alice', reason: WebSocketConnectReason.PASSWORD_RESET } as any, undefined, 'newpw');
const resp = { passwordSalt: 'salt123' }; const resp = { passwordSalt: 'salt123' };
invokeOnSuccess(resp, { responseCode: 0, '.Response_PasswordSalt.ext': resp }); invokeOnSuccess(resp, { responseCode: 0, '.Response_PasswordSalt.ext': resp });
expect(SessionIndexMocks.forgotPasswordReset).toHaveBeenCalled(); expect(SessionIndexMocks.forgotPasswordReset).toHaveBeenCalledWith(expect.any(Object), 'newpw', 'salt123');
}); });
it('onResponseCode RespRegistrationRequired calls updateStatus and disconnect', () => { it('onResponseCode RespRegistrationRequired calls updateStatus and disconnect', () => {
requestPasswordSalt({ userName: 'alice', reason: WebSocketConnectReason.LOGIN } as any); requestPasswordSalt({ userName: 'alice', reason: WebSocketConnectReason.LOGIN } as any, 'pw');
invokeResponseCode(6); invokeResponseCode(6);
expect(SessionIndexMocks.updateStatus).toHaveBeenCalledWith(StatusEnum.DISCONNECTED, expect.any(String)); expect(SessionIndexMocks.updateStatus).toHaveBeenCalledWith(StatusEnum.DISCONNECTED, expect.any(String));
expect(SessionIndexMocks.disconnect).toHaveBeenCalled(); expect(SessionIndexMocks.disconnect).toHaveBeenCalled();
}); });
it('onResponseCode RespRegistrationRequired with ACTIVATE_ACCOUNT calls accountActivationFailed', () => { it('onResponseCode RespRegistrationRequired with ACTIVATE_ACCOUNT calls accountActivationFailed', () => {
requestPasswordSalt({ userName: 'alice', reason: WebSocketConnectReason.ACTIVATE_ACCOUNT } as any); requestPasswordSalt({ userName: 'alice', reason: WebSocketConnectReason.ACTIVATE_ACCOUNT } as any, 'pw');
invokeResponseCode(6); invokeResponseCode(6);
expect(SessionPersistence.accountActivationFailed).toHaveBeenCalled(); expect(SessionPersistence.accountActivationFailed).toHaveBeenCalled();
}); });
it('onError calls updateStatus DISCONNECTED and disconnect', () => { it('onError calls updateStatus DISCONNECTED and disconnect', () => {
requestPasswordSalt({ userName: 'alice', reason: WebSocketConnectReason.LOGIN } as any); requestPasswordSalt({ userName: 'alice', reason: WebSocketConnectReason.LOGIN } as any, 'pw');
invokeOnError(); invokeOnError();
expect(SessionIndexMocks.updateStatus).toHaveBeenCalled(); expect(SessionIndexMocks.updateStatus).toHaveBeenCalled();
expect(SessionIndexMocks.disconnect).toHaveBeenCalled(); expect(SessionIndexMocks.disconnect).toHaveBeenCalled();
}); });
it('onError with PASSWORD_RESET reason calls resetPasswordFailed', () => { it('onError with PASSWORD_RESET reason calls resetPasswordFailed', () => {
requestPasswordSalt({ userName: 'alice', reason: WebSocketConnectReason.PASSWORD_RESET } as any); requestPasswordSalt({ userName: 'alice', reason: WebSocketConnectReason.PASSWORD_RESET } as any, undefined, 'newpw');
invokeOnError(); invokeOnError();
expect(SessionPersistence.resetPasswordFailed).toHaveBeenCalled(); expect(SessionPersistence.resetPasswordFailed).toHaveBeenCalled();
}); });

View file

@ -25,26 +25,26 @@ export function serverIdentification(info: ServerIdentificationData): void {
} }
const getPasswordSalt = passwordSaltSupported(serverOptions); const getPasswordSalt = passwordSaltSupported(serverOptions);
const connectOptions = { ...webClient.options }; const { password, newPassword, ...connectOptions } = webClient.options;
switch (connectOptions.reason) { switch (connectOptions.reason) {
case WebSocketConnectReason.LOGIN: case WebSocketConnectReason.LOGIN:
updateStatus(StatusEnum.LOGGING_IN, 'Logging In...'); updateStatus(StatusEnum.LOGGING_IN, 'Logging In...');
if (getPasswordSalt) { if (getPasswordSalt) {
requestPasswordSalt(connectOptions); requestPasswordSalt(connectOptions, password);
} else { } else {
login(connectOptions); login(connectOptions, password);
} }
break; break;
case WebSocketConnectReason.REGISTER: case WebSocketConnectReason.REGISTER:
const passwordSalt = getPasswordSalt ? generateSalt() : null; const passwordSalt = getPasswordSalt ? generateSalt() : null;
register(connectOptions, passwordSalt); register(connectOptions, password, passwordSalt);
break; break;
case WebSocketConnectReason.ACTIVATE_ACCOUNT: case WebSocketConnectReason.ACTIVATE_ACCOUNT:
if (getPasswordSalt) { if (getPasswordSalt) {
requestPasswordSalt(connectOptions); requestPasswordSalt(connectOptions, password);
} else { } else {
activate(connectOptions); activate(connectOptions, password);
} }
break; break;
case WebSocketConnectReason.PASSWORD_RESET_REQUEST: case WebSocketConnectReason.PASSWORD_RESET_REQUEST:
@ -55,9 +55,9 @@ export function serverIdentification(info: ServerIdentificationData): void {
break; break;
case WebSocketConnectReason.PASSWORD_RESET: case WebSocketConnectReason.PASSWORD_RESET:
if (getPasswordSalt) { if (getPasswordSalt) {
requestPasswordSalt(connectOptions); requestPasswordSalt(connectOptions, undefined, newPassword);
} else { } else {
forgotPasswordReset(connectOptions); forgotPasswordReset(connectOptions, newPassword);
} }
break; break;
default: default:

View file

@ -374,46 +374,66 @@ describe('serverIdentification', () => {
expect(SessionCmds.disconnect).toHaveBeenCalled(); expect(SessionCmds.disconnect).toHaveBeenCalled();
}); });
it('LOGIN reason without salt → calls login', () => { it('LOGIN reason without salt → calls login with password as separate param', () => {
(webClient as any).options = { reason: WebSocketConnectReason.LOGIN }; (webClient as any).options = { reason: WebSocketConnectReason.LOGIN, password: 'secret' };
(Utils.passwordSaltSupported as jest.Mock).mockReturnValue(0); (Utils.passwordSaltSupported as jest.Mock).mockReturnValue(0);
serverIdentification({ serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 0 } as any); serverIdentification({ serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 0 } as any);
expect(SessionCmds.login).toHaveBeenCalled(); expect(SessionCmds.login).toHaveBeenCalledWith(
expect.not.objectContaining({ password: expect.anything() }),
'secret'
);
}); });
it('LOGIN reason with salt → calls requestPasswordSalt', () => { it('LOGIN reason with salt → calls requestPasswordSalt with password as separate param', () => {
(webClient as any).options = { reason: WebSocketConnectReason.LOGIN }; (webClient as any).options = { reason: WebSocketConnectReason.LOGIN, password: 'secret' };
(Utils.passwordSaltSupported as jest.Mock).mockReturnValue(1); (Utils.passwordSaltSupported as jest.Mock).mockReturnValue(1);
serverIdentification({ serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 1 } as any); serverIdentification({ serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 1 } as any);
expect(SessionCmds.requestPasswordSalt).toHaveBeenCalled(); expect(SessionCmds.requestPasswordSalt).toHaveBeenCalledWith(
expect.not.objectContaining({ password: expect.anything() }),
'secret'
);
}); });
it('REGISTER reason without salt → calls register with null salt', () => { it('REGISTER reason without salt → calls register with password and null salt', () => {
(webClient as any).options = { reason: WebSocketConnectReason.REGISTER }; (webClient as any).options = { reason: WebSocketConnectReason.REGISTER, password: 'secret' };
(Utils.passwordSaltSupported as jest.Mock).mockReturnValue(0); (Utils.passwordSaltSupported as jest.Mock).mockReturnValue(0);
serverIdentification({ serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 0 } as any); serverIdentification({ serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 0 } as any);
expect(SessionCmds.register).toHaveBeenCalledWith(expect.any(Object), null); expect(SessionCmds.register).toHaveBeenCalledWith(
expect.not.objectContaining({ password: expect.anything() }),
'secret',
null
);
}); });
it('REGISTER reason with salt → calls register with generated salt', () => { it('REGISTER reason with salt → calls register with password and generated salt', () => {
(webClient as any).options = { reason: WebSocketConnectReason.REGISTER }; (webClient as any).options = { reason: WebSocketConnectReason.REGISTER, password: 'secret' };
(Utils.passwordSaltSupported as jest.Mock).mockReturnValue(1); (Utils.passwordSaltSupported as jest.Mock).mockReturnValue(1);
serverIdentification({ serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 1 } as any); serverIdentification({ serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 1 } as any);
expect(SessionCmds.register).toHaveBeenCalledWith(expect.any(Object), 'newSalt'); expect(SessionCmds.register).toHaveBeenCalledWith(
expect.not.objectContaining({ password: expect.anything() }),
'secret',
'newSalt'
);
}); });
it('ACTIVATE_ACCOUNT reason without salt → calls activate', () => { it('ACTIVATE_ACCOUNT reason without salt → calls activate with password as separate param', () => {
(webClient as any).options = { reason: WebSocketConnectReason.ACTIVATE_ACCOUNT }; (webClient as any).options = { reason: WebSocketConnectReason.ACTIVATE_ACCOUNT, password: 'secret' };
(Utils.passwordSaltSupported as jest.Mock).mockReturnValue(0); (Utils.passwordSaltSupported as jest.Mock).mockReturnValue(0);
serverIdentification({ serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 0 } as any); serverIdentification({ serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 0 } as any);
expect(SessionCmds.activate).toHaveBeenCalled(); expect(SessionCmds.activate).toHaveBeenCalledWith(
expect.not.objectContaining({ password: expect.anything() }),
'secret'
);
}); });
it('ACTIVATE_ACCOUNT reason with salt → calls requestPasswordSalt', () => { it('ACTIVATE_ACCOUNT reason with salt → calls requestPasswordSalt with password as separate param', () => {
(webClient as any).options = { reason: WebSocketConnectReason.ACTIVATE_ACCOUNT }; (webClient as any).options = { reason: WebSocketConnectReason.ACTIVATE_ACCOUNT, password: 'secret' };
(Utils.passwordSaltSupported as jest.Mock).mockReturnValue(1); (Utils.passwordSaltSupported as jest.Mock).mockReturnValue(1);
serverIdentification({ serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 1 } as any); serverIdentification({ serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 1 } as any);
expect(SessionCmds.requestPasswordSalt).toHaveBeenCalled(); expect(SessionCmds.requestPasswordSalt).toHaveBeenCalledWith(
expect.not.objectContaining({ password: expect.anything() }),
'secret'
);
}); });
it('PASSWORD_RESET_REQUEST reason → calls forgotPasswordRequest', () => { it('PASSWORD_RESET_REQUEST reason → calls forgotPasswordRequest', () => {
@ -428,18 +448,25 @@ describe('serverIdentification', () => {
expect(SessionCmds.forgotPasswordChallenge).toHaveBeenCalled(); expect(SessionCmds.forgotPasswordChallenge).toHaveBeenCalled();
}); });
it('PASSWORD_RESET reason without salt → calls forgotPasswordReset', () => { it('PASSWORD_RESET reason without salt → calls forgotPasswordReset with newPassword as separate param', () => {
(webClient as any).options = { reason: WebSocketConnectReason.PASSWORD_RESET }; (webClient as any).options = { reason: WebSocketConnectReason.PASSWORD_RESET, newPassword: 'newpw' };
(Utils.passwordSaltSupported as jest.Mock).mockReturnValue(0); (Utils.passwordSaltSupported as jest.Mock).mockReturnValue(0);
serverIdentification({ serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 0 } as any); serverIdentification({ serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 0 } as any);
expect(SessionCmds.forgotPasswordReset).toHaveBeenCalled(); expect(SessionCmds.forgotPasswordReset).toHaveBeenCalledWith(
expect.not.objectContaining({ newPassword: expect.anything() }),
'newpw'
);
}); });
it('PASSWORD_RESET reason with salt → calls requestPasswordSalt', () => { it('PASSWORD_RESET reason with salt → calls requestPasswordSalt with newPassword as separate param', () => {
(webClient as any).options = { reason: WebSocketConnectReason.PASSWORD_RESET }; (webClient as any).options = { reason: WebSocketConnectReason.PASSWORD_RESET, newPassword: 'newpw' };
(Utils.passwordSaltSupported as jest.Mock).mockReturnValue(1); (Utils.passwordSaltSupported as jest.Mock).mockReturnValue(1);
serverIdentification({ serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 1 } as any); serverIdentification({ serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 1 } as any);
expect(SessionCmds.requestPasswordSalt).toHaveBeenCalled(); expect(SessionCmds.requestPasswordSalt).toHaveBeenCalledWith(
expect.not.objectContaining({ newPassword: expect.anything() }),
undefined,
'newpw'
);
}); });
it('unknown reason → updateStatus DISCONNECTED and disconnect', () => { it('unknown reason → updateStatus DISCONNECTED and disconnect', () => {