From 98ce317ee1915ed6b20f36742dd7aaeab9bbe07b Mon Sep 17 00:00:00 2001 From: seavor Date: Sun, 12 Apr 2026 15:55:00 -0500 Subject: [PATCH] remove naked password from redux layer --- .../websocket/commands/session/activate.ts | 4 +- .../commands/session/forgotPasswordReset.ts | 4 +- .../src/websocket/commands/session/login.ts | 9 +- .../websocket/commands/session/register.ts | 9 +- .../commands/session/requestPasswordSalt.ts | 8 +- .../session/sessionCommands-complex.spec.ts | 133 ++++++++++-------- .../events/session/serverIdentification.ts | 16 +-- .../events/session/sessionEvents.spec.ts | 75 ++++++---- 8 files changed, 151 insertions(+), 107 deletions(-) diff --git a/webclient/src/websocket/commands/session/activate.ts b/webclient/src/websocket/commands/session/activate.ts index 4cd0e8c4e..6fd909ca8 100644 --- a/webclient/src/websocket/commands/session/activate.ts +++ b/webclient/src/websocket/commands/session/activate.ts @@ -8,7 +8,7 @@ import { SessionPersistence } from '../../persistence'; 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; BackendService.sendSessionCommand('Command_Activate', { @@ -19,7 +19,7 @@ export function activate(options: WebSocketConnectOptions, passwordSalt?: string onResponseCode: { [ProtoController.root.Response.ResponseCode.RespActivationAccepted]: () => { SessionPersistence.accountActivationSuccess(); - login(options, passwordSalt); + login(options, password, passwordSalt); }, }, onError: () => { diff --git a/webclient/src/websocket/commands/session/forgotPasswordReset.ts b/webclient/src/websocket/commands/session/forgotPasswordReset.ts index d9a775816..21e5842f6 100644 --- a/webclient/src/websocket/commands/session/forgotPasswordReset.ts +++ b/webclient/src/websocket/commands/session/forgotPasswordReset.ts @@ -8,8 +8,8 @@ import { hashPassword } from '../../utils'; import { disconnect, updateStatus } from '.'; -export function forgotPasswordReset(options: WebSocketConnectOptions, passwordSalt?: string): void { - const { userName, token, newPassword } = options as unknown as ForgotPasswordResetParams; +export function forgotPasswordReset(options: WebSocketConnectOptions, newPassword?: string, passwordSalt?: string): void { + const { userName, token } = options as unknown as ForgotPasswordResetParams; const params: any = { ...webClient.clientConfig, diff --git a/webclient/src/websocket/commands/session/login.ts b/webclient/src/websocket/commands/session/login.ts index c98128917..adbd45b5d 100644 --- a/webclient/src/websocket/commands/session/login.ts +++ b/webclient/src/websocket/commands/session/login.ts @@ -12,8 +12,8 @@ import { updateStatus, } from './'; -export function login(options: WebSocketConnectOptions, passwordSalt?: string): void { - const { userName, password, hashedPassword } = options; +export function login(options: WebSocketConnectOptions, password?: string, passwordSalt?: string): void { + const { userName, hashedPassword } = options; const loginConfig: any = { ...webClient.clientConfig, @@ -71,7 +71,10 @@ export function login(options: WebSocketConnectOptions, passwordSalt?: string): onLoginError('Login failed: server error'), [ResponseCode.RespAccountNotActivated]: () => onLoginError('Login failed: account not activated', - () => SessionPersistence.accountAwaitingActivation(options) + () => { + const { password: _p, newPassword: _np, ...safeOptions } = options; + SessionPersistence.accountAwaitingActivation(safeOptions); + } ), }, onError: (responseCode) => diff --git a/webclient/src/websocket/commands/session/register.ts b/webclient/src/websocket/commands/session/register.ts index a25b85868..31bd37a09 100644 --- a/webclient/src/websocket/commands/session/register.ts +++ b/webclient/src/websocket/commands/session/register.ts @@ -9,8 +9,8 @@ import { hashPassword } from '../../utils'; import { login, disconnect, updateStatus } from './'; -export function register(options: WebSocketConnectOptions, passwordSalt?: string): void { - const { userName, password, email, country, realName } = options as ServerRegisterParams; +export function register(options: WebSocketConnectOptions, password?: string, passwordSalt?: string): void { + const { userName, email, country, realName } = options as ServerRegisterParams; const params: any = { ...webClient.clientConfig, @@ -37,12 +37,13 @@ export function register(options: WebSocketConnectOptions, passwordSalt?: string BackendService.sendSessionCommand('Command_Register', params, { onResponseCode: { [ResponseCode.RespRegistrationAccepted]: () => { - login(options, passwordSalt); + login(options, password, passwordSalt); SessionPersistence.registrationSuccess(); }, [ResponseCode.RespRegistrationAcceptedNeedsActivation]: () => { updateStatus(StatusEnum.DISCONNECTED, 'Registration accepted, awaiting activation'); - SessionPersistence.accountAwaitingActivation(options); + const { password: _p, newPassword: _np, ...safeOptions } = options; + SessionPersistence.accountAwaitingActivation(safeOptions); disconnect(); }, [ResponseCode.RespUserAlreadyExists]: () => onRegistrationError( diff --git a/webclient/src/websocket/commands/session/requestPasswordSalt.ts b/webclient/src/websocket/commands/session/requestPasswordSalt.ts index a3d1fc05c..e3635da70 100644 --- a/webclient/src/websocket/commands/session/requestPasswordSalt.ts +++ b/webclient/src/websocket/commands/session/requestPasswordSalt.ts @@ -14,7 +14,7 @@ import { updateStatus } from './'; -export function requestPasswordSalt(options: WebSocketConnectOptions): void { +export function requestPasswordSalt(options: WebSocketConnectOptions, password?: string, newPassword?: string): void { const { userName } = options as RequestPasswordSaltParams; const onFailure = () => { @@ -41,13 +41,13 @@ export function requestPasswordSalt(options: WebSocketConnectOptions): void { switch (options.reason) { case WebSocketConnectReason.ACTIVATE_ACCOUNT: - activate(options, passwordSalt); + activate(options, password, passwordSalt); break; case WebSocketConnectReason.PASSWORD_RESET: - forgotPasswordReset(options, passwordSalt); + forgotPasswordReset(options, newPassword, passwordSalt); break; default: - login(options, passwordSalt); + login(options, password, passwordSalt); } }, onResponseCode: { diff --git a/webclient/src/websocket/commands/session/sessionCommands-complex.spec.ts b/webclient/src/websocket/commands/session/sessionCommands-complex.spec.ts index 1d61c8a36..97a163081 100644 --- a/webclient/src/websocket/commands/session/sessionCommands-complex.spec.ts +++ b/webclient/src/websocket/commands/session/sessionCommands-complex.spec.ts @@ -124,7 +124,7 @@ describe('login', () => { const { login } = jest.requireActual('./login'); 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( 'Command_Login', expect.objectContaining({ userName: 'alice', password: 'pw' }), @@ -133,7 +133,7 @@ describe('login', () => { }); 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( 'Command_Login', expect.objectContaining({ hashedPassword: 'hashed_pw' }), @@ -142,7 +142,7 @@ describe('login', () => { }); 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( 'Command_Login', expect.objectContaining({ hashedPassword: 'pre_hashed' }), @@ -151,7 +151,7 @@ describe('login', () => { }); 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' } }; invokeOnSuccess(loginResp, { responseCode: 0, '.Response_Login.ext': loginResp }); expect(SessionPersistence.updateBuddyList).toHaveBeenCalledWith([]); @@ -164,7 +164,7 @@ describe('login', () => { }); 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' } }; invokeOnSuccess(loginResp, { responseCode: 0, '.Response_Login.ext': loginResp }); 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', () => { - login({ userName: 'alice', password: 'pw' } as any, 'salt'); + login({ userName: 'alice' } as any, 'pw', 'salt'); const loginResp = { buddyList: [], ignoreList: [], userInfo: { name: 'alice' } }; invokeOnSuccess(loginResp, { responseCode: 0, '.Response_Login.ext': loginResp }); const calledWith = (SessionPersistence.loginSuccessful as jest.Mock).mock.calls[0][0]; @@ -180,63 +180,65 @@ describe('login', () => { }); it('onResponseCode RespClientUpdateRequired calls onLoginError', () => { - login({ userName: 'alice', password: 'pw' } as any); + login({ userName: 'alice' } as any, 'pw'); invokeResponseCode(1); expect(SessionPersistence.loginFailed).toHaveBeenCalled(); expect(SessionIndexMocks.disconnect).toHaveBeenCalled(); }); it('onResponseCode RespWrongPassword', () => { - login({ userName: 'alice', password: 'pw' } as any); + login({ userName: 'alice' } as any, 'pw'); invokeResponseCode(2); expect(SessionPersistence.loginFailed).toHaveBeenCalled(); }); it('onResponseCode RespUsernameInvalid', () => { - login({ userName: 'alice', password: 'pw' } as any); + login({ userName: 'alice' } as any, 'pw'); invokeResponseCode(3); expect(SessionPersistence.loginFailed).toHaveBeenCalled(); }); it('onResponseCode RespWouldOverwriteOldSession', () => { - login({ userName: 'alice', password: 'pw' } as any); + login({ userName: 'alice' } as any, 'pw'); invokeResponseCode(4); expect(SessionPersistence.loginFailed).toHaveBeenCalled(); }); it('onResponseCode RespUserIsBanned', () => { - login({ userName: 'alice', password: 'pw' } as any); + login({ userName: 'alice' } as any, 'pw'); invokeResponseCode(5); expect(SessionPersistence.loginFailed).toHaveBeenCalled(); }); it('onResponseCode RespRegistrationRequired', () => { - login({ userName: 'alice', password: 'pw' } as any); + login({ userName: 'alice' } as any, 'pw'); invokeResponseCode(6); expect(SessionPersistence.loginFailed).toHaveBeenCalled(); }); it('onResponseCode RespClientIdRequired', () => { - login({ userName: 'alice', password: 'pw' } as any); + login({ userName: 'alice' } as any, 'pw'); invokeResponseCode(7); expect(SessionPersistence.loginFailed).toHaveBeenCalled(); }); it('onResponseCode RespContextError', () => { - login({ userName: 'alice', password: 'pw' } as any); + login({ userName: 'alice' } as any, 'pw'); invokeResponseCode(8); expect(SessionPersistence.loginFailed).toHaveBeenCalled(); }); - it('onResponseCode RespAccountNotActivated calls accountAwaitingActivation', () => { - login({ userName: 'alice', password: 'pw' } as any); + it('onResponseCode RespAccountNotActivated calls accountAwaitingActivation without password in options', () => { + login({ userName: 'alice', password: 'leaked' } as any, 'pw'); invokeResponseCode(9); - expect(SessionPersistence.accountAwaitingActivation).toHaveBeenCalled(); + expect(SessionPersistence.accountAwaitingActivation).toHaveBeenCalledWith( + expect.not.objectContaining({ password: expect.anything() }) + ); expect(SessionPersistence.loginFailed).toHaveBeenCalled(); }); it('onError calls onLoginError with unknown error message', () => { - login({ userName: 'alice', password: 'pw' } as any); + login({ userName: 'alice' } as any, 'pw'); invokeOnError(999); expect(SessionPersistence.loginFailed).toHaveBeenCalled(); }); @@ -249,7 +251,7 @@ describe('register', () => { const { register } = jest.requireActual('./register'); 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( 'Command_Register', expect.objectContaining({ userName: 'alice', password: 'pw' }), @@ -258,7 +260,7 @@ describe('register', () => { }); 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( 'Command_Register', expect.objectContaining({ hashedPassword: 'hashed_pw' }), @@ -267,76 +269,78 @@ describe('register', () => { }); it('RespRegistrationAccepted calls login without salt and registrationSuccess', () => { - register({ userName: 'alice', password: 'pw' } as any); + register({ userName: 'alice' } as any, 'pw'); invokeResponseCode(10); - expect(SessionIndexMocks.login).toHaveBeenCalledWith(expect.any(Object), undefined); + expect(SessionIndexMocks.login).toHaveBeenCalledWith(expect.any(Object), 'pw', undefined); expect(SessionPersistence.registrationSuccess).toHaveBeenCalled(); }); it('RespRegistrationAccepted forwards salt to login', () => { - register({ userName: 'alice', password: 'pw' } as any, 'mySalt'); + register({ userName: 'alice' } as any, 'pw', 'mySalt'); invokeResponseCode(10); - expect(SessionIndexMocks.login).toHaveBeenCalledWith(expect.any(Object), 'mySalt'); + expect(SessionIndexMocks.login).toHaveBeenCalledWith(expect.any(Object), 'pw', 'mySalt'); expect(SessionPersistence.registrationSuccess).toHaveBeenCalled(); }); - it('RespRegistrationAcceptedNeedsActivation calls accountAwaitingActivation', () => { - register({ userName: 'alice', password: 'pw' } as any); + it('RespRegistrationAcceptedNeedsActivation calls accountAwaitingActivation without password in options', () => { + register({ userName: 'alice', password: 'leaked' } as any, 'pw'); invokeResponseCode(11); - expect(SessionPersistence.accountAwaitingActivation).toHaveBeenCalled(); + expect(SessionPersistence.accountAwaitingActivation).toHaveBeenCalledWith( + expect.not.objectContaining({ password: expect.anything() }) + ); expect(SessionIndexMocks.disconnect).toHaveBeenCalled(); }); it('RespUserAlreadyExists calls registrationUserNameError', () => { - register({ userName: 'alice', password: 'pw' } as any); + register({ userName: 'alice' } as any, 'pw'); invokeResponseCode(12); expect(SessionPersistence.registrationUserNameError).toHaveBeenCalled(); }); it('RespUsernameInvalid calls registrationUserNameError', () => { - register({ userName: 'alice', password: 'pw' } as any); + register({ userName: 'alice' } as any, 'pw'); invokeResponseCode(3); expect(SessionPersistence.registrationUserNameError).toHaveBeenCalled(); }); it('RespPasswordTooShort calls registrationPasswordError', () => { - register({ userName: 'alice', password: 'pw' } as any); + register({ userName: 'alice' } as any, 'pw'); invokeResponseCode(13); expect(SessionPersistence.registrationPasswordError).toHaveBeenCalled(); }); it('RespEmailRequiredToRegister calls registrationRequiresEmail', () => { - register({ userName: 'alice', password: 'pw' } as any); + register({ userName: 'alice' } as any, 'pw'); invokeResponseCode(14); expect(SessionPersistence.registrationRequiresEmail).toHaveBeenCalled(); }); it('RespEmailBlackListed calls registrationEmailError', () => { - register({ userName: 'alice', password: 'pw' } as any); + register({ userName: 'alice' } as any, 'pw'); invokeResponseCode(15); expect(SessionPersistence.registrationEmailError).toHaveBeenCalled(); }); it('RespTooManyRequests calls registrationEmailError', () => { - register({ userName: 'alice', password: 'pw' } as any); + register({ userName: 'alice' } as any, 'pw'); invokeResponseCode(16); expect(SessionPersistence.registrationEmailError).toHaveBeenCalled(); }); it('RespRegistrationDisabled calls registrationFailed', () => { - register({ userName: 'alice', password: 'pw' } as any); + register({ userName: 'alice' } as any, 'pw'); invokeResponseCode(17); expect(SessionPersistence.registrationFailed).toHaveBeenCalled(); }); 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 }); expect(SessionPersistence.registrationFailed).toHaveBeenCalledWith('bad user', 9999); }); it('onError calls registrationFailed', () => { - register({ userName: 'alice', password: 'pw' } as any); + register({ userName: 'alice' } as any, 'pw'); invokeOnError(); expect(SessionPersistence.registrationFailed).toHaveBeenCalled(); }); @@ -348,16 +352,25 @@ describe('register', () => { describe('activate', () => { const { activate } = jest.requireActual('./activate'); - it('sends Command_Activate', () => { - activate({ userName: 'alice', token: 'tok' } as any); - expect(BackendService.sendSessionCommand).toHaveBeenCalledWith('Command_Activate', expect.any(Object), expect.any(Object)); + it('sends Command_Activate with userName and token, not password', () => { + activate({ userName: 'alice', token: 'tok' } as any, 'pw'); + 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', () => { - activate({ userName: 'alice', token: 'tok' } as any, 'salt'); + it('RespActivationAccepted calls accountActivationSuccess and forwards password+salt to login', () => { + activate({ userName: 'alice', token: 'tok' } as any, 'pw', 'salt'); invokeResponseCode(18); 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', () => { @@ -438,7 +451,7 @@ describe('forgotPasswordReset', () => { const { forgotPasswordReset } = jest.requireActual('./forgotPasswordReset'); 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( 'Command_ForgotPasswordReset', expect.objectContaining({ newPassword: 'newpw' }), @@ -447,7 +460,7 @@ describe('forgotPasswordReset', () => { }); 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( 'Command_ForgotPasswordReset', expect.objectContaining({ hashedNewPassword: 'hashed_pw' }), @@ -456,14 +469,14 @@ describe('forgotPasswordReset', () => { }); it('onSuccess calls resetPasswordSuccess and disconnect', () => { - forgotPasswordReset({ userName: 'alice', token: 'tok', newPassword: 'newpw' } as any); + forgotPasswordReset({ userName: 'alice', token: 'tok' } as any, 'newpw'); invokeOnSuccess(); expect(SessionPersistence.resetPasswordSuccess).toHaveBeenCalled(); expect(SessionIndexMocks.disconnect).toHaveBeenCalled(); }); it('onError calls resetPasswordFailed and disconnect', () => { - forgotPasswordReset({ userName: 'alice', token: 'tok', newPassword: 'newpw' } as any); + forgotPasswordReset({ userName: 'alice', token: 'tok' } as any, 'newpw'); invokeOnError(); expect(SessionPersistence.resetPasswordFailed).toHaveBeenCalled(); expect(SessionIndexMocks.disconnect).toHaveBeenCalled(); @@ -477,53 +490,53 @@ describe('requestPasswordSalt', () => { const { requestPasswordSalt } = jest.requireActual('./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)); }); - it('onSuccess with LOGIN reason calls login', () => { - requestPasswordSalt({ userName: 'alice', reason: WebSocketConnectReason.LOGIN } as any); + it('onSuccess with LOGIN reason forwards password+salt to login', () => { + requestPasswordSalt({ userName: 'alice', reason: WebSocketConnectReason.LOGIN } as any, 'pw'); const resp = { passwordSalt: 'salt123' }; 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', () => { - requestPasswordSalt({ userName: 'alice', reason: WebSocketConnectReason.ACTIVATE_ACCOUNT } as any); + it('onSuccess with ACTIVATE_ACCOUNT reason forwards password+salt to activate', () => { + requestPasswordSalt({ userName: 'alice', reason: WebSocketConnectReason.ACTIVATE_ACCOUNT } as any, 'pw'); const resp = { passwordSalt: 'salt123' }; 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', () => { - requestPasswordSalt({ userName: 'alice', reason: WebSocketConnectReason.PASSWORD_RESET } as any); + it('onSuccess with PASSWORD_RESET reason forwards newPassword+salt to forgotPasswordReset', () => { + requestPasswordSalt({ userName: 'alice', reason: WebSocketConnectReason.PASSWORD_RESET } as any, undefined, 'newpw'); const resp = { passwordSalt: 'salt123' }; 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', () => { - requestPasswordSalt({ userName: 'alice', reason: WebSocketConnectReason.LOGIN } as any); + requestPasswordSalt({ userName: 'alice', reason: WebSocketConnectReason.LOGIN } as any, 'pw'); invokeResponseCode(6); expect(SessionIndexMocks.updateStatus).toHaveBeenCalledWith(StatusEnum.DISCONNECTED, expect.any(String)); expect(SessionIndexMocks.disconnect).toHaveBeenCalled(); }); 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); expect(SessionPersistence.accountActivationFailed).toHaveBeenCalled(); }); 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(); expect(SessionIndexMocks.updateStatus).toHaveBeenCalled(); expect(SessionIndexMocks.disconnect).toHaveBeenCalled(); }); 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(); expect(SessionPersistence.resetPasswordFailed).toHaveBeenCalled(); }); diff --git a/webclient/src/websocket/events/session/serverIdentification.ts b/webclient/src/websocket/events/session/serverIdentification.ts index 87ae79453..594d26e6d 100644 --- a/webclient/src/websocket/events/session/serverIdentification.ts +++ b/webclient/src/websocket/events/session/serverIdentification.ts @@ -25,26 +25,26 @@ export function serverIdentification(info: ServerIdentificationData): void { } const getPasswordSalt = passwordSaltSupported(serverOptions); - const connectOptions = { ...webClient.options }; + const { password, newPassword, ...connectOptions } = webClient.options; switch (connectOptions.reason) { case WebSocketConnectReason.LOGIN: updateStatus(StatusEnum.LOGGING_IN, 'Logging In...'); if (getPasswordSalt) { - requestPasswordSalt(connectOptions); + requestPasswordSalt(connectOptions, password); } else { - login(connectOptions); + login(connectOptions, password); } break; case WebSocketConnectReason.REGISTER: const passwordSalt = getPasswordSalt ? generateSalt() : null; - register(connectOptions, passwordSalt); + register(connectOptions, password, passwordSalt); break; case WebSocketConnectReason.ACTIVATE_ACCOUNT: if (getPasswordSalt) { - requestPasswordSalt(connectOptions); + requestPasswordSalt(connectOptions, password); } else { - activate(connectOptions); + activate(connectOptions, password); } break; case WebSocketConnectReason.PASSWORD_RESET_REQUEST: @@ -55,9 +55,9 @@ export function serverIdentification(info: ServerIdentificationData): void { break; case WebSocketConnectReason.PASSWORD_RESET: if (getPasswordSalt) { - requestPasswordSalt(connectOptions); + requestPasswordSalt(connectOptions, undefined, newPassword); } else { - forgotPasswordReset(connectOptions); + forgotPasswordReset(connectOptions, newPassword); } break; default: diff --git a/webclient/src/websocket/events/session/sessionEvents.spec.ts b/webclient/src/websocket/events/session/sessionEvents.spec.ts index f6f39957b..9f8047890 100644 --- a/webclient/src/websocket/events/session/sessionEvents.spec.ts +++ b/webclient/src/websocket/events/session/sessionEvents.spec.ts @@ -374,46 +374,66 @@ describe('serverIdentification', () => { expect(SessionCmds.disconnect).toHaveBeenCalled(); }); - it('LOGIN reason without salt → calls login', () => { - (webClient as any).options = { reason: WebSocketConnectReason.LOGIN }; + it('LOGIN reason without salt → calls login with password as separate param', () => { + (webClient as any).options = { reason: WebSocketConnectReason.LOGIN, password: 'secret' }; (Utils.passwordSaltSupported as jest.Mock).mockReturnValue(0); 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', () => { - (webClient as any).options = { reason: WebSocketConnectReason.LOGIN }; + it('LOGIN reason with salt → calls requestPasswordSalt with password as separate param', () => { + (webClient as any).options = { reason: WebSocketConnectReason.LOGIN, password: 'secret' }; (Utils.passwordSaltSupported as jest.Mock).mockReturnValue(1); 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', () => { - (webClient as any).options = { reason: WebSocketConnectReason.REGISTER }; + it('REGISTER reason without salt → calls register with password and null salt', () => { + (webClient as any).options = { reason: WebSocketConnectReason.REGISTER, password: 'secret' }; (Utils.passwordSaltSupported as jest.Mock).mockReturnValue(0); 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', () => { - (webClient as any).options = { reason: WebSocketConnectReason.REGISTER }; + it('REGISTER reason with salt → calls register with password and generated salt', () => { + (webClient as any).options = { reason: WebSocketConnectReason.REGISTER, password: 'secret' }; (Utils.passwordSaltSupported as jest.Mock).mockReturnValue(1); 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', () => { - (webClient as any).options = { reason: WebSocketConnectReason.ACTIVATE_ACCOUNT }; + it('ACTIVATE_ACCOUNT reason without salt → calls activate with password as separate param', () => { + (webClient as any).options = { reason: WebSocketConnectReason.ACTIVATE_ACCOUNT, password: 'secret' }; (Utils.passwordSaltSupported as jest.Mock).mockReturnValue(0); 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', () => { - (webClient as any).options = { reason: WebSocketConnectReason.ACTIVATE_ACCOUNT }; + it('ACTIVATE_ACCOUNT reason with salt → calls requestPasswordSalt with password as separate param', () => { + (webClient as any).options = { reason: WebSocketConnectReason.ACTIVATE_ACCOUNT, password: 'secret' }; (Utils.passwordSaltSupported as jest.Mock).mockReturnValue(1); 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', () => { @@ -428,18 +448,25 @@ describe('serverIdentification', () => { expect(SessionCmds.forgotPasswordChallenge).toHaveBeenCalled(); }); - it('PASSWORD_RESET reason without salt → calls forgotPasswordReset', () => { - (webClient as any).options = { reason: WebSocketConnectReason.PASSWORD_RESET }; + it('PASSWORD_RESET reason without salt → calls forgotPasswordReset with newPassword as separate param', () => { + (webClient as any).options = { reason: WebSocketConnectReason.PASSWORD_RESET, newPassword: 'newpw' }; (Utils.passwordSaltSupported as jest.Mock).mockReturnValue(0); 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', () => { - (webClient as any).options = { reason: WebSocketConnectReason.PASSWORD_RESET }; + it('PASSWORD_RESET reason with salt → calls requestPasswordSalt with newPassword as separate param', () => { + (webClient as any).options = { reason: WebSocketConnectReason.PASSWORD_RESET, newPassword: 'newpw' }; (Utils.passwordSaltSupported as jest.Mock).mockReturnValue(1); 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', () => {