Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.scheduling.annotation.EnableScheduling;

/**
* Entry point for the Link Shortener application.
Expand All @@ -11,6 +12,7 @@
* </p>
*/
@EnableCaching
@EnableScheduling
@SpringBootApplication
public class LinkShortenerApplication {

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package org.decepticons.linkshortener.api.exception;

import org.decepticons.linkshortener.api.exception.BaseException;

/**
* Thrown when a JWT token is expired.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package org.decepticons.linkshortener.api.model;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

/**
* Entity representing a revoked JWT token in the database.
* This is used for stateless token blacklisting.
*/
@Entity
@Table(name = "revoked_tokens")
@Getter
@Setter
@NoArgsConstructor
public class RevokedToken {

/**
* The maximum length of a token string.
*/
private static final int MAX_TOKEN_LENGTH = 500;

/**
* The unique identifier for the revoked token entry.
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

/**
* The revoked JWT token string.
*/
@Column(nullable = false, unique = true, length = MAX_TOKEN_LENGTH)
private String token;

/**
* The date and time at which the token expires.
*/
@Column(name = "expires_at", nullable = false)
private Instant expiresAt;

/**
* Constructs a new RevokedToken with the given token and expiration date.
*
* @param tokenParam the JWT token string to be revoked
* @param expiresAtParam the expiration date of the token
*/
public RevokedToken(final String tokenParam, final Instant expiresAtParam) {
this.token = tokenParam;
this.expiresAt = expiresAtParam;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package org.decepticons.linkshortener.api.repository;

import java.time.Instant;
import java.util.Optional;
import org.decepticons.linkshortener.api.model.RevokedToken;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

/**
* Repository for managing revoked JWT tokens.
*/
@Repository
public interface RevokedTokenRepository extends
JpaRepository<RevokedToken, Long> {

/**
* Finds a revoked token by its unique token string.
*
* @param token The token string to search for.
* @return An Optional containing the found RevokedToken,
* or empty if not found.
*/
Optional<RevokedToken> findByToken(String token);

/**
* Checks if a token exists in the repository.
*
* @param token The token string to check.
* @return true if the token exists, false otherwise.
*/
boolean existsByToken(String token);

/**
* Deletes all revoked tokens that have expired before the given timestamp.
*
* @param now The timestamp used to determine which tokens to delete.
*/
void deleteAllByExpiresAtBefore(Instant now);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import org.decepticons.linkshortener.api.model.User;
import org.decepticons.linkshortener.api.repository.UserRepository;
import org.decepticons.linkshortener.api.security.jwt.JwtAuthenticationFilter;
import org.decepticons.linkshortener.api.security.model.CustomUserDetails;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
Expand Down Expand Up @@ -117,10 +118,7 @@ public UserDetailsService userDetailsService(
"User was not found"
));

return org.springframework.security.core.userdetails.User
.withUsername(user.getUsername())
.password(user.getPasswordHash())
.build();
return new CustomUserDetails(user);
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import org.decepticons.linkshortener.api.exception.InvalidTokenException;
import org.decepticons.linkshortener.api.repository.RevokedTokenRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
Expand All @@ -21,42 +21,64 @@
*/
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
/** Logger for JWT authentication filter. */
private static final Logger LOG =
LoggerFactory.getLogger(JwtAuthenticationFilter.class);

/** HTTP header containing the JWT token. */
/**
* Logger for the JwtAuthenticationFilter class.
*/
private static final Logger LOG = LoggerFactory.getLogger(
JwtAuthenticationFilter.class
);

/**
* The name of the authorization header.
*/
private static final String AUTHORIZATION_HEADER = "Authorization";

/** Prefix used in the Authorization header. */
/**
* The prefix for a Bearer token.
*/
private static final String BEARER_PREFIX = "Bearer ";

/** Utility class for JWT operations. */
/**
* Utility for handling JWT tokens.
*/
private final JwtTokenUtil jwtTokenUtil;

/** Service to load user details from the database. */
/**
* Service for loading user details.
*/
private final UserDetailsService userDetailsService;

/**
* Constructs a JwtAuthenticationFilter with required dependencies.
* Repository to check for revoked tokens.
*/
private final RevokedTokenRepository revokedTokenRepository;

/**
* Constructs a new JwtAuthenticationFilter.
*
* @param inJwtTokenUtil utility to parse and validate JWT tokens
* @param inUserDetailsService service to load user details
* @param jwtTokenUtilParam The JWT utility.
* @param userDetailsServiceParam The user details service.
* @param revokedTokenRepositoryParam The revoked token repository.
*/
public JwtAuthenticationFilter(final JwtTokenUtil inJwtTokenUtil,
final UserDetailsService inUserDetailsService) {
this.jwtTokenUtil = inJwtTokenUtil;
this.userDetailsService = inUserDetailsService;
public JwtAuthenticationFilter(
final JwtTokenUtil jwtTokenUtilParam,
final UserDetailsService userDetailsServiceParam,
final RevokedTokenRepository revokedTokenRepositoryParam
) {
this.jwtTokenUtil = jwtTokenUtilParam;
this.userDetailsService = userDetailsServiceParam;
this.revokedTokenRepository = revokedTokenRepositoryParam;
}

/**
* Filters each request to validate JWT tokens and set authentication.
* Filters incoming requests to validate JWT tokens and authenticate users.
*
* @param request the HTTP request
* @param response the HTTP response
* @param filterChain the filter chain
* @throws ServletException if a servlet error occurs
* @throws IOException if an I/O error occurs
* @param request The servlet request.
* @param response The servlet response.
* @param filterChain The filter chain.
* @throws ServletException if a servlet-specific error occurs.
* @throws IOException if an I/O error occurs.
*/
@Override
protected void doFilterInternal(final HttpServletRequest request,
Expand All @@ -65,20 +87,24 @@ protected void doFilterInternal(final HttpServletRequest request,
throws ServletException, IOException {

final String authorizationHeader = request.getHeader(AUTHORIZATION_HEADER);
String jwtToken;

if (authorizationHeader == null) {
LOG.debug("No JWT token, skipping authentication");
if (authorizationHeader == null
|| !authorizationHeader.startsWith(BEARER_PREFIX)) {
filterChain.doFilter(request, response);
return;
}

if (!authorizationHeader.startsWith(BEARER_PREFIX)) {
LOG.warn("JWT Token does not begin with Bearer String");
throw new InvalidTokenException("JWT Token must start with 'Bearer '");
}
String jwtToken = authorizationHeader.substring(BEARER_PREFIX.length());

jwtToken = authorizationHeader.substring(BEARER_PREFIX.length());
// Check if the token is revoked
if (revokedTokenRepository.existsByToken(jwtToken)) {
LOG.warn("Revoked token used: {}", jwtToken);
SecurityContextHolder.clearContext();
response.sendError(
HttpServletResponse.SC_UNAUTHORIZED,
"Token has been revoked");
return;
}

final String username = jwtTokenUtil.extractUsername(jwtToken);

Expand All @@ -93,25 +119,32 @@ protected void doFilterInternal(final HttpServletRequest request,
}

if (!jwtTokenUtil.validateToken(jwtToken, userDetails)) {
throw new InvalidTokenException("Token is expired or invalid");
SecurityContextHolder.clearContext();
response.sendError(
HttpServletResponse.SC_UNAUTHORIZED,
"Token is expired or invalid");
return;
}

UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(userDetails, null,
new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities());
authToken.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request));
new WebAuthenticationDetailsSource().buildDetails(request)
);
SecurityContextHolder.getContext().setAuthentication(authToken);
}

filterChain.doFilter(request, response);
}

/**
* Determines if this filter should not apply to a given request.
* Determines whether this filter should be applied to the current request.
*
* @param request the HTTP request
* @return true if filter should be skipped, false otherwise
* @param request The servlet request.
* @return true if the filter should not be applied, false otherwise.
*/
@Override
protected boolean shouldNotFilter(final HttpServletRequest request) {
Expand All @@ -125,5 +158,4 @@ protected boolean shouldNotFilter(final HttpServletRequest request) {
|| path.startsWith("/swagger-ui")
|| path.startsWith("/v3/api-docs");
}

}
Loading
Loading