Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions api/com/hpms/api/AuthController.java
Original file line number Diff line number Diff line change
@@ -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<Map<String, String>> login(@RequestBody Map<String, String> credentials) {
String email = credentials.get("email");
String password = credentials.get("password");

String token = userService.authenticateAndGenerateToken(email, password);
return ResponseEntity.ok(Map.of("token", token));
}
}
23 changes: 23 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,29 @@
<version>${spring.boot.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>${spring.boot.version}</version>
</dependency>

<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-gson</artifactId>
<version>0.12.5</version>
<scope>runtime</scope>
</dependency>
</dependencies>

<build>
Expand Down
18 changes: 17 additions & 1 deletion services/com/hpms/services/UserService.java
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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());
}

}
50 changes: 50 additions & 0 deletions src/com/hpms/security/JwtAuthenticationFilter.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
48 changes: 48 additions & 0 deletions src/com/hpms/security/JwtUtil.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
35 changes: 35 additions & 0 deletions src/com/hpms/security/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -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();
}
}
2 changes: 1 addition & 1 deletion tests/com/hpms/api/ApiIntegrationTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion tests/com/hpms/services/UserServiceTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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());
Expand Down
Loading