Search code examples
javaspring-securitykmlremember-megoogle-earth-plugin

Catching Remember-Me Authentication Events in Spring Security


I'm developing an application in which I need to catch and respond to Authentication events to take appropriate action. Currently, I'm catching just fine the AuthenticationSuccessEvent Spring throws when a user logs in manually. I'm now trying to implement Remember-Me functionality. Logging helped me to figure out the the event I want to catch is the InteractiveAuthenticationSuccessEvent. Can someone take a gander at the code below and help me to respond to this new event?

@Override
public void onApplicationEvent(ApplicationEvent event) {
    log.info(event.toString()); // debug only: keep track of all events
    if (event instanceof AuthenticationSuccessEvent) {
        AuthenticationSuccessEvent authEvent = (AuthenticationSuccessEvent)event;
        lock.writeLock().lock();
        try {
            sessionAuthMap.put(((WebAuthenticationDetails)authEvent.getAuthentication().getDetails()).getSessionId(), authEvent.getAuthentication());
        } finally {
            lock.writeLock().unlock();
        }
    } else if (event instanceof HttpSessionDestroyedEvent) {
        HttpSessionDestroyedEvent destroyEvent = (HttpSessionDestroyedEvent)event;
        lock.writeLock().lock();
        try {
            sessionAuthMap.remove(destroyEvent.getId());
        } finally {
            lock.writeLock().unlock();
        }
    }
}

Additional Information:

I didn't mention in the original posting that the requirement of storing the Session Id and Authentication object in a Map is due to the fact that I'm using the Google Earth plugin. GE acts as a separate, unrelated user agent, and thus the user's session information never gets passed to the server by GE. For this reason, I rewrite the request URL from GE to contain the user's active Session Id (from the aforementioned Map) as a parameter so we can verify that said Session Id is indeed valid for a logged in user. All of this is in place because we have KML which GE needs, but we can't allow a user to pick up a direct, unprotected URL via Firebug or what have you.

Spring Config: (sorry, SO kinda fudged the formatting)

<sec:http use-expressions="true">
<sec:intercept-url pattern="/Login.html*" access="permitAll"/>
<sec:intercept-url pattern="/j_spring_security*" access="permitAll" method="POST"/>
<sec:intercept-url pattern="/main.css*" access="permitAll"/>
<sec:intercept-url pattern="/favicon.ico*" access="permitAll"/>
<sec:intercept-url pattern="/images/**" access="permitAll"/>
<sec:intercept-url pattern="/common/**" access="permitAll"/>
<sec:intercept-url pattern="/earth/**" access="permitAll"/>
<sec:intercept-url pattern="/earth/kml/**" access="permitAll"/>
<sec:intercept-url pattern="/earth/js/**" access="permitAll"/>
<sec:intercept-url pattern="/css/**" access="permitAll"/>   
<sec:intercept-url pattern="/resource*" access="permitAll"/>
<sec:intercept-url pattern="/geom*" access="hasRole('ROLE_SUPERUSER')"/>    
<sec:intercept-url pattern="/status/**" access="permitAll"/>    
<sec:intercept-url pattern="/index.html*" access="hasRole('ROLE_USER')"/>
<sec:intercept-url pattern="/project.html*" access="hasRole('ROLE_USER')"/>
<sec:intercept-url pattern="/js/**" access="hasRole('ROLE_USER')"/>
<sec:intercept-url pattern="/help/**" access="hasRole('ROLE_USER')"/>
<sec:intercept-url pattern="/app/**" access="hasRole('ROLE_USER')"/>
<sec:intercept-url pattern="/data/**" access="hasRole('ROLE_USER')"/>   
<sec:intercept-url pattern="/admin/**" access="hasRole('ROLE_ADMIN')"/> 
<sec:intercept-url pattern="/session/**" access="hasRole('ROLE_USER')"/>
<sec:intercept-url pattern="/" access="hasRole('ROLE_USER')"/>
<sec:intercept-url pattern="/**" access="denyAll"/>
<sec:intercept-url pattern="**" access="denyAll"/>

<sec:session-management session-fixation-protection="none" />

<sec:form-login login-page="/Login.html${dev.gwt.codesrv.htmlparam}" default-target-url="/index.html${dev.gwt.codesrv.htmlparam}" authentication-failure-url="/Login.html${dev.gwt.codesrv.htmlparam}"/>
<sec:http-basic/>
<sec:logout invalidate-session="true" logout-success-url="/Login.html${dev.gwt.codesrv.htmlparam}"/>
 <sec:remember-me key="[REMOVED]" />
 </sec:http>

<bean id="authenticationEventPublisher" class="org.springframework.security.authentication.DefaultAuthenticationEventPublisher" />

<bean id="org.springframework.security.authenticationManager" class="org.springframework.security.authentication.ProviderManager">
    <property name="authenticationEventPublisher" ref="authenticationEventPublisher"/>
    <property name="providers">
        <list>
            <ref bean="authenticationProvider" />
            <ref bean="anonymousProvider" />
        </list>
    </property>
</bean>

<bean id="authenticationProvider" class="org.springframework.security.authentication.dao.DaoAuthenticationProvider">
    <property name="passwordEncoder" ref="passwordEncoder"/>
    <property name="saltSource" ref="saltSource"/>
    <property name="userDetailsService" ref="userService" />
</bean>

<bean id="anonymousProvider" class="org.springframework.security.authentication.AnonymousAuthenticationProvider">
    <property name="key" value="[REMOVED]" />
</bean>

Solution

  • According to the spring docs, "In Spring Security 3, the user is first authenticated by the AuthenticationManager and once they are successfully authenticated, a session is created."

    Instead, you could implement your own AuthenticationSuccessHandler (probably by subclassing SavedRequestAwareAuthenticationSuccessHandler). You can put whatever logic you want in the onAuthenticationSuccess method, so move your existing logic there:

    public class MyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
        // declare and initialize lock and sessionAuthMap at some point...
        @Override
        public void onAuthenticationSuccess(HttpServletRequest request, 
                HttpServletResponse response, Authentication authentication) 
                throws ServletException, IOException {
            lock.writeLock().lock();
            try {
                sessionAuthMap.put(request.getSession().getId(), authentication);
            } finally {
                lock.writeLock().unlock();
            }
            super.onAuthenticationSuccess(request, response, authentication);
        }
    }
    

    Then, update your configs so that Spring Security invokes this class during the authentication process. Here's how:

    Step 1: customize the UsernamePasswordAuthenticationFilter which is created by the <form-login> element. In particular, put this into your <http> element:

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

    Step 2: define myFilter, and hook MyAuthenticationSuccessHandler into it.

    <bean id="myFilter" 
        class="org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter">
        <property name="authenticationManager" ref="authenticationManager" />
        <property name="authenticationFailureHandler" ref="myAuthenticationSuccessHandler" />
        <property name="authenticationSuccessHandler" ref="myAuthenticationFailureHandler" />
    </bean>
    
    <bean id="myAuthenticationSuccessHandler" 
        class="my.MyAuthenticationSuccessHandler">
    <!-- set properties here -->
    </bean>
    
    <!-- you can subclass this or one of its parents, too -->
    <bean id="myAuthenticationFailureHandler" 
        class="org.springframework.security.web.authentication.ExceptionMappingAuthenticationFailureHandler">
        <!-- set properties such as exceptionMappings here -->
    </bean>
    

    For more details, see http://static.springsource.org/spring-security/site/docs/3.0.x/reference/ns-config.html. Also see the AbstractAuthenticationProcessingFilter docs.

    BTW your problem reminds me of OAuth. Essentially you're issuing an access token to the client as a result of the resource owner authorization.