cdeszaq
cdeszaq

Reputation: 31300

How can I allow a user override with Spring Security?

In my Spring MVC web application, there are certain areas accessible only to users with sufficient privileges. Rather than just have a "access denied" message, I need to be able to allow users to log in as a different user in order to use these pages (sort of like an override).

How can I do this with Spring Security?

Here's the flow I am looking to have, with a bit more detail:

  1. User A comes in to page X from external application and is authenticated via headers
  2. User A does not have permission to use page X, and so is taken to the login screen with a message indicating that they must log in as a user with sufficient privilages to use this page
  3. User B logs in, and has sufficient privilages, and is taken to page X.

Note: Page X has a big, long query string that needs to be preserved.

How can I do this with Spring Security?


Here's my spring security config file:

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/security"
    xmlns:beans="http://www.springframework.org/schema/beans"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans 
        http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
            http://www.springframework.org/schema/security 
            http://www.springframework.org/schema/security/spring-security-3.1.xsd">

    <debug />

    <global-method-security pre-post-annotations="enabled">
        <!-- AspectJ pointcut expression that locates our "post" method and applies 
            security that way <protect-pointcut expression="execution(* bigbank.*Service.post*(..))" 
            access="ROLE_TELLER"/> -->
    </global-method-security>

    <!-- Allow anyone to get the static resources and the login page by not applying the security filter chain -->
    <http pattern="/resources/**" security="none" />
    <http pattern="/css/**" security="none" />
    <http pattern="/img/**" security="none" />
    <http pattern="/js/**" security="none" />

    <!-- Lock everything down -->
    <http 
        auto-config="true"
        use-expressions="true" 
        disable-url-rewriting="true">

        <!-- Define the URL access rules -->
        <intercept-url pattern="/login" access="permitAll" />
        <intercept-url pattern="/about/**" access="permitAll and !hasRole('blocked')" />
        <intercept-url pattern="/users/**" access="hasRole('user')" />
        <intercept-url pattern="/reviews/new**" access="hasRole('reviewer')" />
        <intercept-url pattern="/**" access="hasRole('user')" />

        <form-login 
            login-page="/login" />

        <logout logout-url="/logout" /> 

        <access-denied-handler error-page="/login?reason=accessDenied"/>

        <!-- Limit the number of sessions a user can have to only 1 -->
        <session-management>
            <concurrency-control max-sessions="1" />
        </session-management>
    </http>

    <authentication-manager>
        <authentication-provider ref="adAuthenticationProvider" />
        <authentication-provider>
            <user-service>
                <user name="superadmin" password="superadminpassword" authorities="user" />
            </user-service>
        </authentication-provider>
    </authentication-manager>

    <beans:bean id="adAuthenticationProvider" class="[REDACTED Package].NestedGroupActiveDirectoryLdapAuthenticationProvider">
        <beans:constructor-arg value="[REDACTED FQDN]" />
        <beans:constructor-arg value="[REDACTED LDAP URL]" />
        <beans:property name="convertSubErrorCodesToExceptions" value="true" />
        <beans:property name="[REDACTED Group Sub-Tree DN]" />
        <beans:property name="userDetailsContextMapper" ref="peerReviewLdapUserDetailsMapper" />
    </beans:bean>

    <beans:bean id="peerReviewLdapUserDetailsMapper" class="[REDACTED Package].PeerReviewLdapUserDetailsMapper">
        <beans:constructor-arg ref="UserDAO" />
    </beans:bean>

</beans:beans>

I'm using a slightly modified version of the Spring Security 3.1 Active Directory connection capabilities. The modifications simply load all of a user's groups, including those reached by group nesting, rather than only the ones the user is directly a member of. I'm also using a custom user object that has my application's User object embedded in it, and a custom LDAP mapper that does the normal LDAP mapping, and then adds in my user.

There is a special authentication scenario that has not been implemented yet where the user is authenticated based on a username passed from an external application (or via Kerberos) in a Single-Sign-On fashion.

Upvotes: 6

Views: 3781

Answers (2)

Bhashit Parikh
Bhashit Parikh

Reputation: 3131

Solution 1

Register an application wide ExeptionResolver using anyway you like. For ex.

public class MyApplicationErrorResolver extends SimpleMappingExceptionResolver {

    @Autowired
    private List<LogoutHandler> logoutHandlers;

    @Override
    protected ModelAndView doResolveException(HttpServletRequest request,
        HttpServletResponse response, Object handler, Exception ex) {

        if(ex instanceof AccessDeniedException) {
            for(LogoutHandler lh : logoutHandlers) {
                lh.logout(request, response, SecurityContextHolder.getContext().getAuthentication());
            }
            // Not present as a bean. So create it manually.
            SecurityContextLogoutHandler logoutHandler = new SecurityContextLogoutHandler();
            logoutHandler.setInvalidateHttpSession(true);
            logoutHandler.logout(request, response, SecurityContextHolder.getContext().getAuthentication());

            return new ModelAndView(new RedirectView(request.getRequestURL().toString()));
        }

        return super.doResolveException(request, response, handler, ex);
    }
}

register it as a bean:

<bean class="package.path.MyApplicationErrorResolver" />

(that's all you need to register it). This will work for your configuration. Bu you will probably need to remove the <access-denied-handler> element from the config.

Solution 2

Another way is to use an AccessDeniedHandler. For ex:

public class MyAccessDeniedExceptionHandler implements AccessDeniedHandler {

    @Autowired
    private List<LogoutHandler> logoutHandlers;

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
        for(LogoutHandler lh : logoutHandlers) {
            lh.logout(request, response, SecurityContextHolder.getContext().getAuthentication());
        }

        SecurityContextLogoutHandler logoutHandler = new SecurityContextLogoutHandler();
        logoutHandler.setInvalidateHttpSession(true);
        logoutHandler.logout(request, response, SecurityContextHolder.getContext().getAuthentication());

        response.sendRedirect(request.getRequestURL().toString());
    }

}

register it as a bean:

<bean id="accesssDeniedHandler" class="package.path.MyAccessDeniedExceptionHandler" />

and specify it in your config:

<access-denied-handler ref="accesssDeniedHandler" />

Upvotes: 0

Simeon
Simeon

Reputation: 7792

How do you check for roles ?

If you define them in your security context like this:

<intercept-url pattern="/adminStuff.html**" access="hasRole('ROLE_ADMIN')" />

You can set the defaultFailureUrl in your SimpleUrlAuthenticationFailureHandler and when a user with lesser privileges tries to access a secured URL the FaliureHandler should redirect you to the defaultFailureUrl which could be your login page.

You can inject a FaliureHandler in the filter at the FORM_LOGIN_FILTER position.

<bean id="myFaliureHandler" 
    class="org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler">
    <property name="defaultFailureUrl" value="http://yourdomain.com/your-login.html"/>
</bean>

<bean id="myFilter"
   class="org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter">
   <property name="authenticationFailureHandler" ref="myFaliureHandler"/>
</bean>    

<http>
  <custom-filter position="FORM_LOGIN_FILTER" ref="myFilter" />
</http>

Answering 1) in the comment.

This would be a little more work than I thought given your namespace configuration.

What you need to do is remove the <form-login> definition and instead of it add a 'custom' UsernamePasswordAuthenticationFilter (this is the filter that handles the <form-login> element).

You also need to remove the <access-denied-handler>.

So your configuration would look something like:

<bean id="myFaliureHandler" 
    class="org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler">
    <property name="defaultFailureUrl" value="http://yourdomain.com/your-login.html"/>
</bean>

<bean id="myFilter"
   class="org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter">
   <property name="authenticationFailureHandler" ref="myFaliureHandler"/>
   <!-- there are more required properties, but you can read about them in the docs -->
</bean>

<bean id="loginUrlAuthenticationEntryPoint"
   class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
   <property name="loginFormUrl" value="/login"/>
</bean>

<http entry-point-ref="authenticationEntryPoint" auto-config="false">

  <!-- your other http config goes here, just omit the form-login element and the access denied handler -->

  <custom-filter position="FORM_LOGIN_FILTER" ref="myFilter" />
</http>

Generally also have a look at the spring docs on custom filters, if you haven't already. We currently use this config in my current company forcing users to relogin if the don't have required privileges on a page.

Upvotes: 2

Related Questions