Skip to content

Commit

Permalink
Added JWT support (#11)
Browse files Browse the repository at this point in the history
* Added SecurityConfig class with PasswordEncoder and SecurityFilterChain beans. Implemented UserDetails interface in the User class. Added the implementation for the UserDetailsService interface. Added Role entity. Added RoleRepository. Added roles field to the User entity. Added Liquibase changesets to insert roles into DB and assign roles for users.

* Added JWT support

* Added handling of JwtException and AuthenticationException in GlobalExceptionHandler

* Removed redundant Key to SecretKey cast in JwtUtil in lines 38 & 53

* Added swagger request matchers

* Added token prefix and auth header strings to constant. Removed redundant AuthenticationException handling in GlobalExceptionHandler. Added max size limits for email and password in UserLoginRequestDto
  • Loading branch information
nklimovych authored May 15, 2024
1 parent afcb489 commit 7e352d8
Show file tree
Hide file tree
Showing 15 changed files with 217 additions and 11 deletions.
15 changes: 15 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,21 @@
<artifactId>spring-boot-starter-security</artifactId>
<version>3.2.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.5</version>
</dependency>
</dependencies>

<build>
Expand Down
22 changes: 21 additions & 1 deletion src/main/java/mate/academy/bookstore/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
import static org.springframework.security.config.Customizer.withDefaults;

import lombok.RequiredArgsConstructor;
import mate.academy.bookstore.security.JwtAuthenticationFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
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.configurers.AbstractHttpConfigurer;
Expand All @@ -13,12 +16,14 @@
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@EnableMethodSecurity
@Configuration
@RequiredArgsConstructor
public class SecurityConfig {
private final UserDetailsService userDetailsService;
private final JwtAuthenticationFilter jwtAuthFilter;

@Bean
public PasswordEncoder getPasswordEncoder() {
Expand All @@ -32,15 +37,30 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(
auth -> auth
.requestMatchers("/auth/**", "/error", "/swagger-ui/**")
.requestMatchers(
"/auth/**",
"/error",
"/swagger-ui/**",
"/v3/api-docs/**",
"/swagger-resources/**",
"/swagger-ui.html",
"/webjars/**")
.permitAll()
.anyRequest()
.authenticated()
)
.httpBasic(withDefaults())
.sessionManagement(
session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
.userDetailsService(userDetailsService)
.build();
}

@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration authenticationConfiguration
) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import mate.academy.bookstore.dto.user.UserLoginRequestDto;
import mate.academy.bookstore.dto.user.UserLoginResponseDto;
import mate.academy.bookstore.dto.user.UserRegistrationRequestDto;
import mate.academy.bookstore.dto.user.UserResponseDto;
import mate.academy.bookstore.dto.user.UserRegistrationResponseDto;
import mate.academy.bookstore.exception.RegistrationException;
import mate.academy.bookstore.security.AuthenticationService;
import mate.academy.bookstore.service.UserService;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.PostMapping;
Expand All @@ -21,12 +24,18 @@
@RequestMapping(value = "/auth")
public class AuthenticationController {
private final UserService userService;
private final AuthenticationService authenticationService;

@PostMapping("/registration")
@ResponseStatus(HttpStatus.CREATED)
@Operation(summary = "Register new user", description = "Register new users with unique email"
) public UserResponseDto register(
) public UserRegistrationResponseDto register(
@RequestBody @Valid UserRegistrationRequestDto userDto) throws RegistrationException {
return userService.save(userDto);
}

@PostMapping("/login")
public UserLoginResponseDto login(@RequestBody @Valid UserLoginRequestDto requestDto) {
return authenticationService.authenticate(requestDto);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package mate.academy.bookstore.dto.user;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

public record UserLoginRequestDto(
@NotBlank
@Size(min = 8, max = 20)
@Email
String email,
@NotBlank
@Size(min = 8, max = 20, message = "Password must be at least 8 characters long")
String password
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package mate.academy.bookstore.dto.user;

public record UserLoginResponseDto(String token) {
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package mate.academy.bookstore.dto.user;

public record UserResponseDto(
public record UserRegistrationResponseDto(
Long id,
String email,
String firstName,
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/mate/academy/bookstore/mapper/UserMapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@

import mate.academy.bookstore.config.MapperConfig;
import mate.academy.bookstore.dto.user.UserRegistrationRequestDto;
import mate.academy.bookstore.dto.user.UserResponseDto;
import mate.academy.bookstore.dto.user.UserRegistrationResponseDto;
import mate.academy.bookstore.model.User;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;

@Mapper(config = MapperConfig.class)
public interface UserMapper {
UserResponseDto toDto(User user);
UserRegistrationResponseDto toDto(User user);

@Mapping(target = "id", ignore = true)
@Mapping(target = "roles", ignore = true)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package mate.academy.bookstore.security;

import lombok.RequiredArgsConstructor;
import mate.academy.bookstore.dto.user.UserLoginRequestDto;
import mate.academy.bookstore.dto.user.UserLoginResponseDto;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service;

@RequiredArgsConstructor
@Service
public class AuthenticationService {
private final JwtUtil jwtUtil;
private final AuthenticationManager authenticationManager;

public UserLoginResponseDto authenticate(UserLoginRequestDto request) {
final Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(request.email(), request.password())
);

String token = jwtUtil.generateToken(authentication.getName());
return new UserLoginResponseDto(token);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package mate.academy.bookstore.security;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

@RequiredArgsConstructor
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private static final String BEARER_PREFIX = "Bearer ";
private static final String AUTHORIZATION = "Authorization";
private final UserDetailsService userDetailsService;
private final JwtUtil jwtUtil;

@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain
) throws ServletException, IOException {
String token = getToken(request);

if (token != null && jwtUtil.isValidToken(token)) {
String username = jwtUtil.getUsername(token);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
Authentication authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities()
);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}

private String getToken(HttpServletRequest request) {
String bearerToken = request.getHeader(AUTHORIZATION);
if (bearerToken != null && bearerToken.startsWith(BEARER_PREFIX)) {
return bearerToken.substring(BEARER_PREFIX.length());
}
return null;
}
}
59 changes: 59 additions & 0 deletions src/main/java/mate/academy/bookstore/security/JwtUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package mate.academy.bookstore.security;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.function.Function;
import javax.crypto.SecretKey;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class JwtUtil {
private final SecretKey secret;

@Value("${jwt.expiration}")
private Long expiration;

public JwtUtil(@Value("${jwt.secret}") String secretKey) {
secret = Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8));
}

public String generateToken(String username) {
return Jwts.builder()
.subject(username)
.issuedAt(new Date(System.currentTimeMillis()))
.expiration(new Date(System.currentTimeMillis() + expiration))
.signWith(secret)
.compact();
}

public boolean isValidToken(String token) {
try {
Jws<Claims> claimsJws = Jwts.parser()
.verifyWith(secret)
.build()
.parseSignedClaims(token);
return !claimsJws.getPayload().getExpiration().before(new Date());
} catch (JwtException | IllegalArgumentException e) {
throw new JwtException("Expired or invalid JWT token");
}
}

public String getUsername(String token) {
return getClaimsFromToken(token, Claims::getSubject);
}

private <T> T getClaimsFromToken(String token, Function<Claims, T> claimsResolver) {
final Claims claims = Jwts.parser()
.verifyWith(secret)
.build()
.parseSignedClaims(token)
.getPayload();
return claimsResolver.apply(claims);
}
}
5 changes: 3 additions & 2 deletions src/main/java/mate/academy/bookstore/service/UserService.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package mate.academy.bookstore.service;

import mate.academy.bookstore.dto.user.UserRegistrationRequestDto;
import mate.academy.bookstore.dto.user.UserResponseDto;
import mate.academy.bookstore.dto.user.UserRegistrationResponseDto;
import mate.academy.bookstore.exception.RegistrationException;

public interface UserService {
UserResponseDto save(UserRegistrationRequestDto requestDto) throws RegistrationException;
UserRegistrationResponseDto save(UserRegistrationRequestDto requestDto)
throws RegistrationException;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import java.util.Collections;
import lombok.RequiredArgsConstructor;
import mate.academy.bookstore.dto.user.UserRegistrationRequestDto;
import mate.academy.bookstore.dto.user.UserResponseDto;
import mate.academy.bookstore.dto.user.UserRegistrationResponseDto;
import mate.academy.bookstore.exception.RegistrationException;
import mate.academy.bookstore.mapper.UserMapper;
import mate.academy.bookstore.model.RoleName;
Expand All @@ -23,7 +23,7 @@ public class UserServiceImpl implements UserService {
private final RoleRepository roleRepository;

@Override
public UserResponseDto save(UserRegistrationRequestDto requestDto)
public UserRegistrationResponseDto save(UserRegistrationRequestDto requestDto)
throws RegistrationException {
String email = requestDto.getEmail();
if (userRepository.existsByEmailIgnoreCase(email)) {
Expand Down
3 changes: 3 additions & 0 deletions src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,6 @@ spring.jpa.show-sql=true
spring.jpa.open-in-view=false

server.servlet.context-path=/api

jwt.expiration=300000
jwt.secret=${JWT_SECRET}
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@ databaseChangeLog:
type: enum('USER', 'ADMIN')
constraints:
nullable: false
unique: true
unique: true
3 changes: 3 additions & 0 deletions src/test/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@ spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect

jwt.expiration=300000
jwt.secret=JustAnotherSuperSecretString1234!

0 comments on commit 7e352d8

Please sign in to comment.