Commit 7a114ddc authored by Phùng Quốc Toàn's avatar Phùng Quốc Toàn

feat: add exception for unauthenticate api

parent 2b4e8dad
......@@ -53,16 +53,19 @@
<artifactId>mapstruct</artifactId>
<version>${mapstruct.version}</version>
</dependency>
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>9.30.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
<build>
......
package com.devteria.identityservice.configuration;
import com.devteria.identityservice.entity.User;
import com.devteria.identityservice.enums.Role;
import com.devteria.identityservice.repository.UserRepository;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import lombok.experimental.FieldDefaults;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.password.PasswordEncoder;
import java.util.HashSet;
@Configuration
@RequiredArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
@Slf4j
public class ApplicationInitConfig {
PasswordEncoder passwordEncoder;
@Bean
ApplicationRunner runner(UserRepository userRepository) {
return args -> {
if (!userRepository.existsByUsername("admin")) {
var roles = new HashSet<String>();
roles.add(Role.ADMIN.name());
User user = User.builder()
.username("admin")
.password(passwordEncoder.encode("admin"))
.roles(roles)
.build();
userRepository.save(user);
log.warn("Admin user created with username: admin and password: admin");
}
};
}
}
package com.devteria.identityservice.configuration;
import com.devteria.identityservice.dto.request.ApiResponse;
import com.devteria.identityservice.exception.ErrorCode;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import java.io.IOException;
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
ErrorCode errorCode = ErrorCode.UNAUTHENTICATED;
response.setStatus(errorCode.getStatusCode().value());
response.setContentType("application/json");
ApiResponse<?> apiResponse = ApiResponse.builder()
.code(errorCode.getCode())
.message(errorCode.getMessage())
.build();
ObjectMapper objectMapper = new ObjectMapper();
response.getWriter().write(objectMapper.writeValueAsString(apiResponse));
response.flushBuffer();
}
}
package com.devteria.identityservice.configuration;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import org.springframework.security.web.SecurityFilterChain;
import javax.crypto.spec.SecretKeySpec;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
private final String[] PUBLIC_ENDPOINTS = new String[]{"/auth/token", "/auth/introspect", "/users"};
@Value("${jwt.signerKey}")
private String signerKey;
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity.authorizeHttpRequests(request -> request.requestMatchers(HttpMethod.POST, PUBLIC_ENDPOINTS)
.permitAll()
.anyRequest()
.authenticated());
httpSecurity.oauth2ResourceServer(oauth2 ->
oauth2.jwt(jwtConfigurer ->
jwtConfigurer.decoder(jwtDecoder())
.jwtAuthenticationConverter(jwtConverter()))
.authenticationEntryPoint(new JwtAuthenticationEntryPoint()));
httpSecurity.csrf(AbstractHttpConfigurer::disable);
return httpSecurity.build();
}
@Bean
JwtAuthenticationConverter jwtConverter() {
JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
jwtGrantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
return jwtAuthenticationConverter;
}
@Bean
JwtDecoder jwtDecoder() {
SecretKeySpec secretKeySpec = new SecretKeySpec(signerKey.getBytes(), "HS512");
return NimbusJwtDecoder.withSecretKey(secretKeySpec).macAlgorithm(MacAlgorithm.HS512).build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(10);
}
}
\ No newline at end of file
......@@ -9,6 +9,8 @@ import jakarta.validation.Valid;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import lombok.experimental.FieldDefaults;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;
import java.util.List;
......@@ -17,42 +19,56 @@ import java.util.List;
@RequestMapping("/users")
@RequiredArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
@Slf4j
public class UserController {
UserService userService;
@PostMapping
ApiResponse<UserResponse> createUser(@RequestBody @Valid UserCreationRequest request){
ApiResponse<UserResponse> createUser(@RequestBody @Valid UserCreationRequest request) {
return ApiResponse.<UserResponse>builder()
.result(userService.createUser(request))
.build();
}
@GetMapping
ApiResponse<List<UserResponse>> getUsers(){
ApiResponse<List<UserResponse>> getUsers() {
var authentication = SecurityContextHolder.getContext().getAuthentication();
log.info("User :{}", authentication.getName());
authentication.getAuthorities().forEach(grantedAuthority -> log.info(grantedAuthority.getAuthority()));
return ApiResponse.<List<UserResponse>>builder()
.result(userService.getUsers())
.build();
}
@GetMapping("/{userId}")
ApiResponse<UserResponse> getUser(@PathVariable("userId") String userId){
ApiResponse<UserResponse> getUser(@PathVariable("userId") String userId) {
return ApiResponse.<UserResponse>builder()
.result(userService.getUser(userId))
.build();
}
@PutMapping("/{userId}")
ApiResponse<UserResponse> updateUser(@PathVariable String userId, @RequestBody UserUpdateRequest request){
ApiResponse<UserResponse> updateUser(@PathVariable String userId, @RequestBody UserUpdateRequest request) {
return ApiResponse.<UserResponse>builder()
.result(userService.updateUser(userId, request))
.build();
}
@DeleteMapping("/{userId}")
ApiResponse<String> deleteUser(@PathVariable String userId){
ApiResponse<String> deleteUser(@PathVariable String userId) {
userService.deleteUser(userId);
return ApiResponse.<String>builder()
.result("User has been deleted")
.build();
}
@GetMapping("/my-info")
ApiResponse<UserResponse> getMyInfo() {
return ApiResponse.<UserResponse>builder()
.result(userService.getMyInfo())
.build();
}
}
......@@ -4,6 +4,7 @@ import lombok.*;
import lombok.experimental.FieldDefaults;
import java.time.LocalDate;
import java.util.Set;
@Data
@NoArgsConstructor
......@@ -13,8 +14,9 @@ import java.time.LocalDate;
public class UserResponse {
String id;
String username;
String password;
String firstName;
String lastName;
LocalDate dob;
Set<String> roles;
}
......@@ -4,65 +4,28 @@ import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.*;
import lombok.experimental.FieldDefaults;
import java.time.LocalDate;
import java.util.Set;
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE)
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private String id;
private String username;
private String password;
private String firstName;
private String lastName;
private LocalDate dob;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public LocalDate getDob() {
return dob;
}
String id;
String username;
String password;
String firstName;
String lastName;
LocalDate dob;
Set<String> roles;
public void setDob(LocalDate dob) {
this.dob = dob;
}
}
package com.devteria.identityservice.enums;
public enum Role {
ADMIN,
USER,
}
\ No newline at end of file
package com.devteria.identityservice.exception;
import lombok.Getter;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
@Getter
public enum ErrorCode {
UNCATEGORIZED_EXCEPTION(9999, "Uncategorized error"),
INVALID_KEY(1001, "Uncategorized error"),
USER_EXISTED(1002, "User existed"),
USERNAME_INVALID(1003, "Username must be at least 3 characters"),
INVALID_PASSWORD(1004, "Password must be at least 8 characters"),
USER_NOT_EXISTED(1005, "User not existed"),
UNAUTHENTICATED(1006, "Unauthenticated"),
UNCATEGORIZED_EXCEPTION(9999, "Uncategorized error", HttpStatus.INTERNAL_SERVER_ERROR),
INVALID_KEY(1001, "Uncategorized error", HttpStatus.BAD_REQUEST),
USER_EXISTED(1002, "User existed", HttpStatus.BAD_REQUEST),
USERNAME_INVALID(1003, "Username must be at least 3 characters", HttpStatus.BAD_REQUEST),
INVALID_PASSWORD(1004, "Password must be at least 8 characters", HttpStatus.BAD_REQUEST),
USER_NOT_EXISTED(1005, "User not existed", HttpStatus.NOT_FOUND),
UNAUTHENTICATED(1006, "Unauthenticated", HttpStatus.UNAUTHORIZED),
UNAUTHORIZED(1007, "You do not have permission", HttpStatus.FORBIDDEN),
;
ErrorCode(int code, String message) {
this.code = code;
this.message = message;
}
private int code;
private String message;
private HttpStatusCode statusCode;
public int getCode() {
return code;
ErrorCode(int code, String message, HttpStatusCode statusCode) {
this.code = code;
this.message = message;
this.statusCode = statusCode;
}
public String getMessage() {
return message;
}
}
package com.devteria.identityservice.exception;
import com.devteria.identityservice.dto.request.ApiResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(value = Exception.class)
ResponseEntity<ApiResponse> handlingRuntimeException(RuntimeException exception){
ResponseEntity<ApiResponse> handlingRuntimeException(RuntimeException exception) {
log.error("Exception: ", exception);
ApiResponse apiResponse = new ApiResponse();
apiResponse.setCode(ErrorCode.UNCATEGORIZED_EXCEPTION.getCode());
......@@ -20,25 +24,25 @@ public class GlobalExceptionHandler {
}
@ExceptionHandler(value = AppException.class)
ResponseEntity<ApiResponse> handlingAppException(AppException exception){
ResponseEntity<ApiResponse> handlingAppException(AppException exception) {
ErrorCode errorCode = exception.getErrorCode();
ApiResponse apiResponse = new ApiResponse();
apiResponse.setCode(errorCode.getCode());
apiResponse.setMessage(errorCode.getMessage());
return ResponseEntity.badRequest().body(apiResponse);
return ResponseEntity.status(errorCode.getStatusCode()).body(apiResponse);
}
@ExceptionHandler(value = MethodArgumentNotValidException.class)
ResponseEntity<ApiResponse> handlingValidation(MethodArgumentNotValidException exception){
ResponseEntity<ApiResponse> handlingValidation(MethodArgumentNotValidException exception) {
String enumKey = exception.getFieldError().getDefaultMessage();
ErrorCode errorCode = ErrorCode.INVALID_KEY;
try {
errorCode = ErrorCode.valueOf(enumKey);
} catch (IllegalArgumentException e){
} catch (IllegalArgumentException e) {
}
......@@ -49,4 +53,11 @@ public class GlobalExceptionHandler {
return ResponseEntity.badRequest().body(apiResponse);
}
@ExceptionHandler(value = AccessDeniedException.class)
ResponseEntity<ApiResponse> handlingAccessDeniedException(AccessDeniedException exception) {
ErrorCode errorCode = ErrorCode.UNAUTHORIZED;
return ResponseEntity.status(errorCode.getStatusCode()).body(ApiResponse.builder().code(errorCode.getCode()).message(errorCode.getMessage()).build());
}
}
......@@ -4,6 +4,7 @@ import com.devteria.identityservice.dto.request.AuthenticationRequest;
import com.devteria.identityservice.dto.request.IntrospectRequest;
import com.devteria.identityservice.dto.response.AuthenticationResponse;
import com.devteria.identityservice.dto.response.IntrospectResponse;
import com.devteria.identityservice.entity.User;
import com.devteria.identityservice.exception.AppException;
import com.devteria.identityservice.exception.ErrorCode;
import com.devteria.identityservice.repository.UserRepository;
......@@ -21,11 +22,13 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.text.ParseException;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import java.util.StringJoiner;
@Service
@RequiredArgsConstructor
......@@ -55,7 +58,7 @@ public class AuthenticationService {
.build();
}
public AuthenticationResponse authenticate(AuthenticationRequest request){
public AuthenticationResponse authenticate(AuthenticationRequest request) {
PasswordEncoder passwordEncoder = new BCryptPasswordEncoder(10);
var user = userRepository.findByUsername(request.getUsername())
.orElseThrow(() -> new AppException(ErrorCode.USER_NOT_EXISTED));
......@@ -65,7 +68,7 @@ public class AuthenticationService {
if (!authenticated)
throw new AppException(ErrorCode.UNAUTHENTICATED);
var token = generateToken(request.getUsername());
var token = generateToken(user);
return AuthenticationResponse.builder()
.token(token)
......@@ -73,17 +76,17 @@ public class AuthenticationService {
.build();
}
private String generateToken(String username) {
private String generateToken(User user) {
JWSHeader header = new JWSHeader(JWSAlgorithm.HS512);
JWTClaimsSet jwtClaimsSet = new JWTClaimsSet.Builder()
.subject(username)
.issuer("devteria.com")
.subject(user.getUsername())
.issuer("demo_jwt")
.issueTime(new Date())
.expirationTime(new Date(
Instant.now().plus(1, ChronoUnit.HOURS).toEpochMilli()
))
.claim("userId", "Custom")
.claim("scope", buildScope(user))
.build();
Payload payload = new Payload(jwtClaimsSet.toJSONObject());
......@@ -98,4 +101,14 @@ public class AuthenticationService {
throw new RuntimeException(e);
}
}
private String buildScope(User user) {
StringJoiner stringJoiner = new StringJoiner("");
if (!CollectionUtils.isEmpty(user.getRoles())) {
user.getRoles().forEach(stringJoiner::add);
}
return stringJoiner.toString();
}
}
......@@ -4,6 +4,7 @@ import com.devteria.identityservice.dto.request.UserCreationRequest;
import com.devteria.identityservice.dto.request.UserUpdateRequest;
import com.devteria.identityservice.dto.response.UserResponse;
import com.devteria.identityservice.entity.User;
import com.devteria.identityservice.enums.Role;
import com.devteria.identityservice.exception.AppException;
import com.devteria.identityservice.exception.ErrorCode;
import com.devteria.identityservice.mapper.UserMapper;
......@@ -11,27 +12,36 @@ import com.devteria.identityservice.repository.UserRepository;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import lombok.experimental.FieldDefaults;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.HashSet;
import java.util.List;
@Service
@RequiredArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
@Slf4j
public class UserService {
UserRepository userRepository;
UserMapper userMapper;
PasswordEncoder passwordEncoder;
public UserResponse createUser(UserCreationRequest request){
public UserResponse createUser(UserCreationRequest request) {
if (userRepository.existsByUsername(request.getUsername()))
throw new AppException(ErrorCode.USER_EXISTED);
User user = userMapper.toUser(request);
PasswordEncoder passwordEncoder = new BCryptPasswordEncoder(10);
user.setPassword(passwordEncoder.encode(request.getPassword()));
HashSet<String> roles = new HashSet<>();
roles.add(Role.USER.name());
user.setRoles(roles);
return userMapper.toUserResponse(userRepository.save(user));
}
......@@ -44,17 +54,28 @@ public class UserService {
return userMapper.toUserResponse(userRepository.save(user));
}
public void deleteUser(String userId){
public void deleteUser(String userId) {
userRepository.deleteById(userId);
}
public List<UserResponse> getUsers(){
@PreAuthorize("hasRole('ADMIN')")
public List<UserResponse> getUsers() {
log.info("Get all users");
return userRepository.findAll().stream()
.map(userMapper::toUserResponse).toList();
}
public UserResponse getUser(String id){
@PostAuthorize("returnObject.username == authentication.name or hasRole('ADMIN')")
public UserResponse getUser(String id) {
log.info("Get user with id: {}", id);
return userMapper.toUserResponse(userRepository.findById(id)
.orElseThrow(() -> new RuntimeException("User not found")));
}
public UserResponse getMyInfo() {
String name = SecurityContextHolder.getContext().getAuthentication().getName();
User user = userRepository.findByUsername(name).orElseThrow(() -> new AppException(ErrorCode.USER_NOT_EXISTED));
return userMapper.toUserResponse(user);
}
}
......@@ -5,9 +5,9 @@ server:
spring:
datasource:
url: "jdbc:mysql://localhost:3306/identity_service"
username: root
password: root
url: "jdbc:mysql://10.84.86.33:3306/demo_jwt"
username: rdvivas
password: rdvivas@123
jpa:
hibernate:
ddl-auto: update
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment