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
7 changes: 7 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,19 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-webmvc'
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
implementation 'io.jsonwebtoken:jjwt-api:0.13.0'

compileOnly 'org.projectlombok:lombok'

annotationProcessor 'org.projectlombok:lombok'

testImplementation 'org.springframework.boot:spring-boot-starter-data-mongodb-test'
testImplementation 'org.springframework.boot:spring-boot-starter-security-test'
testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test'

testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.13.0'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.13.0'
}

dependencyManagement {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.javanauta.taskscheduler.business.dto;

import lombok.*;

import java.util.List;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class UserDTO {
private String email;
private String password;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.javanauta.taskscheduler.infrastructure.client;

import com.javanauta.taskscheduler.business.dto.UserDTO;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestParam;

@FeignClient(name = "user", url = "${user.url}")
Comment thread
enzobbom marked this conversation as resolved.
Resolved
public interface UserClient {

@GetMapping("/user")
UserDTO getUserByEmail(@RequestParam("email") String email, @RequestHeader("Authorization") String token);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.javanauta.taskscheduler.infrastructure.security;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

// Defines the JwtRequestFilter class, which extends OncePerRequestFilter
public class JwtRequestFilter extends OncePerRequestFilter {

// Defines properties to store instances of JwtUtil and UserDetailsService
private final JwtUtil jwtUtil;
private final UserDetailsServiceImpl userDetailsService;

// Constructor that initializes the properties with provided instances
public JwtRequestFilter(JwtUtil jwtUtil, UserDetailsServiceImpl userDetailsService) {
this.jwtUtil = jwtUtil;
this.userDetailsService = userDetailsService;
}

// Method called once per request to process the filter
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {

// Gets the value of the "Authorization" header from the request
final String authorizationHeader = request.getHeader("Authorization");

// Checks whether the header exists and starts with "Bearer "
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
// Extracts the JWT token from the header
final String token = authorizationHeader.substring(7);
// Extracts the username from the JWT token
final String username = jwtUtil.extractUsername(token);

// If the username is not null and the user is not yet authenticated
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
// Loads user details from the username
UserDetails userDetails = userDetailsService.loadUserDetails(username, token);
// Validates the JWT token
if (jwtUtil.validateToken(token, username)) {
// Creates an authentication object with user information
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
// Sets authentication in the security context
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}

// Continues the filter chain, allowing the request to proceed
chain.doFilter(request, response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.javanauta.taskscheduler.infrastructure.security;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import org.springframework.stereotype.Service;

import java.nio.charset.StandardCharsets;
import java.util.Date;

@Service
public class JwtUtil {

// Secret key used to sign and verify JWT tokens
private final String secretKey = "your-super-long-secret-key-that-needs-to-be-very-difficult";

// Extracts claims from the JWT token
public Claims extractClaims(String token) {
return Jwts.parser()
.setSigningKey(Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8))) // Sets secret key to validate signature
.build()
.parseClaimsJws(token) // Parses the JWT token and gets claims
.getBody(); // Returns claims body
}

// Extracts username from the JWT token
public String extractUsername(String token) {
// Gets the subject (username) from the claims
return extractClaims(token).getSubject();
}

// Checks whether the JWT token is expired
public boolean isTokenExpired(String token) {
// Compares token expiration date with current date
return extractClaims(token).getExpiration().before(new Date());
}

// Validates the JWT token by checking username match and expiration
public boolean validateToken(String token, String username) {
// Extracts username from the token
final String extractedUsername = extractUsername(token);
// Checks if username matches and token is not expired
return (extractedUsername.equals(username) && !isTokenExpired(token));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.javanauta.taskscheduler.infrastructure.security;

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.config.annotation.authentication.configuration.AuthenticationConfiguration;
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.core.userdetails.UserDetailsService;
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;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

// JwtUtil and UserDetailsService instances injected by Spring
private final JwtUtil jwtUtil;
private final UserDetailsServiceImpl userDetailsService;

// Constructor for dependency injection of JwtUtil and UserDetailsService
@Autowired
public SecurityConfig(JwtUtil jwtUtil, UserDetailsServiceImpl userDetailsService) {
this.jwtUtil = jwtUtil;
this.userDetailsService = userDetailsService;
}

// Security filter configuration
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// Creates a JwtRequestFilter instance using JwtUtil and UserDetailsService
JwtRequestFilter jwtRequestFilter = new JwtRequestFilter(jwtUtil, userDetailsService);

http
.csrf(AbstractHttpConfigurer::disable) // Disables CSRF protection for REST APIs (not needed in stateless APIs)
.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated() // Requires authentication for all other requests
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // Sets session policy to stateless
)
.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class); // Adds JWT filter before default authentication filter

// Returns the built security configuration
return http.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.javanauta.taskscheduler.infrastructure.security;

import com.javanauta.taskscheduler.business.dto.UserDTO;
import com.javanauta.taskscheduler.infrastructure.client.UserClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;

@Service
public class UserDetailsServiceImpl {

@Autowired
private UserClient userClient;

public UserDetails loadUserDetails(String email, String token) {
UserDTO userDTO = userClient.getUserByEmail(email, token);

return org.springframework.security.core.userdetails.User
.withUsername(userDTO.getEmail()) // Sets the username as the email
.password(userDTO.getPassword()) // Sets the user's password
.build(); // Builds the UserDetails object
}
}
1 change: 1 addition & 0 deletions src/main/resources/application.properties
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
spring.application.name=task-scheduler
user.url=localhost:8080