Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added JWT support #11

Merged
merged 15 commits into from
May 15, 2024
Merged
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
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();
}
Comment on lines +60 to +65

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it necessary to add this?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added it because Spring can't awtowire AuthenticationManager and app failed to start:
Could not autowire. No beans of 'AuthenticationManager' type found.

}
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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@Email
@Email
@Size(min = 8, max = 20)

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!
Loading