diff --git a/src/main/java/com/neuroncrafters/auth_app/config/SecurityConfig.java b/src/main/java/com/neuroncrafters/auth_app/config/SecurityConfig.java index 89cee9c..b496c51 100644 --- a/src/main/java/com/neuroncrafters/auth_app/config/SecurityConfig.java +++ b/src/main/java/com/neuroncrafters/auth_app/config/SecurityConfig.java @@ -1,11 +1,14 @@ package com.neuroncrafters.auth_app.config; +import com.neuroncrafters.auth_app.dtos.ApiError; import com.neuroncrafters.auth_app.security.JwtAuthenticationFilter; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpStatus; +import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; @@ -44,13 +47,13 @@ public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws .cors(Customizer.withDefaults()) .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) - .authorizeHttpRequests(authorizeRequests -> + .authorizeHttpRequests(authorizeHttpRequests -> // Skip authorization for register and login - authorizeRequests - .requestMatchers("/api/v1/auth/register").permitAll() - .requestMatchers("/api/v1/auth/login").permitAll() + authorizeHttpRequests + .requestMatchers("/api/v1/auth/**").permitAll() .anyRequest().authenticated() - ).exceptionHandling(ex -> + ) + .exceptionHandling(ex -> ex.authenticationEntryPoint( (request, response, @@ -59,12 +62,14 @@ public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws authException.printStackTrace(); response.setStatus(HttpStatus.UNAUTHORIZED.value()); response.setContentType("application/json"); - String message = "Unauthorized Access! " + authException.getMessage(); - Map errorMap = Map.of("message", message, - "statusCode", String.valueOf(401), - "error", "Unauthorized"); + String error = (String) request.getAttribute("error"); + String message = authException.getMessage(); + if (error != null) { + message = error; + } + ApiError apiError = ApiError.of(HttpStatus.UNAUTHORIZED.value(), "Unauthorized Access !!", message, request.getRequestURI()); var objectMapper = new ObjectMapper(); - response.getWriter().write(objectMapper.writeValueAsString(errorMap)); + response.getWriter().write(objectMapper.writeValueAsString(apiError)); })) .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); @@ -76,4 +81,9 @@ public PasswordEncoder passwordEncoder() { } + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration){ + return configuration.getAuthenticationManager(); + } + } diff --git a/src/main/java/com/neuroncrafters/auth_app/controllers/AuthController.java b/src/main/java/com/neuroncrafters/auth_app/controllers/AuthController.java index 8404dc8..475abb8 100644 --- a/src/main/java/com/neuroncrafters/auth_app/controllers/AuthController.java +++ b/src/main/java/com/neuroncrafters/auth_app/controllers/AuthController.java @@ -1,10 +1,22 @@ package com.neuroncrafters.auth_app.controllers; +import com.neuroncrafters.auth_app.dtos.LoginRequest; +import com.neuroncrafters.auth_app.dtos.TokenResponse; import com.neuroncrafters.auth_app.dtos.UserDto; +import com.neuroncrafters.auth_app.entities.User; +import com.neuroncrafters.auth_app.repositories.UserRepository; +import com.neuroncrafters.auth_app.security.JwtService; import com.neuroncrafters.auth_app.services.AuthService; +import com.neuroncrafters.auth_app.services.UserService; import lombok.AllArgsConstructor; +import org.modelmapper.ModelMapper; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.DisabledException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -16,6 +28,33 @@ public class AuthController { private final AuthService authService; + private final AuthenticationManager authenticationManager; + private final UserRepository userRepository; + private final JwtService jwtService; + private final ModelMapper modelMapper; + + @PostMapping("/login") + public ResponseEntity login(@RequestBody LoginRequest loginRequest) { + // 1. authenticate + Authentication authentication = authenticate(loginRequest); + User user = userRepository.findByEmail(loginRequest.email()).orElseThrow(() -> new BadCredentialsException("User not found!")); + if(!user.isEnable()) { + throw new DisabledException("User is disabled"); + } + + // 2. Generate JWT token + String accessToken = jwtService.generateToken(user); + TokenResponse response = TokenResponse.of(accessToken, "", jwtService.getAccessTtlSeconds(), modelMapper.map(user, UserDto.class)); + return ResponseEntity.ok(response); + } + + private Authentication authenticate(LoginRequest loginRequest) { + try { + return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(loginRequest.email(), loginRequest.password())); + } catch (Exception e) { + throw new BadCredentialsException("Invalid email or password"); + } + } @PostMapping("/register") public ResponseEntity registerUser(@RequestBody UserDto user) { diff --git a/src/main/java/com/neuroncrafters/auth_app/dtos/ApiError.java b/src/main/java/com/neuroncrafters/auth_app/dtos/ApiError.java new file mode 100644 index 0000000..a6997bc --- /dev/null +++ b/src/main/java/com/neuroncrafters/auth_app/dtos/ApiError.java @@ -0,0 +1,20 @@ +package com.neuroncrafters.auth_app.dtos; + +import org.springframework.http.HttpStatus; + +import java.time.OffsetDateTime; +import java.time.ZoneOffset; + +public record ApiError( + int status, + String error, + String message, + String path, + OffsetDateTime timestamp +) { + + public static ApiError of(int status, String error, String message, String path) { + return new ApiError(status, error, message, path, OffsetDateTime.now(ZoneOffset.UTC)); + } + +} diff --git a/src/main/java/com/neuroncrafters/auth_app/dtos/LoginRequest.java b/src/main/java/com/neuroncrafters/auth_app/dtos/LoginRequest.java new file mode 100644 index 0000000..f8e0ed0 --- /dev/null +++ b/src/main/java/com/neuroncrafters/auth_app/dtos/LoginRequest.java @@ -0,0 +1,7 @@ +package com.neuroncrafters.auth_app.dtos; + +public record LoginRequest( + String email, + String password +) { +} diff --git a/src/main/java/com/neuroncrafters/auth_app/dtos/TokenResponse.java b/src/main/java/com/neuroncrafters/auth_app/dtos/TokenResponse.java new file mode 100644 index 0000000..05c4576 --- /dev/null +++ b/src/main/java/com/neuroncrafters/auth_app/dtos/TokenResponse.java @@ -0,0 +1,13 @@ +package com.neuroncrafters.auth_app.dtos; + +public record TokenResponse( + String accessToken, + String refreshToken, + long expiresIn, + String tokenType, + UserDto user +){ + public static TokenResponse of(String accessToken, String refreshToken, long expiresIn, UserDto user) { + return new TokenResponse(accessToken, refreshToken, expiresIn, "Bearer", user); + } +} diff --git a/src/main/java/com/neuroncrafters/auth_app/exceptions/GlobalExceptionHandler.java b/src/main/java/com/neuroncrafters/auth_app/exceptions/GlobalExceptionHandler.java index e247d79..d856509 100644 --- a/src/main/java/com/neuroncrafters/auth_app/exceptions/GlobalExceptionHandler.java +++ b/src/main/java/com/neuroncrafters/auth_app/exceptions/GlobalExceptionHandler.java @@ -1,14 +1,34 @@ package com.neuroncrafters.auth_app.exceptions; +import com.neuroncrafters.auth_app.dtos.ApiError; import com.neuroncrafters.auth_app.dtos.ErrorResponse; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.JwtException; +import jakarta.servlet.http.HttpServletRequest; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.CredentialsExpiredException; +import org.springframework.security.authentication.DisabledException; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @RestControllerAdvice public class GlobalExceptionHandler { + @ExceptionHandler({ + UsernameNotFoundException.class, + BadCredentialsException.class, + CredentialsExpiredException.class, + DisabledException.class + }) + public ResponseEntity handleAuthException(Exception e, HttpServletRequest request) { + var apiError = ApiError.of(HttpStatus.BAD_REQUEST.value(), "Bad Request", e.getMessage(), request.getRequestURI()); + return ResponseEntity.badRequest().body(apiError); + } + // resource not found exception handler :: method @ExceptionHandler(ResourceNotFoundException.class) public ResponseEntity handleResourceNotFoundException(ResourceNotFoundException ex){ diff --git a/src/main/java/com/neuroncrafters/auth_app/security/CustomUserDetailService.java b/src/main/java/com/neuroncrafters/auth_app/security/CustomUserDetailService.java index 9426f17..4de060e 100644 --- a/src/main/java/com/neuroncrafters/auth_app/security/CustomUserDetailService.java +++ b/src/main/java/com/neuroncrafters/auth_app/security/CustomUserDetailService.java @@ -3,6 +3,7 @@ import com.neuroncrafters.auth_app.repositories.UserRepository; import lombok.AllArgsConstructor; import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; @@ -16,6 +17,6 @@ public class CustomUserDetailService implements UserDetailsService { @Override public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { - return userRepository.findByEmail(email).orElseThrow(() -> new UsernameNotFoundException(email)); + return userRepository.findByEmail(email).orElseThrow(() -> new BadCredentialsException(email)); } } diff --git a/src/main/java/com/neuroncrafters/auth_app/security/JwtAuthenticationFilter.java b/src/main/java/com/neuroncrafters/auth_app/security/JwtAuthenticationFilter.java index cdd39f8..19639ec 100644 --- a/src/main/java/com/neuroncrafters/auth_app/security/JwtAuthenticationFilter.java +++ b/src/main/java/com/neuroncrafters/auth_app/security/JwtAuthenticationFilter.java @@ -89,16 +89,16 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse } }); } catch (ExpiredJwtException e) { - e.printStackTrace(); - } catch (MalformedJwtException e) { - e.printStackTrace(); - } catch (JwtException e) { - e.printStackTrace(); + request.setAttribute("error", "Token is expired"); } catch (Exception e) { - e.printStackTrace(); + request.setAttribute("error", "Token is invalid"); } - } filterChain.doFilter(request, response); } + + @Override + protected boolean shouldNotFilter(HttpServletRequest request) { + return request.getRequestURI().startsWith("/api/v1/auth/"); + } } diff --git a/src/main/java/com/neuroncrafters/auth_app/security/JwtService.java b/src/main/java/com/neuroncrafters/auth_app/security/JwtService.java index b52453d..8c51493 100644 --- a/src/main/java/com/neuroncrafters/auth_app/security/JwtService.java +++ b/src/main/java/com/neuroncrafters/auth_app/security/JwtService.java @@ -7,6 +7,8 @@ import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.security.Keys; +import lombok.Getter; +import lombok.Setter; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @@ -21,6 +23,8 @@ import java.util.UUID; @Service +@Getter +@Setter public class JwtService { private final SecretKey key; private final long accessTtlSeconds; diff --git a/src/main/resources/application-dev.yaml b/src/main/resources/application-dev.yaml index 457fef2..df241a5 100644 --- a/src/main/resources/application-dev.yaml +++ b/src/main/resources/application-dev.yaml @@ -35,7 +35,7 @@ logging: security: jwt: - secret: ${JWT_SECRET:2Ajw}a#.Eyd/8IBos6y@4Ixvyvp75zz/Oyep?n[p}h} + secret: ${JWT_SECRET:258b67b9da8be40b7af5960068fdf0e4faa81738bf56615e146574f9e117cc73a86ce0803718b114c2a811a24065484345855013354b31723a4f947f27baf1aa} issuer: ${JWT_ISSUER:api.substring.com} access-ttl-seconds: ${JWT_ACCESS_TTL_SECONDS:3600} refresh-ttl-seconds: ${JWT_REFRESH_TTL_SECONDS:86400}