From 47d8d863559dc64b214c23be510859b9ef08d8a3 Mon Sep 17 00:00:00 2001 From: Syphax Date: Thu, 19 Dec 2024 09:02:39 +0100 Subject: [PATCH 1/9] update deploy CI to use gradle --- .github/workflows/ci.yaml | 8 ++++---- Dockerfile | 4 +--- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b4e72e9..831d399 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -37,13 +37,13 @@ jobs: uses: actions/setup-java@v2 with: distribution: 'temurin' - java-version: '11' + java-version: '17' - name: Run tests - run: mvn test + run: ./gradlew test - - name: Build with Maven - run: mvn clean package + - name: Build with Gradle + run: ./gradlew clean build - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 diff --git a/Dockerfile b/Dockerfile index a8f4c0c..6682964 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,6 @@ FROM openjdk:11-jre RUN rm -rf /usr/local/tomcat/webapps/* RUN mkdir -p /logs -COPY ./target/API-Gateway-0.0.1-SNAPSHOT.jar /usr/local/tomcat/webapps/API-Gateway-0.0.1-SNAPSHOT.jar +COPY ./build/libs/api-gateway-1.0-SNAPSHOT.jar /usr/local/tomcat/webapps/API-Gateway-0.0.1-SNAPSHOT.jar EXPOSE 8080 CMD ["sh","-c", "java -jar /usr/local/tomcat/webapps/API-Gateway-0.0.1-SNAPSHOT.jar"] - - From 4e58d1c9b1e9aba6ac4ef7379348124aa8ee9333 Mon Sep 17 00:00:00 2001 From: Syphax Date: Fri, 20 Dec 2024 10:15:25 +0100 Subject: [PATCH 2/9] add PostgreSQL database and H2 for test --- docker-compose.yaml | 33 ++++++++++++++++++++--- src/main/resources/application.properties | 13 ++++----- src/test/resources/application.properties | 7 +++++ 3 files changed, 43 insertions(+), 10 deletions(-) create mode 100644 src/test/resources/application.properties diff --git a/docker-compose.yaml b/docker-compose.yaml index 8f2cd01..5f94a04 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,5 +1,4 @@ -version: '3.8' - +version: '3.9' services: api-gateway-backend: build: @@ -8,11 +7,37 @@ services: container_name: api-gateway ports: - "8080:8080" + restart: always + profiles: + - all + depends_on: + - postgres environment: ONTOPORTAL_APIKEY: "put here APIKEY" + SPRING_DATASOURCE_URL: jdbc:postgresql://0.0.0.0:5432/db + POSTGRES_PASSWORD: password + POSTGRES_USER: backend + SPRING_JPA_HIBERNATE_DDL_AUTO: update + + postgres: + image: postgres restart: always + environment: + - POSTGRES_DB=db + - POSTGRES_PASSWORD=password + - POSTGRES_USER=developer + ports: + - "5432:5432" networks: - - api-gateway + - network + adminer: + image: adminer + restart: always + ports: + - 8081:8080 + networks: + - network networks: - api-gateway: + network: + driver: bridge diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index d949904..18b60c3 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,10 +1,11 @@ - #webserver server.servlet.context-path=/api-gateway/ - -#open api documentation -springdoc.swagger-ui.path=/swagger-ui.html -springdoc.api-docs.path=/openapi - # caching spring.cache.type=simple +# Database +spring.datasource.driver-class-name=org.postgresql.Driver +spring.jpa.hibernate.ddl-auto=update +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect +# Authentification +jwt.secret=3BTTxwtSBaY3KguZsrrZBnlsJbGFrsonbOQZ0hlZuhU= +jwt.expiration=86400000 diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties new file mode 100644 index 0000000..1941888 --- /dev/null +++ b/src/test/resources/application.properties @@ -0,0 +1,7 @@ +spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE +spring.datasource.driver-class-name=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password=password +spring.jpa.database-platform=org.hibernate.dialect.H2Dialect +spring.jpa.hibernate.ddl-auto=update +spring.jpa.show-sql=false From d55e9910a74a682518590cf74f9f293393957e76 Mon Sep 17 00:00:00 2001 From: Syphax Date: Fri, 20 Dec 2024 10:16:31 +0100 Subject: [PATCH 3/9] add user model and repository layers --- .../semantics/apigateway/model/user/Role.java | 5 +++ .../semantics/apigateway/model/user/User.java | 40 +++++++++++++++++++ .../service/auth/UserRepository.java | 17 ++++++++ 3 files changed, 62 insertions(+) create mode 100644 src/main/java/org/semantics/apigateway/model/user/Role.java create mode 100644 src/main/java/org/semantics/apigateway/model/user/User.java create mode 100644 src/main/java/org/semantics/apigateway/service/auth/UserRepository.java diff --git a/src/main/java/org/semantics/apigateway/model/user/Role.java b/src/main/java/org/semantics/apigateway/model/user/Role.java new file mode 100644 index 0000000..c32fbac --- /dev/null +++ b/src/main/java/org/semantics/apigateway/model/user/Role.java @@ -0,0 +1,5 @@ +package org.semantics.apigateway.model.user; + +public enum Role { + ADMIN, USER +} diff --git a/src/main/java/org/semantics/apigateway/model/user/User.java b/src/main/java/org/semantics/apigateway/model/user/User.java new file mode 100644 index 0000000..c63e964 --- /dev/null +++ b/src/main/java/org/semantics/apigateway/model/user/User.java @@ -0,0 +1,40 @@ +package org.semantics.apigateway.model.user; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.Positive; +import lombok.*; + +import java.util.Set; + + +@Entity +@Setter +@Getter +@AllArgsConstructor +@NoArgsConstructor +@Table(name = "users") +public class User { + @Id + @Positive + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, unique = true) + @NonNull + @NotEmpty + private String username; + + @Column(nullable = false) + @NonNull + @NotEmpty + @JsonIgnore + private String password; + + + @Enumerated(EnumType.STRING) + @ElementCollection(fetch = FetchType.EAGER) + private Set roles; + +} diff --git a/src/main/java/org/semantics/apigateway/service/auth/UserRepository.java b/src/main/java/org/semantics/apigateway/service/auth/UserRepository.java new file mode 100644 index 0000000..75d9e2a --- /dev/null +++ b/src/main/java/org/semantics/apigateway/service/auth/UserRepository.java @@ -0,0 +1,17 @@ +package org.semantics.apigateway.service.auth; + +import org.semantics.apigateway.model.user.User; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Optional; + + +public interface UserRepository extends JpaRepository { + Optional findByUsername(String username); +} From 0fe3b3f7d8bc3a1f76b84aef915bb003527508a4 Mon Sep 17 00:00:00 2001 From: Syphax Date: Fri, 20 Dec 2024 10:18:59 +0100 Subject: [PATCH 4/9] add jwt token generator and filter validator --- build.gradle | 5 ++ .../apigateway/service/auth/AuthService.java | 30 ++++++++ .../service/auth/JwtAuthenticationFilter.java | 66 +++++++++++++++++ .../apigateway/service/auth/JwtUtil.java | 71 +++++++++++++++++++ .../service/auth/TokenBlacklist.java | 18 +++++ 5 files changed, 190 insertions(+) create mode 100644 src/main/java/org/semantics/apigateway/service/auth/AuthService.java create mode 100644 src/main/java/org/semantics/apigateway/service/auth/JwtAuthenticationFilter.java create mode 100644 src/main/java/org/semantics/apigateway/service/auth/JwtUtil.java create mode 100644 src/main/java/org/semantics/apigateway/service/auth/TokenBlacklist.java diff --git a/build.gradle b/build.gradle index 99bd58b..66ae76e 100644 --- a/build.gradle +++ b/build.gradle @@ -47,6 +47,11 @@ dependencies { // Swagger UI implementation group: 'org.springdoc', name: 'springdoc-openapi-starter-webmvc-ui', version: '2.6.0' + // Jwt authentification + implementation 'io.jsonwebtoken:jjwt-api:0.11.5' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' + // Tests testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'com.h2database:h2' diff --git a/src/main/java/org/semantics/apigateway/service/auth/AuthService.java b/src/main/java/org/semantics/apigateway/service/auth/AuthService.java new file mode 100644 index 0000000..432531d --- /dev/null +++ b/src/main/java/org/semantics/apigateway/service/auth/AuthService.java @@ -0,0 +1,30 @@ +package org.semantics.apigateway.service.auth; + +import org.semantics.apigateway.model.user.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + + +@Service +public class AuthService implements UserDetailsService { + + private final UserRepository userRepository; + + public AuthService(UserRepository userRepository) { + this.userRepository = userRepository; + } + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + User user = userRepository.findByUsername(username) + .orElseThrow(() -> new UsernameNotFoundException("User not found")); + return org.springframework.security.core.userdetails.User.builder() + .username(user.getUsername()) + .password(user.getPassword()) + .roles(user.getRoles().stream().map(x -> x.toString()).toArray(String[]::new)) + .build(); + } + +} diff --git a/src/main/java/org/semantics/apigateway/service/auth/JwtAuthenticationFilter.java b/src/main/java/org/semantics/apigateway/service/auth/JwtAuthenticationFilter.java new file mode 100644 index 0000000..58ebe62 --- /dev/null +++ b/src/main/java/org/semantics/apigateway/service/auth/JwtAuthenticationFilter.java @@ -0,0 +1,66 @@ +package org.semantics.apigateway.service.auth; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.semantics.apigateway.model.user.InvalidJwtException; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Service; +import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.servlet.HandlerExceptionResolver; + +import java.io.IOException; + +@Service +public class JwtAuthenticationFilter extends OncePerRequestFilter { + private final JwtUtil jwtUtil; + private final AuthService userDetailsService; + private final TokenBlacklist tokenBlacklist; + private final HandlerExceptionResolver exceptionResolver; + + public JwtAuthenticationFilter(JwtUtil jwtUtil, AuthService userDetailsService, TokenBlacklist tokenBlacklist, @Qualifier("handlerExceptionResolver") HandlerExceptionResolver exceptionResolver) { + this.jwtUtil = jwtUtil; + this.userDetailsService = userDetailsService; + this.tokenBlacklist = tokenBlacklist; + this.exceptionResolver = exceptionResolver; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws ServletException, IOException { + + final String authHeader = request.getHeader("Authorization"); + + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + chain.doFilter(request, response); + return; + } + + String jwtToken = authHeader.substring(7); + + if (tokenBlacklist.isTokenBlacklisted(jwtToken)) { + this.exceptionResolver.resolveException(request, response, null, new InvalidJwtException("Invalid JWT token")); + return; + } + + String username = jwtUtil.extractUsername(jwtToken); + + if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { + UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); + + if (jwtUtil.validateToken(jwtToken, userDetails)) { + var authentication = new UsernamePasswordAuthenticationToken( + userDetails, null, userDetails.getAuthorities()); + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + } + + chain.doFilter(request, response); + } +} diff --git a/src/main/java/org/semantics/apigateway/service/auth/JwtUtil.java b/src/main/java/org/semantics/apigateway/service/auth/JwtUtil.java new file mode 100644 index 0000000..4697000 --- /dev/null +++ b/src/main/java/org/semantics/apigateway/service/auth/JwtUtil.java @@ -0,0 +1,71 @@ +package org.semantics.apigateway.service.auth; + +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import lombok.Getter; +import org.semantics.apigateway.model.user.InvalidJwtException; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Service; + +import java.nio.charset.StandardCharsets; +import java.security.Key; +import java.util.Date; + +@Service +@Getter +public class JwtUtil { + + @Value("${jwt.secret}") + private String secret; + + @Value("${jwt.expiration}") + private long expiration; + + public String generateToken(String username) { + return Jwts.builder() + .setSubject(username) + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() + expiration)) + .signWith(getSecretKey()) + .compact(); + } + + public String extractUsername(String token) { + try { + return Jwts.parserBuilder() + .setSigningKey(getSecretKey()) + .build() + .parseClaimsJws(token) + .getBody() + .getSubject(); + } catch (Exception e) { + throw new InvalidJwtException("Invalid or expired JWT token"); + } + } + + public boolean validateToken(String token) { + try { + Jwts.parserBuilder() + .setSigningKey(getSecretKey()) + .build() + .parseClaimsJws(token); + return true; + } catch (JwtException e) { + throw new InvalidJwtException("Invalid or expired JWT token"); + } + } + + public boolean validateToken(String token, UserDetails user) { + return validateToken(token) && user.getUsername().equals(extractUsername(token)); + } + + private Key getSecretKey() { + return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); + } + + public Date extractExpiration(String token) { + return Jwts.parserBuilder().setSigningKey(getSecretKey()).build().parseClaimsJws(token).getBody().getExpiration(); + } +} diff --git a/src/main/java/org/semantics/apigateway/service/auth/TokenBlacklist.java b/src/main/java/org/semantics/apigateway/service/auth/TokenBlacklist.java new file mode 100644 index 0000000..d2d0333 --- /dev/null +++ b/src/main/java/org/semantics/apigateway/service/auth/TokenBlacklist.java @@ -0,0 +1,18 @@ +package org.semantics.apigateway.service.auth; + +import org.springframework.stereotype.Service; + +import java.util.concurrent.ConcurrentHashMap; + +@Service +public class TokenBlacklist { + private final ConcurrentHashMap blacklistedTokens = new ConcurrentHashMap<>(); + + public void blacklistToken(String token) { + blacklistedTokens.put(token, true); + } + + public boolean isTokenBlacklisted(String token) { + return blacklistedTokens.getOrDefault(token, false); + } +} From 43e5d6683cb1803dc70b183d6b9d3d8f0c17a8e0 Mon Sep 17 00:00:00 2001 From: Syphax Date: Fri, 20 Dec 2024 10:21:22 +0100 Subject: [PATCH 5/9] add spring security using the jwtAuthenticationFilter and users --- .../semantics/apigateway/SecurityConfig.java | 71 +++++++++++++++++++ .../service/auth/PasswordEncoder.java | 4 ++ 2 files changed, 75 insertions(+) create mode 100644 src/main/java/org/semantics/apigateway/SecurityConfig.java create mode 100644 src/main/java/org/semantics/apigateway/service/auth/PasswordEncoder.java diff --git a/src/main/java/org/semantics/apigateway/SecurityConfig.java b/src/main/java/org/semantics/apigateway/SecurityConfig.java new file mode 100644 index 0000000..0e71f1b --- /dev/null +++ b/src/main/java/org/semantics/apigateway/SecurityConfig.java @@ -0,0 +1,71 @@ +package org.semantics.apigateway; + +import jakarta.servlet.http.HttpServletResponse; +import org.semantics.apigateway.service.auth.AuthService; +import org.semantics.apigateway.service.auth.JwtAuthenticationFilter; +import org.semantics.apigateway.service.auth.TokenBlacklist; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +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.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + private final AuthService userDetailsService; + private final TokenBlacklist tokenBlacklist; + private final JwtAuthenticationFilter jwtAuthenticationFilter; + + public SecurityConfig(AuthService userDetailsService, TokenBlacklist tokenBlacklist, JwtAuthenticationFilter jwtAuthenticationFilter) { + this.userDetailsService = userDetailsService; + this.tokenBlacklist = tokenBlacklist; + this.jwtAuthenticationFilter = jwtAuthenticationFilter; + } + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .csrf(AbstractHttpConfigurer::disable) + .authorizeHttpRequests(auth -> auth + .requestMatchers("/user/**").authenticated() + .anyRequest().permitAll() + ) + .addFilterBefore(jwtAuthenticationFilter, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter.class) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .logout(logout -> logout + .logoutUrl("/auth/logout") + .logoutSuccessHandler((request, response, authentication) -> { + String token = request.getHeader("Authorization"); + if (token != null && token.startsWith("Bearer ")) { + token = token.substring(7); + tokenBlacklist.blacklistToken(token); + } + response.setStatus(HttpServletResponse.SC_OK); + response.getWriter().write("You have been logged out successfully."); + }) + ); + + return http.build(); + } + + @Bean + public AuthenticationManager authManager(HttpSecurity http) throws Exception { + AuthenticationManagerBuilder authenticationManagerBuilder = http.getSharedObject(AuthenticationManagerBuilder.class); + authenticationManagerBuilder.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); + return authenticationManagerBuilder.build(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + +} diff --git a/src/main/java/org/semantics/apigateway/service/auth/PasswordEncoder.java b/src/main/java/org/semantics/apigateway/service/auth/PasswordEncoder.java new file mode 100644 index 0000000..88a02ce --- /dev/null +++ b/src/main/java/org/semantics/apigateway/service/auth/PasswordEncoder.java @@ -0,0 +1,4 @@ +package org.semantics.apigateway.service.auth; + +public class PasswordEncoder { +} From 042472f809bd0821be1895e15031388a911dbf7f Mon Sep 17 00:00:00 2001 From: Syphax Date: Fri, 20 Dec 2024 10:23:40 +0100 Subject: [PATCH 6/9] add AuthController to register, login and logout users --- .../apigateway/controller/AuthController.java | 75 +++++++++++++++++++ .../model/responses/SuccessResponse.java | 12 +++ .../apigateway/model/user/AuthResponse.java | 24 ++++++ .../apigateway/model/user/LoginRequest.java | 16 ++++ .../model/user/RegisterRequest.java | 11 +++ 5 files changed, 138 insertions(+) create mode 100644 src/main/java/org/semantics/apigateway/controller/AuthController.java create mode 100644 src/main/java/org/semantics/apigateway/model/responses/SuccessResponse.java create mode 100644 src/main/java/org/semantics/apigateway/model/user/AuthResponse.java create mode 100644 src/main/java/org/semantics/apigateway/model/user/LoginRequest.java create mode 100644 src/main/java/org/semantics/apigateway/model/user/RegisterRequest.java diff --git a/src/main/java/org/semantics/apigateway/controller/AuthController.java b/src/main/java/org/semantics/apigateway/controller/AuthController.java new file mode 100644 index 0000000..2d3bb5f --- /dev/null +++ b/src/main/java/org/semantics/apigateway/controller/AuthController.java @@ -0,0 +1,75 @@ +package org.semantics.apigateway.controller; + +import io.swagger.v3.oas.annotations.Operation; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.Valid; +import org.semantics.apigateway.model.responses.SuccessResponse; +import org.semantics.apigateway.model.user.*; +import org.semantics.apigateway.service.auth.JwtUtil; +import org.semantics.apigateway.service.auth.UserRepository; +import org.springframework.http.HttpStatus; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.web.bind.annotation.*; + +import java.util.Collections; +import java.util.Date; + +@RestController +@RequestMapping("/auth") +public class AuthController { + + private final AuthenticationManager authenticationManager; + private final JwtUtil jwtUtil; + private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; + + public AuthController(AuthenticationManager authenticationManager, JwtUtil jwtUtil, + UserRepository userRepository, PasswordEncoder passwordEncoder) { + this.authenticationManager = authenticationManager; + this.jwtUtil = jwtUtil; + this.userRepository = userRepository; + this.passwordEncoder = passwordEncoder; + } + + @PostMapping("/register") + @Operation(tags = {"Users"}) + @ResponseStatus(HttpStatus.CREATED) + public SuccessResponse registerUser(@Valid @RequestBody RegisterRequest user) { + User newUser = new User(); + newUser.setUsername(user.getUsername()); + newUser.setPassword(passwordEncoder.encode(user.getPassword())); + newUser.setRoles(Collections.singleton(Role.USER)); + userRepository.save(newUser); + + return new SuccessResponse( + "User created successfully", + "success"); + } + + @PostMapping("/login") + @Operation(tags = {"Users"}) + public AuthResponse loginUser(@RequestBody LoginRequest loginRequest) { + Authentication authentication = authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword())); + String token = jwtUtil.generateToken(authentication.getName()); + UserDetails userDetails = (UserDetails) authentication.getPrincipal(); + + Date expiration = jwtUtil.extractExpiration(token); + GrantedAuthority role = userDetails.getAuthorities().stream().findFirst().orElse(null); + return new AuthResponse(token, loginRequest.getUsername(), role != null ? role.getAuthority() : "", expiration); + + } + + @GetMapping("/logout") + @Operation(tags = {"Users"}) + public SuccessResponse logout(HttpServletRequest request) { + SecurityContextHolder.clearContext(); + return new SuccessResponse("Logged out successfully.", "success"); + } +} diff --git a/src/main/java/org/semantics/apigateway/model/responses/SuccessResponse.java b/src/main/java/org/semantics/apigateway/model/responses/SuccessResponse.java new file mode 100644 index 0000000..2c4c918 --- /dev/null +++ b/src/main/java/org/semantics/apigateway/model/responses/SuccessResponse.java @@ -0,0 +1,12 @@ +package org.semantics.apigateway.model.responses; + + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public class SuccessResponse { + private String message; + private String status; +} diff --git a/src/main/java/org/semantics/apigateway/model/user/AuthResponse.java b/src/main/java/org/semantics/apigateway/model/user/AuthResponse.java new file mode 100644 index 0000000..7dd39fb --- /dev/null +++ b/src/main/java/org/semantics/apigateway/model/user/AuthResponse.java @@ -0,0 +1,24 @@ +package org.semantics.apigateway.model.user; + +import lombok.Getter; +import lombok.Setter; + +import java.util.Date; + +@Setter +@Getter +public class AuthResponse { + + private String token; + private String username; + private String role; + private Date expiration; + + public AuthResponse(String token, String username, String role, Date expiration) { + this.token = token; + this.username = username; + this.role = role; + this.expiration = expiration; + } + +} diff --git a/src/main/java/org/semantics/apigateway/model/user/LoginRequest.java b/src/main/java/org/semantics/apigateway/model/user/LoginRequest.java new file mode 100644 index 0000000..c090552 --- /dev/null +++ b/src/main/java/org/semantics/apigateway/model/user/LoginRequest.java @@ -0,0 +1,16 @@ +package org.semantics.apigateway.model.user; + +import lombok.Getter; +import lombok.Setter; +import jakarta.validation.constraints.NotNull; + +@Getter +@Setter +public class LoginRequest { + + @NotNull + private String username; + + @NotNull + private String password; +} diff --git a/src/main/java/org/semantics/apigateway/model/user/RegisterRequest.java b/src/main/java/org/semantics/apigateway/model/user/RegisterRequest.java new file mode 100644 index 0000000..249acde --- /dev/null +++ b/src/main/java/org/semantics/apigateway/model/user/RegisterRequest.java @@ -0,0 +1,11 @@ +package org.semantics.apigateway.model.user; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class RegisterRequest { + String username; + String password; +} From 1ff5994aee13782fb22375406f1086083e5d499c Mon Sep 17 00:00:00 2001 From: Syphax Date: Fri, 20 Dec 2024 10:23:56 +0100 Subject: [PATCH 7/9] add a global exception handler --- .../controller/GlobalExceptionHandler.java | 35 +++++++++++++++++++ .../model/responses/ErrorResponse.java | 11 ++++++ .../model/user/InvalidJwtException.java | 10 ++++++ 3 files changed, 56 insertions(+) create mode 100644 src/main/java/org/semantics/apigateway/controller/GlobalExceptionHandler.java create mode 100644 src/main/java/org/semantics/apigateway/model/responses/ErrorResponse.java create mode 100644 src/main/java/org/semantics/apigateway/model/user/InvalidJwtException.java diff --git a/src/main/java/org/semantics/apigateway/controller/GlobalExceptionHandler.java b/src/main/java/org/semantics/apigateway/controller/GlobalExceptionHandler.java new file mode 100644 index 0000000..d39dea1 --- /dev/null +++ b/src/main/java/org/semantics/apigateway/controller/GlobalExceptionHandler.java @@ -0,0 +1,35 @@ +package org.semantics.apigateway.controller; + +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.MalformedJwtException; +import org.semantics.apigateway.model.responses.ErrorResponse; +import org.semantics.apigateway.model.user.InvalidJwtException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; + +import java.security.SignatureException; + +@ControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(InvalidJwtException.class) + public ResponseEntity handleJwtException(InvalidJwtException ex) { + ErrorResponse errorResponse = new ErrorResponse("JWT_ERROR", ex.getMessage()); + return new ResponseEntity<>(errorResponse, HttpStatus.UNAUTHORIZED); + } + + @ExceptionHandler({ExpiredJwtException.class, MalformedJwtException.class, SignatureException.class}) + public ResponseEntity handleJwtProcessingException(Exception ex) { + ErrorResponse errorResponse = new ErrorResponse("JWT_ERROR", "Invalid or expired token: " + ex.getMessage()); + return new ResponseEntity<>(errorResponse, HttpStatus.UNAUTHORIZED); + } + + @ExceptionHandler({BadCredentialsException.class}) + public ResponseEntity handleBadCredentialsException(Exception ex) { + ErrorResponse errorResponse = new ErrorResponse("BAD_CREDENTIALS", ex.getMessage()); + return new ResponseEntity<>(errorResponse, HttpStatus.UNAUTHORIZED); + } +} diff --git a/src/main/java/org/semantics/apigateway/model/responses/ErrorResponse.java b/src/main/java/org/semantics/apigateway/model/responses/ErrorResponse.java new file mode 100644 index 0000000..0c4e76c --- /dev/null +++ b/src/main/java/org/semantics/apigateway/model/responses/ErrorResponse.java @@ -0,0 +1,11 @@ +package org.semantics.apigateway.model.responses; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public class ErrorResponse { + private String error; + private String message; +} diff --git a/src/main/java/org/semantics/apigateway/model/user/InvalidJwtException.java b/src/main/java/org/semantics/apigateway/model/user/InvalidJwtException.java new file mode 100644 index 0000000..9bf6127 --- /dev/null +++ b/src/main/java/org/semantics/apigateway/model/user/InvalidJwtException.java @@ -0,0 +1,10 @@ +package org.semantics.apigateway.model.user; + +import lombok.Getter; + +@Getter +public class InvalidJwtException extends RuntimeException { + public InvalidJwtException(String message) { + super(message); + } +} From a3d7ee3dbf343c714f8d7fab32595d7d5dc5e4ff Mon Sep 17 00:00:00 2001 From: Syphax Date: Fri, 20 Dec 2024 10:37:20 +0100 Subject: [PATCH 8/9] add properties tests for jwt --- src/main/resources/application.properties | 2 +- src/test/resources/application.properties | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 18b60c3..67d33e8 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -7,5 +7,5 @@ spring.datasource.driver-class-name=org.postgresql.Driver spring.jpa.hibernate.ddl-auto=update spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect # Authentification -jwt.secret=3BTTxwtSBaY3KguZsrrZBnlsJbGFrsonbOQZ0hlZuhU= +jwt.secret=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx jwt.expiration=86400000 diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties index 1941888..baf61de 100644 --- a/src/test/resources/application.properties +++ b/src/test/resources/application.properties @@ -5,3 +5,6 @@ spring.datasource.password=password spring.jpa.database-platform=org.hibernate.dialect.H2Dialect spring.jpa.hibernate.ddl-auto=update spring.jpa.show-sql=false +# Authentification +jwt.secret=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +jwt.expiration=86400000 From 2c047962d36293a29b9158c3a370e013956b5dcb Mon Sep 17 00:00:00 2001 From: Syphax Date: Mon, 23 Dec 2024 12:22:41 +0100 Subject: [PATCH 9/9] add better swagger documentation for the authentication features --- .../semantics/apigateway/SecurityConfig.java | 4 +++- .../apigateway/controller/AuthController.java | 7 +++---- .../apigateway/service/auth/AuthService.java | 21 +++++++++++++++++++ 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/semantics/apigateway/SecurityConfig.java b/src/main/java/org/semantics/apigateway/SecurityConfig.java index 0e71f1b..877c140 100644 --- a/src/main/java/org/semantics/apigateway/SecurityConfig.java +++ b/src/main/java/org/semantics/apigateway/SecurityConfig.java @@ -15,6 +15,7 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration @EnableWebSecurity @@ -35,10 +36,11 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti http .csrf(AbstractHttpConfigurer::disable) .authorizeHttpRequests(auth -> auth + .requestMatchers("/admin/**").hasRole("ADMIN") .requestMatchers("/user/**").authenticated() .anyRequest().permitAll() ) - .addFilterBefore(jwtAuthenticationFilter, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter.class) + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .logout(logout -> logout .logoutUrl("/auth/logout") diff --git a/src/main/java/org/semantics/apigateway/controller/AuthController.java b/src/main/java/org/semantics/apigateway/controller/AuthController.java index 2d3bb5f..de5680a 100644 --- a/src/main/java/org/semantics/apigateway/controller/AuthController.java +++ b/src/main/java/org/semantics/apigateway/controller/AuthController.java @@ -1,6 +1,6 @@ package org.semantics.apigateway.controller; -import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; import org.semantics.apigateway.model.responses.SuccessResponse; @@ -22,6 +22,7 @@ @RestController @RequestMapping("/auth") +@Tag(name = "Users - Authentification") public class AuthController { private final AuthenticationManager authenticationManager; @@ -38,7 +39,6 @@ public AuthController(AuthenticationManager authenticationManager, JwtUtil jwtUt } @PostMapping("/register") - @Operation(tags = {"Users"}) @ResponseStatus(HttpStatus.CREATED) public SuccessResponse registerUser(@Valid @RequestBody RegisterRequest user) { User newUser = new User(); @@ -53,13 +53,13 @@ public SuccessResponse registerUser(@Valid @RequestBody RegisterRequest user) { } @PostMapping("/login") - @Operation(tags = {"Users"}) public AuthResponse loginUser(@RequestBody LoginRequest loginRequest) { Authentication authentication = authenticationManager.authenticate( new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword())); String token = jwtUtil.generateToken(authentication.getName()); UserDetails userDetails = (UserDetails) authentication.getPrincipal(); + Date expiration = jwtUtil.extractExpiration(token); GrantedAuthority role = userDetails.getAuthorities().stream().findFirst().orElse(null); return new AuthResponse(token, loginRequest.getUsername(), role != null ? role.getAuthority() : "", expiration); @@ -67,7 +67,6 @@ public AuthResponse loginUser(@RequestBody LoginRequest loginRequest) { } @GetMapping("/logout") - @Operation(tags = {"Users"}) public SuccessResponse logout(HttpServletRequest request) { SecurityContextHolder.clearContext(); return new SuccessResponse("Logged out successfully.", "success"); diff --git a/src/main/java/org/semantics/apigateway/service/auth/AuthService.java b/src/main/java/org/semantics/apigateway/service/auth/AuthService.java index 432531d..b641a81 100644 --- a/src/main/java/org/semantics/apigateway/service/auth/AuthService.java +++ b/src/main/java/org/semantics/apigateway/service/auth/AuthService.java @@ -1,6 +1,8 @@ package org.semantics.apigateway.service.auth; import org.semantics.apigateway.model.user.User; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; @@ -27,4 +29,23 @@ public UserDetails loadUserByUsername(String username) throws UsernameNotFoundEx .build(); } + public String getCurrentUsername() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + if (authentication != null && authentication.isAuthenticated()) { + Object principal = authentication.getPrincipal(); + + if (principal instanceof UserDetails) { + return ((UserDetails) principal).getUsername(); + } else { + return principal.toString(); // For anonymous users, this could return something like "anonymousUser". + } + } + + return null; // No authenticated user + } + + public User getCurrentUser() { + return this.userRepository.findByUsername(getCurrentUsername()).orElseThrow(); + } }