From 5449a2c9343038874ed716f815f9646e963a9616 Mon Sep 17 00:00:00 2001 From: JesseBrault0709 <62299747+JesseBrault0709@users.noreply.github.com> Date: Tue, 18 Jun 2024 14:26:20 +0200 Subject: [PATCH] Basic security working. --- build.gradle | 6 ++ .../mealsmadeeasy/api/GreetingController.java | 16 +++++ .../api/MealsMadeEasyApiApplication.java | 25 ++++++++ .../mealsmadeeasy/api/security/JwtFilter.java | 58 +++++++++++++++++++ .../api/security/JwtService.java | 5 ++ .../api/security/JwtServiceImpl.java | 42 ++++++++++++++ .../api/security/SecurityConfiguration.java | 55 ++++++++++++++++++ src/main/resources/application.properties | 6 ++ src/main/resources/templates/home.html | 9 +++ 9 files changed, 222 insertions(+) create mode 100644 src/main/java/app/mealsmadeeasy/api/GreetingController.java create mode 100644 src/main/java/app/mealsmadeeasy/api/security/JwtFilter.java create mode 100644 src/main/java/app/mealsmadeeasy/api/security/JwtService.java create mode 100644 src/main/java/app/mealsmadeeasy/api/security/JwtServiceImpl.java create mode 100644 src/main/java/app/mealsmadeeasy/api/security/SecurityConfiguration.java create mode 100644 src/main/resources/templates/home.html diff --git a/build.gradle b/build.gradle index afb2959..f3775cd 100644 --- a/build.gradle +++ b/build.gradle @@ -18,6 +18,7 @@ repositories { } dependencies { + // From Spring Initalizr implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-web' @@ -25,6 +26,11 @@ dependencies { testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.security:spring-security-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + + // Custom + implementation 'io.jsonwebtoken:jjwt-api:0.12.5' + implementation 'io.jsonwebtoken:jjwt-jackson:0.12.5' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.5' } tasks.named('test') { diff --git a/src/main/java/app/mealsmadeeasy/api/GreetingController.java b/src/main/java/app/mealsmadeeasy/api/GreetingController.java new file mode 100644 index 0000000..c8bc97f --- /dev/null +++ b/src/main/java/app/mealsmadeeasy/api/GreetingController.java @@ -0,0 +1,16 @@ +package app.mealsmadeeasy.api; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +@Controller("/") +public class GreetingController { + + @GetMapping("/greeting") + @ResponseBody + public String get() { + return "Hello, World!"; + } + +} diff --git a/src/main/java/app/mealsmadeeasy/api/MealsMadeEasyApiApplication.java b/src/main/java/app/mealsmadeeasy/api/MealsMadeEasyApiApplication.java index 0cc9264..b2478ad 100644 --- a/src/main/java/app/mealsmadeeasy/api/MealsMadeEasyApiApplication.java +++ b/src/main/java/app/mealsmadeeasy/api/MealsMadeEasyApiApplication.java @@ -1,7 +1,15 @@ package app.mealsmadeeasy.api; +import io.jsonwebtoken.Jwts; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; + +import javax.crypto.SecretKey; @SpringBootApplication public class MealsMadeEasyApiApplication { @@ -10,4 +18,21 @@ public class MealsMadeEasyApiApplication { SpringApplication.run(MealsMadeEasyApiApplication.class, args); } + @Bean + public SecretKey secretKey() { + return Jwts.SIG.HS256.key().build(); + } + + @Bean + public UserDetailsService userDetailsService() { + @SuppressWarnings("deprecation") + UserDetails testUser = User + .withDefaultPasswordEncoder() + .username("test") + .password("test") + .roles("USER") + .build(); + return new InMemoryUserDetailsManager(testUser); + } + } diff --git a/src/main/java/app/mealsmadeeasy/api/security/JwtFilter.java b/src/main/java/app/mealsmadeeasy/api/security/JwtFilter.java new file mode 100644 index 0000000..41bdd07 --- /dev/null +++ b/src/main/java/app/mealsmadeeasy/api/security/JwtFilter.java @@ -0,0 +1,58 @@ +package app.mealsmadeeasy.api.security; + +import io.jsonwebtoken.Jwts; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.http.HttpHeaders; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.crypto.SecretKey; +import java.io.IOException; + +public final class JwtFilter extends OncePerRequestFilter { + + private final SecretKey secretKey; + private final UserDetailsService userDetailsService; + + public JwtFilter(SecretKey secretKey, UserDetailsService userDetailsService) { + this.secretKey = secretKey; + this.userDetailsService = userDetailsService; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + final String authorizationHeader = request.getHeader(HttpHeaders.AUTHORIZATION); + if (authorizationHeader == null) { + filterChain.doFilter(request, response); + return; + } + + if (authorizationHeader.startsWith("Bearer ") + && authorizationHeader.length() > 7) { + final String token = authorizationHeader.substring(7); + final var jws = Jwts.parser() + .verifyWith(this.secretKey) + .build() + .parseSignedClaims(token); + final String username = jws.getPayload().getSubject(); + final UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); + final var authenticationToken = new UsernamePasswordAuthenticationToken( + userDetails, + null, + userDetails.getAuthorities() + ); + authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authenticationToken); + } + filterChain.doFilter(request, response); + } + +} diff --git a/src/main/java/app/mealsmadeeasy/api/security/JwtService.java b/src/main/java/app/mealsmadeeasy/api/security/JwtService.java new file mode 100644 index 0000000..d16e49c --- /dev/null +++ b/src/main/java/app/mealsmadeeasy/api/security/JwtService.java @@ -0,0 +1,5 @@ +package app.mealsmadeeasy.api.security; + +public interface JwtService { + String generateToken(String username); +} diff --git a/src/main/java/app/mealsmadeeasy/api/security/JwtServiceImpl.java b/src/main/java/app/mealsmadeeasy/api/security/JwtServiceImpl.java new file mode 100644 index 0000000..7c68794 --- /dev/null +++ b/src/main/java/app/mealsmadeeasy/api/security/JwtServiceImpl.java @@ -0,0 +1,42 @@ +package app.mealsmadeeasy.api.security; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.jackson.io.JacksonSerializer; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import javax.crypto.SecretKey; +import java.time.Instant; +import java.util.Date; + +@Service +public final class JwtServiceImpl implements JwtService { + + private final ObjectMapper objectMapper; + private final long tokenLifetime; + private final SecretKey secretKey; + + public JwtServiceImpl( + ObjectMapper objectMapper, + @Value("${app.mealsmadeeasy.api.security.token-lifetime}") Long tokenLifetime, + SecretKey secretKey + ) { + this.objectMapper = objectMapper; + this.tokenLifetime = tokenLifetime; + this.secretKey = secretKey; + } + + @Override + public String generateToken(String username) { + final Instant now = Instant.now(); + return Jwts.builder() + .subject(username) + .issuedAt(Date.from(now)) + .expiration(Date.from(Instant.now().plusSeconds(this.tokenLifetime))) + .signWith(this.secretKey) + .json(new JacksonSerializer<>(this.objectMapper)) + .compact(); + } + +} diff --git a/src/main/java/app/mealsmadeeasy/api/security/SecurityConfiguration.java b/src/main/java/app/mealsmadeeasy/api/security/SecurityConfiguration.java new file mode 100644 index 0000000..8c85e15 --- /dev/null +++ b/src/main/java/app/mealsmadeeasy/api/security/SecurityConfiguration.java @@ -0,0 +1,55 @@ +package app.mealsmadeeasy.api.security; + +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.Customizer; +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.configuration.WebSecurityCustomizer; +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.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +import javax.crypto.SecretKey; + +@Configuration +@EnableWebSecurity +public class SecurityConfiguration { + + private final SecretKey secretKey; + private final UserDetailsService userDetailsService; + + public SecurityConfiguration(SecretKey secretKey, UserDetailsService userDetailsService) { + this.secretKey = secretKey; + this.userDetailsService = userDetailsService; + } + + @Bean + public WebSecurityCustomizer webSecurityCustomizer() { + return web -> web.ignoring().requestMatchers("/greeting"); + } + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception { + httpSecurity.authorizeHttpRequests(requests -> requests.anyRequest().authenticated()); + httpSecurity.csrf(AbstractHttpConfigurer::disable); + httpSecurity.cors(Customizer.withDefaults()); + httpSecurity.sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy( + SessionCreationPolicy.STATELESS + )); + httpSecurity.exceptionHandling(exceptionHandling -> { + exceptionHandling.authenticationEntryPoint((request, response, authException) -> { + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage()); + }); + }); + httpSecurity.addFilterBefore( + new JwtFilter(this.secretKey, this.userDetailsService), + UsernamePasswordAuthenticationFilter.class + ); + return httpSecurity.build(); + } + +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 206dffb..03c8309 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,7 @@ spring.application.name=meals-made-easy-api +spring.jpa.hibernate.ddl-auto=update +spring.datasource.url=jdbc:mysql://localhost:55001/meals_made_easy_api +spring.datasource.username=meals-made-easy-api-user +spring.datasource.password=devpass +spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver +app.mealsmadeeasy.api.security.token-lifetime=60 diff --git a/src/main/resources/templates/home.html b/src/main/resources/templates/home.html new file mode 100644 index 0000000..844c978 --- /dev/null +++ b/src/main/resources/templates/home.html @@ -0,0 +1,9 @@ + + + + Home + + +

Hello, World!

+ +