diff --git a/spring-security-modules/pom.xml b/spring-security-modules/pom.xml index 223f0894d5..030f1ef876 100644 --- a/spring-security-modules/pom.xml +++ b/spring-security-modules/pom.xml @@ -33,6 +33,7 @@ spring-security-web-boot-2 spring-security-web-boot-3 spring-security-web-boot-4 + spring-security-web-boot-5 spring-security-web-digest-auth spring-security-web-login spring-security-web-login-2 diff --git a/spring-security-modules/spring-security-web-boot-5/pom.xml b/spring-security-modules/spring-security-web-boot-5/pom.xml new file mode 100644 index 0000000000..f4e2e3ad92 --- /dev/null +++ b/spring-security-modules/spring-security-web-boot-5/pom.xml @@ -0,0 +1,51 @@ + + + 4.0.0 + spring-security-web-boot-5 + 0.0.1-SNAPSHOT + spring-security-web-boot-5 + jar + Spring Security Custom Auth Application + + + com.baeldung + parent-boot-2 + 0.0.1-SNAPSHOT + ../../parent-boot-2 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + repackage + + + + + + + + \ No newline at end of file diff --git a/spring-security-modules/spring-security-web-boot-5/src/main/java/com/baeldung/customauth/SpringSecurityApplication.java b/spring-security-modules/spring-security-web-boot-5/src/main/java/com/baeldung/customauth/SpringSecurityApplication.java new file mode 100644 index 0000000000..500b8fbef8 --- /dev/null +++ b/spring-security-modules/spring-security-web-boot-5/src/main/java/com/baeldung/customauth/SpringSecurityApplication.java @@ -0,0 +1,12 @@ +package com.baeldung.customauth; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SpringSecurityApplication { + + public static void main(String[] args) { + SpringApplication.run(SpringSecurityApplication.class, args); + } +} diff --git a/spring-security-modules/spring-security-web-boot-5/src/main/java/com/baeldung/customauth/authprovider/RequestHeaderAuthenticationProvider.java b/spring-security-modules/spring-security-web-boot-5/src/main/java/com/baeldung/customauth/authprovider/RequestHeaderAuthenticationProvider.java new file mode 100644 index 0000000000..e9741f7a7a --- /dev/null +++ b/spring-security-modules/spring-security-web-boot-5/src/main/java/com/baeldung/customauth/authprovider/RequestHeaderAuthenticationProvider.java @@ -0,0 +1,36 @@ +package com.baeldung.customauth.authprovider; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; + +@Service +public class RequestHeaderAuthenticationProvider implements AuthenticationProvider { + + @Value("${api.auth.secret}") + private String apiAuthSecret; + + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + String authSecretKey = String.valueOf(authentication.getPrincipal()); + + if(StringUtils.isBlank(authSecretKey) || !authSecretKey.equals(apiAuthSecret)) { + throw new BadCredentialsException("Bad Request Header Credentials"); + } + + return new PreAuthenticatedAuthenticationToken(authentication.getPrincipal(), null, new ArrayList<>()); + } + + @Override + public boolean supports(Class authentication) { + return authentication.equals(PreAuthenticatedAuthenticationToken.class); + } +} diff --git a/spring-security-modules/spring-security-web-boot-5/src/main/java/com/baeldung/customauth/configuration/SecurityConfig.java b/spring-security-modules/spring-security-web-boot-5/src/main/java/com/baeldung/customauth/configuration/SecurityConfig.java new file mode 100644 index 0000000000..53ab890792 --- /dev/null +++ b/spring-security-modules/spring-security-web-boot-5/src/main/java/com/baeldung/customauth/configuration/SecurityConfig.java @@ -0,0 +1,68 @@ +package com.baeldung.customauth.configuration; + +import com.baeldung.customauth.authprovider.RequestHeaderAuthenticationProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +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.authentication.ProviderManager; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.preauth.RequestHeaderAuthenticationFilter; +import org.springframework.security.web.header.HeaderWriterFilter; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; + +import javax.servlet.http.HttpServletResponse; +import java.util.Collections; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + private final RequestHeaderAuthenticationProvider requestHeaderAuthenticationProvider; + + @Value("${api.auth.header.name}") + private String apiAuthHeaderName; + + @Autowired + public SecurityConfig( RequestHeaderAuthenticationProvider requestHeaderAuthenticationProvider){ + this.requestHeaderAuthenticationProvider = requestHeaderAuthenticationProvider; + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http.cors().and().csrf() + .disable() + .sessionManagement() + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .and() + .addFilterAfter(requestHeaderAuthenticationFilter(), HeaderWriterFilter.class) + .authorizeHttpRequests() + .antMatchers(HttpMethod.GET,"/health").permitAll() + .antMatchers("/api/**").authenticated().and() + .exceptionHandling().authenticationEntryPoint((request, response, authException) -> + response.sendError(HttpServletResponse.SC_UNAUTHORIZED)); + + return http.build(); + } + + @Bean + public RequestHeaderAuthenticationFilter requestHeaderAuthenticationFilter() { + RequestHeaderAuthenticationFilter filter = new RequestHeaderAuthenticationFilter(); + filter.setPrincipalRequestHeader(apiAuthHeaderName); + filter.setExceptionIfHeaderMissing(false); + filter.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/api/**")); + filter.setAuthenticationManager(authenticationManager()); + + return filter; + } + + @Bean + protected AuthenticationManager authenticationManager() { + return new ProviderManager(Collections.singletonList(requestHeaderAuthenticationProvider)); + } +} \ No newline at end of file diff --git a/spring-security-modules/spring-security-web-boot-5/src/main/java/com/baeldung/customauth/controller/ApiController.java b/spring-security-modules/spring-security-web-boot-5/src/main/java/com/baeldung/customauth/controller/ApiController.java new file mode 100644 index 0000000000..89a66b6a0e --- /dev/null +++ b/spring-security-modules/spring-security-web-boot-5/src/main/java/com/baeldung/customauth/controller/ApiController.java @@ -0,0 +1,13 @@ +package com.baeldung.customauth.controller; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class ApiController { + + @GetMapping(path = "/api/hello") + public String hello(){ + return "hello"; + } +} diff --git a/spring-security-modules/spring-security-web-boot-5/src/main/java/com/baeldung/customauth/controller/HealthCheckController.java b/spring-security-modules/spring-security-web-boot-5/src/main/java/com/baeldung/customauth/controller/HealthCheckController.java new file mode 100644 index 0000000000..38d96b9b3d --- /dev/null +++ b/spring-security-modules/spring-security-web-boot-5/src/main/java/com/baeldung/customauth/controller/HealthCheckController.java @@ -0,0 +1,13 @@ +package com.baeldung.customauth.controller; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class HealthCheckController { + + @GetMapping(path = "/health") + public String getHealthStatus(){ + return "OK"; + } +} diff --git a/spring-security-modules/spring-security-web-boot-5/src/main/resources/application.properties b/spring-security-modules/spring-security-web-boot-5/src/main/resources/application.properties new file mode 100644 index 0000000000..3a431720c2 --- /dev/null +++ b/spring-security-modules/spring-security-web-boot-5/src/main/resources/application.properties @@ -0,0 +1,5 @@ +spring.application.name=spring-security-app +server.servlet.context-path=/app +server.port=8085 +api.auth.header.name=x-auth-secret-key +logging.level.org.springframework.security=DEBUG \ No newline at end of file diff --git a/spring-security-modules/spring-security-web-boot-5/src/test/java/com/baeldung/customauth/SpringContextTest.java b/spring-security-modules/spring-security-web-boot-5/src/test/java/com/baeldung/customauth/SpringContextTest.java new file mode 100644 index 0000000000..a63d9d2bce --- /dev/null +++ b/spring-security-modules/spring-security-web-boot-5/src/test/java/com/baeldung/customauth/SpringContextTest.java @@ -0,0 +1,16 @@ +package com.baeldung.customauth; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit.jupiter.SpringExtension; + + +@ExtendWith(SpringExtension.class) +@SpringBootTest(classes = SpringSecurityApplication.class) +class SpringContextTest { + + @Test + void whenSpringContextIsBootstrapped_thenNoExceptions() { + } +} diff --git a/spring-security-modules/spring-security-web-boot-5/src/test/java/com/baeldung/customauth/controller/ApiControllerIntegrationTest.java b/spring-security-modules/spring-security-web-boot-5/src/test/java/com/baeldung/customauth/controller/ApiControllerIntegrationTest.java new file mode 100644 index 0000000000..f04dcc30d4 --- /dev/null +++ b/spring-security-modules/spring-security-web-boot-5/src/test/java/com/baeldung/customauth/controller/ApiControllerIntegrationTest.java @@ -0,0 +1,68 @@ +package com.baeldung.customauth.controller; + +import com.baeldung.customauth.SpringSecurityApplication; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.http.*; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.net.URI; + +import static org.junit.jupiter.api.Assertions.assertEquals; + + +@SpringBootTest(classes = SpringSecurityApplication.class, + webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) +@ExtendWith(SpringExtension.class) +class ApiControllerIntegrationTest { + + private final TestRestTemplate restTemplate = new TestRestTemplate(); + + private static final String API_ENDPOINT = "http://localhost:8080/app/api/hello"; + + @Test + void givenAuthHeaderSecretIsValid_whenApiControllerCalled_thenReturnOk() throws Exception { + HttpHeaders headers = new HttpHeaders(); + headers.add("x-auth-secret-key", "test-secret"); + + ResponseEntity response = restTemplate.exchange(new URI(API_ENDPOINT), HttpMethod.GET, + new HttpEntity<>(headers), String.class); + + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertEquals("hello", response.getBody()); + } + + @Test + void givenAAuthHeaderIsInvalid_whenApiControllerCalled_thenReturnUnAuthorised() throws Exception { + HttpHeaders headers = new HttpHeaders(); + headers.add("x-auth-secret-key", "invalid"); + + ResponseEntity response = restTemplate.exchange(new URI(API_ENDPOINT), HttpMethod.GET, + new HttpEntity<>(headers), String.class); + + assertEquals(HttpStatus.UNAUTHORIZED, response.getStatusCode()); + } + + @Test + void givenAuthHeaderNameIsInValid_whenApiControllerCalled_thenReturnUnAuthorised() throws Exception { + HttpHeaders headers = new HttpHeaders(); + headers.add("x-auth-secret", "test-secret"); + + ResponseEntity response = restTemplate.exchange(new URI(API_ENDPOINT), HttpMethod.GET, + new HttpEntity<>(headers), String.class); + + assertEquals(HttpStatus.UNAUTHORIZED, response.getStatusCode()); + } + + @Test + void givenAuthHeaderIsMissing_whenApiControllerCalled_thenReturnUnAuthorised() throws Exception { + HttpHeaders headers = new HttpHeaders(); + + ResponseEntity response = restTemplate.exchange(new URI(API_ENDPOINT), HttpMethod.GET, + new HttpEntity<>(headers), String.class); + + assertEquals(HttpStatus.UNAUTHORIZED, response.getStatusCode()); + } +} diff --git a/spring-security-modules/spring-security-web-boot-5/src/test/java/com/baeldung/customauth/controller/HealthCheckControllerIntegrationTest.java b/spring-security-modules/spring-security-web-boot-5/src/test/java/com/baeldung/customauth/controller/HealthCheckControllerIntegrationTest.java new file mode 100644 index 0000000000..ea6fdcf292 --- /dev/null +++ b/spring-security-modules/spring-security-web-boot-5/src/test/java/com/baeldung/customauth/controller/HealthCheckControllerIntegrationTest.java @@ -0,0 +1,34 @@ +package com.baeldung.customauth.controller; + +import com.baeldung.customauth.SpringSecurityApplication; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.http.*; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.net.URI; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@SpringBootTest(classes = SpringSecurityApplication.class, + webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) +@ExtendWith(SpringExtension.class) +class HealthCheckControllerIntegrationTest { + + private final TestRestTemplate restTemplate = new TestRestTemplate(); + + private static final String HEALTH_CHECK_ENDPOINT = "http://localhost:8080/app/health"; + + @Test + void givenApplicationIsRunning_whenHealthCheckControllerCalled_thenReturnOk() throws Exception { + HttpHeaders headers = new HttpHeaders(); + + ResponseEntity response = restTemplate.exchange(new URI(HEALTH_CHECK_ENDPOINT), HttpMethod.GET, + new HttpEntity<>(headers), String.class); + + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertEquals("OK", response.getBody()); + } +} diff --git a/spring-security-modules/spring-security-web-boot-5/src/test/resources/application.properties b/spring-security-modules/spring-security-web-boot-5/src/test/resources/application.properties new file mode 100644 index 0000000000..27e9cad28a --- /dev/null +++ b/spring-security-modules/spring-security-web-boot-5/src/test/resources/application.properties @@ -0,0 +1,5 @@ +spring.application.name=spring-security-app +server.servlet.context-path=/app +server.port=8080 +api.auth.header.name=x-auth-secret-key +api.auth.secret=test-secret