Catching Remember-Me Authentication Events in Spring Security
Asked Answered
D

2

7

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>
Desireah answered 16/8, 2011 at 14:55 Comment(1)
Post your spring security configs, please.Squabble
S
2

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.

Squabble answered 24/8, 2011 at 15:16 Comment(6)
The reason we're storing the authentication based on the session id in a map is because we're working with the Google Earth plugin, which acts as it's own user agent, and won't pass along any session or cookie info from the browser's session. Since we have to secure a KML document that is being retrieved by GE, we rewrite GE's request URL to contain the user's session ID as a parameter. When the request comes in from GE, we have to grab the Authentication object from the map in order to confirm that the user indeed has access to our KML. If not, we can't send the data back. Make sense?Desireah
P.S. I realize this isn't the MOST secure option, but it's what we have to work with until I can come up with a better way of securing that KML. We can't afford to have someone watch the requests (via firebug or what have you) and pick up that KML's URL, which could then be downloaded if we weren't securing it in this way.Desireah
Makes sense. I believe that the response above still stands.Squabble
It doesn't stand up to this model. I think you're still missing the point of storing the authentication: we need to pass along via Google Earth (a completely different user and client, as far as the server and Spring are concerned) some sort of (secure, temporary) identifier that we can tie back to the user's actual session and authentication object, which resides only on the browser. If you think I'm missing something here, please clarify.Desireah
Instead of watching for Authentication events, move your ID-to-auth caching code (the stuff inside the "event instanceof AuthenticationSuccessEvent" if statement) to a custom AuthenticationSuccessHandler. I described how to implement a custom handler above. Your custom handler will be called after the session is created, so in the custom handler you can generate a temporary token and associate it with the real authentication. I'd make the token usable once and assign it an expiration time.Squabble
Added more details to match your specific security configurations. I haven't run them, so there might be typos, but the general idea should be right.Squabble
R
4

Please read the update at bottom of this post

Have you tried just adding another "else if" based on "event instance of InteractiveAuthenticationSuccessEvent"?

    @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 InteractiveAuthenticationSuccessEvent) {
        InteractiveAuthenticationSuccessEvent authEvent = (InteractiveAuthenticationSuccessEvent)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();
        }
    }
}

UPDATE: Your question is basically, "How can I get one http client (i.e. the Google Earth plugin) to appear authenticated to my site as someone who logged in using another http client (the user's browser)?" Even if you could get that to work, it doesn't seem like a good idea, security-wise. Another interesting question would be, "How can I load KML into the Google Earth plugin other than by having the plugin request the KML file over http?" According to their docs, there is a method, parsekml(), which takes a String containing KML data. So in theory you could load the protected KML data using a JavaScript/AJAX call from the user's browser, which would be compatible with your site's normal security setup, then pass the returned KML to parsekml().

Reube answered 17/8, 2011 at 1:47 Comment(10)
I have tried that. The problems with this approach is that the sessionId is always null in an InteractiveAuthenticationSuccessEventDesireah
The session id is indeed null when that event is fired. What do you need it for? You may have to jump through some hoops to get the session id, like when you handle the InteractiveAuthenticationSuccessEvent, you set some kind of flag somewhere to indicate that later on you need to get session id somehow in some other code once it's available.Reube
Maybe you could store the username from the InteractiveAuthenticationSuccessEvent, then when ServletRequestHandledEvents come in, check if the username from the incoming event is one that you've stored somewhere, and if you, get the session id from the incoming event and do what you need to do with it.Reube
The problem with storing the username is that we'd be wide open to attacks from persons who know only a valid username and get lucky enough to hit our servers while that user is indeed logged in. I considered using a UUID for the session, but then we're stuck passing around UUIDs and in not much better of a situation than the current one.Desireah
OK, I'm reading your explanations in response to jtoberon now and getting caught up, sorry I didn't see those. I think you and he are on the track to a solution. My suggestion was made without knowing the bigger context of your question and I don't think it's going to apply to your situation. It might help people looking at this thread if you edited your original question to provide the context about the Google Earth plugin, the KML file, and your security requirements for those.Reube
OK, one last comment, took a quick look at the Google Earth JavaScript API. Maybe what you want to do is download the KML file via an Ajax call (which would pass the logged in user's cookie and could be secured by Spring Security the normal way) and then pass the KML data to parsekml()? code.google.com/apis/earth/documentation/kml.html#parsekml Anyway, good luck.Reube
Really last comment for real. ;) If you can't use Ajax and parsekml() this article seems to be about solving the problem you're having: keelypavan.blogspot.com/2011/07/…Reube
Thanks for the info. I added additional information to the OP for clarity.Desireah
Did you look into using a JavaScript call from the browser (which would provide the user's security credentials per usual) to retrieve the KML file then using parsekml() to pass the KML to the Google Earth plugin?Reube
Requesting via AJAX isn't possible because Google Earth handles the NetworkLink calls based on LOD and a lot of other stuff the web client can't know about. If the KML wasn't MASSIVE, we could in theory use this method, but it is, so GE handles how much of the KML to actually display until the user zooms far enough to display more (or less) of the KML's points.Desireah
S
2

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.

Squabble answered 24/8, 2011 at 15:16 Comment(6)
The reason we're storing the authentication based on the session id in a map is because we're working with the Google Earth plugin, which acts as it's own user agent, and won't pass along any session or cookie info from the browser's session. Since we have to secure a KML document that is being retrieved by GE, we rewrite GE's request URL to contain the user's session ID as a parameter. When the request comes in from GE, we have to grab the Authentication object from the map in order to confirm that the user indeed has access to our KML. If not, we can't send the data back. Make sense?Desireah
P.S. I realize this isn't the MOST secure option, but it's what we have to work with until I can come up with a better way of securing that KML. We can't afford to have someone watch the requests (via firebug or what have you) and pick up that KML's URL, which could then be downloaded if we weren't securing it in this way.Desireah
Makes sense. I believe that the response above still stands.Squabble
It doesn't stand up to this model. I think you're still missing the point of storing the authentication: we need to pass along via Google Earth (a completely different user and client, as far as the server and Spring are concerned) some sort of (secure, temporary) identifier that we can tie back to the user's actual session and authentication object, which resides only on the browser. If you think I'm missing something here, please clarify.Desireah
Instead of watching for Authentication events, move your ID-to-auth caching code (the stuff inside the "event instanceof AuthenticationSuccessEvent" if statement) to a custom AuthenticationSuccessHandler. I described how to implement a custom handler above. Your custom handler will be called after the session is created, so in the custom handler you can generate a temporary token and associate it with the real authentication. I'd make the token usable once and assign it an expiration time.Squabble
Added more details to match your specific security configurations. I haven't run them, so there might be typos, but the general idea should be right.Squabble

© 2022 - 2024 — McMap. All rights reserved.