diff --git a/app/src/main/java/com/ibrahimokic/ordermanagement/controller/api/UserController.java b/app/src/main/java/com/ibrahimokic/ordermanagement/controller/api/UserController.java index f43a82c..e57a3e2 100644 --- a/app/src/main/java/com/ibrahimokic/ordermanagement/controller/api/UserController.java +++ b/app/src/main/java/com/ibrahimokic/ordermanagement/controller/api/UserController.java @@ -76,7 +76,7 @@ public ResponseEntity getUserById(@PathVariable Long userId) { } } - @PostMapping + @PostMapping("/register") @ResponseStatus(HttpStatus.CREATED) @Operation(summary = "Create new user", description = "Create new user based on request body") @ApiResponses(value = { @@ -84,14 +84,28 @@ public ResponseEntity getUserById(@PathVariable Long userId) { @ApiResponse(responseCode = "400", description = "Bad request", content = @Content), @ApiResponse(responseCode = "500", description = "Internal server error", content = @Content) }) - public ResponseEntity createUser(@RequestBody(required = false) @Valid UserDto userDto) { + public ResponseEntity createUser(@RequestBody(required = false) @Valid UserDto userDto, HttpServletResponse response) { try { if (userDto == null) { return ResponseEntity.badRequest().build(); } + + User user = userMapper.mapFrom(userDto); User createdUser = userService.createUser(user); + String accessToken = jwtIssuer.issue( + user.getUserId(), + user.getUsername(), + user.getRole() + ); + + Cookie cookie = new Cookie("accessToken", accessToken); + cookie.setHttpOnly(true); + cookie.setMaxAge(24 * 60 * 60); + cookie.setPath("/"); + response.addCookie(cookie); + return ResponseEntity.status(HttpStatus.CREATED).body(createdUser); } catch (MappingException mappingException) { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); diff --git a/app/src/main/java/com/ibrahimokic/ordermanagement/security/JwtAuthenticationFilter.java b/app/src/main/java/com/ibrahimokic/ordermanagement/security/JwtAuthenticationFilter.java new file mode 100644 index 0000000..884ca38 --- /dev/null +++ b/app/src/main/java/com/ibrahimokic/ordermanagement/security/JwtAuthenticationFilter.java @@ -0,0 +1,50 @@ +package com.ibrahimokic.ordermanagement.security; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Optional; + +@Component +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends OncePerRequestFilter { + private final JwtDecoder jwtDecoder; + private final JwtToPrincipalConverter jwtToPrincipalConverter; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + extractTokenFromRequest(request) + .map(jwtDecoder::decode) + .map(jwtToPrincipalConverter::convert) + .map(UserPrincipalAuthenticationToken::new) + .ifPresent(authentication -> SecurityContextHolder.getContext().setAuthentication(authentication)); + + filterChain.doFilter(request, response); + } + + public Optional extractTokenFromRequest(HttpServletRequest request) { + Cookie[] cookies = request.getCookies(); + + if (cookies != null) { + Optional accessTokenCookie = Arrays.stream(cookies) + .filter(cookie -> "accessToken".equals(cookie.getName())) + .findFirst(); + + if (accessTokenCookie.isPresent()) { + return Optional.ofNullable(accessTokenCookie.get().getValue()); + } + } + + return Optional.empty(); + } +} diff --git a/app/src/main/java/com/ibrahimokic/ordermanagement/security/JwtDecoder.java b/app/src/main/java/com/ibrahimokic/ordermanagement/security/JwtDecoder.java new file mode 100644 index 0000000..dccdd6d --- /dev/null +++ b/app/src/main/java/com/ibrahimokic/ordermanagement/security/JwtDecoder.java @@ -0,0 +1,20 @@ +package com.ibrahimokic.ordermanagement.security; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.interfaces.DecodedJWT; +import com.ibrahimokic.ordermanagement.config.JwtConfig; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class JwtDecoder { + private final JwtConfig jwtConfig; + public DecodedJWT decode(String token) { + return JWT + .require(Algorithm.HMAC256(jwtConfig.getSecretKey())) + .build() + .verify(token); + } +} diff --git a/app/src/main/java/com/ibrahimokic/ordermanagement/security/JwtToPrincipalConverter.java b/app/src/main/java/com/ibrahimokic/ordermanagement/security/JwtToPrincipalConverter.java new file mode 100644 index 0000000..1e19364 --- /dev/null +++ b/app/src/main/java/com/ibrahimokic/ordermanagement/security/JwtToPrincipalConverter.java @@ -0,0 +1,27 @@ +package com.ibrahimokic.ordermanagement.security; + +import com.auth0.jwt.interfaces.DecodedJWT; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +public class JwtToPrincipalConverter { + public UserPrincipal convert(DecodedJWT jwt) { + return UserPrincipal.builder() + .userId(Long.valueOf(jwt.getSubject())) + .email(jwt.getClaim("email").asString()) + .authorities(extractAuthoritiesFromClaim(jwt)) + .build(); + } + + private List extractAuthoritiesFromClaim(DecodedJWT jwt) { + var claim = jwt.getClaim("role"); + + if(claim.isNull() || claim.isMissing()) + return List.of(); + + return claim.asList(SimpleGrantedAuthority.class); + } +} diff --git a/app/src/main/java/com/ibrahimokic/ordermanagement/security/UserPrincipal.java b/app/src/main/java/com/ibrahimokic/ordermanagement/security/UserPrincipal.java new file mode 100644 index 0000000..f3afcad --- /dev/null +++ b/app/src/main/java/com/ibrahimokic/ordermanagement/security/UserPrincipal.java @@ -0,0 +1,51 @@ +package com.ibrahimokic.ordermanagement.security; + +import lombok.Builder; +import lombok.Data; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; + +@Data +@Builder +public class UserPrincipal implements UserDetails { + private final Long userId; + private final String email; + private final Collection authorities; + + @Override + public Collection getAuthorities() { + return authorities; + } + + @Override + public String getPassword() { + return null; + } + + @Override + public String getUsername() { + return email; + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } +} diff --git a/app/src/main/java/com/ibrahimokic/ordermanagement/security/UserPrincipalAuthenticationToken.java b/app/src/main/java/com/ibrahimokic/ordermanagement/security/UserPrincipalAuthenticationToken.java new file mode 100644 index 0000000..f4c277f --- /dev/null +++ b/app/src/main/java/com/ibrahimokic/ordermanagement/security/UserPrincipalAuthenticationToken.java @@ -0,0 +1,23 @@ +package com.ibrahimokic.ordermanagement.security; + +import org.springframework.security.authentication.AbstractAuthenticationToken; + +public class UserPrincipalAuthenticationToken extends AbstractAuthenticationToken { + private final UserPrincipal principal; + + public UserPrincipalAuthenticationToken(UserPrincipal principal) { + super(principal.getAuthorities()); + this.principal = principal; + setAuthenticated(true); + } + + @Override + public Object getCredentials() { + return null; + } + + @Override + public UserPrincipal getPrincipal() { + return principal; + } +} diff --git a/app/src/main/java/com/ibrahimokic/ordermanagement/security/WebSecurityConfig.java b/app/src/main/java/com/ibrahimokic/ordermanagement/security/WebSecurityConfig.java index bd1adb7..e7729d4 100644 --- a/app/src/main/java/com/ibrahimokic/ordermanagement/security/WebSecurityConfig.java +++ b/app/src/main/java/com/ibrahimokic/ordermanagement/security/WebSecurityConfig.java @@ -1,5 +1,6 @@ package com.ibrahimokic.ordermanagement.security; +import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; @@ -7,13 +8,17 @@ 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 +@RequiredArgsConstructor public class WebSecurityConfig { + private final JwtAuthenticationFilter jwtAuthenticationFilter; @Bean public SecurityFilterChain applicationSecurity(HttpSecurity http) throws Exception { return http + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) .cors(AbstractHttpConfigurer::disable) .csrf(AbstractHttpConfigurer::disable) .securityMatcher("/api/**") @@ -21,6 +26,7 @@ public SecurityFilterChain applicationSecurity(HttpSecurity http) throws Excepti -> sessionManagementConfigurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .formLogin(AbstractHttpConfigurer::disable) .authorizeHttpRequests(registry -> registry + .requestMatchers("/api/users/register").permitAll() .requestMatchers("/api/users/login").permitAll() .anyRequest().authenticated() )