Skip to content

Commit

Permalink
Merge pull request #29 from kaizerpwn/feature/auth
Browse files Browse the repository at this point in the history
Authentication using httpOnly cookie
  • Loading branch information
kaizerpwn authored Mar 26, 2024
2 parents 53d769e + 6de4ddb commit cd6b073
Show file tree
Hide file tree
Showing 7 changed files with 193 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -76,22 +76,36 @@ 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 = {
@ApiResponse(responseCode = "201", description = "User created", content = @Content(mediaType = "application/json", schema = @Schema(implementation = User.class))),
@ApiResponse(responseCode = "400", description = "Bad request", content = @Content),
@ApiResponse(responseCode = "500", description = "Internal server error", content = @Content)
})
public ResponseEntity<User> createUser(@RequestBody(required = false) @Valid UserDto userDto) {
public ResponseEntity<User> 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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> extractTokenFromRequest(HttpServletRequest request) {
Cookie[] cookies = request.getCookies();

if (cookies != null) {
Optional<Cookie> accessTokenCookie = Arrays.stream(cookies)
.filter(cookie -> "accessToken".equals(cookie.getName()))
.findFirst();

if (accessTokenCookie.isPresent()) {
return Optional.ofNullable(accessTokenCookie.get().getValue());
}
}

return Optional.empty();
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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<SimpleGrantedAuthority> extractAuthoritiesFromClaim(DecodedJWT jwt) {
var claim = jwt.getClaim("role");

if(claim.isNull() || claim.isMissing())
return List.of();

return claim.asList(SimpleGrantedAuthority.class);
}
}
Original file line number Diff line number Diff line change
@@ -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<? extends GrantedAuthority> authorities;

@Override
public Collection<? extends GrantedAuthority> 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;
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -1,26 +1,32 @@
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;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
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/**")
.sessionManagement(sessionManagementConfigurer
-> sessionManagementConfigurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.formLogin(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(registry -> registry
.requestMatchers("/api/users/register").permitAll()
.requestMatchers("/api/users/login").permitAll()
.anyRequest().authenticated()
)
Expand Down

0 comments on commit cd6b073

Please sign in to comment.