AuthController.java
package com.MedilaboSolutions.gateway.controller;
import com.MedilaboSolutions.gateway.dto.AuthRequest;
import com.MedilaboSolutions.gateway.dto.AuthResponse;
import com.MedilaboSolutions.gateway.service.UserService;
import com.MedilaboSolutions.gateway.utils.AuthUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpCookie;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseCookie;
import org.springframework.http.ResponseEntity;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.time.Duration;
import java.util.List;
@Slf4j
@RestController
@RequiredArgsConstructor
public class AuthController {
@Value("${jwt.accessTokenExpirationMs}")
private long accessTokenExpirationMs;
@Value("${jwt.refreshTokenExpirationMs}")
private long refreshTokenExpirationMs;
private final ReactiveUserDetailsService reactiveUserDetailsService;
private final UserService userService;
private final AuthUtil authUtil;
private final PasswordEncoder encoder;
@PostMapping("/login")
public Mono<ResponseEntity<AuthResponse>> login(@RequestBody AuthRequest authRequest,
ServerHttpResponse response) {
log.info("login start");
return reactiveUserDetailsService
.findByUsername(authRequest.getUsername())
.filter(user -> encoder.matches(authRequest.getPassword(), user.getPassword()))
.switchIfEmpty(Mono.error(new BadCredentialsException("Invalid credentials")))
.flatMap(userDetails -> userService.findByUsername(userDetails.getUsername()))
.flatMap(user -> {
String username = user.getUsername();
String role = "ROLE_" + user.getRole();
String pictureUrl = user.getUrlPicture();
// Generate access and refresh tokens
String accessToken = authUtil.generateAccessToken(username, role, null);
String refreshToken = authUtil.generateRefreshToken(username);
// Store refresh token in an HttpOnly cookie which will be sent only in requests from the same domain
ResponseCookie refreshCookie = ResponseCookie.from("refreshToken", refreshToken)
.httpOnly(true) // Prevent access from JavaScript (XSS protection)
.secure(true) // ⚠️ In production, need to be true (HTTPS)
.sameSite("Strict") // Cross-site requests won’t include the cookie, (CSRF attacks protection)
.maxAge(Duration.ofMillis(refreshTokenExpirationMs))
.path("/") // Cookie included in requests to all paths on the same domain
.build();
response.addCookie(refreshCookie);
// Send access token in the response body
AuthResponse loginResponse = new AuthResponse(
accessToken,
accessTokenExpirationMs,
user.getEmail(),
user.getUsername(),
pictureUrl
);
log.info("Login success for user: {}", username);
return Mono.just(ResponseEntity.ok(loginResponse));
})
.doOnError(e -> log.warn("Login failed for user {}: {}", authRequest.getUsername(), e.getMessage()))
.onErrorResume(e -> {
if (e instanceof UsernameNotFoundException || e instanceof BadCredentialsException) {
return Mono.just(ResponseEntity.status(HttpStatus.UNAUTHORIZED).build());
}
return Mono.just(ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build());
});
}
@PostMapping("/refresh")
public Mono<ResponseEntity<AuthResponse>> refresh(ServerWebExchange exchange) {
log.info("refresh start");
return Mono
// Get refresh token from the cookie, validate it, then extract the username
.fromCallable(() -> {
MultiValueMap<String, HttpCookie> cookies = exchange.getRequest().getCookies();
List<HttpCookie> refreshCookies = cookies.get("refreshToken");
if (refreshCookies == null || refreshCookies.isEmpty()) {
log.warn("Refresh token missing");
throw new BadCredentialsException("Refresh token missing");
}
String refreshToken = refreshCookies.getFirst().getValue();
if (!authUtil.isValidToken(refreshToken) || !authUtil.isValidRefreshTokenType(refreshToken)) {
log.warn("Invalid refresh token");
throw new BadCredentialsException("Invalid refresh token");
}
log.info("refresh success");
return authUtil.getAllClaimsFromToken(refreshToken).getSubject();
})
.flatMap(username ->
reactiveUserDetailsService.findByUsername(username)
.switchIfEmpty(Mono.error(new BadCredentialsException("User not found: " + username)))
// UserDetails found, now retrieve full User entity
.flatMap(userDetails -> userService.findByUsername(userDetails.getUsername()))
)
.map(user -> {
String role = "ROLE_" + user.getRole();
String username = user.getUsername();
String pictureUrl = user.getUrlPicture();
String newAccessToken = authUtil.generateAccessToken(user.getUsername(), role, pictureUrl);
log.info("Refresh token success for user: {}", username);
// Send access token in the response body
AuthResponse refreshResponse = new AuthResponse(
newAccessToken,
accessTokenExpirationMs,
user.getEmail(),
user.getUsername(),
pictureUrl
);
return ResponseEntity.ok(refreshResponse);
})
.onErrorResume(e -> {
log.warn("Refresh token failed: {}", e.getMessage());
return Mono.just(ResponseEntity.status(HttpStatus.UNAUTHORIZED).build());
});
}
}