Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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,
Expand All @@ -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<String, String> 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();
Expand All @@ -76,4 +81,9 @@ public PasswordEncoder passwordEncoder() {
}


@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration){
return configuration.getAuthenticationManager();
}

}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<TokenResponse> 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<UserDto> registerUser(@RequestBody UserDto user) {
Expand Down
20 changes: 20 additions & 0 deletions src/main/java/com/neuroncrafters/auth_app/dtos/ApiError.java
Original file line number Diff line number Diff line change
@@ -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));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.neuroncrafters.auth_app.dtos;

public record LoginRequest(
String email,
String password
) {
}
13 changes: 13 additions & 0 deletions src/main/java/com/neuroncrafters/auth_app/dtos/TokenResponse.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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<ApiError> 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<ErrorResponse> handleResourceNotFoundException(ResourceNotFoundException ex){
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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/");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -21,6 +23,8 @@
import java.util.UUID;

@Service
@Getter
@Setter
public class JwtService {
private final SecretKey key;
private final long accessTtlSeconds;
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/application-dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down