Authors:
Kacper Bohaczyk
Sergej Rychkov
Date: 17 October 2024
The objective of this exercise is to implement a web-based user authentication system. Users should be able to register and log in to the system. Communication between the client and the service will be established using a REST API.
- Java 17 (or higher)
- Gradle as the build tool
- PostgreSQL as the database
- Spring Boot with the following dependencies:
- Spring Web
- Spring Security
- Spring Data JPA
- Lombok
- PostgreSQL JDBC Driver
- JSON Web Token (JWT)
Create a new Spring Boot project using Spring Initializr or manually with Gradle. The key dependencies are:
- Spring Web: For the REST API.
- Spring Data JPA: For database communication.
- Spring Security: For authentication and authorization.
- PostgreSQL JDBC Driver: For connecting to the PostgreSQL database.
- Lombok: To reduce boilerplate code.
- jjwt: For JWT token handling.
Visible in commit number 1
In this version, we have opted for a login system for users based on PostgreSQL.
- Pull the Docker PostgreSQL image and create a database.
- Run the server.
plugins {
id 'org.springframework.boot' version '3.2.5'
id 'io.spring.dependency-management' version '1.1.0'
id 'java'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-security' // Ensure this line is present
implementation 'org.postgresql:postgresql'
implementation 'io.jsonwebtoken:jjwt:0.9.1'
implementation 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
spring.datasource.url=jdbc:postgresql://localhost:5432/postgres-gk961
spring.datasource.username=postgres
spring.datasource.password=sergej
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
UserMan.java
package com.example.demo; // Update this to your package name
import jakarta.persistence.*;
import lombok.Data;
import java.util.List;
@Entity
@Data // Lombok annotation to generate getters, setters, equals, hashCode, and toString methods
public class UserMan {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true)
private String email;
private String name;
private String password;
@ElementCollection(fetch = FetchType.EAGER)
private List<String> roles; // ADMIN, READER, MODERATOR
}
UserManRepository.java
package com.example.demo;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserManRepository extends JpaRepository<UserMan, Long> {
UserMan findByEmail(String email);
}
SecurityConfig.java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/auth/admin/register").hasRole("ADMIN")
.antMatchers("/auth/signin").permitAll()
.antMatchers("/auth/verify").authenticated()
.and()
.httpBasic();
}
}
AuthController.java
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/auth")
public class AuthController {
@Autowired
private UserManRepository userManRepository;
private final String SECRET_KEY = "geheimeschluessel"; // Use a secure secret!
@PostMapping("/admin/register")
public ResponseEntity<String> register(@RequestBody UserMan userMan) {
// Encrypt password
userMan.setPassword(new BCryptPasswordEncoder().encode(userMan.getPassword()));
userManRepository.save(userMan);
return ResponseEntity.ok("User registered successfully.");
}
@PostMapping("/signin")
public ResponseEntity<Map<String, Object>> signin(@RequestBody UserMan loginRequest) {
UserMan userMan = userManRepository.findByEmail(loginRequest.getEmail());
if (userMan != null && new BCryptPasswordEncoder().matches(loginRequest.getPassword(), userMan.getPassword())) {
String jwt = generateToken(userMan);
Map<String, Object> response = new HashMap<>();
response.put("token", jwt);
response.put("roles", userMan.getRoles());
return ResponseEntity.ok(response);
}
return ResponseEntity.status(401).body(Map.of("message", "Invalid credentials"));
}
@GetMapping("/verify")
public ResponseEntity<String> verify(@RequestHeader("Authorization") String token) {
String email = Jwts.parser()
.setSigningKey(SECRET_KEY)
.parseClaimsJws(token.replace("Bearer ", ""))
.getBody()
.getSubject();
if (email != null) {
return ResponseEntity.ok("Token is valid for user: " + email);
}
return ResponseEntity.status(403).body("Unauthorized");
}
private String generateToken(UserMan userMan) {
return Jwts.builder()
.setSubject(userMan.getEmail())
.claim("roles", userMan.getRoles())
.signWith(SignatureAlgorithm.HS256, SECRET_KEY)
.compact();
}
}
DataInitializer.java
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.File;
import java.util.List;
@Component
public class DataInitializer implements CommandLineRunner {
private final UserManRepository userManRepository;
public DataInitializer(UserManRepository userManRepository) {
this.userManRepository = userManRepository;
}
@Override
public void run(String... args) throws Exception {
ObjectMapper objectMapper = new ObjectMapper();
UserMan[] users = objectMapper.readValue(new File("path/to/users.json"), UserMan[].class);
for (UserMan userMan : users) {
userMan.setPassword(new BCryptPasswordEncoder().encode(userMan.getPassword()));
userManRepository.save(userMan);
}
}
}
Create a JSON file with users:
[
{
"email": "admin@example.com",
"name": "Admin User",
"password": "admin123",
"roles": ["ADMIN"]
},
{
"email": "reader@example.com",
"name": "Reader User",
"password": "reader123",
"roles": ["READER"]
}
]
curl -X POST http://localhost:8080/auth/admin/register -H "Content-Type: application/json" -d '{
"email": "admin@example.com",
"name": "Admin User",
"password": "admin123",
"roles": ["ADMIN"]
}'
curl -X POST http://localhost:8080/auth/signin -H "Content-Type: application/json" -d '{
"email": "admin@example.com",
"password": "admin123"
}'
- Establishing a connection to the PostgreSQL database
Solution: The error[3D000] FATAL: database "my-postgres" does not exist
was resolved by creating the database with the correct name.
[1] "Structure and Processing Steps in Developing" . Accessed Oct. 24, 2024ChatGPT Resource].