Public Access
Added jwt token validation and api endpoint versioning
This commit is contained in:
1 parent
2e8ff1b079
commit
64b84b1dc0
11 files changed
+253
-8
No files matched your search
@@ -14,8 +14,10 @@
|
||||
<name>To be Played - Auth</name>
|
||||
<properties>
|
||||
<java.version>25</java.version>
|
||||
<jwt.version>0.13.0</jwt.version>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<!-- Spring -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
@@ -32,6 +34,30 @@
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-webmvc</artifactId>
|
||||
</dependency>
|
||||
<!-- Rest Caching -->
|
||||
<dependency>
|
||||
<groupId>com.github.ben-manes.caffeine</groupId>
|
||||
<artifactId>caffeine</artifactId>
|
||||
</dependency>
|
||||
<!-- JWT -->
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-api</artifactId>
|
||||
<version>${jwt.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-impl</artifactId>
|
||||
<version>${jwt.version}</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-jackson</artifactId>
|
||||
<version>${jwt.version}</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<!-- Database -->
|
||||
<dependency>
|
||||
<groupId>org.flywaydb</groupId>
|
||||
<artifactId>flyway-core</artifactId>
|
||||
@@ -42,17 +68,21 @@
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
<artifactId>spring-boot-starter-flyway</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Observability -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-flyway</artifactId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.micrometer</groupId>
|
||||
<artifactId>micrometer-registry-prometheus</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- testing -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-docker-compose</artifactId>
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package auth_service.configuration.security;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jspecify.annotations.NonNull;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
|
||||
private final JwtService jwtService;
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(
|
||||
HttpServletRequest request,
|
||||
@NonNull HttpServletResponse response,
|
||||
@NonNull FilterChain filterChain
|
||||
) throws ServletException, IOException {
|
||||
|
||||
final String authHeader = request.getHeader("Authorization");
|
||||
|
||||
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
|
||||
filterChain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
final String jwt = authHeader.substring(7);
|
||||
if (!jwtService.isTokenValid(jwt)) {
|
||||
filterChain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final String userEmail = jwtService.extractEmail(jwt);
|
||||
|
||||
if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) {
|
||||
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
|
||||
userEmail,
|
||||
null,
|
||||
Collections.emptyList()
|
||||
);
|
||||
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
||||
SecurityContextHolder.getContext().setAuthentication(authToken);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("JWT Validation failed: {}", e.getMessage());
|
||||
}
|
||||
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package auth_service.configuration.security;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "security.jwt")
|
||||
public class JwtConfig {
|
||||
|
||||
private String secret;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package auth_service.configuration.security;
|
||||
|
||||
import auth_service.domain.login.model.User;
|
||||
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.util.Date;
|
||||
|
||||
/**
|
||||
* Service responsible for generating and validating JSON Web Tokens (JWT).
|
||||
*/
|
||||
@Service
|
||||
public class JwtService {
|
||||
|
||||
// Token validity: 24 hours in milliseconds
|
||||
private static final long JWT_EXPIRATION_MS = 86400000;
|
||||
private final JwtConfig jwtConfig;
|
||||
|
||||
public JwtService(JwtConfig jwtConfig) {
|
||||
this.jwtConfig = jwtConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a secure JWT for the authenticated user.
|
||||
* * @param user The authenticated user entity
|
||||
*
|
||||
* @return A signed JWT string
|
||||
*/
|
||||
public String generateToken(User user) {
|
||||
return Jwts.builder()
|
||||
.subject(user.getEmail())
|
||||
.issuedAt(new Date(System.currentTimeMillis()))
|
||||
.expiration(new Date(System.currentTimeMillis() + JWT_EXPIRATION_MS))
|
||||
.signWith(getSigningKey())
|
||||
.compact();
|
||||
}
|
||||
|
||||
public String extractEmail(String token) {
|
||||
return Jwts.parser()
|
||||
.verifyWith(getSigningKey())
|
||||
.build()
|
||||
.parseSignedClaims(token)
|
||||
.getPayload()
|
||||
.getSubject();
|
||||
}
|
||||
|
||||
public boolean isTokenValid(String token) {
|
||||
try {
|
||||
Jwts.parser()
|
||||
.verifyWith(getSigningKey())
|
||||
.build()
|
||||
.parseSignedClaims(token);
|
||||
return true;
|
||||
} catch (Exception _) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the cryptographic key used for signing the token.
|
||||
*/
|
||||
private SecretKey getSigningKey() {
|
||||
String secretKey = jwtConfig.getSecret();
|
||||
byte[] keyBytes = secretKey.getBytes(StandardCharsets.UTF_8);
|
||||
return Keys.hmacShaKeyFor(keyBytes);
|
||||
}
|
||||
}
|
||||
+9
-2
@@ -1,5 +1,6 @@
|
||||
package auth_service.configuration;
|
||||
package auth_service.configuration.security;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
@@ -10,6 +11,7 @@ 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;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.CorsConfigurationSource;
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||
@@ -23,6 +25,7 @@ import java.util.List;
|
||||
*/
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@RequiredArgsConstructor
|
||||
public class SecurityConfig {
|
||||
|
||||
/**
|
||||
@@ -34,6 +37,9 @@ public class SecurityConfig {
|
||||
"/actuator/prometheus", // Allow Prometheus to scrape metrics
|
||||
"/error" // Default Spring Boot error page routing
|
||||
};
|
||||
|
||||
private final JwtAuthenticationFilter jwtAuthFilter;
|
||||
|
||||
/**
|
||||
* Injects the allowed CORS origins from the active application.yml profile.
|
||||
*/
|
||||
@@ -78,7 +84,8 @@ public class SecurityConfig {
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers(WHITE_LIST_URLS).permitAll()
|
||||
.anyRequest().authenticated()
|
||||
);
|
||||
)
|
||||
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
|
||||
|
||||
return http.build();
|
||||
}
|
||||
@@ -33,7 +33,7 @@ public class LoginController {
|
||||
public ResponseEntity<LoginResponse> login(@RequestBody LoginRequest request) {
|
||||
|
||||
var user = ApiMapper.toModel(request);
|
||||
loginService.login(user);
|
||||
return ResponseEntity.ok(new LoginResponse("Login successful", user.getEmail()));
|
||||
var jwt = loginService.login(user);
|
||||
return ResponseEntity.ok(new LoginResponse("Login successful", jwt));
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package auth_service.domain.login.service;
|
||||
|
||||
import auth_service.configuration.security.JwtService;
|
||||
import auth_service.domain.login.exception.InvalidCredentialsException;
|
||||
import auth_service.domain.login.exception.UserNotFoundException;
|
||||
import auth_service.domain.login.model.User;
|
||||
@@ -15,14 +16,18 @@ import org.springframework.stereotype.Service;
|
||||
public class LoginService {
|
||||
private final UserRepository userRepository;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final JwtService jwtService;
|
||||
|
||||
public String login(User userToLogin) {
|
||||
var userEntity = userRepository.findByEmail(userToLogin.getEmail()).orElseThrow(() -> new UserNotFoundException("User not found with given email"));
|
||||
|
||||
log.info("Login attempt for user {}", passwordEncoder.encode(userToLogin.getEncryptedPassword()) + " " + userEntity.getEncryptedPassword());
|
||||
|
||||
var loginSuccessful = passwordEncoder.matches(userToLogin.getEncryptedPassword(), userEntity.getEncryptedPassword());
|
||||
if (!loginSuccessful) {
|
||||
throw new InvalidCredentialsException("Invalid credentials");
|
||||
}
|
||||
|
||||
return "";
|
||||
return jwtService.generateToken(userToLogin);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package auth_service.domain.user.controller;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.security.Principal;
|
||||
|
||||
/**
|
||||
* Controller handling user profile operations for the dashboard.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/users")
|
||||
public class UserController {
|
||||
|
||||
/**
|
||||
* Version 1 of the 'me' endpoint, accessed via /api/users/me?v=1
|
||||
* * @return The user's dashboard data
|
||||
*/
|
||||
@GetMapping(value = "/me", version = "1.0", produces = "application/json")
|
||||
@Timed(value = "user.me.v1.requests.time", description = "Time taken to process v1 user profile requests")
|
||||
public ResponseEntity<UserProfileResponse> getCurrentUserV1(Principal principal) {
|
||||
|
||||
// principal.getName() enthält die E-Mail, die der Türsteher aus dem Token gelesen hat!
|
||||
String authenticatedEmail = principal.getName();
|
||||
|
||||
return ResponseEntity.ok(new UserProfileResponse(
|
||||
authenticatedEmail,
|
||||
"USER",
|
||||
"Welcome to the v1 Dashboard, " + authenticatedEmail + "!"
|
||||
));
|
||||
}
|
||||
|
||||
public record UserProfileResponse(String email, String role, String dashboardMessage) {
|
||||
}
|
||||
}
|
||||
@@ -12,3 +12,7 @@ app:
|
||||
spring:
|
||||
jpa:
|
||||
show-sql: true
|
||||
|
||||
security:
|
||||
jwt:
|
||||
secret: "NzC9tQP1eTp496SxoVYslH1bqn81Tbfz74NjkFqdcVe7wayjcRN8IeZvJWd1s6NEWgwEXe9rdccvOn2HtJ4fdq"
|
||||
@@ -13,6 +13,13 @@ spring:
|
||||
baseline-on-migrate: true
|
||||
# Explicitly state where to look for scripts
|
||||
locations: classpath:db/migration
|
||||
mvc:
|
||||
apiversion:
|
||||
use:
|
||||
query-parameter: version
|
||||
default: "1.0"
|
||||
supported: 1.0, 2,0
|
||||
|
||||
|
||||
# Actuator & Micrometer Settings
|
||||
management:
|
||||
@@ -29,6 +36,11 @@ management:
|
||||
export:
|
||||
enabled: true
|
||||
|
||||
# Security stuff
|
||||
security:
|
||||
jwt:
|
||||
secret: ${JWT_SECRET}
|
||||
|
||||
# Custom properties for CORS
|
||||
app:
|
||||
cors:
|
||||
|
||||
@@ -10,4 +10,4 @@ CREATE TABLE users
|
||||
-- Insert a pragmatic test user for local development and testing
|
||||
-- The password hash corresponds to 'password123'
|
||||
INSERT INTO users (email, password, enabled)
|
||||
VALUES ('test@example.com', '$2a$10$8.UnVuG9HHgffUDAlk8qfOuVGkqRzgVymGe07xd00dmOn.jptTVyG', true);
|
||||
VALUES ('test@example.com', '$2a$10$3FApyf15qVEvFLEvl/d7aecSMZlslQdXiYlDgbOa4DfGWu4l0VCY.', true);
|
||||
Reference in new issue
Block a user