Compare commits

...

11 Commits

Author SHA1 Message Date
litoral05 0bb25521a1 Creates default admin user 2026-06-03 15:45:08 +01:00
litoral05 8de3e42576 Load jwt configuriation from .env 2026-06-03 15:17:10 +01:00
litoral05 d5ef831efc remove debug controllers 2026-06-03 15:05:28 +01:00
litoral05 15e73677c4 Verify role based access control 2026-06-03 14:59:59 +01:00
litoral05 a72ee91bd4 Require admin role for admin endpoints 2026-06-03 14:46:29 +01:00
litoral05 102e906b36 Require authentication for backend proxy 2026-06-03 14:39:23 +01:00
litoral05 45419f23c1 Route proxy using authenticated client 2026-06-03 14:35:41 +01:00
litoral05 2203b0f2c3 Add jwt authentication filter 2026-06-03 14:29:02 +01:00
litoral05 4b67ba9995 Add jwt token parsing 2026-06-03 14:13:10 +01:00
litoral05 5d422e1608 Add jwt token generation 2026-06-03 14:08:00 +01:00
litoral05 04f4732da1 Add login endpoint 2026-06-03 12:23:08 +01:00
20 changed files with 450 additions and 11 deletions
+2
View File
@@ -31,3 +31,5 @@ build/
### VS Code ###
.vscode/
.env
Binary file not shown.
+19
View File
@@ -72,6 +72,25 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.7</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.7</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.7</version>
<scope>runtime</scope>
</dependency>
</dependencies>
<build>
@@ -1,12 +1,16 @@
package com.litoralregas.backend_gateway;
import com.litoralregas.backend_gateway.gateway.ProxyProperties;
import com.litoralregas.backend_gateway.security.JwtProperties;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
@SpringBootApplication
@EnableConfigurationProperties(ProxyProperties.class)
@EnableConfigurationProperties({
ProxyProperties.class,
JwtProperties.class
})
public class BackendGatewayApplication {
public static void main(String[] args) {
@@ -0,0 +1,28 @@
package com.litoralregas.backend_gateway.auth;
import com.litoralregas.backend_gateway.auth.dto.LoginRequest;
import com.litoralregas.backend_gateway.auth.dto.LoginResponse;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/auth")
public class AuthController {
private final AuthService authService;
private final PasswordEncoder passwordEncoder;
public AuthController(
AuthService authService,
PasswordEncoder passwordEncoder
) {
this.authService = authService;
this.passwordEncoder = passwordEncoder;
}
@PostMapping("/login")
public LoginResponse login(@RequestBody LoginRequest request) {
System.out.println(passwordEncoder.encode("admin123"));
return authService.login(request);
}
}
@@ -0,0 +1,58 @@
package com.litoralregas.backend_gateway.auth;
import com.litoralregas.backend_gateway.auth.dto.LoginRequest;
import com.litoralregas.backend_gateway.auth.dto.LoginResponse;
import com.litoralregas.backend_gateway.security.JwtService;
import com.litoralregas.backend_gateway.user.UserEntity;
import com.litoralregas.backend_gateway.user.UserRepository;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
@Service
public class AuthService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final JwtService jwtService;
public AuthService(
UserRepository userRepository,
PasswordEncoder passwordEncoder,
JwtService jwtService
) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
this.jwtService = jwtService;
}
public LoginResponse login(LoginRequest request) {
UserEntity user = userRepository.findByUsername(request.username())
.orElseThrow(InvalidCredentialsException::new);
if (!user.isEnabled()) {
throw new InvalidCredentialsException();
}
boolean valid = passwordEncoder.matches(
request.password(),
user.getPasswordHash()
);
if (!valid) {
throw new InvalidCredentialsException();
}
String accessToken = jwtService.generateToken(user);
return new LoginResponse(
accessToken,
"Bearer",
user.getId(),
user.getClient().getId(),
user.getClient().getName(),
user.getUsername(),
user.getRole()
);
}
}
@@ -0,0 +1,12 @@
package com.litoralregas.backend_gateway.auth;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public class InvalidCredentialsException extends RuntimeException {
public InvalidCredentialsException() {
super("Invalid credentials");
}
}
@@ -0,0 +1,7 @@
package com.litoralregas.backend_gateway.auth.dto;
public record LoginRequest(
String username,
String password
) {
}
@@ -0,0 +1,14 @@
package com.litoralregas.backend_gateway.auth.dto;
import com.litoralregas.backend_gateway.user.UserRole;
public record LoginResponse(
String accessToken,
String tokenType,
Long userId,
Long clientId,
String clientName,
String username,
UserRole role
) {
}
@@ -7,4 +7,5 @@ import java.util.Optional;
public interface ClientRepository extends JpaRepository<ClientEntity, Long> {
Optional<ClientEntity> findByName(String name);
Optional<ClientEntity> findByIdAndEnabledTrue(Long id);
}
@@ -0,0 +1,33 @@
package com.litoralregas.backend_gateway.client;
import com.litoralregas.backend_gateway.security.AuthenticatedUser;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
@Service
public class ClientResolver {
private final ClientRepository clientRepository;
public ClientResolver(ClientRepository clientRepository) {
this.clientRepository = clientRepository;
}
public ClientEntity resolveCurrentClient() {
Authentication authentication =
SecurityContextHolder.getContext().getAuthentication();
if (authentication == null) {
throw new RuntimeException("Not authenticated");
}
AuthenticatedUser user =
(AuthenticatedUser) authentication.getPrincipal();
return clientRepository
.findByIdAndEnabledTrue(user.clientId())
.orElseThrow(() -> new RuntimeException("Client not found"));
}
}
@@ -0,0 +1,17 @@
package com.litoralregas.backend_gateway.auth;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class AuthExceptionHandler {
@ExceptionHandler(InvalidCredentialsException.class)
public ResponseEntity<String> handleInvalidCredentials() {
return ResponseEntity
.status(HttpStatus.UNAUTHORIZED)
.body("Invalid credentials");
}
}
@@ -1,22 +1,32 @@
package com.litoralregas.backend_gateway.config;
import com.litoralregas.backend_gateway.security.JwtAuthenticationFilter;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) {
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/api/backend/**").authenticated()
.anyRequest().permitAll()
)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
@@ -1,5 +1,6 @@
package com.litoralregas.backend_gateway.gateway;
import com.litoralregas.backend_gateway.client.ClientResolver;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
@@ -12,19 +13,18 @@ import org.springframework.web.reactive.function.client.WebClientResponseExcepti
public class BackendProxyService {
private final WebClient webClient;
private final ProxyProperties proxyProperties;
private final ClientResolver clientResolver;
public BackendProxyService(WebClient webClient, ProxyProperties proxyProperties) {
public BackendProxyService(
WebClient webClient,
ClientResolver clientResolver
) {
this.webClient = webClient;
this.proxyProperties = proxyProperties;
this.clientResolver = clientResolver;
}
public String getHealth() {
return webClient.get()
.uri(proxyProperties.getBackendBaseUrl() + "/actuator/health")
.retrieve()
.bodyToMono(String.class)
.block();
return "UP";
}
public ResponseEntity<String> proxy(HttpServletRequest request, String body) {
@@ -33,7 +33,7 @@ public class BackendProxyService {
String query = request.getQueryString();
String targetUrl =
proxyProperties.getBackendBaseUrl()
resolveBackendUrl()
+ path
+ (query != null ? "?" + query : "");
@@ -76,4 +76,10 @@ public class BackendProxyService {
.body("Backend unavailable");
}
}
private String resolveBackendUrl() {
return clientResolver
.resolveCurrentClient()
.getBackendBaseUrl();
}
}
@@ -0,0 +1,11 @@
package com.litoralregas.backend_gateway.security;
import com.litoralregas.backend_gateway.user.UserRole;
public record AuthenticatedUser(
Long userId,
Long clientId,
String username,
UserRole role
) {
}
@@ -0,0 +1,70 @@
package com.litoralregas.backend_gateway.security;
import com.litoralregas.backend_gateway.user.UserRole;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.List;
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtService jwtService;
public JwtAuthenticationFilter(JwtService jwtService) {
this.jwtService = jwtService;
}
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain
) throws ServletException, IOException {
String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
String token = authHeader.substring(7);
if (!jwtService.isValid(token)) {
filterChain.doFilter(request, response);
return;
}
String username = jwtService.extractUsername(token);
Long userId = jwtService.extractUserId(token);
Long clientId = jwtService.extractClientId(token);
UserRole role = UserRole.valueOf(jwtService.extractRole(token));
AuthenticatedUser authenticatedUser = new AuthenticatedUser(
userId,
clientId,
username,
role
);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
authenticatedUser,
null,
List.of(new SimpleGrantedAuthority("ROLE_" + role.name()))
);
SecurityContextHolder.getContext().setAuthentication(authentication);
filterChain.doFilter(request, response);
}
}
@@ -0,0 +1,26 @@
package com.litoralregas.backend_gateway.security;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "jwt")
public class JwtProperties {
private String secret;
private long expirationMinutes;
public String getSecret() {
return secret;
}
public long getExpirationMinutes() {
return expirationMinutes;
}
public void setSecret(String secret) {
this.secret = secret;
}
public void setExpirationMinutes(long expirationMinutes) {
this.expirationMinutes = expirationMinutes;
}
}
@@ -0,0 +1,101 @@
package com.litoralregas.backend_gateway.security;
import com.litoralregas.backend_gateway.user.UserEntity;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import org.springframework.stereotype.Service;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.Date;
@Service
public class JwtService {
private final JwtProperties jwtProperties;
public JwtService(JwtProperties jwtProperties) {
this.jwtProperties = jwtProperties;
}
public String generateToken(UserEntity user) {
Instant now = Instant.now();
Instant expiresAt = now.plusSeconds(jwtProperties.getExpirationMinutes() * 60);
return Jwts.builder()
.subject(user.getUsername())
.claim("userId", user.getId())
.claim("clientId", user.getClient().getId())
.claim("role", user.getRole().name())
.issuedAt(Date.from(now))
.expiration(Date.from(expiresAt))
.signWith(getSigningKey())
.compact();
}
private SecretKey getSigningKey() {
return Keys.hmacShaKeyFor(
jwtProperties.getSecret().getBytes(StandardCharsets.UTF_8)
);
}
private Claims getClaims(String token) {
return Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token)
.getPayload();
}
public boolean isValid(String token) {
try {
getClaims(token);
return true;
} catch (JwtException ex) {
return false;
}
}
public String extractUsername(String token) {
return getClaims(token).getSubject();
}
public Long extractClientId(String token) {
Object value = getClaims(token).get("clientId");
if (value instanceof Integer integer) {
return integer.longValue();
}
if (value instanceof Long longValue) {
return longValue;
}
return Long.parseLong(value.toString());
}
public String extractRole(String token) {
return getClaims(token).get("role", String.class);
}
public Long extractUserId(String token) {
Object value = getClaims(token).get("userId");
if (value instanceof Integer integer) {
return integer.longValue();
}
if (value instanceof Long longValue) {
return longValue;
}
return Long.parseLong(value.toString());
}
}
+5 -1
View File
@@ -23,4 +23,8 @@ gateway:
proxy:
backend-base-url: http://10.100.1.2:18450
connect-timeout: 3s
response-timeout: 10s
response-timeout: 10s
jwt:
secret: ${JWT_SECRET}
expiration-minutes: ${JWT_EXPIRATION_MINUTES:1440}
@@ -0,0 +1,16 @@
INSERT INTO clients (name, backend_base_url, enabled)
SELECT 'dev-local', 'http://10.100.1.2:18450', 1
WHERE NOT EXISTS (
SELECT 1 FROM clients WHERE name = 'dev-local'
);
INSERT INTO users (client_id, username, password_hash, role, enabled)
SELECT
(SELECT id FROM clients WHERE name = 'dev-local'),
'admin',
'$2a$10$it1vy5t1FXISQTWit2A39udaMR0N0yqJtJUnxMqF1Xz4SuNBaam6u',
'ADMIN',
1
WHERE NOT EXISTS (
SELECT 1 FROM users WHERE username = 'admin'
);