How to intercept not authorazied request to a rest controller (method level authorization) - Spring Boot - spring

In a REST Controller I have the following method.
#GetMapping("/activate_user")
#RolesAllowed({Role.ROLE_ADMIN})
public void activateUser() {
// Some code here
}
If a user with ROLE_ADMIN calls this method, it works like it should.
If a user without ROLE_ADMIN calls this method, it return an Http-Status 403. That is also ok, but I want now to intercept this call in case the user is not authorized, run some custom code and return some JSON data back to the caller.
I don't know how it could be done with Spring?!

You can override the accessdenied exception and this way it will only be executed for 403 unauthorized.
#Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.getWriter().write("Custom Access Denied Message");
}
you can use MVC Interceptor Configuration to intercept specific URLs/APIs
#Configuration
#EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {
#Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LocaleInterceptor());
registry.addInterceptor(new ThemeInterceptor()).addPathPatterns("/**").excludePathPatterns("/adminRole/**");
registry.addInterceptor(new SecurityInterceptor()).addPathPatterns("/auth/*", "/ui/**", "/xyz/**");
}
}
You can even exclude specific URL's.

Related

Configure Spring Security to return JSON response after authentication

I have a legacy application that is in spring 4 with UI as JSP. Need move the presentation layer from spring to react app. When I call /login with parameters it gives me an HTML, How do I change my existing spring security logic so that it returns a json response.
Here is the code snippet
protected void configure(HttpSecurity http) throws Exception {
http.sessionManagement().maximumSessions(1).and().invalidSessionUrl(URLConstants.LOGIN_URL);
http.csrf().disable();
http.anonymous().disable()
.authorizeRequests().antMatchers("/")
.access("hasRole('USER') or hasRole('ADMIN') or hasRole('DC MANAGER')")
.and().formLogin() .loginProcessingUrl(URLConstants.LOGIN_URL).usernameParameter("ssoId").passwordParameter("password").and()
.rememberMe().rememberMeParameter("remember-me").tokenRepository(tokenRepository) .tokenValiditySeconds(18400).and().exceptionHandling().accessDeniedPage("/Access_Denied");
}
Write a custom AuthenticationSuccessHandler that writes your JSON and plug it into your formLogin().
.formLogin().successHandler(yourSucessHandlerBean);
Your handler could roughly look like this:
#Component
public class Securityhandler implements AuthenticationSuccessHandler {
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
// write your JSON here, directly to the HttpServletResponse
}
}

Spring Boot Redirect to requested URL after login

I have a Spring Boot UI application. I am trying to redirect users to the originally requested URL after login.
When a user requests http://www.example.com/myapp/user/22, the application aptly redirects to http://www.example.com/myapp/login. Once the user logs in, the application redirects to http://www.example.com/myapp/dashboard. I would like the application to redirect to http://www.example.com/myapp/user/22.
I have gone through several links and feel I have a proper configuration, yet, redirection is not working as expected.
My Security Config is
public class SecurityConfig extends WebSecurityConfigurerAdapter {
.....
....
#Autowired
private MyAuthenticationSuccessHandler authenticationSuccessHandler;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.
authorizeRequests()
.antMatchers("/user/**").authenticated()
.and().csrf().disable().formLogin()
.successHandler(authenticationSuccessHandler)
......
and My Success Handler is
#Component
public class MyAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
...
public MyAuthenticationSuccessHandler() {
super();
this.setDefaultTargetUrl("/myapp/dashboard");
this.setUseReferer(true);
}
#Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
//Do something ..........
........
.........
super.onAuthenticationSuccess(request, response, authentication);
}
I tried using SavedRequestAwareAuthenticationSuccessHandler too.
I notice that my success handler is invoked, but the target URL is always /user/login and my login controller is invoked..
#RequestMapping("/login")
public ModelAndView login(#ModelAttribute() {
if(!userIdentified) {
//go to login page
} else {
new ModelAndView("redirect:/myapp/dashboard");
}
}
and the user is redirected to "dashboard".
What else am I missing?
Use "Referer" from session attribute to get the latest request URL. On my app, i use this one
public class CustomAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
public static final String REDIRECT_URL_SESSION_ATTRIBUTE_NAME = "REDIRECT_URL";
#Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
Object redirectURLObject = request.getSession().getAttribute(REDIRECT_URL_SESSION_ATTRIBUTE_NAME);
if(redirectURLObject != null)
setDefaultTargetUrl(redirectURLObject.toString());
else{
setDefaultTargetUrl("/");
}
request.getSession().removeAttribute(REDIRECT_URL_SESSION_ATTRIBUTE_NAME);
super.onAuthenticationSuccess(request, response, authentication);
}
}
Edit :
Sorry i forgot to show the login controller
#RequestMapping(method = RequestMethod.GET, value = {"/login"})
String login(Model model, Principal principal, HttpServletRequest request) throws Exception{
String referer = request.getHeader("Referer"); //Get previous URL before call '/login'
//save referer URL to session, for later use on CustomAuthenticationSuccesshandler
request.getSession().setAttribute(CustomAuthenticationSuccessHandler.REDIRECT_URL_SESSION_ATTRIBUTE_NAME, referer);
return principal == null ? "login" : "redirect:/";
}
Although Singgih S answer works, BUT there is a better way as below :
Ref:
https://www.baeldung.com/spring-security-redirect-login
There is no magic in these easy to use features in Spring Security.
When a secured resource is being requested, the request will be
filtered by a chain of various filters. Authentication principals and
permissions will be checked. If the request session is not
authenticated yet, AuthenticationException will be thrown.
The AuthenticationException will be caught in the
ExceptionTranslationFilter, in which an authentication process will be
commenced, resulting in a redirection to the login page.
Therefore :
1. When redirection to the "/login" page occurs, your secured request url is saved in the session as DefaultSavedRequest object.
2. Also we know when a successful form based login occurs, one of the implementations of AuthenticationSuccessHandler is called.
so we can create a custom class and get DefaultSavedRequest in it as below :
public class CustomAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
#Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
DefaultSavedRequest defaultSavedRequest = (DefaultSavedRequest) request.getSession().getAttribute("SPRING_SECURITY_SAVED_REQUEST");
if(defaultSavedRequest != null){
getRedirectStrategy().sendRedirect(request, response, defaultSavedRequest.getRedirectUrl());
}else{
super.onAuthenticationSuccess(request, response, authentication);
}
}
}
3. We have to introduce this class in WebSecurityConfigurerAdapter :
#Override
protected void configure(HttpSecurity http) throws Exception {
http.(...).anyRequest().authenticated().and()
.formLogin()
.loginPage("/login")
.loginProcessingUrl("/login")
.successHandler(new CustomAuthenticationSuccessHandler());
So you can implement your logic in the above onAuthenticationSuccess method.
Best wishes
The Spring route, ala extending SavedRequestAwareAuthenticationSuccessHandler or SimpleUrlAuthenticationSuccessHandler can be a bit clunky to implement. In the controller (ex. a POST method that processes logins), you can do the header request yourself; ex:
HttpServletRequest request =null;
String priorUrl = request.getHeader("Referer");
You will notice that you will have the URL prior to either a manual (initiated by user) logout or a session timeout (as handled by Spring session): you'll get an https://iAmPriorUrl.com/.... Then you can do whatever you want with it.

How to perform custom steps during Login POST method(using spring boot)

Spring boot provide its own implementation for login (POST method), if I want to maintain history(count) of successful/unsuccessful log in of users, how can I do it?
Did the following to make it working:
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler{
#Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {
// TODO Auto-generated method stub
......
}

Spring Security: How can I set a RememberMe cookie url path, that differs from the context path?

How in Spring Security can I set a RememberMe cookie url path, that differs from the context path?
Supposing my website's homepage url is (url rewrite):
https://www.mysuperspecialdomain.com
And that my login page has a url like this:
https://www.mysuperspecialdomain.com/shop/account/login
After succesful login the RememberMe cookie has the path /shop (visible in the browser, e.g. Chrome). This is the project's context path.
This leads to the situation, that when I'm going to my homepage, RememberMe is not logging in. Only when I navigate to a url, that starts with https://www.myspecialdomain.com/shop it's doing it.
If you use Spring Security 4.1.0 or higher, you can configure the cookie domain, see RememberMeConfigurer#rememberMeCookieDomain:
The domain name within which the remember me cookie is visible.
but you can't change the context path.
So you have to implement your own RememberMeServices (you could create a sub class of an existing one) and add it with RememberMeConfigurer#rememberMeServices to your security configuration.
I've found a solution to my own question - manipulation of the path of the RememberMe-cookie can be done via an HttpServletResponseWrapper. This is the solution (based on this answer https://stackoverflow.com/a/7047298/7095884):
Define an HttpServletResponseWrapper:
public class RememberMeCookieResponseWrapper extends HttpServletResponseWrapper {
public RememberMeCookieResponseWrapper(HttpServletResponse response) {
super(response);
}
#Override
public void addCookie(Cookie cookie) {
if (cookie.getName().equals("shop")) {
cookie.setPath("/");
}
super.addCookie(cookie);
}
}
Define a filter, that wraps the servlet response with the just defined wrapper:
public class RememberMeCookieFilter implements Filter {
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
if (response instanceof HttpServletResponse) {
HttpServletResponse newResponse =
new RememberMeCookieResponseWrapper((HttpServletResponse)response);
chain.doFilter(request, newResponse);
}
}
}
Add this filter to the Spring Filter Chain in front of the authentication part:
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilterBefore(new RememberMeCookieFilter(), UsernamePasswordAuthenticationFilter.class)
...

Add Maintenance Mode to Spring (Security) app

I'm looking for a way to implement a Maintenance Mode in my Spring app.
While the app is in Maintenance Mode only users role = MAINTENANCE should be allowed to log in. Everyone else gets redirected to login page.
Right now I just built a Filter:
#Component
public class MaintenanceFilter extends GenericFilterBean {
#Autowired SettingStore settings;
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
if(settingStore.get(MaintenanceMode.KEY).isEnabled()) {
HttpServletResponse res = (HttpServletResponse) response;
res.setStatus(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
} else {
chain.doFilter(request, response);
}
}
}
And added it using:
#Override
protected void configure(HttpSecurity http) throws Exception {
http
// omitted other stuff
.addFilterAfter(maintenanceFilter, SwitchUserFilter.class);
}
Because as far as I figured out SwitchUserFilter should be the last filter in Spring Security's filter chain.
Now every request gets canceled with a 503 response. Though there's no way to access the login page.
If I add a redirect to the Filter, this will result in an infinite loop, because access to login page is also denied.
Additionally I can't figure out a nice way to get the current users roles. Or should I just go with SecurityContextHolder ?
I'm looking for a way to redirect every user to the login page (maybe with a query param ?maintenance=true) and every user with role = MAINTENANCE can use the application.
So the Filter / Interceptor should behave like:
if(maintenance.isEnabled()) {
if(currentUser.hasRole(MAINTENANCE)) {
// this filter does nothing
} else {
redirectTo(loginPage?maintenance=true);
}
}
I now found two similar solutions which work, but the place where I inject the code doesn't look that nice.
For both I add a custom RequestMatcher, which get's #Autowired and checks if Maintenance Mode is enabled or not.
Solution 1:
#Component
public class MaintenanceRequestMatcher implements RequestMatcher {
#Autowired SettingStore settingStore;
#Override
public boolean matches(HttpServletRequest request) {
return settingStore.get(MaintenanceMode.KEY).isEnabled()
}
}
And in my Security Config:
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired MaintenanceRequestMatcher maintenanceRequestMatcher;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/public/**").permitAll()
.requestMatchers(maintenanceRequestMatcher).hasAuthority("MY_ROLE")
.anyRequest().authenticated()
// ...
}
Solution 2:
Very similar, but uses HttpServletRequest.isUserInRole(...):
#Component
public class MaintenanceRequestMatcher implements RequestMatcher {
#Autowired SettingStore settingStore;
#Override
public boolean matches(HttpServletRequest request) {
return settingStore.get(MaintenanceMode.KEY).isEnabled() && !request.isUserInRole("MY_ROLE");
}
}
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired MaintenanceRequestMatcher maintenanceRequestMatcher;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/public/**").permitAll()
.requestMatchers(maintenanceRequestMatcher).denyAll()
.anyRequest().authenticated()
// ...
}
This will perform a denyAll() if Maintenance Mode is enabled and the current user does not have MY_ROLE.
The only disadvantage is, that I cannot set a custom response. I'd prefer to return a 503 Service Unavailable. Maybe someone can figure out how to do this.
It's kinda of a chicken or egg dilemma, you want to show unauthorized users a "we're in maintenance mode ..." message, while allow authorized users to login, but you don't know if they are authorized until they log in. Ideally it would be nice to have this in some sort of filter, but I found in practice it was easier for me to solve a similar issue by putting the logic after login, like in the UserDetailsService.
Here's how I solved it on a project. When I'm in maintenance mode, I set a flag for the view to show the "we're in maintenance mode .." message, in a global header or on the login page. So users, regardless of who they are know it's maintenance mode. Login should work as normal.
After user is authenticated, and in my custom UserDetailsService, where their user details are loaded with their roles, I do the following:
// if we're in maintenance mode and does not have correct role
if(maintenance.isEnabled() && !loadedUser.hasRole(MAINTENANCE)) {
throw new UnauthorizedException(..)
}
// else continue as normal
It's not pretty, but it was simple to understand (which I think is good for security configuration stuff) and it works.
Update:
With you solution I'd have to destroy everyone's session, else a user
which was logged in before maintenance mode was enabled, is still able
to work with the system
On our project we don't allow any users to be logged in while in maintenance mode. An admin, kicks off a task which enables "maintenance..." msg, with a count down, then at the end, we expire everyone's session using SessionRegistry.
I was a similar situation and found this answer is helpful. I followed the second approach and also managed to return custom response.
Here is what I have done to return the custom response.
1- Define a controller method that returns the needed custom response.
#RestController
public class CustomAccessDeniedController {
#GetMapping("/access-denied")
public String getAccessDeniedResponse() {
return "my-access-denied-page";
}
}
2- In your security context, you should permit this URL to be accessible.
http.authorizeRequests().antMatchers("/access-denied").permitAll()
3- Create a custom access denied exception handler
#Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
#Autowired
private SettingStore settingStore;
#Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
if(settingStore.get(MaintenanceMode.KEY).isEnabled()) {
response.sendRedirect(request.getContextPath() + "/access-denied");
}
}
}
4- Register the custom access denier exception handler in the security config
#Autowired
private CustomAccessDeniedHandler accessDeniedHandler;
http.exceptionHandling().accessDeniedHandler(accessDeniedHandler);

Resources