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 './';
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: () => {

View file

@ -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,

View file

@ -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) =>

View file

@ -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(

View file

@ -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: {

View file

@ -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();
});

View file

@ -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:

View file

@ -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', () => {