- Added basic JWT auth logic

- Added user database management
- Added password hashing
- Added user config
This commit is contained in:
Max W. 2024-04-07 22:42:53 +02:00
parent a93288e31c
commit 4dd5093abc
7 changed files with 123 additions and 14 deletions

View File

@ -3,33 +3,43 @@ package de.w665.sharepulse.db;
import com.rethinkdb.RethinkDB; import com.rethinkdb.RethinkDB;
import com.rethinkdb.gen.exc.ReqlOpFailedError; import com.rethinkdb.gen.exc.ReqlOpFailedError;
import com.rethinkdb.net.Connection; import com.rethinkdb.net.Connection;
import de.w665.sharepulse.db.repo.UserRepository;
import de.w665.sharepulse.model.User;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy; import jakarta.annotation.PreDestroy;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.Optional;
@Slf4j @Slf4j
@Service @Service
public class RethinkDBService { public class RethinkDBService {
private final RethinkDBConfig config; private final RethinkDBConfig config;
private final RethinkDB r; private final RethinkDB r;
private final Connection connection; private final Connection connection;
private final UserRepository userRepository;
@Value("${sharepulse.auto-reset-on-startup}") @Value("${sharepulse.auto-reset-on-startup}")
private boolean autoResetOnStartup; private boolean autoResetOnStartup;
@Value("${sharepulse.management.user.username}")
private String defaultUsername;
@Value("${sharepulse.management.user.password}")
private String defaultPassword;
private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
@Autowired @Autowired
public RethinkDBService(RethinkDBConfig config, RethinkDBConnector connector) { public RethinkDBService(RethinkDBConfig config, RethinkDBConnector connector, UserRepository userRepository) {
this.config = config; this.config = config;
// mapping to private vars for easier access // mapping to private vars for easier access
this.r = connector.getR(); this.r = connector.getR();
this.connection = connector.getConnection(); this.connection = connector.getConnection();
this.userRepository = userRepository;
} }
@PostConstruct @PostConstruct
@ -81,9 +91,37 @@ public class RethinkDBService {
log.debug("Table 'expired_file_uploads' cleared successfully."); log.debug("Table 'expired_file_uploads' cleared successfully.");
} }
} }
// rethinkdb check if table users exists
try {
r.db(config.getDatabase()).tableCreate("users").run(connection).stream();
log.debug("Table 'users' created successfully.");
} catch (ReqlOpFailedError e) {
log.debug("Table 'users' already exists.");
if(autoResetOnStartup) {
log.debug("Clearing content...");
r.db(config.getDatabase()).table("users").delete().run(connection);
log.debug("Table 'users' cleared successfully.");
}
}
initializeAdminUser();
log.info("Database ready for operation!"); log.info("Database ready for operation!");
} }
private void initializeAdminUser() {
Optional<User> adminUser = userRepository.retrieveUserByUsername("admin");
if(adminUser.isEmpty()) {
User user = new User();
user.setUsername(defaultUsername);
user.setPassword(passwordEncoder.encode(defaultPassword));
user.setRole("admin");
userRepository.insertUser(user);
log.debug("Admin user created with default credentials. Username: admin, Password: admin");
}
}
@PreDestroy @PreDestroy
public void close() { public void close() {
if (connection != null) { if (connection != null) {

View File

@ -1,4 +1,41 @@
package de.w665.sharepulse.db.repo; package de.w665.sharepulse.db.repo;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.rethinkdb.RethinkDB;
import com.rethinkdb.net.Connection;
import de.w665.sharepulse.db.RethinkDBConfig;
import de.w665.sharepulse.db.RethinkDBConnector;
import de.w665.sharepulse.model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import java.util.*;
@Repository
public class UserRepository { public class UserRepository {
private final RethinkDB r;
private final Connection connection;
private final RethinkDBConfig config;
@Autowired
public UserRepository(RethinkDBConnector connector, RethinkDBConfig config) {
this.r = connector.getR();
this.connection = connector.getConnection();
this.config = config;
}
public Optional<User> retrieveUserByUsername(String username) {
try {
User user = r.db(config.getDatabase()).table("users")
.filter(r.hashMap("username", username))
.run(connection, User.class)
.next();
return Optional.ofNullable(user);
} catch (NoSuchElementException e) {
return Optional.empty();
}
}
public void insertUser(User user) {
r.db(config.getDatabase()).table("users").insert(user).run(connection);
}
} }

View File

@ -1,10 +1,12 @@
package de.w665.sharepulse.rest.mappings; package de.w665.sharepulse.rest.mappings;
import de.w665.sharepulse.rest.ro.AuthenticationRequest;
import de.w665.sharepulse.service.AuthenticationService; import de.w665.sharepulse.service.AuthenticationService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap; import java.util.HashMap;
@ -22,11 +24,13 @@ public class AuthenticationController {
} }
@PostMapping("/login") @PostMapping("/login")
public ResponseEntity<Object> createAuthenticationToken() { public ResponseEntity<Object> createAuthenticationToken(@RequestBody AuthenticationRequest authenticationRequest) {
String token = authenticationService.authenticate("test", "test"); log.debug("Received AuthenticationRequest for username: " + authenticationRequest.getUsername());
String token = authenticationService.authenticate(authenticationRequest.getUsername(), authenticationRequest.getPassword());
Map<String, Object> response = new HashMap<>(); Map<String, Object> response = new HashMap<>();
response.put("token", token); response.put("token", token);
response.put("success", token != null);
return new ResponseEntity<>(response, HttpStatus.OK); return new ResponseEntity<>(response, HttpStatus.OK);
} }

View File

@ -35,7 +35,7 @@ public class Upload extends ApiRestController {
@RequestParam(value = "singleDownload", defaultValue = "false") boolean singleDownload, @RequestParam(value = "singleDownload", defaultValue = "false") boolean singleDownload,
@RequestParam(value = "fileDescription", required = false) String fileDescription) { @RequestParam(value = "fileDescription", required = false) String fileDescription) {
// TODO: Handle shortStorage0 // TODO: Handle shortStorage
if (file.isEmpty()) { if (file.isEmpty()) {
log.debug("User tried to upload an empty file. IP: " + request.getRemoteAddr()); log.debug("User tried to upload an empty file. IP: " + request.getRemoteAddr());
@ -53,7 +53,7 @@ public class Upload extends ApiRestController {
return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST); return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
} }
log.debug("User uploaded file " + file.getOriginalFilename() + " from IP " + request.getRemoteAddr() + " successfully."); log.info("User uploaded file " + file.getOriginalFilename() + " from IP " + request.getRemoteAddr() + " successfully.");
Map<String, Object> response = new HashMap<>(); Map<String, Object> response = new HashMap<>();
response.put("fileId", fileUpload.getFileId()); response.put("fileId", fileUpload.getFileId());

View File

@ -0,0 +1,15 @@
package de.w665.sharepulse.rest.ro;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
@NoArgsConstructor
@Getter
@Setter
@ToString
public class AuthenticationRequest {
private String username;
private String password;
}

View File

@ -1,39 +1,51 @@
package de.w665.sharepulse.service; package de.w665.sharepulse.service;
import de.w665.sharepulse.db.repo.UserRepository;
import de.w665.sharepulse.model.User;
import io.jsonwebtoken.Jwts; import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys; import io.jsonwebtoken.security.Keys;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import javax.crypto.SecretKey; import javax.crypto.SecretKey;
import java.util.Base64; import java.util.Base64;
import java.util.Date; import java.util.Date;
import java.util.Optional;
@Slf4j @Slf4j
@Service @Service
public class AuthenticationService { public class AuthenticationService {
private final UserRepository userRepository;
@Value("${secureapi.jwt.secret}") @Value("${secureapi.jwt.secret}")
private String secretString; private String secretString;
@Value("${secureapi.jwt.expiration}") @Value("${secureapi.jwt.expiration}")
private long expirationTime; // in milliseconds private long expirationTime; // in milliseconds
private SecretKey secretKey; private SecretKey secretKey;
private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
public AuthenticationService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@PostConstruct @PostConstruct
public void init() { public void init() {
System.out.println("Initializing secret key"); log.debug("Initializing secret key");
byte[] encodedKey = Base64.getEncoder().encode(secretString.getBytes()); // encode the secret key byte[] encodedKey = Base64.getEncoder().encode(secretString.getBytes()); // encode the secret key
this.secretKey = Keys.hmacShaKeyFor(encodedKey); this.secretKey = Keys.hmacShaKeyFor(encodedKey);
} }
public String authenticate(String username, String password) { public String authenticate(String username, String password) {
// validate user credentials with repository and password hash algorithm Optional<User> user = userRepository.retrieveUserByUsername(username);
if (user.isPresent() && passwordEncoder.matches(password, user.get().getPassword())) {
return generateToken(username); return generateToken(username);
// throw new RuntimeException("User authentication failed"); }
return null;
} }
private String generateToken(String username) { private String generateToken(String username) {

View File

@ -5,6 +5,9 @@ sharepulse.fileid.length=6
sharepulse.fileid.charset=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 sharepulse.fileid.charset=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789
sharepulse.filepassword.length=6 sharepulse.filepassword.length=6
sharepulse.filepassword.charset=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 sharepulse.filepassword.charset=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789
sharepulse.management.user.username=admin
sharepulse.management.user.password=admin
# Database # Database
rethinkdb.host=localhost rethinkdb.host=localhost