Skip to content
This repository was archived by the owner on Apr 5, 2024. It is now read-only.

Commit 0ecaab8

Browse files
authored
FF-173 - Password Hashing (#108)
* added password hashing and updated unit tests. * Fixed Integrationtests * changed plain text passwords to hashed versions * added salt to pws in PrepareDataBase * updated date of last change and url to blog
1 parent 53fb6a4 commit 0ecaab8

File tree

16 files changed

+272
-179
lines changed

16 files changed

+272
-179
lines changed

pom.xml

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,18 @@
3939
<optional>true</optional>
4040
</dependency>
4141

42+
<dependency>
43+
<groupId>org.springframework.boot</groupId>
44+
<artifactId>spring-boot</artifactId>
45+
<version>2.4.4</version>
46+
</dependency>
47+
48+
<dependency>
49+
<groupId>org.springframework.security</groupId>
50+
<artifactId>spring-security-crypto</artifactId>
51+
<version>5.3.4.RELEASE</version>
52+
</dependency>
53+
4254
<dependency>
4355
<groupId>org.projectlombok</groupId>
4456
<artifactId>lombok</artifactId>
@@ -122,11 +134,6 @@
122134
</exclusion>
123135
</exclusions>
124136
</dependency>
125-
<dependency>
126-
<groupId>org.springframework.boot</groupId>
127-
<artifactId>spring-boot</artifactId>
128-
<version>2.4.4</version>
129-
</dependency>
130137
<!-- TESTING -->
131138

132139
</dependencies>

src/main/java/de/filefighter/rest/configuration/PrepareDataBase.java

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import org.springframework.context.annotation.Configuration;
1818
import org.springframework.context.annotation.Profile;
1919
import org.springframework.core.env.Environment;
20+
import org.springframework.security.crypto.password.PasswordEncoder;
2021

2122
import java.time.Instant;
2223
import java.util.ArrayList;
@@ -66,7 +67,7 @@ CommandLineRunner veryImportantFileFighterStartScript(Environment environment) {
6667
System.out.println("Running on http://localhost:" + serverPort);
6768
System.out.println();
6869
System.out.println("Developed by Gimleux, Valentin, Open-Schnick.");
69-
System.out.println("Development Blog: https://blog.filefighter.de");
70+
System.out.println("Development Blog: https://filefighter.de");
7071
System.out.println("The code can be found at: https://www.github.com/filefighter");
7172
System.out.println();
7273
System.out.println("-------------------------------< REST API >-------------------------------");
@@ -76,14 +77,14 @@ CommandLineRunner veryImportantFileFighterStartScript(Environment environment) {
7677

7778
@Bean
7879
@Profile({"prod", "stage"})
79-
CommandLineRunner initDataBaseProd(UserRepository userRepository, FileSystemRepository fileSystemRepository, AccessTokenRepository accessTokenRepository) {
80+
CommandLineRunner initDataBaseProd(UserRepository userRepository, FileSystemRepository fileSystemRepository, AccessTokenRepository accessTokenRepository, PasswordEncoder passwordEncoder) {
8081
return args -> {
8182
ArrayList<UserEntity> foundUsers = (ArrayList<UserEntity>) userRepository.findAll();
8283
ArrayList<UserEntity> foundFileSystemEntities = (ArrayList<UserEntity>) userRepository.findAll();
8384
accessTokenRepository.deleteAll(); // Cleanup purposes.
8485

8586
if (foundUsers.isEmpty() && foundFileSystemEntities.isEmpty()) {
86-
addDefaultAdminAndRuntimeUser(userRepository);
87+
addDefaultAdminAndRuntimeUser(userRepository, passwordEncoder);
8788
log.info("Inserting Home directories and default structure: {} {}.", fileSystemRepository.save(FileSystemEntity
8889
.builder()
8990
.lastUpdatedBy(RUNTIME_USER_ID)
@@ -132,7 +133,7 @@ CommandLineRunner initDataBaseProd(UserRepository userRepository, FileSystemRepo
132133

133134
@Bean
134135
@Profile({"dev", "debug"})
135-
CommandLineRunner initDataBaseDev(UserRepository userRepository, AccessTokenRepository accessTokenRepository, FileSystemRepository fileSystemRepository) {
136+
CommandLineRunner initDataBaseDev(UserRepository userRepository, AccessTokenRepository accessTokenRepository, FileSystemRepository fileSystemRepository, PasswordEncoder passwordEncoder) {
136137
return args -> {
137138
log.info("Starting with clean user collection.");
138139
userRepository.deleteAll();
@@ -141,7 +142,7 @@ CommandLineRunner initDataBaseDev(UserRepository userRepository, AccessTokenRepo
141142
log.info("Starting with clean accessToken collection.");
142143
accessTokenRepository.deleteAll();
143144

144-
addDevUsers(userRepository);
145+
addDevUsers(userRepository, passwordEncoder);
145146
addTestingFileSystemItems(fileSystemRepository);
146147

147148
log.info("Inserting default tokens: {} {}",
@@ -183,7 +184,7 @@ CommandLineRunner finishDatabaseWork(IdGenerationService idGenerationService) {
183184
return args -> idGenerationService.initializeService();
184185
}
185186

186-
private void addDevUsers(UserRepository userRepository) {
187+
private void addDevUsers(UserRepository userRepository, PasswordEncoder passwordEncoder) {
187188
log.info("Inserting system runtime user. {}", userRepository.save(UserEntity
188189
.builder()
189190
.userId(RUNTIME_USER_ID)
@@ -200,7 +201,7 @@ private void addDevUsers(UserRepository userRepository) {
200201
.userId(1)
201202
.username("user")
202203
.lowercaseUsername("user")
203-
.password("1234")
204+
.password(passwordEncoder.encode("D3500EF92337ED226F500EE57084D8FEEE559D0E411A635BC861DFD8159C0FBC")) // 1234 with salt
204205
.refreshToken("rft1234")
205206
.groupIds(new long[]{ADMIN.getGroupId()})
206207
.build()),
@@ -209,13 +210,13 @@ private void addDevUsers(UserRepository userRepository) {
209210
.userId(2)
210211
.username("user1")
211212
.lowercaseUsername("user1")
212-
.password("12345")
213+
.password(passwordEncoder.encode("6216104FA48274A78E291166EA2083E2EAAA5F4F200B2A8E83EE5B30D18019F0")) // 12345
213214
.refreshToken("rft")
214215
.groupIds(new long[]{FAMILY.getGroupId()})
215216
.build()));
216217
}
217218

218-
private void addDefaultAdminAndRuntimeUser(UserRepository userRepository) {
219+
private void addDefaultAdminAndRuntimeUser(UserRepository userRepository, PasswordEncoder passwordEncoder) {
219220
log.info("Database seems to be empty. Creating new default entities...");
220221
log.info("Inserting system runtime user: {}", userRepository.save(UserEntity
221222
.builder()
@@ -231,7 +232,7 @@ private void addDefaultAdminAndRuntimeUser(UserRepository userRepository) {
231232
.userId(1)
232233
.username("Admin")
233234
.lowercaseUsername("admin")
234-
.password("admin")
235+
.password(passwordEncoder.encode("B6381E537A537181763A1A0E4CB00F6E70B57E01F18A2BF9AB602783BF8C22B3")) // admin
235236
.refreshToken("rft1234")
236237
.groupIds(new long[]{ADMIN.getGroupId()})
237238
.build()));
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package de.filefighter.rest.configuration;
2+
3+
import org.springframework.context.annotation.Bean;
4+
import org.springframework.context.annotation.Configuration;
5+
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
6+
import org.springframework.security.crypto.password.PasswordEncoder;
7+
8+
@Configuration
9+
public class Security {
10+
11+
@Bean
12+
public PasswordEncoder encoder() {
13+
return new BCryptPasswordEncoder();
14+
}
15+
}

src/main/java/de/filefighter/rest/domain/authentication/AuthenticationBusinessService.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import de.filefighter.rest.domain.user.exceptions.UserNotAuthenticatedException;
1111
import de.filefighter.rest.domain.user.group.Group;
1212
import lombok.extern.log4j.Log4j2;
13+
import org.springframework.security.crypto.password.PasswordEncoder;
1314
import org.springframework.stereotype.Service;
1415

1516
import java.nio.charset.StandardCharsets;
@@ -22,11 +23,13 @@ public class AuthenticationBusinessService {
2223
private final UserRepository userRepository;
2324
private final UserDTOService userDtoService;
2425
private final InputSanitizerService inputSanitizerService;
26+
private final PasswordEncoder passwordEncoder;
2527

26-
public AuthenticationBusinessService(UserRepository userRepository, UserDTOService userDtoService, InputSanitizerService inputSanitizerService) {
28+
public AuthenticationBusinessService(UserRepository userRepository, UserDTOService userDtoService, InputSanitizerService inputSanitizerService, PasswordEncoder passwordEncoder) {
2729
this.userRepository = userRepository;
2830
this.userDtoService = userDtoService;
2931
this.inputSanitizerService = inputSanitizerService;
32+
this.passwordEncoder = passwordEncoder;
3033
}
3134

3235
public User authenticateUserWithUsernameAndPassword(String base64encodedUserAndPassword) {
@@ -47,10 +50,16 @@ public User authenticateUserWithUsernameAndPassword(String base64encodedUserAndP
4750
String lowerCaseUsername = inputSanitizerService.sanitizeString(split[0].toLowerCase());
4851
String password = inputSanitizerService.sanitizeString(split[1]);
4952

50-
UserEntity userEntity = userRepository.findByLowercaseUsernameAndPassword(lowerCaseUsername, password);
53+
if (!inputSanitizerService.passwordIsValid(password))
54+
throw new UserNotAuthenticatedException("The password didnt match requirenments, please hash the password with SHA-256.");
55+
56+
UserEntity userEntity = userRepository.findByLowercaseUsername(lowerCaseUsername);
5157
if (null == userEntity)
5258
throw new UserNotAuthenticatedException("No User found with this username and password.");
5359

60+
if (!passwordEncoder.matches(password, userEntity.getPassword()))
61+
throw new UserNotAuthenticatedException("No User found with this username and password.");
62+
5463
return userDtoService.createDto(userEntity);
5564
}
5665

src/main/java/de/filefighter/rest/domain/common/InputSanitizerService.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import de.filefighter.rest.domain.common.exceptions.RequestDidntMeetFormalRequirementsException;
44
import de.filefighter.rest.domain.filesystem.data.dto.upload.FileSystemUpload;
5+
import org.springframework.beans.factory.annotation.Value;
56
import org.springframework.stereotype.Service;
67

78
import java.util.regex.Matcher;
@@ -10,6 +11,9 @@
1011
@Service
1112
public class InputSanitizerService {
1213

14+
@Value("${filefighter.disable-password-check}")
15+
private boolean passwordCheckDisabled;
16+
1317
public static boolean stringIsValid(String s) {
1418
return !(null == s || s.isEmpty() || s.isBlank());
1519
}
@@ -51,6 +55,14 @@ public String sanitizeRequestHeader(String header, String testString) {
5155
return split[1];
5256
}
5357

58+
public boolean passwordIsValid(String password) {
59+
if (this.passwordCheckDisabled)
60+
return true;
61+
62+
Pattern pattern = Pattern.compile("\\b[A-Fa-f0-9]{64}\\b");
63+
return pattern.matcher(password).matches();
64+
}
65+
5466
public boolean pathIsValid(String path) {
5567
String validString = sanitizeString(path);
5668

src/main/java/de/filefighter/rest/domain/user/business/UserBusinessService.java

Lines changed: 21 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package de.filefighter.rest.domain.user.business;
22

3+
import de.filefighter.rest.domain.common.InputSanitizerService;
34
import de.filefighter.rest.domain.token.business.AccessTokenBusinessService;
45
import de.filefighter.rest.domain.token.data.dto.RefreshToken;
56
import de.filefighter.rest.domain.user.data.dto.User;
@@ -11,16 +12,15 @@
1112
import de.filefighter.rest.domain.user.exceptions.UserNotUpdatedException;
1213
import de.filefighter.rest.domain.user.group.Group;
1314
import de.filefighter.rest.domain.user.group.GroupRepository;
14-
import org.springframework.beans.factory.annotation.Value;
1515
import org.springframework.data.mongodb.core.MongoTemplate;
1616
import org.springframework.data.mongodb.core.query.Criteria;
1717
import org.springframework.data.mongodb.core.query.Query;
1818
import org.springframework.data.mongodb.core.query.Update;
19+
import org.springframework.security.crypto.password.PasswordEncoder;
1920
import org.springframework.stereotype.Service;
2021

2122
import java.security.SecureRandom;
2223
import java.util.Arrays;
23-
import java.util.regex.Pattern;
2424

2525
import static de.filefighter.rest.domain.common.InputSanitizerService.stringIsValid;
2626

@@ -32,14 +32,16 @@ public class UserBusinessService {
3232
private final UserDTOService userDtoService;
3333
private final GroupRepository groupRepository;
3434
private final MongoTemplate mongoTemplate;
35-
@Value("${filefighter.disable-password-check}")
36-
public boolean passwordCheckDisabled;
35+
private final InputSanitizerService inputSanitizerService;
36+
private final PasswordEncoder passwordEncoder;
3737

38-
public UserBusinessService(UserRepository userRepository, UserDTOService userDtoService, GroupRepository groupRepository, MongoTemplate mongoTemplate) {
38+
public UserBusinessService(UserRepository userRepository, UserDTOService userDtoService, GroupRepository groupRepository, MongoTemplate mongoTemplate, InputSanitizerService inputSanitizerService, PasswordEncoder passwordEncoder) {
3939
this.userRepository = userRepository;
4040
this.userDtoService = userDtoService;
4141
this.groupRepository = groupRepository;
4242
this.mongoTemplate = mongoTemplate;
43+
this.inputSanitizerService = inputSanitizerService;
44+
this.passwordEncoder = passwordEncoder;
4345
}
4446

4547
public long getUserCount() {
@@ -96,15 +98,12 @@ public UserEntity registerNewUser(UserRegisterForm newUser) {
9698
if (!stringIsValid(password) || !stringIsValid(confirmationPassword))
9799
throw new UserNotRegisteredException("Wanted to change password, but password was not valid.");
98100

99-
if (!passwordIsValid(password))
100-
throw new UserNotRegisteredException("Password needs to be at least 8 characters long and, contains at least one uppercase and lowercase letter and a number.");
101+
if (!inputSanitizerService.passwordIsValid(password))
102+
throw new UserNotRegisteredException("Password needs to be a valid SHA-265 hash.");
101103

102104
if (!password.contentEquals(confirmationPassword))
103105
throw new UserNotRegisteredException("Passwords do not match.");
104106

105-
if (password.toLowerCase().contains(username.toLowerCase()))
106-
throw new UserNotRegisteredException("Username must not appear in password.");
107-
108107
//check groups
109108
long[] userGroups = newUser.getGroupIds();
110109
if (null == userGroups)
@@ -121,25 +120,20 @@ public UserEntity registerNewUser(UserRegisterForm newUser) {
121120
}
122121
}
123122

123+
// hash password.
124+
String hashedPassword = passwordEncoder.encode(password);
125+
124126
//create new user.
125127
return userRepository.save(UserEntity.builder()
126128
.lowercaseUsername(username.toLowerCase())
127129
.username(username)
128130
.groupIds(userGroups)
129-
.password(password)
131+
.password(hashedPassword)
130132
.refreshToken(AccessTokenBusinessService.generateRandomTokenValue())
131133
.userId(generateRandomUserId())
132134
.build());
133135
}
134136

135-
public boolean passwordIsValid(String password) {
136-
if (this.passwordCheckDisabled)
137-
return true;
138-
139-
Pattern pattern = Pattern.compile("^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=\\S+).{8,20}$");
140-
return pattern.matcher(password).matches();
141-
}
142-
143137
/**
144138
* @param username username to find.
145139
* @return null or the found user.
@@ -164,17 +158,14 @@ public void updateUser(long userId, UserRegisterForm userToUpdate, User authenti
164158
if (null == userEntityToUpdate)
165159
throw new UserNotUpdatedException("User does not exist, use register endpoint.");
166160

167-
if (Arrays.stream(userEntityToUpdate.getGroupIds()).asDoubleStream().anyMatch(id -> id == Group.SYSTEM.getGroupId()))
161+
if (Arrays.stream(userEntityToUpdate.getGroupIds()).anyMatch(id -> id == Group.SYSTEM.getGroupId()))
168162
throw new UserNotUpdatedException("Runtime users cannot be modified.");
169163

170164
Update newUpdate = new Update();
171165

172166
boolean changesWereMade = updateUserName(newUpdate, userToUpdate.getUsername());
173-
boolean usernameIsValid = stringIsValid(userToUpdate.getUsername());
174-
String lowerCaseUsername = usernameIsValid ? userToUpdate.getUsername().toLowerCase() : userEntityToUpdate.getLowercaseUsername();
175-
boolean passwordWasUpdated = updatePassword(newUpdate, userToUpdate.getPassword(), userToUpdate.getConfirmationPassword(), lowerCaseUsername);
167+
boolean passwordWasUpdated = updatePassword(newUpdate, userToUpdate.getPassword(), userToUpdate.getConfirmationPassword());
176168
changesWereMade = passwordWasUpdated || changesWereMade;
177-
178169
boolean userGroupsWereUpdated = updateGroups(newUpdate, userToUpdate.getGroupIds(), authenticatedUserIsAdmin);
179170
changesWereMade = userGroupsWereUpdated || changesWereMade;
180171

@@ -205,22 +196,22 @@ private boolean updateGroups(Update newUpdate, long[] groupIds, boolean authenti
205196
return false;
206197
}
207198

208-
private boolean updatePassword(Update newUpdate, String password, String confirmationPassword, String lowercaseUserName) {
199+
private boolean updatePassword(Update newUpdate, String password, String confirmationPassword) {
209200
if (null != password) {
210201

211202
if (!stringIsValid(password) || !stringIsValid(confirmationPassword))
212203
throw new UserNotUpdatedException("Wanted to change password, but password was not valid.");
213204

214-
if (!passwordIsValid(password))
215-
throw new UserNotUpdatedException("Password needs to be at least 8 characters long and, contains at least one uppercase and lowercase letter and a number.");
205+
if (!inputSanitizerService.passwordIsValid(password))
206+
throw new UserNotUpdatedException("Password needs to be a valid SHA-256 hash.");
216207

217208
if (!password.contentEquals(confirmationPassword))
218209
throw new UserNotUpdatedException("Passwords do not match.");
219210

220-
if (password.toLowerCase().contains(lowercaseUserName))
221-
throw new UserNotUpdatedException("Username must not appear in password.");
211+
// hash password.
212+
String hashedPassword = passwordEncoder.encode(password);
222213

223-
newUpdate.set("password", password);
214+
newUpdate.set("password", hashedPassword);
224215
//update refreshToken
225216
String newRefreshToken = AccessTokenBusinessService.generateRandomTokenValue();
226217
newUpdate.set("refreshToken", newRefreshToken);

src/main/java/de/filefighter/rest/domain/user/data/persistence/UserRepository.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
@Service
77
public interface UserRepository extends MongoRepository<UserEntity, String> {
88
UserEntity findByUserIdAndUsername(long userId, String username);
9-
UserEntity findByLowercaseUsernameAndPassword(String username, String password);
109
UserEntity findByRefreshToken(String refreshToken);
1110
UserEntity findByUserId(long userId);
1211
UserEntity findByLowercaseUsername(String lowercaseUsername);

src/main/resources/application.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,5 @@ spring.data.mongodb.port=20000
1717
spring.data.mongodb.auto-index-creation=true
1818
#------------------- Custom -----------------------
1919
filefighter.version=0.0.8
20-
filefighter.date=11.05.2021
20+
filefighter.date=16.05.2021
2121
filefighter.disable-password-check=false

0 commit comments

Comments
 (0)