Skip to content

Commit

Permalink
DEAR-105: add cors, security config and JWT filter
Browse files Browse the repository at this point in the history
  • Loading branch information
baurnick authored Jul 8, 2024
1 parent c838fbf commit 4a7f061
Show file tree
Hide file tree
Showing 16 changed files with 448 additions and 18 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/ci-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ jobs:

- name: Build with Gradle
run: ./gradlew build
env:
DATABASE_PASSWORD: ${{ secrets.DATABASE_PASSWORD }}
JWT_SECRET: ${{ secrets.JWT_SECRET }}

- name: Login to GitHub Container Registry
run: echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u ${{ github.repository_owner }} --password-stdin
Expand Down
5 changes: 5 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,16 @@ repositories {
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.1.0'
implementation 'io.jsonwebtoken:jjwt:0.9.1'
implementation 'javax.xml.bind:jaxb-api:2.3.1'
runtimeOnly 'org.postgresql:postgresql'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.mockito:mockito-core:4.0.0'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

Expand Down
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ services:
SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/yappi_db
SPRING_DATASOURCE_USERNAME: yappi_db_admin
SPRING_DATASOURCE_PASSWORD: ${DATABASE_PASSWORD}
JWT_SECRET: ${JWT_SECRET}
depends_on:
- db

Expand Down
22 changes: 22 additions & 0 deletions src/main/java/ch/fhnw/deardevbackend/config/CorsConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package ch.fhnw.deardevbackend.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class CorsConfig {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:3000")
.allowedMethods("GET", "PUT", "POST", "PATCH", "DELETE", "OPTIONS")
.allowCredentials(true);
}
};
}
}
48 changes: 48 additions & 0 deletions src/main/java/ch/fhnw/deardevbackend/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package ch.fhnw.deardevbackend.config;

import ch.fhnw.deardevbackend.filter.JWTFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
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.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

@Autowired
private JWTFilter filter;

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(authz -> authz
.requestMatchers("/auth/token").permitAll()
.anyRequest().authenticated())
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.formLogin(AbstractHttpConfigurer::disable)
.logout(logout -> logout.logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler(HttpStatus.OK))
.deleteCookies("JSESSIONID").invalidateHttpSession(true));

http
.addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class);

return http.build();
}

@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package ch.fhnw.deardevbackend.controller;

import ch.fhnw.deardevbackend.entities.User;
import ch.fhnw.deardevbackend.services.UserService;
import ch.fhnw.deardevbackend.util.JWTUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.Optional;

@RestController
@RequestMapping("/auth")
public class AuthController {

@Autowired
private JWTUtil jwtUtil;

@Autowired
private UserService userService;

@PostMapping("/token")
public ResponseEntity<String> generateToken(@RequestParam String email, @RequestParam int id) {
Optional<User> userOptional = userService.findUserByEmail(email);
if (userOptional.isPresent()) {
User user = userOptional.get();
if (user.getId() == id) {
String token = jwtUtil.generateToken(user);
return ResponseEntity.ok(token);
} else {
return ResponseEntity.status(401).body("User ID does not match.");
}
} else {
return ResponseEntity.status(404).body("User not found");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,27 @@
import ch.fhnw.deardevbackend.services.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("v1/users")
@RequestMapping("v1/")
public class UserController {

@Autowired
private UserService userService;

@CrossOrigin(origins = "http://localhost:3000")
@GetMapping("")
@GetMapping("/users")
public ResponseEntity<List<User>> getAllUsers() {
return ResponseEntity.ok().body(userService.getAllUsers());
}

@GetMapping("/user/{id}")
public ResponseEntity<User> getUserById(@PathVariable Integer id) {
return ResponseEntity.ok().body(userService.getUserById(id));
}
}
73 changes: 73 additions & 0 deletions src/main/java/ch/fhnw/deardevbackend/filter/JWTFilter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package ch.fhnw.deardevbackend.filter;

import ch.fhnw.deardevbackend.entities.User;
import ch.fhnw.deardevbackend.services.UserService;
import ch.fhnw.deardevbackend.util.JWTUtil;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.MalformedJwtException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

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

@Slf4j
@Component
public class JWTFilter extends OncePerRequestFilter {

@Autowired
private JWTUtil jwtUtil;

@Autowired
private UserService userService;

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
final String requestTokenHeader = request.getHeader("Authorization");

String email = null;
String jwtToken = null;

if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) {
jwtToken = requestTokenHeader.substring(7);
try {
email = jwtUtil.extractEmail(jwtToken);
} catch (IllegalArgumentException e) {
log.warn("Unable to get JWT Token");
} catch (ExpiredJwtException e) {
log.warn("JWT Token has expired");
} catch (MalformedJwtException e) {
log.warn("JWT Token is invalid");
}
} else {
log.warn("JWT Token does not begin with Bearer String");
}

if (email != null && SecurityContextHolder.getContext().getAuthentication() == null) {
Optional<User> userOptional = userService.findUserByEmail(email);
if (userOptional.isPresent()) {
User user = userOptional.get();
if (jwtUtil.validateToken(jwtToken, user)) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
user, null, new ArrayList<>());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
} else {
log.warn("JWT Token is not valid or expired");
}
}
}
chain.doFilter(request, response);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
public interface UserRepository extends JpaRepository<User, Integer> {
Optional<User> findByEmail(String email);
}
15 changes: 15 additions & 0 deletions src/main/java/ch/fhnw/deardevbackend/services/UserService.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Optional;

@Service
@RequiredArgsConstructor
Expand All @@ -17,4 +18,18 @@ public class UserService {
public List<User> getAllUsers() {
return userRepository.findAll();
}

public User getUserById(Integer id) {
return userRepository.findById(id).orElse(null);
}

// used for JWT filter
public Optional<User> findUserByEmail(String email) {
return userRepository.findByEmail(email);
}

// used for JWT filter
public Optional<User> findUserById(Integer id) {
return userRepository.findById(id);
}
}
67 changes: 67 additions & 0 deletions src/main/java/ch/fhnw/deardevbackend/util/JWTUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package ch.fhnw.deardevbackend.util;

import ch.fhnw.deardevbackend.entities.User;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.function.Function;

@Component
public class JWTUtil {

@Value("${jwt.secret}")
private String secret;

public String extractEmail(String token) {
return extractClaim(token, claims -> claims.get("email", String.class));
}

public int extractUserId(String token) {
Claims claims = extractAllClaims(token);
return claims.get("id", Integer.class);
}

public Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}

public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}

private Claims extractAllClaims(String token) {
try {
return Jwts.parser()
.setSigningKey(secret.getBytes())
.parseClaimsJws(token)
.getBody();
} catch (SignatureException e) {
throw new IllegalArgumentException("Invalid JWT signature");
}
}

private Boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}

public Boolean validateToken(String token, User user) {
final String email = extractEmail(token);
final int tokenUserId = extractUserId(token);
return (email.equals(user.getEmail()) && tokenUserId == user.getId() && !isTokenExpired(token));
}

public String generateToken(User user) {
return Jwts.builder()
.claim("id", user.getId())
.claim("email", user.getEmail())
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10))
.signWith(io.jsonwebtoken.SignatureAlgorithm.HS256, secret.getBytes())
.compact();
}
}
6 changes: 5 additions & 1 deletion src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,8 @@ spring.jpa.properties.hibernate.temp.use_jdbc_metadata_defaults=false
spring.config.import=optional:file:.env[.properties]
# Customize the OpenAPI path
springdoc.api-docs.path=/api-docs
springdoc.swagger-ui.path=/swagger-ui.html
springdoc.swagger-ui.path=/swagger-ui.html
# JWT Configuration
jwt.secret=${JWT_SECRET}
#Logging Configuration
logging.level.root=WARN
13 changes: 0 additions & 13 deletions src/test/java/ch/fhnw/deardevbackend/ApplicationTests.java

This file was deleted.

Loading

0 comments on commit 4a7f061

Please sign in to comment.