diff --git a/api/com/hpms/api/AuthController.java b/api/com/hpms/api/AuthController.java new file mode 100644 index 0000000..4c029f6 --- /dev/null +++ b/api/com/hpms/api/AuthController.java @@ -0,0 +1,31 @@ +package com.hpms.api; + +import com.hpms.services.UserService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +@RestController +@RequestMapping("/api/auth") +@Tag(name = "Authentication") +public class AuthController { + + private final UserService userService; + + public AuthController(UserService userService) { + this.userService = userService; + } + + @PostMapping("/login") + @Operation(summary = "Authenticate user and return JWT") + public ResponseEntity> login(@RequestBody Map credentials) { + String email = credentials.get("email"); + String password = credentials.get("password"); + + String token = userService.authenticateAndGenerateToken(email, password); + return ResponseEntity.ok(Map.of("token", token)); + } +} \ No newline at end of file diff --git a/pom.xml b/pom.xml index 051256f..f1abe5b 100644 --- a/pom.xml +++ b/pom.xml @@ -40,6 +40,29 @@ ${spring.boot.version} test + + org.springframework.boot + spring-boot-starter-security + ${spring.boot.version} + + + + io.jsonwebtoken + jjwt-api + 0.12.5 + + + io.jsonwebtoken + jjwt-impl + 0.12.5 + runtime + + + io.jsonwebtoken + jjwt-gson + 0.12.5 + runtime + diff --git a/services/com/hpms/services/UserService.java b/services/com/hpms/services/UserService.java index 4fb51df..f489149 100644 --- a/services/com/hpms/services/UserService.java +++ b/services/com/hpms/services/UserService.java @@ -12,9 +12,11 @@ @Service public class UserService { private final UserRepository userRepository; + private final com.hpms.security.JwtUtil jwtUtil; - public UserService(UserRepository userRepository) { + public UserService(UserRepository userRepository, com.hpms.security.JwtUtil jwtUtil) { this.userRepository = userRepository; + this.jwtUtil = jwtUtil; } public User createUser(String firstName, @@ -70,4 +72,18 @@ private void validateRequired(String value, String fieldName) { throw new BusinessRuleException(fieldName + " is required."); } } + public String authenticateAndGenerateToken(String email, String password) { + User user = userRepository.findAll().stream() + .filter(u -> u.getEmail().equals(email)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Invalid credentials.")); + + // Use their existing domain logic to validate password and update login attempts + user.login(email, password); + userRepository.save(user); + + // Return the real JWT instead of the dummy domain token + return jwtUtil.generateToken(user.getEmail(), user.getRole()); + } + } \ No newline at end of file diff --git a/src/com/hpms/security/JwtAuthenticationFilter.java b/src/com/hpms/security/JwtAuthenticationFilter.java new file mode 100644 index 0000000..b1f36b1 --- /dev/null +++ b/src/com/hpms/security/JwtAuthenticationFilter.java @@ -0,0 +1,50 @@ +package com.hpms.security; + +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.Collections; + +@Component +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private final JwtUtil jwtUtil; + + public JwtAuthenticationFilter(JwtUtil jwtUtil) { + this.jwtUtil = jwtUtil; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + + final String authHeader = request.getHeader("Authorization"); + + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + filterChain.doFilter(request, response); + return; + } + + final String token = authHeader.substring(7); + + if (jwtUtil.isTokenValid(token) && SecurityContextHolder.getContext().getAuthentication() == null) { + String email = jwtUtil.extractEmail(token); + String role = jwtUtil.extractClaims(token).get("role", String.class); + + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( + email, null, Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + role)) + ); + + SecurityContextHolder.getContext().setAuthentication(authToken); + } + filterChain.doFilter(request, response); + } +} \ No newline at end of file diff --git a/src/com/hpms/security/JwtUtil.java b/src/com/hpms/security/JwtUtil.java new file mode 100644 index 0000000..f8c847a --- /dev/null +++ b/src/com/hpms/security/JwtUtil.java @@ -0,0 +1,48 @@ +package com.hpms.security; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import java.util.Date; + +@Component +public class JwtUtil { + // 256-bit secret key for HS256 algorithm + private final String SECRET = "HpmsUltraSecureSecretKeyForJwtAuthentication2026!"; + private final SecretKey key = Keys.hmacShaKeyFor(SECRET.getBytes()); + private final long EXPIRATION_TIME = 86400000; // 24 hours + + public String generateToken(String email, String role) { + return Jwts.builder() + .subject(email) + .claim("role", role) + .issuedAt(new Date()) + .expiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME)) + .signWith(key) + .compact(); + } + + public Claims extractClaims(String token) { + return Jwts.parser() + .verifyWith(key) + .build() + .parseSignedClaims(token) + .getPayload(); + } + + public String extractEmail(String token) { + return extractClaims(token).getSubject(); + } + + public boolean isTokenValid(String token) { + try { + extractClaims(token); + return true; + } catch (Exception e) { + return false; + } + } +} \ No newline at end of file diff --git a/src/com/hpms/security/SecurityConfig.java b/src/com/hpms/security/SecurityConfig.java new file mode 100644 index 0000000..1cedf36 --- /dev/null +++ b/src/com/hpms/security/SecurityConfig.java @@ -0,0 +1,35 @@ +package com.hpms.security; + +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.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + private final JwtAuthenticationFilter jwtAuthFilter; + + public SecurityConfig(JwtAuthenticationFilter jwtAuthFilter) { + this.jwtAuthFilter = jwtAuthFilter; + } + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .csrf(AbstractHttpConfigurer::disable) + .authorizeHttpRequests(auth -> auth + .requestMatchers("/api/auth/**", "/swagger-ui/**", "/v3/api-docs/**").permitAll() + .anyRequest().authenticated() + ) + .sessionManagement(sess -> sess.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } +} \ No newline at end of file diff --git a/tests/com/hpms/api/ApiIntegrationTest.java b/tests/com/hpms/api/ApiIntegrationTest.java index 467c07b..d07a56d 100644 --- a/tests/com/hpms/api/ApiIntegrationTest.java +++ b/tests/com/hpms/api/ApiIntegrationTest.java @@ -17,7 +17,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @SpringBootTest(classes = HpmsApiApplication.class) -@AutoConfigureMockMvc +@AutoConfigureMockMvc(addFilters = false) @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) class ApiIntegrationTest { @Autowired diff --git a/tests/com/hpms/services/UserServiceTest.java b/tests/com/hpms/services/UserServiceTest.java index d0cf619..63d145b 100644 --- a/tests/com/hpms/services/UserServiceTest.java +++ b/tests/com/hpms/services/UserServiceTest.java @@ -4,6 +4,7 @@ import com.hpms.repositories.inmemory.InMemoryUserRepository; import com.hpms.services.exceptions.BusinessRuleException; import org.junit.jupiter.api.Test; +import com.hpms.security.JwtUtil; import java.util.UUID; @@ -13,7 +14,8 @@ class UserServiceTest { @Test void deactivateCannotRunTwice() { - UserService service = new UserService(new InMemoryUserRepository()); + JwtUtil jwtUtil = new JwtUtil(); + UserService service = new UserService(new InMemoryUserRepository(), jwtUtil); User user = service.createUser("Mia", "N", "mia@hpms.org", "password123", "NURSE", UUID.randomUUID()); service.deactivateUser(user.getUserId());