Toggle Spring Security for the requests with particular Request Header - java

I am trying to toggle/bypass/disable Spring Security (Authentication and Authorization) for all the requests having particular Request Header.
For example, if a request url is hit with that Request Header, Spring Security should be bypassed, if not it should not be bypassed.
For this, I am using following requestMatchers Spring Security config:
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring()
.antMatchers(HttpMethod.GET)
.antMatchers(HttpMethod.OPTIONS)
.requestMatchers(new RequestHeaderRequestMatcher("TEST-HEADER","TEST-VALUE"));
}
My remaining Security Config is :
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity (prePostEnabled = true)
#ConditionalOnProperty (name = "security.enabled", havingValue = "true", matchIfMissing = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private SecurityProps securityProps;
#Autowired
private MyUserDetailsService myUserDetailsService;
#Autowired
private MyAuthenticationEntryPoint myAuthenticationEntryPoint;
#Autowired
private MyCORSFilter myCORSFilter;
public SecurityConfig() {
SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.csrf().disable()
.addFilterBefore(myCORSFilter, SessionManagementFilter.class)
.addFilterBefore(requestHeaderFilter(), RequestHeaderAuthenticationFilter.class)
.authenticationProvider(preauthAuthProvider())
.authorizeRequests()
.antMatchers(HttpMethod.GET, securityProps.getNoAuthGetPattern()).permitAll()
.antMatchers(HttpMethod.OPTIONS, securityProps.getNoAuthOptionsPattern()).permitAll()
.requestMatchers(new RequestHeaderRequestMatcher("TEST-HEADER","TEST-VALUE")).permitAll()
.anyRequest().authenticated()
.and()
.exceptionHandling()
.authenticationEntryPoint(myAuthenticationEntryPoint);
}
#Autowired
#Override
protected void configure(final AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(preauthAuthProvider());
}
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring()
.antMatchers(HttpMethod.GET)
.antMatchers(HttpMethod.OPTIONS)
.requestMatchers(new RequestHeaderRequestMatcher("TEST-HEADER","TEST-VALUE"));
}
public RequestHeaderAuthenticationFilter requestHeaderFilter() throws Exception {
RequestHeaderAuthenticationFilter requestHeaderAuthenticationFilter = new RequestHeaderAuthenticationFilter();
requestHeaderAuthenticationFilter.setPrincipalRequestHeader(MySecurityConstants.LOGIN_HEADER);
requestHeaderAuthenticationFilter.setAuthenticationManager(authenticationManager());
requestHeaderAuthenticationFilter.setExceptionIfHeaderMissing(false);
requestHeaderAuthenticationFilter.setAuthenticationFailureHandler(new AuthenticationFailureHandler() {
#Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
if (exception instanceof MySecurityException) {
myAuthenticationEntryPoint.commenceMySecurityException(request, response, (MySecurityException) exception);
} else if (exception instanceof UsernameNotFoundException) {
myAuthenticationEntryPoint.commenceUsernameNotFoundException(request, response,
(UsernameNotFoundException) exception);
} else if (exception instanceof PreAuthenticatedCredentialsNotFoundException) {
myAuthenticationEntryPoint.commence(request, response, exception);
}
}
});
return requestHeaderAuthenticationFilter;
}
#Bean
public PreAuthenticatedAuthenticationProvider preauthAuthProvider() throws Exception {
PreAuthenticatedAuthenticationProvider authProvider = new PreAuthenticatedAuthenticationProvider();
authProvider.setPreAuthenticatedUserDetailsService(userDetailsServiceWrapper());
return authProvider;
}
#Bean
public UserDetailsByNameServiceWrapper<PreAuthenticatedAuthenticationToken> userDetailsServiceWrapper()
throws Exception {
UserDetailsByNameServiceWrapper<PreAuthenticatedAuthenticationToken> wrapper =
new UserDetailsByNameServiceWrapper<>();
wrapper.setUserDetailsService(ivyUserDetailsService);
return wrapper;
}
}
With the above settings, I am unable to disable/bypass Spring Security and I am getting the AuthenticationCredentialsNotFoundException exception:
org.springframework.security.authentication.AuthenticationCredentialsNotFoundException: An Authentication object was not found in the SecurityContext
Can anyone help me by identifying what am I doing wrong? Is my approach correct or I need to do something else to achieve this?
EDIT :
I am getting this exception in org.springframework.security.access.intercept.AbstractSecurityInterceptor class in beforeInvocation() method where it tries to get the authentication object from SecurityContextHolder. AbstractSecurityInterceptor is invoked by its subclass MethodSecurityInterceptor which is invoked from my Spring Controller which is annotated with #PreAuthorize.

I think your bypass is working fine. Its skipping the check.
The security's authorization check part gets the authenticated object from SecurityContext, which will be set when a request gets through the spring security filter.
So when you skip security filter SecurityContext is not set yet thus the error
You can do something like this to set it manually for your Custom Header Case
try {
SecurityContext ctx = SecurityContextHolder.createEmptyContext();
SecurityContextHolder.setContext(ctx);
ctx.setAuthentication(event.getAuthentication());
} finally {
SecurityContextHolder.clearContext();
}
Edit 1:
Answering all the queries.
But if thats the case, then I guess all GET call should also have
failed, but my GET calls are working fine.
Since you have added this line All your GET calls are skipped from security check.
.antMatchers(HttpMethod.GET, securityProps.getNoAuthGetPattern()).permitAll()
where can I add the code you have mentioned? Any particular filter or
somewhere else ?
I have done something like this in a Filter.
Refer Here
Look at TokenAuthenticationFilter Class in Answer. Where am manually setting.
Note: Its JWT implementation but good to refer
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (tokenHelper.validateToken(authToken, userDetails)) {
// create authentication
TokenBasedAuthentication authentication = new TokenBasedAuthentication(userDetails);
authentication.setToken(authToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
What is event in your answer?
I just got that case from Some Answer, cant find its link now. But you can setAuthentication like this or like above
Authentication authentication = new PreAuthenticatedAuthenticationToken("system", null);
authentication.setAuthenticated(true);
context.setAuthentication(authentication);

Related

Getting 403 forbidden after authentication using custom authentication filter that parses JSON payload

What I'm trying to do is just authenticate in-memory default user using a custom authentication filter that parses a JSON payload that contain the username and the password.
SecurityConfig.java
package ali.yousef.authdemo.config.security;
#Configuration
#EnableWebSecurity
public class SecurityConfig
{
#Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception
{
AuthenticationManager authenticationManager = authenticationConfiguration.getAuthenticationManager();
return authenticationManager;
}
#Bean
PasswordEncoder passwordEncoder()
{
return new BCryptPasswordEncoder();
}
#Bean
public SecurityFilterChain filterChain(HttpSecurity http, AuthenticationManager authenticationManager) throws Exception
{
JsonUserPasswordAuthenticationFilter jsonUserPasswordAuthenticationFilter = new JsonUserPasswordAuthenticationFilter();
jsonUserPasswordAuthenticationFilter.setAuthenticationManager(authenticationManager);
http
.csrf().disable()
.formLogin().disable()
.addFilterAt(jsonUserPasswordAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.authorizeHttpRequests()
.requestMatchers("/api/**").authenticated()
.anyRequest().permitAll();
return http.build();
}
}
JsonUserPasswordAuthenticationFilter.java
package ali.yousef.authdemo.config.security;
public class JsonUserPasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter
{
protected JsonUserPasswordAuthenticationFilter(AuthenticationManager authenticationManager)
{
this.setAuthenticationManager(authenticationManager);
}
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException
{
UsernamePasswordDto usernamePasswordDto;
try
{
usernamePasswordDto = new ObjectMapper().readValue(request.getInputStream(), UsernamePasswordDto.class);
System.out.println(usernamePasswordDto.toString());
}
catch (IOException ioe)
{
throw new AuthenticationServiceException(ioe.getMessage(), ioe);
}
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(usernamePasswordDto.getUsername(), usernamePasswordDto.getPassword());
return this.getAuthenticationManager().authenticate(authToken);
}
}
TestController.java
#RestController
public class TestController
{
#GetMapping("/api/hello")
public String hello(Principal principal)
{
return "hello " + principal.getName();
}
}
When authenticating the default user it gets authenticated and return the home page but when I try to send a request to /api/hello it respond with 403.
EDIT:
I edited how I register the custom authentication filter. But the same problem is present. It seems like the security context gets cleared after successful authentication and I get anonymousUser from principal.
Extending a UsernamePassWordAuthenticationFilter brings in more customizations than you need to set up a custom auth filter.. see this post for more details why your filter is not getting called - link
You can achieve the same using OncePerRequestFilter as below -
#Component
public class JsonUserPasswordAuthenticationFilter extends OncePerRequestFilter
{
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
UsernamePasswordAuthenticationToken authToken = YOUR_LOGIC(); // new UsernamePasswordAuthenticationToken("test", "passwd",Collections.emptyList()); as you have no authorities empty list is important here...
SecurityContextHolder.getContext().setAuthentication(authToken);
filterChain.doFilter(request, response);
}
}
#Configuration
#EnableWebSecurity
public class SecurityConfig
{
#Bean
PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
#Bean
public SecurityFilterChain filterChain(HttpSecurity http, JsonUserPasswordAuthenticationFilter filter) throws Exception {
http
.csrf().disable()
.formLogin().disable()
.addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class)
.authorizeHttpRequests(auth -> {
auth.requestMatchers(new AntPathRequestMatcher("/api/**")).authenticated()
.anyRequest().permitAll();
}) ;
return http.build();
}
When you send a request to /api/hello your filter’s attemptAuthentication() never takes action. This is because your custom implementation extends the UsernamePasswordAuthenticationFilter (which in its turn extends the AbstractAuthenticationProcessingFilter). The UsernamePasswordAuthenticationFilter, by default, is used for .formLogin authentication, and handles the default AntRequestMatcher "/login". Sending a request to /api/hello is an endpoint that is not handled by your filter. Since your security configuration requires that any /api/** endpoint should be authenticated, you receive the error. So, it should be made clear that an implementation of either UsernamePasswordAuthenticationFilter or AbstractAuthenticationProcessingFilter, is usually being used for Authenticating a user.
However, you can use/add your testing /api/hello endpoint to your filter and confirm that it works. For instance, you can override the default “/login” AntRequestMatcher from within your custom filter constructor, by using something like that (use the appropriate for you Http action, GET, POST, etc:
protected JsonUserPasswordAuthenticationFilter(AuthenticationManager authenticationManager) {
this.setAuthenticationManager(authenticationManager);
super.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/api/hello", "POST"));
}

Is it possible to have custom error response with jwt authentication?

I started building my project based on a custom error response in order to send the json body with only fields that i need. For this reason i have a
#RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler
that catches all exceptions and returns ResponseEntity having ther custom error body.
I have a postgres database where i save users. Currently i have /signin, /signup and /profile endpoints. I wanted to use jwt authentication. I used this github repo and i can get the token when i send user's credentials on the /signin endpoint.
Here's the problem. Read this part of JwtTokenFilter.java
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
String token = jwtTokenProvider.resolveToken(httpServletRequest);
try {
if (token != null && jwtTokenProvider.validateToken(token)) {
Authentication auth = jwtTokenProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(auth);
}
} catch (CustomException ex) {
//this is very important, since it guarantees the user is not authenticated at all
SecurityContextHolder.clearContext();
httpServletResponse.sendError(ex.getHttpStatus().value(), ex.getMessage());
return;
}
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
Suppose i want to signup a new user. Then my request's header won't have a token (token is null) and the program will execute filterChain.doFilter(httpServletRequest, httpServletResponse);. That work's fine, the user gets signed up and i get the 201 that my controller returns upon successful registration.
However, suppose i make a GET request at /profile endpoint again having no token. This too will execute filterChain.doFilter. However this time spring will respond with a NON-custom 403 error response.
I can't find a way to catch the exception on my RestControllerHandler because spring handles it for me.
Also, when i throw an exception inside doFilterInternal, the exception again won't be handled by my GlobalHandler, spring handles it.
Will have to add custom AuthenticationFailureHandler
public class CustomAuthenticationFailureHandler
implements AuthenticationFailureHandler {
private ObjectMapper objectMapper = new ObjectMapper();
#Override
public void onAuthenticationFailure(
HttpServletRequest request,
HttpServletResponse response,
AuthenticationException exception)
throws IOException, ServletException {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
Map<String, Object> data = new HashMap<>();
data.put(
"timestamp",
Calendar.getInstance().getTime());
data.put(
"exception",
exception.getMessage());
response.getOutputStream()
.println(objectMapper.writeValueAsString(data));
}
}
and then configure this here
#Configuration
#EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(AuthenticationManagerBuilder auth)
throws Exception {
auth.inMemoryAuthentication()
.withUser("user1").password(passwordEncoder.encode("user1Pass")).roles("USER");
}
#Override
protected void configure(HttpSecurity http)
throws Exception {
http
.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.formLogin()
.failureHandler(authenticationFailureHandler());
}
#Bean
public AuthenticationFailureHandler authenticationFailureHandler() {
return new CustomAuthenticationFailureHandler();
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}

Spring Security responds empty body with empty basic auth credentials

I'm trying to make a basic authentication service, for some business logic i need to acceept all basic auth credentials and make them hit another service (and there it will be fail if the credentials are wrong).
So I'm trying to throw an exception when the basic auth is not present, or are empty credentials.
This is my SecurityConfigurer:
#Configuration
#EnableWebSecurity
public class SecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
#Autowired
STGAuthenticationProvider authProvider;
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authProvider);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests().anyRequest().authenticated()
.and().httpBasic();
}
}
And this is my CustomAuthProvider:
#Component
public class STGAuthenticationProvider implements AuthenticationProvider {
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName();
String password = authentication.getCredentials().toString();
if(!StringUtils.isBlank(username) && !StringUtils.isBlank(password)) {
return new UsernamePasswordAuthenticationToken(username, password, new ArrayList<>());
} else {
throw new STGNoCredentialsException(Constants.Error.NO_CREDENTIALS);
}
}
#Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}
Actually my app gives me "401 Unauthorized" if i send a request with no auth (I would really like to get my custom Exception you can see at my CustomAuthProvider).
And when i send just 1 credential (username or password), or no one, my service answer me with empty body at POSTMAN. Can you guys help me?
From what I understand, your issue is similar to one I had a few days ago: I needed to return a 401 instead of a 403 whenever an endpoint was called with no authorisation or with auth token expired.
With respect to your code, I would add .exceptionHandling().authenticationEntryPoint(...) to your WebSecurityConfigurerAdapter as follows
#Configuration
#EnableWebSecurity
public class SecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
/* other stuff */
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests().anyRequest().authenticated()
.and().httpBasic()
.exceptionHandling().authenticationEntryPoint(/*custom exception*/);
}
}
and then, instead of /*custom exception*/ add something as new MyAuthException(), where MyAuthException looks like the following:
#Component
public class MyAuthException implements AuthenticationEntryPoint {
#Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) /*throws ...*/ {
response.setStatus(/* your status */);
response.getWriter().write(/*the body of your answer*/);
/* whatever else you want to add to your response */
/* or you could throw an exception, I guess*/
}
}
(I don't remember and right now I can't check whether this class needs to be marked as #Component, I think not).

Why doesn't RestController pick up request after AuthenticationFilter has finished

I have a working Spring Boot with Spring Security. Everything mostly works. What I'm not understanding is why the RestController never fires following the filter authorizing the request.
In other words, I have a rest controller set up to accept POST requests from /foo/bar and I have an AuthenticationFilter set up to first verify the users credentials before performing what the user requested.
Given that my RestController never fires, I've had to implement my code in the Success Handler, but my code belongs in the Controller instead.
I've attempted to debug this by stepping through Spring Security code, but nothing appears to suggest it would skip my RestController.
#RestController
#RequestMapping("foo")
public class FooController {
#PostMapping("bar") // this never executes
public ResponseEntity<FooResponse> foobar(#RequestBody Credentials creds) {
return new ResponseEntity<>(new FooResponse("baz"), HttpStatus.OK);
}
}
#Configuration
#EnableWebSecurity
#RequiredArgsConstructor
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Bean
public BCryptPasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Bean
public AuthenticationFilter authenticationTokenFilter() throws Exception {
AuthenticationFilter filter = new AuthenticationFilter();
filter.setAuthenticationManager(authenticationManager());
filter.setAuthenticationSuccessHandler(new AuthenticationSuccessHandlerImpl());
return filter;
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(accountService)
.passwordEncoder(passwordEncoder());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors()
.and()
.csrf().disable()
.authorizeRequests()
.antMatchers(HttpMethod.POST, "/foo/bar").permitAll()
.and()
.addFilter(new AuthorizationFilter(properties, authenticationManager(), tokenService)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
public class AuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public AuthenticationFilter() {
super("/foo/bar");
}
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) {
Credentials creds = new ObjectMapper().readValue(request.getInputStream(), Credentials.class);
return getAuthenticationManager().authenticate(
new UsernamePasswordAuthenticationToken(
creds.getUsername(),
creds.getPassword(),
new ArrayList<>())
);
}
}
public class AuthenticationSuccessHandlerImpl implements AuthenticationSuccessHandler {
#Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
// do nothing and prevent redirect to /login, /logout, etc
}
}
You are using http.cors(). Have you configured it elsewhere? If not, it probably considers you are not authenticated. Check this link out: https://docs.spring.io/spring-security/site/docs/5.0.x/reference/html/cors.html
I would suggest you to remove it if you are not going to use it
Also you have here an example of an easy security configuration: https://www.baeldung.com/spring-security-login
From my point of view, removing cors() should fix your problem (or configuring it the right way :)

Handle Security exceptions in Spring Boot Resource Server

How can I get my custom ResponseEntityExceptionHandler or OAuth2ExceptionRenderer to handle Exceptions raised by Spring security on a pure resource server?
We implemented a
#ControllerAdvice
#RestController
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
so whenever there is an error on the resource server we want it to answer with
{
"message": "...",
"type": "...",
"status": 400
}
The resource server uses the application.properties setting:
security.oauth2.resource.userInfoUri: http://localhost:9999/auth/user
to authenticate and authorize a request against our auth server.
However any spring security error will always bypass our exception handler at
#ExceptionHandler(InvalidTokenException.class)
public ResponseEntity<Map<String, Object>> handleInvalidTokenException(InvalidTokenException e) {
return createErrorResponseAndLog(e, 401);
}
and produce either
{
"timestamp": "2016-12-14T10:40:34.122Z",
"status": 403,
"error": "Forbidden",
"message": "Access Denied",
"path": "/api/templates/585004226f793042a094d3a9/schema"
}
or
{
"error": "invalid_token",
"error_description": "5d7e4ab5-4a88-4571-b4a4-042bce0a076b"
}
So how do I configure the security exception handling for a resource server? All I ever find are examples on how to customize the Auth Server by implementing a custom OAuth2ExceptionRenderer. But I can't find where to wire this to the resource server's security chain.
Our only configuration/setup is this:
#SpringBootApplication
#Configuration
#ComponentScan(basePackages = {"our.packages"})
#EnableAutoConfiguration
#EnableResourceServer
As noted in previous comments the request is rejected by the security framework before it reaches the MVC layer so #ControllerAdvice is not an option here.
There are 3 interfaces in the Spring Security framework that may be of interest here:
org.springframework.security.web.authentication.AuthenticationSuccessHandler
org.springframework.security.web.authentication.AuthenticationFailureHandler
org.springframework.security.web.access.AccessDeniedHandler
You can create implementations of each of these Interfaces in order to customize the response sent for various events: successful login, failed login, attempt to access protected resource with insufficient permissions.
The following would return a JSON response on unsuccessful login attempt:
#Component
public class RestAuthenticationFailureHandler implements AuthenticationFailureHandler
{
#Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException ex) throws IOException, ServletException
{
response.setStatus(HttpStatus.FORBIDDEN.value());
Map<String, Object> data = new HashMap<>();
data.put("timestamp", new Date());
data.put("status",HttpStatus.FORBIDDEN.value());
data.put("message", "Access Denied");
data.put("path", request.getRequestURL().toString());
OutputStream out = response.getOutputStream();
com.fasterxml.jackson.databind.ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(out, data);
out.flush();
}
}
You also need to register your implementation(s) with the Security framework. In Java config this looks like the below:
#Configuration
#EnableWebSecurity
#ComponentScan("...")
public class SecurityConfiguration extends WebSecurityConfigurerAdapter
{
#Override
public void configure(HttpSecurity http) throws Exception
{
http
.addFilterBefore(corsFilter(), ChannelProcessingFilter.class)
.logout()
.deleteCookies("JESSIONID")
.logoutUrl("/api/logout")
.logoutSuccessHandler(logoutSuccessHandler())
.and()
.formLogin()
.loginPage("/login")
.loginProcessingUrl("/api/login")
.failureHandler(authenticationFailureHandler())
.successHandler(authenticationSuccessHandler())
.and()
.csrf()
.disable()
.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint())
.accessDeniedHandler(accessDeniedHandler());
}
/**
* #return Custom {#link AuthenticationFailureHandler} to send suitable response to REST clients in the event of a
* failed authentication attempt.
*/
#Bean
public AuthenticationFailureHandler authenticationFailureHandler()
{
return new RestAuthenticationFailureHandler();
}
/**
* #return Custom {#link AuthenticationSuccessHandler} to send suitable response to REST clients in the event of a
* successful authentication attempt.
*/
#Bean
public AuthenticationSuccessHandler authenticationSuccessHandler()
{
return new RestAuthenticationSuccessHandler();
}
/**
* #return Custom {#link AccessDeniedHandler} to send suitable response to REST clients in the event of an attempt to
* access resources to which the user has insufficient privileges.
*/
#Bean
public AccessDeniedHandler accessDeniedHandler()
{
return new RestAccessDeniedHandler();
}
}
In case if you're using #EnableResourceServer, you may also find convenient to extend ResourceServerConfigurerAdapter instead of WebSecurityConfigurerAdapter in your #Configuration class. By doing this, you may simply register a custom AuthenticationEntryPoint by overriding configure(ResourceServerSecurityConfigurer resources) and using resources.authenticationEntryPoint(customAuthEntryPoint()) inside the method.
Something like this:
#Configuration
#EnableResourceServer
public class CommonSecurityConfig extends ResourceServerConfigurerAdapter {
#Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.authenticationEntryPoint(customAuthEntryPoint());
}
#Bean
public AuthenticationEntryPoint customAuthEntryPoint(){
return new AuthFailureHandler();
}
}
There's also a nice OAuth2AuthenticationEntryPoint that can be extended (since it's not final) and partially re-used while implementing a custom AuthenticationEntryPoint. In particular, it adds "WWW-Authenticate" headers with error-related details.
You are not able to make use of Spring MVC Exception handler annotations such as #ControllerAdvice because spring security filters kicks in much before Spring MVC.
If you're using token validation URL with config similar to Configuring resource server with RemoteTokenServices in Spring Security Oauth2 which returns HTTP status 401 in case of unauthorized:
#Primary
#Bean
public RemoteTokenServices tokenService() {
RemoteTokenServices tokenService = new RemoteTokenServices();
tokenService.setCheckTokenEndpointUrl("https://token-validation-url.com");
tokenService.setTokenName("token");
return tokenService;
}
Implementing custom authenticationEntryPoint as described in other answers (https://stackoverflow.com/a/44372313/5962766) won't work because RemoteTokenService use 400 status and throws unhandled exceptions for other statuses like 401:
public RemoteTokenServices() {
restTemplate = new RestTemplate();
((RestTemplate) restTemplate).setErrorHandler(new DefaultResponseErrorHandler() {
#Override
// Ignore 400
public void handleError(ClientHttpResponse response) throws IOException {
if (response.getRawStatusCode() != 400) {
super.handleError(response);
}
}
});
}
So you need to set custom RestTemplate in RemoteTokenServices config which would handle 401 without throwing exception:
#Primary
#Bean
public RemoteTokenServices tokenService() {
RemoteTokenServices tokenService = new RemoteTokenServices();
tokenService.setCheckTokenEndpointUrl("https://token-validation-url.com");
tokenService.setTokenName("token");
RestOperations restTemplate = new RestTemplate();
restTemplate.setRequestFactory(new HttpComponentsClientHttpRequestFactory());
((RestTemplate) restTemplate).setErrorHandler(new DefaultResponseErrorHandler() {
#Override
// Ignore 400 and 401
public void handleError(ClientHttpResponse response) throws IOException {
if (response.getRawStatusCode() != 400 && response.getRawStatusCode() != 401) {
super.handleError(response);
}
}
});
}
tokenService.setRestTemplate(restTemplate);
return tokenService;
}
And add dependency for HttpComponentsClientHttpRequestFactory:
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
OAuth2ExceptionRenderer is for an Authorization Server. The correct answer is likely to handle it like detailed in this post (that is, ignore that it's oauth and treat it like any other spring security authentication mechanism): https://stackoverflow.com/a/26502321/5639571
Of course, this will catch oauth related exceptions (which are thrown before you reach your resource endpoint), but any exceptions happening within your resource endpoint will still require an #ExceptionHandler method.
We can use this security handler to pass the handler to spring mvc #ControllerAdvice
#Component
public class AuthExceptionHandler implements AuthenticationEntryPoint, AccessDeniedHandler {
private static final Logger LOG = LoggerFactory.getLogger(AuthExceptionHandler.class);
private final HandlerExceptionResolver resolver;
#Autowired
public AuthExceptionHandler(#Qualifier("handlerExceptionResolver") final HandlerExceptionResolver resolver) {
this.resolver = resolver;
}
#Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
LOG.error("Responding with unauthorized error. Message - {}", authException.getMessage());
resolver.resolveException(request, response, null, authException);
}
#Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
LOG.error("Responding with access denied error. Message - {}", accessDeniedException.getMessage());
resolver.resolveException(request, response, null, accessDeniedException);
}
}
Then define the exception by using #ControllerAdvice so that we can manage the global exception handler in one place..
This is possible. Since the original question is for a REST controller that needs to return a custom JSON response, I will write up a complete answer step by step which worked for me. First and foremost, it seems you cannot handle this with a #ControllerAdvice that extends ControllResponseEntityExceptionHandler. You need a separate handler that extends AccessDeniedHandler. Follow the below steps.
Step 1: Create a custom handler class that extends AccessDeniedHandler
#Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
private static final String JSON_TYPE = "application/json";
#Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException {
MyErrorList errors = new MyErrorList();
errors.addError(new MyError("", "You do not have permission to access this resource."));
response.setStatus(HttpStatus.FORBIDDEN.value());
response.setContentType(JSON_TYPE);
OutputStream output = response.getOutputStream();
ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(output, errors);
output.flush();
}
}
'MyError' above is a simple POJO to represent an error json structure and MyErrorList is another POJO that holds a list of 'MyError's.
Step 2: Inject the Handler created above into the Security configuration
#Autowired
private VOMSAccessDeniedHandler accessDeniedHandler;
Step 3: Register the accessDeniedHandler in your configure method
.and().exceptionHandling().accessDeniedHandler(accessDeniedHandler)
With Step 2 and Step 3, Your SecurityConfiguration should look something like this (Note that I am omitting code that is not relevant to this problem to shorten the length of this answer):
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
private MyAccessDeniedHandler accessDeniedHandler;
// Other stuff
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/register").permitAll()
.antMatchers("/authenticate").permitAll()
.antMatchers("/public").permitAll()
.anyRequest().authenticated()
.and().exceptionHandling().accessDeniedHandler(accessDeniedHandler)
.and().sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
}
}
Adapting the accepted answer for use with Spring OAuth2ResourceServer for JWT authentication, because without special configuration, it will register its own BearerTokenAuthenticationEntryPoint, and ignore the one we set in .exceptionHandling().authenticationEntryPoint()
Hence, in our WebSecurityConfigurerAdapter we have:
#Autowired
private AuthenticationFailureHandler authenticationFailureHandler;
protected void configure(HttpSecurity http) throws Exception {
http
// ... all the usual stuff ...
// configure OAuth2 (OIDC) JWT and set a custom authentication failure handler
.oauth2ResourceServer((resourceServer) -> resourceServer
.jwt().and()
.authenticationEntryPoint(authenticationFailureHandler));
}
where AuthenticationFailureHandler is coded as suggested in earlier answers:
#Component
public class AuthenticationFailureHandler implements AuthenticationEntryPoint {
public AuthenticationFailureHandler() {
}
// Autowire our own CustomExceptionHandler: must be qualified because Spring Boot has others in the classpath
#Autowired
#Qualifier("handlerExceptionResolver")
private HandlerExceptionResolver resolver;
#Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws
IOException {
resolver.resolveException(request, response, null, authException);
}
}
In our CustomExceptionHandler (which is autowired above, but not mentioned by class name explicitly) we add a method for AuthenticationException handling:
#ExceptionHandler(value = {AuthenticationException.class})
protected ResponseEntity<?> handleAuthenticationException(RuntimeException ex, WebRequest request) {
return ... something ... // create custom error response here
}
Spring 3.0 Onwards,You can use #ControllerAdvice (At Class Level) and extends org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler class from CustomGlobalExceptionHandler
#ExceptionHandler({com.test.CustomException1.class,com.test.CustomException2.class})
public final ResponseEntity<CustomErrorMessage> customExceptionHandler(RuntimeException ex){
return new ResponseEntity<CustomErrorMessage>(new CustomErrorMessage(false,ex.getMessage(),404),HttpStatus.BAD_REQUEST);
}

Categories