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 endpoint refresh token #129

Merged
merged 3 commits into from
Nov 24, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package com.alura.aluraflixapi.controller.authentication;

import com.alura.aluraflixapi.controller.dto.token.RefreshTokenVO;
import com.alura.aluraflixapi.domain.user.User;
import com.alura.aluraflixapi.domain.user.dto.AuthenticationDto;
import com.alura.aluraflixapi.infraestructure.security.TokenService;
import com.alura.aluraflixapi.infraestructure.security.dto.TokenJwtDto;
import com.alura.aluraflixapi.infraestructure.service.token.RefreshTokenService;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
Expand All @@ -21,10 +23,12 @@ public class AuthenticationController {

private final AuthenticationManager authenticationManager;
private final TokenService tokenService;
private final RefreshTokenService refreshTokenService;

public AuthenticationController(AuthenticationManager authenticationManager, TokenService tokenService) {
public AuthenticationController(AuthenticationManager authenticationManager, TokenService tokenService, RefreshTokenService refreshTokenService) {
this.authenticationManager = authenticationManager;
this.tokenService = tokenService;
this.refreshTokenService = refreshTokenService;
}

@PostMapping
Expand All @@ -35,9 +39,17 @@ public ResponseEntity<TokenJwtDto> login(@RequestBody @Valid AuthenticationDto d
dto.password());
//authenticationManager compare the password of the request with the password from database.
final var authentication = this.authenticationManager.authenticate(authenticationToken);
final var tokenJWT = tokenService.generateTokenJWT((User) authentication.getPrincipal());
final var tokenJwt = tokenService.generateTokenJwt((User) authentication.getPrincipal());
final var refreshToken = tokenService.generateRefreshToken(dto.username());
log.info("Token Generated with Success!");
return ResponseEntity.ok().body(new TokenJwtDto(tokenJWT));
return ResponseEntity.ok().body(new TokenJwtDto(tokenJwt, refreshToken));
}

@PostMapping("/refresh")
public ResponseEntity<TokenJwtDto> refreshAccessToken(@RequestBody RefreshTokenVO refreshToken) {
log.info("Request to renew refresh accessToken");
final var tokenJwtDto = refreshTokenService.refreshAccessToken(refreshToken.refreshToken());
return ResponseEntity.ok(tokenJwtDto);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,28 @@
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice(assignableTypes = AuthenticationController.class)
@RestControllerAdvice
public class AuthenticationControllerAdvice {

@ExceptionHandler(UsernameNotFoundException.class)
public ResponseEntity<ErrorMessageVO> handlerAuthenticationException(UsernameNotFoundException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorMessageVO(ex.getMessage(), HttpStatus.NOT_FOUND));
}

@ExceptionHandler(BadCredentialsException.class)
public ResponseEntity<ErrorMessageVO> handleBadCredentialsException(BadCredentialsException ex) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new ErrorMessageVO(ex.getMessage(), HttpStatus.BAD_REQUEST));
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new ErrorMessageVO("Invalid username " +
"or password", HttpStatus.BAD_REQUEST));
}

@ExceptionHandler(InsufficientAuthenticationException.class)
public ResponseEntity<ErrorMessageVO> handleInvalidTokenException(InsufficientAuthenticationException ex) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new ErrorMessageVO("Invalid token, " +
"please verify expiration", HttpStatus.BAD_REQUEST));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.alura.aluraflixapi.controller.dto.token;

import jakarta.validation.constraints.NotBlank;

public record RefreshTokenVO(@NotBlank String refreshToken) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.alura.aluraflixapi.domain.token;

import com.alura.aluraflixapi.domain.user.User;
import com.auth0.jwt.JWT;
import lombok.*;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;

import java.time.Instant;

@Document
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
@Builder
public class RefreshToken {
@Id
private String id;
private String token;
private String userName;
private Instant expiryDate;

public static RefreshToken buildRefreshTokenEntity(User user, String accessToken) {
return RefreshToken.builder()
.userName(user.getUsername())
.token(accessToken)
.expiryDate(JWT.decode(accessToken).getExpiresAtAsInstant())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.alura.aluraflixapi.infraestructure.exception;

public class JwtRefreshTokenExpiredException extends RuntimeException {
public JwtRefreshTokenExpiredException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.alura.aluraflixapi.infraestructure.repository;

import com.alura.aluraflixapi.domain.token.RefreshToken;
import org.springframework.data.mongodb.repository.MongoRepository;

public interface RefreshTokenRepository extends MongoRepository<RefreshToken, String> {
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@


public interface UserRepository extends MongoRepository<User, String> {
UserDetails findByUsername(String username);
User findByUsername(String username);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.alura.aluraflixapi.infraestructure.security;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerExceptionResolver;

/**
* This class handles unsuccessful JWT exceptions
*/
@Slf4j
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Qualifier("handlerExceptionResolver")
private final HandlerExceptionResolver resolver;

public JwtAuthenticationEntryPoint(HandlerExceptionResolver handlerExceptionResolver) {
this.resolver = handlerExceptionResolver;
}

@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) {
resolver.resolveException(request, response, null, authException);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
public class SecurityConfigurations {

private final SecurityFilter securityFilter;
private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
Expand All @@ -39,13 +40,16 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.sessionManagement(managementConfigurer ->
managementConfigurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(httpRequest -> httpRequest
.requestMatchers(HttpMethod.POST, "/login").permitAll()
.requestMatchers(HttpMethod.POST, "/login/**").permitAll()
.requestMatchers("/v3/api-docs/**", "/swagger-ui.html", "/swagger-ui/**").permitAll()
//any other request has to be authenticated
.anyRequest().authenticated()
)
//tell to spring to user our filter SecurityFilter.class instead their
//tell to spring to use our filter SecurityFilter.class instead their
.addFilterBefore(securityFilter, UsernamePasswordAuthenticationFilter.class)
// //tell to spring to use this filter to handle any exception about JWT exception
.exceptionHandling(exceptionHandler ->
exceptionHandler.authenticationEntryPoint(jwtAuthenticationEntryPoint))
.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,62 +5,63 @@
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Objects;
import java.util.Optional;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.util.Objects;
import java.util.Optional;

/**
* Spring validate each request received only once
*/
@Slf4j
@Component
public class SecurityFilter extends OncePerRequestFilter {

private static final String PREFIX_LOGGING = "[SecurityFilter]";
public static final String AUTHORIZATION = "Authorization";
private final TokenService tokenService;
private final UserRepository userRepository;
private static final String PREFIX_LOGGING = "[SecurityFilter]";
public static final String AUTHORIZATION = "Authorization";
private final TokenService tokenService;
private final UserRepository userRepository;

public SecurityFilter(final TokenService tokenService, final UserRepository userRepository) {
this.tokenService = tokenService;
this.userRepository = userRepository;
}
public SecurityFilter(final TokenService tokenService, final UserRepository userRepository) {
this.tokenService = tokenService;
this.userRepository = userRepository;
}

//doFilterInternal is called for each request received
//All request to endpoint/login don´t have token in theirs headers
@Override
protected void doFilterInternal(final HttpServletRequest request,
final HttpServletResponse response,
final FilterChain filterChain) throws ServletException, IOException {
//doFilterInternal is called for each request received
//All request to endpoint/login don´t have accessToken in theirs headers
@Override
protected void doFilterInternal(final HttpServletRequest request,
final HttpServletResponse response,
final FilterChain filterChain) throws ServletException, IOException {

log.info("{} request received intercepted by Internal Filter", PREFIX_LOGGING);
final var tokenJWT = this.getTokenJWT(request);
log.info("{} request received intercepted by Internal Filter", PREFIX_LOGGING);
final var tokenJWT = this.getTokenJWT(request);

if (Objects.nonNull(tokenJWT)) {
//Retrieve user from Token JWT
final var subject = this.tokenService.getSubject(tokenJWT);
var user = this.userRepository.findByUsername(subject);
if (Objects.nonNull(tokenJWT)) {
//Retrieve user from Token JWT
final var username = this.tokenService.verifyTokenJWT(tokenJWT);
final var user = this.userRepository.findByUsername(username);

//after retrieve the user we need to tell to Spring framework to authenticate him in the context
//this is done by calling UsernamePasswordAuthenticationToken and SecurityContextHolder methods
log.info("{} Authenticating user: {} ", PREFIX_LOGGING, user.getUsername());
var authentication = new UsernamePasswordAuthenticationToken(user, null,
user.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
log.info("{} User authenticated: {}", PREFIX_LOGGING, authentication.getPrincipal());
//after retrieve the user we need to tell to Spring framework to authenticate him in the context
//this is done by calling UsernamePasswordAuthenticationToken and SecurityContextHolder methods
log.info("{} Authenticating user: {} ", PREFIX_LOGGING, user.getUsername());
var authentication = new UsernamePasswordAuthenticationToken(user, null,
user.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
log.info("{} User authenticated: {}", PREFIX_LOGGING, authentication.getPrincipal());
}
//continue the flow
filterChain.doFilter(request, response);
}
//continue the flow
filterChain.doFilter(request, response);
}

private String getTokenJWT(final HttpServletRequest request) {
return Optional.ofNullable(request.getHeader(AUTHORIZATION))
.map(token -> token.replace("Bearer", "").trim())
.orElse(null);
}
private String getTokenJWT(final HttpServletRequest request) {
return Optional.ofNullable(request.getHeader(AUTHORIZATION))
.map(token -> token.replace("Bearer", "").trim())
.orElse(null);
}
}
Loading