Skip to content

Commit

Permalink
feat: refresh token
Browse files Browse the repository at this point in the history
  • Loading branch information
Toto-hitori committed Feb 6, 2024
1 parent 7820bf5 commit 5a0c70c
Show file tree
Hide file tree
Showing 10 changed files with 103 additions and 47 deletions.
15 changes: 8 additions & 7 deletions api/src/main/java/lab/en2b/quizapi/auth/AuthService.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package lab.en2b.quizapi.auth;

import lab.en2b.quizapi.auth.config.UserDetailsImpl;
import lab.en2b.quizapi.auth.dtos.JwtResponseDto;
import lab.en2b.quizapi.auth.dtos.LoginDto;
import lab.en2b.quizapi.auth.dtos.RefreshTokenDto;
import lab.en2b.quizapi.auth.dtos.RegisterDto;
import lab.en2b.quizapi.auth.dtos.*;
import lab.en2b.quizapi.auth.jwt.JwtUtils;
import lab.en2b.quizapi.commons.exceptions.TokenRefreshException;
import lab.en2b.quizapi.auth.dtos.RefreshTokenResponseDto;
import lab.en2b.quizapi.user.User;
import lab.en2b.quizapi.user.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
Expand All @@ -19,7 +19,6 @@
import java.util.Set;

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class AuthService {
private final JwtUtils jwtUtils;
Expand All @@ -39,7 +38,7 @@ public ResponseEntity<JwtResponseDto> login(LoginDto loginRequest){

return ResponseEntity.ok(new JwtResponseDto(
jwtUtils.generateJwtTokenUserPassword(authentication),
jwtUtils.createRefreshToken(userDetails.getId()),
userService.assignNewRefreshToken(userDetails.getId()),
userDetails.getId(),
userDetails.getUsername(),
userDetails.getEmail(),
Expand All @@ -53,6 +52,8 @@ public ResponseEntity<?> register(RegisterDto registerRequest) {
}

public ResponseEntity<?> refreshToken(RefreshTokenDto refreshTokenRequest) {
throw new UnsupportedOperationException();
User user = userService.findByRefreshToken(refreshTokenRequest.getRefreshToken()).orElseThrow(() -> new TokenRefreshException(
"Refresh token is not in database!"));
return ResponseEntity.ok(new RefreshTokenResponseDto(jwtUtils.generateTokenFromEmail(user.getEmail()), user.obtainRefreshIfValid()));
}
}
56 changes: 25 additions & 31 deletions api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
package lab.en2b.quizapi.auth.config;

import lab.en2b.quizapi.user.UserService;
import lab.en2b.quizapi.auth.jwt.JwtAuthFilter;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
Expand All @@ -22,11 +22,8 @@

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {

@Autowired
private JwtAuthFilter authFilter;
@Autowired
public UserService userService;
@Bean
Expand All @@ -45,39 +42,36 @@ public CorsFilter corsFilter() {
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
/**
* Builds the authorization manager taking into account password encoding
* @param http the http request to secure
* @return the newly created authentication manager
* @throws Exception if something goes wrong when creating the manager
*/
@Bean
public AuthenticationManager authManager(HttpSecurity http) throws Exception {
AuthenticationManagerBuilder authenticationManagerBuilder =
http.getSharedObject(AuthenticationManagerBuilder.class);

DaoAuthenticationProvider userPasswordProvider = new DaoAuthenticationProvider();
userPasswordProvider.setUserDetailsService(userService);
userPasswordProvider.setPasswordEncoder(passwordEncoder());

authenticationManagerBuilder.authenticationProvider(userPasswordProvider);
return authenticationManagerBuilder.build();
}
/**
* Security filter used for filtering all petitions, applying cors and asking for authentication among other things
* @param http the http request to filter
* @return the filtered request
* @throws Exception if any problem happens when filtering
*/
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http.cors(Customizer.withDefaults()).authorizeHttpRequests(authorize ->
authorize.requestMatchers("/auth/login", "/auth/register","/auth/refresh-token").permitAll()
.anyRequest().authenticated())
//TODO: add exception handling
public SecurityFilterChain securityFilterChain(HttpSecurity http, AuthenticationManager authenticationManager) throws Exception {
return http
.cors(Customizer.withDefaults())
.sessionManagement(configuration -> configuration.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(authorize -> authorize
.requestMatchers(HttpMethod.POST,"/auth/**").permitAll()
.anyRequest().authenticated())
.csrf(AbstractHttpConfigurer::disable)
.authenticationManager(authenticationManager)
.build();
//TODO: add exception handling
}


/**
* Builds the authorization manager taking into account password encoding
* @param http the http request to secure
* @return the newly created authentication manager
* @throws Exception if something goes wrong when creating the manager
*/
@Bean
public AuthenticationManager authManager(HttpSecurity http) throws Exception {
AuthenticationManagerBuilder authenticationManagerBuilder = http.getSharedObject(AuthenticationManagerBuilder.class);
authenticationManagerBuilder.userDetailsService(userService).passwordEncoder(passwordEncoder());
return authenticationManagerBuilder.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package lab.en2b.quizapi.auth.dtos;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class RefreshTokenResponseDto {

private String token;
@JsonProperty("refresh_token")
private String refreshToken;

public RefreshTokenResponseDto(String accessToken, String refreshToken) {
this.token = accessToken;
this.refreshToken = refreshToken;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull Ht
if(token != null){
email = jwtUtils.getSubjectFromJwtToken(token);
}
System.out.println("entered");

if ( email != null && SecurityContextHolder.getContext().getAuthentication() == null && isValidJwt(token)) {
UserDetails userDetails = userDetailsService.loadUserByUsername(email);
Expand Down
22 changes: 14 additions & 8 deletions api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,18 @@ public class JwtUtils {

//MUST BE SET AS ENVIRONMENT VARIABLE
@Value("${JWT_SECRET}")
private String jwtSecret;

private String JWT_SECRET;
@Value("${JWT_EXPIRATION_MS}")
private int jwtExpirationMs;
private Long JWT_EXPIRATION_MS;


public String createRefreshToken(Long id) {
throw new UnsupportedOperationException();
}
public String generateJwtTokenUserPassword(Authentication authentication) {
UserDetailsImpl userPrincipal = (UserDetailsImpl) authentication.getPrincipal();

return Jwts.builder()
.subject(userPrincipal.getEmail())
.issuedAt(new Date())
.expiration(new Date((new Date()).getTime() + jwtExpirationMs))
.expiration(new Date((new Date()).getTime() + JWT_EXPIRATION_MS))
.signWith(getSignInKey())
.compact();
}
Expand Down Expand Up @@ -76,7 +73,16 @@ public String getSubjectFromJwtToken(String token) {
}
}
private SecretKey getSignInKey(){
byte[] keyBytes = Decoders.BASE64.decode(jwtSecret);
byte[] keyBytes = Decoders.BASE64.decode(JWT_SECRET);
return Keys.hmacShaKeyFor(keyBytes);
}

public String generateTokenFromEmail(String email) {
return Jwts.builder()
.subject(email)
.issuedAt(new Date())
.expiration(new Date((new Date()).getTime() + JWT_EXPIRATION_MS))
.signWith(getSignInKey())
.compact();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package lab.en2b.quizapi.commons.exceptions;

public class TokenRefreshException extends RuntimeException{
public TokenRefreshException(String message) {
super(message);
}
}
8 changes: 8 additions & 0 deletions api/src/main/java/lab/en2b/quizapi/user/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lab.en2b.quizapi.commons.exceptions.TokenRefreshException;
import lab.en2b.quizapi.user.role.Role;
import lombok.*;

Expand Down Expand Up @@ -65,4 +66,11 @@ public class User {
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler", "permissions"})
@JsonProperty("role")
private Set<Role> roles;

public String obtainRefreshIfValid() {
if(getRefreshExpiration() == null || getRefreshExpiration().compareTo(Instant.now()) < 0){
throw new TokenRefreshException( "Invalid refresh token. Please make a new login request");
}
return getRefreshToken();
}
}
2 changes: 2 additions & 0 deletions api/src/main/java/lab/en2b/quizapi/user/UserRepository.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ public interface UserRepository extends JpaRepository<User,Long> {
Boolean existsByUsername(String username);

Boolean existsByEmail(String email);

Optional<User> findByRefreshToken(String refreshToken);
}
18 changes: 18 additions & 0 deletions api/src/main/java/lab/en2b/quizapi/user/UserService.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,26 @@
import lab.en2b.quizapi.auth.dtos.RegisterDto;
import lab.en2b.quizapi.user.role.RoleRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

import java.time.Instant;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
public class UserService implements UserDetailsService {
private final UserRepository userRepository;
private final RoleRepository roleRepository;
@Value("${REFRESH_TOKEN_DURATION_MS}")
private Long REFRESH_TOKEN_DURATION_MS;
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
return UserDetailsImpl.build(userRepository.findByEmail(email).orElseThrow());
Expand All @@ -34,4 +40,16 @@ public void createUser(RegisterDto registerRequest, Set<String> roleNames){
.roles(roleNames.stream().map( roleName -> roleRepository.findByName(roleName).orElseThrow()).collect(Collectors.toSet()))
.build());
}

public Optional<User> findByRefreshToken(String refreshToken) {
return userRepository.findByRefreshToken(refreshToken);
}

public String assignNewRefreshToken(Long id) {
User user = userRepository.findById(id).orElseThrow();
user.setRefreshToken(UUID.randomUUID().toString());
user.setRefreshExpiration(Instant.now().plusMillis(REFRESH_TOKEN_DURATION_MS));
userRepository.save(user);
return user.getRefreshToken();
}
}
1 change: 1 addition & 0 deletions api/src/main/resources/application.properties
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
JWT_EXPIRATION_MS=86400000
REFRESH_TOKEN_DURATION_MS=86400000
spring.jpa.hibernate.ddl-auto=update
spring.datasource.url=${DATABASE_URL}
spring.datasource.username=${DATABASE_USER}
Expand Down

0 comments on commit 5a0c70c

Please sign in to comment.