views:

317

answers:

2

Hey, we are using Spring Security 2.0.4. We have a TransactionTokenBean which generates a unique token each POST, the bean is session scoped. The token is used for the duplicate form submission problem (and security). The TransactionTokenBean is called from a Servlet filter. Our problem is the following, after a session timeout occured, when you do a POST in the application Spring Security redirects to the logon page, saving the original request. After logging on again the TransactionTokenBean is created again, since it is session scoped, but then Spring forwards to the originally accessed url, also sending the token that was generated at that time. Since the TransactionTokenBean is created again, the tokens do not match and our filter throws an Exception. I don't quite know how to handle this elegantly, (or for that matter, I can't even fix it with a hack), any ideas?

This is the code of the TransactionTokenBean:

public class TransactionTokenBean implements Serializable {

public static final int TOKEN_LENGTH = 8;

private RandomizerBean randomizer;

private transient Logger logger;

private String expectedToken;

public String getUniqueToken() {
    return expectedToken;
}

public void init() {
    resetUniqueToken();
}

public final void verifyAndResetUniqueToken(String actualToken) {
    verifyUniqueToken(actualToken);
    resetUniqueToken();
}

public void resetUniqueToken() {
    expectedToken = randomizer.getRandomString(TOKEN_LENGTH, RandomizerBean.ALPHANUMERICS);
    getLogger().debug("reset token to: " + expectedToken);
}

public void verifyUniqueToken(String actualToken) {
    if (getLogger().isDebugEnabled()) {
        getLogger().debug("verifying token.  expected=" + expectedToken + ", actual=" + actualToken);
    }

    if (expectedToken == null || actualToken == null || !isValidToken(actualToken)) {
        throw new IllegalArgumentException("missing or invalid transaction token");
    }

    if (!expectedToken.equals(actualToken)) {
        throw new InvalidTokenException();
    }
}

private boolean isValidToken(String actualToken) {
    return StringUtils.isAlphanumeric(actualToken);
}

public void setRandomizer(RandomizerBean randomizer) {
    this.randomizer = randomizer;
}

private Logger getLogger() {
    if (logger == null) {
        logger = Logger.getLogger(TransactionTokenBean.class);
    }
    return logger;
}

}

and this is the Servlet filter (ignore the Ajax stuff):

public class SecurityFilter implements Filter {

static final String AJAX_TOKEN_PARAM = "ATXTOKEN";
static final String TOKEN_PARAM = "TXTOKEN";

private WebApplicationContext webApplicationContext;

private Logger logger = Logger.getLogger(SecurityFilter.class);

public void init(FilterConfig config) {
    setWebApplicationContext(WebApplicationContextUtils.getWebApplicationContext(config.getServletContext()));
}

public void destroy() {
}

public void doFilter(ServletRequest req, ServletResponse response, FilterChain chain) throws IOException,
        ServletException {

    HttpServletRequest request = (HttpServletRequest) req;


    if (isPostRequest(request)) {
        if (isAjaxRequest(request)) {
            log("verifying token for AJAX request " + request.getRequestURI());
            getTransactionTokenBean(true).verifyUniqueToken(request.getParameter(AJAX_TOKEN_PARAM));
        } else {
            log("verifying and resetting token for non-AJAX request " + request.getRequestURI());
            getTransactionTokenBean(false).verifyAndResetUniqueToken(request.getParameter(TOKEN_PARAM));
        }
    }

    chain.doFilter(request, response);
}

private void log(String line) {
    if (logger.isDebugEnabled()) {
        logger.debug(line);
    }
}

private boolean isPostRequest(HttpServletRequest request) {
    return "POST".equals(request.getMethod().toUpperCase());
}

private boolean isAjaxRequest(HttpServletRequest request) {
    return request.getParameter("AJAXREQUEST") != null;
}

private TransactionTokenBean getTransactionTokenBean(boolean ajax) {
    return (TransactionTokenBean) webApplicationContext.getBean(ajax ? "ajaxTransactionTokenBean"
            : "transactionTokenBean");
}

void setWebApplicationContext(WebApplicationContext context) {
    this.webApplicationContext = context;
}

}

relevant part of web.xml:

<filter>
    <filter-name>SecurityFilter</filter-name>
    <filter-class>
        xxx.common.web.security.SecurityFilter
    </filter-class>
</filter>

<filter-mapping>
    <filter-name>SecurityFilter</filter-name>
    <servlet-name>SpringServlet</servlet-name>
    <dispatcher>REQUEST</dispatcher>
</filter-mapping>

<filter>
    <filter-name>springSecurityFilterChain</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>

<filter-mapping>
    <filter-name>springSecurityFilterChain</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

the TransactionTokenBean:

<bean id="transactionTokenBean" class="xxx.common.web.bean.support.TransactionTokenBean"
    init-method="init" scope="session">
    <property name="randomizer" ref="randomizer" />
</bean>
A: 

Why make the bean session scoped? Sounds more like you want a token that lives forever - even through a new login. This sounds more like a job for a cookie with no timeout.

Gandalf
The servlet resets the token every POST. The client sends the token on every request, that should match with the one last generated by the TransactionTokenBean. Hence session scoped.
dfuse
A: 

Do you want to accept that first POST request or not (since you say the token is intended for security purposes as well as preventing duplicate form submission)? It wouldn't normally be the case that you would want to accept a POST from a previous session when you're using synchronizer tokens, so why not just start the user at a clearly defined URL when they log in (which Spring Security supports)?

If you really want to continue the previous transaction, you can extend Spring Security's AuthenticationProcessingFilter's onSuccessfulAuthentication method and introspect the SavedRequest (stored in the session) to determine the previous token value. You could then initialize your TransactionTokenBean with this value so that it would be accepted on the subsequent request.

The request caching code in Spring Security 3 is a lot more flexible, so if you can upgrade that would be advisable.

Munkymisheen
We do start on a clearly defined URL, and I don't want to let the user continue with his work after logging in again after timeout but that seems to be the default behaviour of the Spring Security. I would prefer to just let the user login again and start on the usual first page. Any idea how to accomplish that?I've already tried to reach the SavedRequest from within the SecurityFilter, but the SavedRequestWrapper doesn't expose it. Extending the AuthenticationProcessingFilter might be an option, I'll try that unless I find out how to not continue with the previous transaction.
dfuse
If you want to start at the same page every time (and ignore the SavedRequest), use the default-target-url and always-use-default-target-url attributes on the namespace form-login element.
Munkymisheen