diff --git a/back/methods/vn-user/sign-in.js b/back/methods/vn-user/sign-in.js
new file mode 100644
index 0000000000..73cc705de0
--- /dev/null
+++ b/back/methods/vn-user/sign-in.js
@@ -0,0 +1,102 @@
+const ForbiddenError = require('vn-loopback/util/forbiddenError');
+const UserError = require('vn-loopback/util/user-error');
+
+module.exports = Self => {
+ Self.remoteMethodCtx('signIn', {
+ description: 'Login a user with username/email and password',
+ accepts: [
+ {
+ arg: 'user',
+ type: 'String',
+ description: 'The user name or email',
+ required: true
+ }, {
+ arg: 'password',
+ type: 'String',
+ description: 'The password'
+ }
+ ],
+ returns: {
+ type: 'object',
+ root: true
+ },
+ http: {
+ path: `/sign-in`,
+ verb: 'POST'
+ }
+ });
+
+ Self.signIn = async function(ctx, user, password, options) {
+ const myOptions = {};
+ if (typeof options == 'object')
+ Object.assign(myOptions, options);
+
+ const where = Self.userUses(user);
+ const vnUser = await Self.findOne({
+ fields: ['id', 'name', 'password', 'active', 'email', 'passExpired', 'twoFactor'],
+ where
+ }, myOptions);
+
+ const validCredentials = vnUser
+ && await vnUser.hasPassword(password);
+
+ if (validCredentials) {
+ if (!vnUser.active)
+ throw new UserError('User disabled');
+ await Self.sendTwoFactor(ctx, vnUser, myOptions);
+ await Self.passExpired(vnUser, myOptions);
+
+ if (vnUser.twoFactor)
+ throw new ForbiddenError(null, 'REQUIRES_2FA');
+ }
+
+ return Self.validateLogin(user, password);
+ };
+
+ Self.passExpired = async(vnUser, myOptions) => {
+ const today = Date.vnNew();
+ today.setHours(0, 0, 0, 0);
+
+ if (vnUser.passExpired && vnUser.passExpired.getTime() <= today.getTime()) {
+ const $ = Self.app.models;
+ const changePasswordToken = await $.AccessToken.create({
+ scopes: ['changePassword'],
+ userId: vnUser.id
+ }, myOptions);
+ const err = new UserError('Pass expired', 'passExpired');
+ changePasswordToken.twoFactor = vnUser.twoFactor ? true : false;
+ err.details = {token: changePasswordToken};
+ throw err;
+ }
+ };
+
+ Self.sendTwoFactor = async(ctx, vnUser, myOptions) => {
+ if (vnUser.twoFactor === 'email') {
+ const $ = Self.app.models;
+
+ const code = String(Math.floor(Math.random() * 999999));
+ const maxTTL = ((60 * 1000) * 5); // 5 min
+ await $.AuthCode.upsertWithWhere({userFk: vnUser.id}, {
+ userFk: vnUser.id,
+ code: code,
+ expires: Date.vnNow() + maxTTL
+ }, myOptions);
+
+ const headers = ctx.req.headers;
+ const platform = headers['sec-ch-ua-platform']?.replace(/['"=]+/g, '');
+ const browser = headers['sec-ch-ua']?.replace(/['"=]+/g, '');
+ const params = {
+ args: {
+ recipientId: vnUser.id,
+ recipient: vnUser.email,
+ code: code,
+ ip: ctx.req?.connection?.remoteAddress,
+ device: platform && browser ? platform + ', ' + browser : headers['user-agent'],
+ },
+ req: {getLocale: ctx.req.getLocale},
+ };
+
+ await Self.sendTemplate(params, 'auth-code', true);
+ }
+ };
+};
diff --git a/back/methods/vn-user/signIn.js b/back/methods/vn-user/signIn.js
deleted file mode 100644
index e52d68df5b..0000000000
--- a/back/methods/vn-user/signIn.js
+++ /dev/null
@@ -1,81 +0,0 @@
-const UserError = require('vn-loopback/util/user-error');
-
-module.exports = Self => {
- Self.remoteMethod('signIn', {
- description: 'Login a user with username/email and password',
- accepts: [
- {
- arg: 'user',
- type: 'String',
- description: 'The user name or email',
- http: {source: 'form'},
- required: true
- }, {
- arg: 'password',
- type: 'String',
- description: 'The password'
- }
- ],
- returns: {
- type: 'object',
- root: true
- },
- http: {
- path: `/signIn`,
- verb: 'POST'
- }
- });
-
- Self.signIn = async function(user, password) {
- const models = Self.app.models;
- const usesEmail = user.indexOf('@') !== -1;
- let token;
-
- const userInfo = usesEmail
- ? {email: user}
- : {username: user};
- const instance = await Self.findOne({
- fields: ['username', 'password'],
- where: userInfo
- });
-
- const where = usesEmail
- ? {email: user}
- : {name: user};
- const vnUser = await Self.findOne({
- fields: ['id', 'active', 'passExpired'],
- where
- });
-
- const today = Date.vnNew();
- today.setHours(0, 0, 0, 0);
-
- const validCredentials = instance
- && await instance.hasPassword(password);
-
- if (validCredentials) {
- if (!vnUser.active)
- throw new UserError('User disabled');
-
- if (vnUser.passExpired && vnUser.passExpired.getTime() <= today.getTime()) {
- const changePasswordToken = await models.AccessToken.create({
- scopes: ['change-password'],
- userId: vnUser.id
- });
- const err = new UserError('Pass expired', 'passExpired');
- err.details = {token: changePasswordToken};
- throw err;
- }
-
- try {
- await models.Account.sync(instance.username, password);
- } catch (err) {
- console.warn(err);
- }
- }
-
- let loginInfo = Object.assign({password}, userInfo);
- token = await Self.login(loginInfo, 'user');
- return {token: token.id, ttl: token.ttl};
- };
-};
diff --git a/back/methods/vn-user/specs/sign-in.spec.js b/back/methods/vn-user/specs/sign-in.spec.js
new file mode 100644
index 0000000000..f4cad88b9c
--- /dev/null
+++ b/back/methods/vn-user/specs/sign-in.spec.js
@@ -0,0 +1,101 @@
+const {models} = require('vn-loopback/server/server');
+
+describe('VnUser Sign-in()', () => {
+ const employeeId = 1;
+ const unauthCtx = {
+ req: {
+ headers: {},
+ connection: {
+ remoteAddress: '127.0.0.1'
+ },
+ getLocale: () => 'en'
+ },
+ args: {}
+ };
+ const {VnUser, AccessToken} = models;
+ describe('when credentials are correct', () => {
+ it('should return the token', async() => {
+ let login = await VnUser.signIn(unauthCtx, 'salesAssistant', 'nightmare');
+ let accessToken = await AccessToken.findById(login.token);
+ let ctx = {req: {accessToken: accessToken}};
+
+ expect(login.token).toBeDefined();
+
+ await VnUser.logout(ctx.req.accessToken.id);
+ });
+
+ it('should return the token if the user doesnt exist but the client does', async() => {
+ let login = await VnUser.signIn(unauthCtx, 'PetterParker', 'nightmare');
+ let accessToken = await AccessToken.findById(login.token);
+ let ctx = {req: {accessToken: accessToken}};
+
+ expect(login.token).toBeDefined();
+
+ await VnUser.logout(ctx.req.accessToken.id);
+ });
+ });
+
+ describe('when credentials are incorrect', () => {
+ it('should throw a 401 error', async() => {
+ let error;
+
+ try {
+ await VnUser.signIn(unauthCtx, 'IDontExist', 'TotallyWrongPassword');
+ } catch (e) {
+ error = e;
+ }
+
+ expect(error).toBeDefined();
+ expect(error.statusCode).toBe(401);
+ expect(error.code).toBe('LOGIN_FAILED');
+ });
+ });
+
+ describe('when two-factor auth is required', () => {
+ it('should throw a 403 error', async() => {
+ const employee = await VnUser.findById(employeeId);
+ const tx = await VnUser.beginTransaction({});
+
+ let error;
+ try {
+ const options = {transaction: tx};
+ await employee.updateAttribute('twoFactor', 'email', options);
+
+ await VnUser.signIn(unauthCtx, 'employee', 'nightmare', options);
+ await tx.rollback();
+ } catch (e) {
+ await tx.rollback();
+ error = e;
+ }
+
+ expect(error).toBeDefined();
+ expect(error.statusCode).toBe(403);
+ expect(error.code).toBe('REQUIRES_2FA');
+ });
+ });
+
+ describe('when passExpired', () => {
+ it('should throw a passExpired error', async() => {
+ const tx = await VnUser.beginTransaction({});
+ const employee = await VnUser.findById(employeeId);
+ const yesterday = Date.vnNew();
+ yesterday.setDate(yesterday.getDate() - 1);
+
+ let error;
+ try {
+ const options = {transaction: tx};
+ await employee.updateAttribute('passExpired', yesterday, options);
+
+ await VnUser.signIn(unauthCtx, 'employee', 'nightmare', options);
+ await tx.rollback();
+ } catch (e) {
+ await tx.rollback();
+ error = e;
+ }
+
+ expect(error).toBeDefined();
+ expect(error.statusCode).toBe(400);
+ expect(error.message).toBe('Pass expired');
+ });
+ });
+});
diff --git a/back/methods/vn-user/specs/signIn.spec.js b/back/methods/vn-user/specs/signIn.spec.js
deleted file mode 100644
index c3f4630c63..0000000000
--- a/back/methods/vn-user/specs/signIn.spec.js
+++ /dev/null
@@ -1,41 +0,0 @@
-const {models} = require('vn-loopback/server/server');
-
-describe('VnUser signIn()', () => {
- describe('when credentials are correct', () => {
- it('should return the token', async() => {
- let login = await models.VnUser.signIn('salesAssistant', 'nightmare');
- let accessToken = await models.AccessToken.findById(login.token);
- let ctx = {req: {accessToken: accessToken}};
-
- expect(login.token).toBeDefined();
-
- await models.VnUser.logout(ctx.req.accessToken.id);
- });
-
- it('should return the token if the user doesnt exist but the client does', async() => {
- let login = await models.VnUser.signIn('PetterParker', 'nightmare');
- let accessToken = await models.AccessToken.findById(login.token);
- let ctx = {req: {accessToken: accessToken}};
-
- expect(login.token).toBeDefined();
-
- await models.VnUser.logout(ctx.req.accessToken.id);
- });
- });
-
- describe('when credentials are incorrect', () => {
- it('should throw a 401 error', async() => {
- let error;
-
- try {
- await models.VnUser.signIn('IDontExist', 'TotallyWrongPassword');
- } catch (e) {
- error = e;
- }
-
- expect(error).toBeDefined();
- expect(error.statusCode).toBe(401);
- expect(error.code).toBe('LOGIN_FAILED');
- });
- });
-});
diff --git a/back/methods/vn-user/specs/validate-auth.spec.js b/back/methods/vn-user/specs/validate-auth.spec.js
new file mode 100644
index 0000000000..8018bd3e1d
--- /dev/null
+++ b/back/methods/vn-user/specs/validate-auth.spec.js
@@ -0,0 +1,52 @@
+const {models} = require('vn-loopback/server/server');
+
+describe('VnUser validate-auth()', () => {
+ describe('validateAuth', () => {
+ it('should signin if data is correct', async() => {
+ await models.AuthCode.create({
+ userFk: 9,
+ code: '555555',
+ expires: Date.vnNow() + (60 * 1000)
+ });
+ const token = await models.VnUser.validateAuth('developer', 'nightmare', '555555');
+
+ expect(token.token).toBeDefined();
+ });
+ });
+
+ describe('validateCode', () => {
+ it('should throw an error for a non existent code', async() => {
+ let error;
+ try {
+ await models.VnUser.validateCode('developer', '123456');
+ } catch (e) {
+ error = e;
+ }
+
+ expect(error).toBeDefined();
+ expect(error.statusCode).toBe(400);
+ expect(error.message).toEqual('Invalid or expired verification code');
+ });
+
+ it('should throw an error when a code doesn`t match the login username', async() => {
+ let error;
+ let authCode;
+ try {
+ authCode = await models.AuthCode.create({
+ userFk: 1,
+ code: '555555',
+ expires: Date.vnNow() + (60 * 1000)
+ });
+
+ await models.VnUser.validateCode('developer', '555555');
+ } catch (e) {
+ authCode && await authCode.destroy();
+ error = e;
+ }
+
+ expect(error).toBeDefined();
+ expect(error.statusCode).toBe(400);
+ expect(error.message).toEqual('Authentication failed');
+ });
+ });
+});
diff --git a/back/methods/vn-user/validate-auth.js b/back/methods/vn-user/validate-auth.js
new file mode 100644
index 0000000000..beab43417a
--- /dev/null
+++ b/back/methods/vn-user/validate-auth.js
@@ -0,0 +1,66 @@
+const UserError = require('vn-loopback/util/user-error');
+
+module.exports = Self => {
+ Self.remoteMethod('validateAuth', {
+ description: 'Login a user with username/email and password',
+ accepts: [
+ {
+ arg: 'user',
+ type: 'String',
+ description: 'The user name or email',
+ required: true
+ },
+ {
+ arg: 'password',
+ type: 'String',
+ description: 'The password'
+ },
+ {
+ arg: 'code',
+ type: 'String',
+ description: 'The auth code'
+ }
+ ],
+ returns: {
+ type: 'object',
+ root: true
+ },
+ http: {
+ path: `/validate-auth`,
+ verb: 'POST'
+ }
+ });
+
+ Self.validateAuth = async(username, password, code, options) => {
+ const myOptions = {};
+ if (typeof options == 'object')
+ Object.assign(myOptions, options);
+
+ const token = Self.validateLogin(username, password);
+ await Self.validateCode(username, code, myOptions);
+ return token;
+ };
+
+ Self.validateCode = async(username, code, myOptions) => {
+ const {AuthCode} = Self.app.models;
+
+ const authCode = await AuthCode.findOne({
+ where: {
+ code: code
+ }
+ }, myOptions);
+
+ const expired = authCode && Date.vnNow() > authCode.expires;
+ if (!authCode || expired)
+ throw new UserError('Invalid or expired verification code');
+
+ const user = await Self.findById(authCode.userFk, {
+ fields: ['name', 'twoFactor']
+ }, myOptions);
+
+ if (user.name !== username)
+ throw new UserError('Authentication failed');
+
+ await authCode.destroy(myOptions);
+ };
+};
diff --git a/back/model-config.json b/back/model-config.json
index d945f32508..0e37bf5278 100644
--- a/back/model-config.json
+++ b/back/model-config.json
@@ -1,7 +1,4 @@
{
- "AccountingType": {
- "dataSource": "vn"
- },
"AccessTokenConfig": {
"dataSource": "vn",
"options": {
@@ -10,6 +7,12 @@
}
}
},
+ "AccountingType": {
+ "dataSource": "vn"
+ },
+ "AuthCode": {
+ "dataSource": "vn"
+ },
"Bank": {
"dataSource": "vn"
},
diff --git a/back/models/auth-code.json b/back/models/auth-code.json
new file mode 100644
index 0000000000..b6a89115fa
--- /dev/null
+++ b/back/models/auth-code.json
@@ -0,0 +1,31 @@
+{
+ "name": "AuthCode",
+ "base": "VnModel",
+ "options": {
+ "mysql": {
+ "table": "salix.authCode"
+ }
+ },
+ "properties": {
+ "userFk": {
+ "type": "number",
+ "required": true,
+ "id": true
+ },
+ "code": {
+ "type": "string",
+ "required": true
+ },
+ "expires": {
+ "type": "number",
+ "required": true
+ }
+ },
+ "relations": {
+ "user": {
+ "type": "belongsTo",
+ "model": "Account",
+ "foreignKey": "userFk"
+ }
+ }
+}
diff --git a/back/models/vn-user.js b/back/models/vn-user.js
index b58395acc8..a7ce120735 100644
--- a/back/models/vn-user.js
+++ b/back/models/vn-user.js
@@ -5,11 +5,12 @@ const {Email} = require('vn-print');
module.exports = function(Self) {
vnModel(Self);
- require('../methods/vn-user/signIn')(Self);
+ require('../methods/vn-user/sign-in')(Self);
require('../methods/vn-user/acl')(Self);
require('../methods/vn-user/recover-password')(Self);
require('../methods/vn-user/validate-token')(Self);
require('../methods/vn-user/privileges')(Self);
+ require('../methods/vn-user/validate-auth')(Self);
require('../methods/vn-user/renew-token')(Self);
Self.definition.settings.acls = Self.definition.settings.acls.filter(acl => acl.property !== 'create');
@@ -111,6 +112,18 @@ module.exports = function(Self) {
return email.send();
});
+ Self.validateLogin = async function(user, password) {
+ let loginInfo = Object.assign({password}, Self.userUses(user));
+ token = await Self.login(loginInfo, 'user');
+ return {token: token.id, ttl: token.ttl};
+ };
+
+ Self.userUses = function(user) {
+ return user.indexOf('@') !== -1
+ ? {email: user}
+ : {username: user};
+ };
+
const _setPassword = Self.prototype.setPassword;
Self.prototype.setPassword = async function(newPassword, options, cb) {
if (cb === undefined && typeof options === 'function') {
@@ -143,8 +156,9 @@ module.exports = function(Self) {
}
};
- Self.sharedClass._methods.find(method => method.name == 'changePassword')
- .accessScopes = ['change-password'];
+ Self.sharedClass._methods.find(method => method.name == 'changePassword').ctor.settings.acls =
+ Self.sharedClass._methods.find(method => method.name == 'changePassword').ctor.settings.acls
+ .filter(acl => acl.property != 'changePassword');
// FIXME: https://redmine.verdnatura.es/issues/5761
// Self.afterRemote('prototype.patchAttributes', async(ctx, instance) => {
diff --git a/back/models/vn-user.json b/back/models/vn-user.json
index 61e42f77ab..9131c9134a 100644
--- a/back/models/vn-user.json
+++ b/back/models/vn-user.json
@@ -59,7 +59,10 @@
},
"passExpired": {
"type": "date"
- }
+ },
+ "twoFactor": {
+ "type": "string"
+ }
},
"relations": {
"role": {
@@ -111,6 +114,13 @@
"principalId": "$authenticated",
"permission": "ALLOW"
},
+ {
+ "property": "validateAuth",
+ "accessType": "EXECUTE",
+ "principalType": "ROLE",
+ "principalId": "$everyone",
+ "permission": "ALLOW"
+ },
{
"property": "privileges",
"accessType": "*",
diff --git a/db/changes/232801/00-authCode.sql b/db/changes/232801/00-authCode.sql
new file mode 100644
index 0000000000..a256db43f5
--- /dev/null
+++ b/db/changes/232801/00-authCode.sql
@@ -0,0 +1,13 @@
+create table `salix`.`authCode`
+(
+ userFk int UNSIGNED not null,
+ code int not null,
+ expires bigint not null,
+ constraint authCode_pk
+ primary key (userFk),
+ constraint authCode_unique
+ unique (code),
+ constraint authCode_user_id_fk
+ foreign key (userFk) references `account`.`user` (id)
+ on update cascade on delete cascade
+);
diff --git a/db/changes/232801/00-department.sql b/db/changes/232801/00-department.sql
new file mode 100644
index 0000000000..d9a91ee30b
--- /dev/null
+++ b/db/changes/232801/00-department.sql
@@ -0,0 +1,24 @@
+alter table `vn`.`department`
+ add `twoFactor` ENUM ('email') null comment 'Default user two-factor auth type';
+
+drop trigger `vn`.`department_afterUpdate`;
+
+DELIMITER $$
+$$
+create definer = root@localhost trigger department_afterUpdate
+ after update
+ on department
+ for each row
+BEGIN
+ IF !(OLD.parentFk <=> NEW.parentFk) THEN
+ UPDATE vn.department_recalc SET isChanged = TRUE;
+ END IF;
+
+ IF !(OLD.twoFactor <=> NEW.twoFactor) THEN
+ UPDATE account.user u
+ JOIN vn.workerDepartment wd ON wd.workerFk = u.id
+ SET u.twoFactor = NEW.twoFactor
+ WHERE wd.departmentFk = NEW.id;
+ END IF;
+END;$$
+DELIMITER ;
diff --git a/db/changes/232801/00-user.sql b/db/changes/232801/00-user.sql
new file mode 100644
index 0000000000..376b3dbb15
--- /dev/null
+++ b/db/changes/232801/00-user.sql
@@ -0,0 +1,5 @@
+alter table `account`.`user`
+ add `twoFactor` ENUM ('email') null comment 'Two-factor auth type';
+
+DELETE FROM `salix`.`ACL`
+ WHERE model = 'VnUser' AND property = 'changePassword';
diff --git a/db/dump/fixtures.sql b/db/dump/fixtures.sql
index c02e3dce4c..14c9fba5d2 100644
--- a/db/dump/fixtures.sql
+++ b/db/dump/fixtures.sql
@@ -77,7 +77,10 @@ INSERT INTO `account`.`user`(`id`,`name`, `nickname`, `role`,`active`,`email`, `
ORDER BY id;
INSERT INTO `account`.`account`(`id`)
- SELECT id FROM `account`.`user`;
+ SELECT `u`.`id`
+ FROM `account`.`user` `u`
+ JOIN `account`.`role` `r` ON `u`.`role` = `r`.`id`
+ WHERE `r`.`name` <> 'customer';
INSERT INTO `vn`.`educationLevel` (`id`, `name`)
VALUES
@@ -2849,8 +2852,8 @@ INSERT INTO `vn`.`profileType` (`id`, `name`)
INSERT INTO `salix`.`url` (`appName`, `environment`, `url`)
VALUES
- ('lilium', 'dev', 'http://localhost:9000/#/'),
- ('salix', 'dev', 'http://localhost:5000/#!/');
+ ('lilium', 'development', 'http://localhost:9000/#/'),
+ ('salix', 'development', 'http://localhost:5000/#!/');
INSERT INTO `vn`.`report` (`id`, `name`, `paperSizeFk`, `method`)
VALUES
diff --git a/e2e/paths/01-salix/05_changePassword.spec.js b/e2e/paths/01-salix/05_changePassword.spec.js
index 6e4cfb7f38..950f773ddd 100644
--- a/e2e/paths/01-salix/05_changePassword.spec.js
+++ b/e2e/paths/01-salix/05_changePassword.spec.js
@@ -16,6 +16,7 @@ describe('ChangePassword path', async() => {
await browser.close();
});
+ const badPassword = 'badpass';
const oldPassword = 'nightmare';
const newPassword = 'newPass.1234';
describe('Bad login', async() => {
@@ -37,13 +38,22 @@ describe('ChangePassword path', async() => {
expect(message.text).toContain('Invalid current password');
// Bad attempt: password not meet requirements
+ message = await page.sendForm($.form, {
+ oldPassword: oldPassword,
+ newPassword: badPassword,
+ repeatPassword: badPassword
+ });
+
+ expect(message.text).toContain('Password does not meet requirements');
+
+ // Bad attempt: same password
message = await page.sendForm($.form, {
oldPassword: oldPassword,
newPassword: oldPassword,
repeatPassword: oldPassword
});
- expect(message.text).toContain('Password does not meet requirements');
+ expect(message.text).toContain('You can not use the same password');
// Correct attempt: change password
message = await page.sendForm($.form, {
diff --git a/front/core/services/auth.js b/front/core/services/auth.js
index 92ff4b0611..844a5145d8 100644
--- a/front/core/services/auth.js
+++ b/front/core/services/auth.js
@@ -24,7 +24,7 @@ export default class Auth {
initialize() {
let criteria = {
to: state => {
- const outLayout = ['login', 'recover-password', 'reset-password', 'change-password'];
+ const outLayout = ['login', 'recover-password', 'reset-password', 'change-password', 'validate-email'];
return !outLayout.some(ol => ol == state.name);
}
};
@@ -60,7 +60,25 @@ export default class Auth {
};
const now = new Date();
- return this.$http.post('VnUsers/signIn', params)
+ return this.$http.post('VnUsers/sign-in', params).then(
+ json => this.onLoginOk(json, now, remember));
+ }
+
+ validateCode(user, password, code, remember) {
+ if (!user) {
+ let err = new UserError('Please enter your username');
+ err.code = 'EmptyLogin';
+ return this.$q.reject(err);
+ }
+
+ let params = {
+ user: user,
+ password: password || undefined,
+ code: code
+ };
+
+ const now = new Date();
+ return this.$http.post('VnUsers/validate-auth', params)
.then(json => this.onLoginOk(json, now, remember));
}
diff --git a/front/core/services/token.js b/front/core/services/token.js
index 8f9f80e5c7..c4b644a897 100644
--- a/front/core/services/token.js
+++ b/front/core/services/token.js
@@ -34,7 +34,6 @@ export default class Token {
remember
});
this.vnInterceptor.setToken(token);
-
try {
if (remember)
this.setStorage(localStorage, token, created, ttl);
diff --git a/front/salix/components/change-password/index.html b/front/salix/components/change-password/index.html
index 8d338d4118..04f66976e8 100644
--- a/front/salix/components/change-password/index.html
+++ b/front/salix/components/change-password/index.html
@@ -21,6 +21,14 @@
type="password"
autocomplete="false">
+