From 3816e3371a26eb13422ac86b1f908d6b16c75d10 Mon Sep 17 00:00:00 2001 From: Sarath Chandra Date: Sun, 5 Jun 2022 19:46:35 +0530 Subject: [PATCH] BAEL-5533: Spring security exceptions --- .../global/exceptionhandler/Application.java | 13 +++++ .../controller/LoginController.java | 25 ++++++++ .../handler/DefaultExceptionHandler.java | 22 +++++++ .../exceptionhandler/handler/RestError.java | 30 ++++++++++ .../handler/RestResponse.java | 20 +++++++ .../CustomAuthenticationEntryPoint.java | 35 +++++++++++ .../security/CustomSecurityConfig.java | 58 +++++++++++++++++++ .../DelegatedAuthenticationEntryPoint.java | 27 +++++++++ .../security/DelegatedSecurityConfig.java | 44 ++++++++++++++ .../DelegatedSecurityConfigUnitTest.java | 52 +++++++++++++++++ .../SecurityConfigUnitTest.java | 54 +++++++++++++++++ 11 files changed, 380 insertions(+) create mode 100644 spring-security-modules/spring-security-core-2/src/main/java/com/baeldung/global/exceptionhandler/Application.java create mode 100644 spring-security-modules/spring-security-core-2/src/main/java/com/baeldung/global/exceptionhandler/controller/LoginController.java create mode 100644 spring-security-modules/spring-security-core-2/src/main/java/com/baeldung/global/exceptionhandler/handler/DefaultExceptionHandler.java create mode 100644 spring-security-modules/spring-security-core-2/src/main/java/com/baeldung/global/exceptionhandler/handler/RestError.java create mode 100644 spring-security-modules/spring-security-core-2/src/main/java/com/baeldung/global/exceptionhandler/handler/RestResponse.java create mode 100644 spring-security-modules/spring-security-core-2/src/main/java/com/baeldung/global/exceptionhandler/security/CustomAuthenticationEntryPoint.java create mode 100644 spring-security-modules/spring-security-core-2/src/main/java/com/baeldung/global/exceptionhandler/security/CustomSecurityConfig.java create mode 100644 spring-security-modules/spring-security-core-2/src/main/java/com/baeldung/global/exceptionhandler/security/DelegatedAuthenticationEntryPoint.java create mode 100644 spring-security-modules/spring-security-core-2/src/main/java/com/baeldung/global/exceptionhandler/security/DelegatedSecurityConfig.java create mode 100644 spring-security-modules/spring-security-core-2/src/test/java/com/baeldung/global/exceptionhandler/DelegatedSecurityConfigUnitTest.java create mode 100644 spring-security-modules/spring-security-core-2/src/test/java/com/baeldung/global/exceptionhandler/SecurityConfigUnitTest.java diff --git a/spring-security-modules/spring-security-core-2/src/main/java/com/baeldung/global/exceptionhandler/Application.java b/spring-security-modules/spring-security-core-2/src/main/java/com/baeldung/global/exceptionhandler/Application.java new file mode 100644 index 0000000000..25985780ad --- /dev/null +++ b/spring-security-modules/spring-security-core-2/src/main/java/com/baeldung/global/exceptionhandler/Application.java @@ -0,0 +1,13 @@ +package com.baeldung.global.exceptionhandler; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + +} diff --git a/spring-security-modules/spring-security-core-2/src/main/java/com/baeldung/global/exceptionhandler/controller/LoginController.java b/spring-security-modules/spring-security-core-2/src/main/java/com/baeldung/global/exceptionhandler/controller/LoginController.java new file mode 100644 index 0000000000..1f0a56d6bf --- /dev/null +++ b/spring-security-modules/spring-security-core-2/src/main/java/com/baeldung/global/exceptionhandler/controller/LoginController.java @@ -0,0 +1,25 @@ +package com.baeldung.global.exceptionhandler.controller; + +import org.springframework.http.MediaType; +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.RestController; + +import com.baeldung.global.exceptionhandler.handler.RestResponse; + +@RestController +@RequestMapping +public class LoginController { + + @PostMapping(value = "/login", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity login() { + return ResponseEntity.ok(new RestResponse("Success")); + } + + @PostMapping(value = "/login-handler", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity loginWithExceptionHandler() { + return ResponseEntity.ok(new RestResponse("Success")); + } + +} diff --git a/spring-security-modules/spring-security-core-2/src/main/java/com/baeldung/global/exceptionhandler/handler/DefaultExceptionHandler.java b/spring-security-modules/spring-security-core-2/src/main/java/com/baeldung/global/exceptionhandler/handler/DefaultExceptionHandler.java new file mode 100644 index 0000000000..b47075c941 --- /dev/null +++ b/spring-security-modules/spring-security-core-2/src/main/java/com/baeldung/global/exceptionhandler/handler/DefaultExceptionHandler.java @@ -0,0 +1,22 @@ +package com.baeldung.global.exceptionhandler.handler; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.AuthenticationException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +@ControllerAdvice +public class DefaultExceptionHandler extends ResponseEntityExceptionHandler { + + @ExceptionHandler({ AuthenticationException.class }) + @ResponseBody + public ResponseEntity handleAuthenticationException(Exception ex) { + + RestError re = new RestError(HttpStatus.UNAUTHORIZED.toString(), "Authentication failed at controller advice"); + return ResponseEntity.status(HttpStatus.UNAUTHORIZED) + .body(re); + } +} diff --git a/spring-security-modules/spring-security-core-2/src/main/java/com/baeldung/global/exceptionhandler/handler/RestError.java b/spring-security-modules/spring-security-core-2/src/main/java/com/baeldung/global/exceptionhandler/handler/RestError.java new file mode 100644 index 0000000000..43f1b2e883 --- /dev/null +++ b/spring-security-modules/spring-security-core-2/src/main/java/com/baeldung/global/exceptionhandler/handler/RestError.java @@ -0,0 +1,30 @@ +package com.baeldung.global.exceptionhandler.handler; + +public class RestError { + + String errorCode; + String errorMessage; + + public RestError(String errorCode, String errorMessage) { + super(); + this.errorCode = errorCode; + this.errorMessage = errorMessage; + } + + public String getErrorCode() { + return errorCode; + } + + public void setErrorCode(String errorCode) { + this.errorCode = errorCode; + } + + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } + +} diff --git a/spring-security-modules/spring-security-core-2/src/main/java/com/baeldung/global/exceptionhandler/handler/RestResponse.java b/spring-security-modules/spring-security-core-2/src/main/java/com/baeldung/global/exceptionhandler/handler/RestResponse.java new file mode 100644 index 0000000000..74f8082a9d --- /dev/null +++ b/spring-security-modules/spring-security-core-2/src/main/java/com/baeldung/global/exceptionhandler/handler/RestResponse.java @@ -0,0 +1,20 @@ +package com.baeldung.global.exceptionhandler.handler; + +public class RestResponse { + + String message; + + public RestResponse(String message) { + super(); + this.message = message; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + +} diff --git a/spring-security-modules/spring-security-core-2/src/main/java/com/baeldung/global/exceptionhandler/security/CustomAuthenticationEntryPoint.java b/spring-security-modules/spring-security-core-2/src/main/java/com/baeldung/global/exceptionhandler/security/CustomAuthenticationEntryPoint.java new file mode 100644 index 0000000000..39e6d87c2a --- /dev/null +++ b/spring-security-modules/spring-security-core-2/src/main/java/com/baeldung/global/exceptionhandler/security/CustomAuthenticationEntryPoint.java @@ -0,0 +1,35 @@ +package com.baeldung.global.exceptionhandler.security; + +import java.io.IOException; +import java.io.OutputStream; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import com.baeldung.global.exceptionhandler.handler.RestError; +import com.fasterxml.jackson.databind.ObjectMapper; + +@Component("customAuthenticationEntryPoint") +public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { + + RestError re = new RestError(HttpStatus.UNAUTHORIZED.toString(), "Authentication failed"); + + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + OutputStream responseStream = response.getOutputStream(); + ObjectMapper mapper = new ObjectMapper(); + mapper.writeValue(responseStream, re); + responseStream.flush(); + + } +} \ No newline at end of file diff --git a/spring-security-modules/spring-security-core-2/src/main/java/com/baeldung/global/exceptionhandler/security/CustomSecurityConfig.java b/spring-security-modules/spring-security-core-2/src/main/java/com/baeldung/global/exceptionhandler/security/CustomSecurityConfig.java new file mode 100644 index 0000000000..cee3e9b62b --- /dev/null +++ b/spring-security-modules/spring-security-core-2/src/main/java/com/baeldung/global/exceptionhandler/security/CustomSecurityConfig.java @@ -0,0 +1,58 @@ +package com.baeldung.global.exceptionhandler.security; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +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.WebSecurityConfigurerAdapter; +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 org.springframework.security.web.AuthenticationEntryPoint; + +@Configuration +@EnableWebSecurity +public class CustomSecurityConfig extends WebSecurityConfigurerAdapter { + + @Autowired + @Qualifier("customAuthenticationEntryPoint") + AuthenticationEntryPoint authEntryPoint; + + @Bean + public UserDetailsService userDetailsService() { + UserDetails admin = User.withUsername("admin") + .password("password") + .roles("ADMIN") + .build(); + InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager(); + userDetailsManager.createUser(admin); + return userDetailsManager; + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.requestMatchers() + .antMatchers("/login") + .and() + .authorizeRequests() + .anyRequest() + .hasRole("ADMIN") + .and() + .httpBasic() + .and() + .exceptionHandling() + .authenticationEntryPoint(authEntryPoint); + } + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + auth.inMemoryAuthentication() + .withUser("admin") + .password("password") + .roles("ADMIN"); + } +} diff --git a/spring-security-modules/spring-security-core-2/src/main/java/com/baeldung/global/exceptionhandler/security/DelegatedAuthenticationEntryPoint.java b/spring-security-modules/spring-security-core-2/src/main/java/com/baeldung/global/exceptionhandler/security/DelegatedAuthenticationEntryPoint.java new file mode 100644 index 0000000000..d34ddfcdf3 --- /dev/null +++ b/spring-security-modules/spring-security-core-2/src/main/java/com/baeldung/global/exceptionhandler/security/DelegatedAuthenticationEntryPoint.java @@ -0,0 +1,27 @@ +package com.baeldung.global.exceptionhandler.security; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerExceptionResolver; + +@Component("delegatedAuthenticationEntryPoint") +public class DelegatedAuthenticationEntryPoint implements AuthenticationEntryPoint { + + @Autowired + @Qualifier("handlerExceptionResolver") + private HandlerExceptionResolver resolver; + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { + resolver.resolveException(request, response, null, authException); + } +} \ No newline at end of file diff --git a/spring-security-modules/spring-security-core-2/src/main/java/com/baeldung/global/exceptionhandler/security/DelegatedSecurityConfig.java b/spring-security-modules/spring-security-core-2/src/main/java/com/baeldung/global/exceptionhandler/security/DelegatedSecurityConfig.java new file mode 100644 index 0000000000..45582d3f5e --- /dev/null +++ b/spring-security-modules/spring-security-core-2/src/main/java/com/baeldung/global/exceptionhandler/security/DelegatedSecurityConfig.java @@ -0,0 +1,44 @@ +package com.baeldung.global.exceptionhandler.security; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +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.WebSecurityConfigurerAdapter; +import org.springframework.security.web.AuthenticationEntryPoint; + +@Configuration +@EnableWebSecurity +@Order(101) +public class DelegatedSecurityConfig extends WebSecurityConfigurerAdapter { + + @Autowired + @Qualifier("delegatedAuthenticationEntryPoint") + AuthenticationEntryPoint authEntryPoint; + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.requestMatchers() + .antMatchers("/login-handler") + .and() + .authorizeRequests() + .anyRequest() + .hasRole("ADMIN") + .and() + .httpBasic() + .and() + .exceptionHandling() + .authenticationEntryPoint(authEntryPoint); + } + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + auth.inMemoryAuthentication() + .withUser("admin") + .password("password") + .roles("ADMIN"); + } +} diff --git a/spring-security-modules/spring-security-core-2/src/test/java/com/baeldung/global/exceptionhandler/DelegatedSecurityConfigUnitTest.java b/spring-security-modules/spring-security-core-2/src/test/java/com/baeldung/global/exceptionhandler/DelegatedSecurityConfigUnitTest.java new file mode 100644 index 0000000000..3346ade380 --- /dev/null +++ b/spring-security-modules/spring-security-core-2/src/test/java/com/baeldung/global/exceptionhandler/DelegatedSecurityConfigUnitTest.java @@ -0,0 +1,52 @@ +package com.baeldung.global.exceptionhandler; + +import static org.hamcrest.Matchers.is; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.context.annotation.Import; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; + +import com.baeldung.global.exceptionhandler.controller.LoginController; +import com.baeldung.global.exceptionhandler.handler.RestError; +import com.baeldung.global.exceptionhandler.security.CustomAuthenticationEntryPoint; +import com.baeldung.global.exceptionhandler.security.DelegatedAuthenticationEntryPoint; +import com.baeldung.global.exceptionhandler.security.DelegatedSecurityConfig; + +@RunWith(SpringRunner.class) +@WebMvcTest(DelegatedSecurityConfig.class) +@Import({ LoginController.class, CustomAuthenticationEntryPoint.class, DelegatedAuthenticationEntryPoint.class }) +public class DelegatedSecurityConfigUnitTest { + + @Autowired + private MockMvc mvc; + + @Test + @WithMockUser(username = "admin", roles = { "ADMIN" }) + public void whenUserAccessLogin_shouldSucceed() throws Exception { + mvc.perform(formLogin("/login-handler").user("username", "admin") + .password("password", "password") + .acceptMediaType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + } + + @Test + public void whenUserAccessWithWrongCredentialsWithDelegatedEntryPoint_shouldFail() throws Exception { + RestError re = new RestError(HttpStatus.UNAUTHORIZED.toString(), "Authentication failed at controller advice"); + mvc.perform(formLogin("/login-handler").user("username", "admin") + .password("password", "wrong") + .acceptMediaType(MediaType.APPLICATION_JSON)) + .andExpect(status().isUnauthorized()) + .andExpect(jsonPath("$.errorMessage", is(re.getErrorMessage()))); + } + +} diff --git a/spring-security-modules/spring-security-core-2/src/test/java/com/baeldung/global/exceptionhandler/SecurityConfigUnitTest.java b/spring-security-modules/spring-security-core-2/src/test/java/com/baeldung/global/exceptionhandler/SecurityConfigUnitTest.java new file mode 100644 index 0000000000..b6a0252c2c --- /dev/null +++ b/spring-security-modules/spring-security-core-2/src/test/java/com/baeldung/global/exceptionhandler/SecurityConfigUnitTest.java @@ -0,0 +1,54 @@ +package com.baeldung.global.exceptionhandler; + +import static org.hamcrest.Matchers.is; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.context.annotation.Import; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; + +import com.baeldung.global.exceptionhandler.controller.LoginController; +import com.baeldung.global.exceptionhandler.handler.RestError; +import com.baeldung.global.exceptionhandler.security.CustomAuthenticationEntryPoint; +import com.baeldung.global.exceptionhandler.security.CustomSecurityConfig; +import com.baeldung.global.exceptionhandler.security.DelegatedAuthenticationEntryPoint; + +@RunWith(SpringRunner.class) +@WebMvcTest(CustomSecurityConfig.class) +@Import({LoginController.class, CustomAuthenticationEntryPoint.class, DelegatedAuthenticationEntryPoint.class}) +public class SecurityConfigUnitTest { + + @Autowired + private MockMvc mvc; + + @Test + @WithMockUser(username = "admin", roles = { "ADMIN" }) + public void whenUserAccessLogin_shouldSucceed() throws Exception { + mvc.perform(formLogin("/login").user("username", "admin") + .password("password", "password") + .acceptMediaType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + } + + @Test + public void whenUserAccessWithWrongCredentialsWithDelegatedEntryPoint_shouldFail() throws Exception { + RestError re = new RestError(HttpStatus.UNAUTHORIZED.toString(), "Authentication failed"); + mvc.perform(formLogin("/login").user("username", "admin") + .password("password", "wrong") + .acceptMediaType(MediaType.APPLICATION_JSON)) + .andExpect(status().isUnauthorized()) + .andExpect(jsonPath("$.errorMessage", is(re.getErrorMessage()))); + } + + + +}