Skip to content

Commit

Permalink
FIXED - #7: Session timeout, Login, POST form resubmission fails
Browse files Browse the repository at this point in the history
- refactor LoginContextServiceImpl.createSavedRequestCookie() based on
https://gist.github.com/dmitrygusev/10573547
- deprecate LoginContextService.saveRequest(String contextPath) as
unnecessary
- enable and implement a few more tests related to saved requests
- add logback as a test dependency and configure it
- change to use jquery provider for tests to get rid of the annoying
javascript errors (htmlunit still complains about use of some selectors)
  • Loading branch information
kaosko committed Oct 8, 2014
1 parent 7bbcd77 commit 8e5384a
Show file tree
Hide file tree
Showing 8 changed files with 172 additions and 112 deletions.
6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@


<dependencies>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.0.3</version>
<scope>test</scope>
</dependency>
<!-- slightly annoyingly, we need to use the jetty originated servlet-api because the jars are signed -->
<dependency>
<groupId>org.eclipse.jetty.orbit</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import java.io.IOException;
import java.util.List;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.shiro.util.StringUtils;
Expand All @@ -18,15 +17,16 @@
public class SecurityExceptionHandlerAssistant implements ExceptionHandlerAssistant {
private final SecurityService securityService;
private final LoginContextService loginContextService;
private final HttpServletRequest servletRequest;
private final Response response;
private final PageResponseRenderer renderer;
private final RequestPageCache pageCache;
public SecurityExceptionHandlerAssistant(final SecurityService securityService, final LoginContextService pageService, final RequestPageCache pageCache, final HttpServletRequest servletRequest, final Response response, final PageResponseRenderer renderer) {

public SecurityExceptionHandlerAssistant(final SecurityService securityService,
final LoginContextService pageService, final RequestPageCache pageCache, final Response response,
final PageResponseRenderer renderer) {
this.securityService =securityService;
this.loginContextService = pageService;
this.pageCache = pageCache;
this.servletRequest = servletRequest;
this.response = response;
this.renderer = renderer;
}
Expand All @@ -42,8 +42,6 @@ public Object handleRequestException(Throwable exception, List<Object> exception
return null;
}

String contextPath = servletRequest.getContextPath();
if ("".equals(contextPath)) contextPath = "/";
loginContextService.saveRequest();
return loginContextService.getLoginPage();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ public interface LoginContextService {

void saveRequest();

@Deprecated
// to be removed in 0.7
void saveRequest(String contextPath);

void redirectToSavedRequest(String fallbackUrl) throws IOException;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,51 @@
import javax.servlet.http.HttpServletResponse;

import org.apache.shiro.web.util.WebUtils;
import org.apache.tapestry5.EventContext;
import org.apache.tapestry5.Link;
import org.apache.tapestry5.SymbolConstants;
import org.apache.tapestry5.internal.services.LinkSource;
import org.apache.tapestry5.internal.services.RequestImpl;
import org.apache.tapestry5.internal.services.ResponseImpl;
import org.apache.tapestry5.internal.services.TapestrySessionFactory;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.ioc.annotations.Symbol;
import org.apache.tapestry5.services.ComponentEventLinkEncoder;
import org.apache.tapestry5.services.ComponentEventRequestParameters;
import org.apache.tapestry5.services.LocalizationSetter;
import org.apache.tapestry5.services.Request;
import org.apache.tapestry5.services.RequestGlobals;
import org.tynamo.security.SecuritySymbols;
import org.tynamo.security.internal.services.LoginContextService;

public class LoginContextServiceImpl implements LoginContextService {

private String loginPage;
private String defaultSuccessPage;
private String unauthorizedPage;
private final HttpServletRequest request;
private final HttpServletResponse response;
protected final String loginPage;
protected final String defaultSuccessPage;
protected final String unauthorizedPage;
protected final HttpServletRequest servletRequest;
protected final HttpServletResponse servletResponse;
protected final ComponentEventLinkEncoder linkEncoder;
protected final TapestrySessionFactory sessionFactory;
protected final RequestGlobals requestGlobals;
protected final String requestEncoding;
private final LinkSource linkSource;
private final LocalizationSetter localizationSetter;

public LoginContextServiceImpl(@Inject @Symbol(SecuritySymbols.SUCCESS_URL) String successUrl,
@Inject @Symbol(SecuritySymbols.LOGIN_URL) String loginUrl,
@Inject @Symbol(SecuritySymbols.UNAUTHORIZED_URL) String unauthorizedUrl, HttpServletRequest request,
HttpServletResponse response,
LocalizationSetter localizationSetter) {
this.request = request;
this.response = response;
@Inject @Symbol(SecuritySymbols.UNAUTHORIZED_URL) String unauthorizedUrl,
@Inject @Symbol(SymbolConstants.CHARSET) String requestEncoding, HttpServletRequest serlvetRequest,
HttpServletResponse servletResponse, LocalizationSetter localizationSetter, LinkSource linkSource,
ComponentEventLinkEncoder linkEncoder, TapestrySessionFactory sessionFactory, RequestGlobals requestGlobals) {
this.servletRequest = serlvetRequest;
this.servletResponse = servletResponse;
this.linkSource = linkSource;
this.linkEncoder = linkEncoder;
this.sessionFactory = sessionFactory;
this.requestGlobals = requestGlobals;
this.localizationSetter = localizationSetter;
this.requestEncoding = requestEncoding;
this.loginPage = urlToPage(loginUrl);
this.defaultSuccessPage = urlToPage(successUrl);
this.unauthorizedPage = urlToPage(unauthorizedUrl);
Expand Down Expand Up @@ -59,7 +81,7 @@ private static String urlToPage(String url) {

@Override
public String getLocalelessPathWithinApplication() {
String path = WebUtils.getPathWithinApplication(request);
String path = WebUtils.getPathWithinApplication(servletRequest);
String locale = getLocaleFromPath(path);
return locale == null ? path : path.substring(locale.length() + 1);
}
Expand All @@ -78,42 +100,82 @@ public String getLocaleFromPath(String path) {
return null;
}

// In 0.7 I plan to remove the contextPath param and make this operation protected for easy overriding
private Cookie createSavedRequestCookie(String contextPath) {
String requestUri = WebUtils.getPathWithinApplication(request);
if (request.getQueryString() != null) requestUri += "?" + request.getQueryString();
Cookie cookie = new Cookie(WebUtils.SAVED_REQUEST_KEY, requestUri);
String requestUri;

// create a T5 request wrapper so we can take advantage of T5'S link decoding services.
// The security filter intentionally runs as part of the HttpServletRequest chain, i.e. before T5 request and response wrappers
// we also need to create the response and store them to requestGlobals because LinkSource uses the request object
final Request request = new RequestImpl(servletRequest, requestEncoding, sessionFactory);
requestGlobals.storeRequestResponse(request, new ResponseImpl(servletRequest, servletResponse));
Cookie cookie = new Cookie(WebUtils.SAVED_REQUEST_KEY, "");
cookie.setPath(contextPath);

if (!"GET".equalsIgnoreCase(servletRequest.getMethod())) {
// POST request? => Redirect to target page via HTTP GET?
ComponentEventRequestParameters eventParameters = linkEncoder.decodeComponentEventRequest(request);
// Event URL => Redirect to target page via HTTP GET
if (eventParameters != null) requestUri = createPageRenderLink(eventParameters);
else {
// REST API call? => Don't do redirects but set a delete cookie
cookie.setMaxAge(0);
return cookie;
}
} else {
ComponentEventRequestParameters eventParameters = linkEncoder.decodeComponentEventRequest(request);

// Event URL => Redirect to target page via HTTP GET
if (eventParameters != null) requestUri = createPageRenderLink(eventParameters);
else {
// Page render request? => Keep the same URL
requestUri = WebUtils.getPathWithinApplication(servletRequest);
if (servletRequest.getQueryString() != null) requestUri += "?" + servletRequest.getQueryString();
}
}

cookie.setValue(requestUri);
return cookie;
}

private String createPageRenderLink(ComponentEventRequestParameters eventParameters) {
EventContext eventContext = eventParameters.getPageActivationContext();
Link link = linkSource.createPageRenderLink(eventParameters.getActivePageName(), true,
(Object[]) eventContext.toStrings());
return link.toRedirectURI();
}

private String getContextPath() {
String contextPath = request.getContextPath();
String contextPath = servletRequest.getContextPath();
if ("".equals(contextPath)) contextPath = "/";
return contextPath;
}

@Override
public void saveRequest() {
saveRequest(getContextPath());
servletResponse.addCookie(createSavedRequestCookie(getContextPath()));
}

@Override
@Deprecated
public void saveRequest(String contextPath) {
response.addCookie(createSavedRequestCookie(contextPath));
servletResponse.addCookie(createSavedRequestCookie(contextPath));
}

@Override
public void redirectToSavedRequest(String fallbackUrl) throws IOException {
Cookie[] cookies = request.getCookies();
public void redirectToSavedRequest(String fallbackUrl) throws IOException {
Cookie[] cookies = servletRequest.getCookies();

String requestUri = null;
if (cookies != null) for (Cookie cookie : cookies) if (WebUtils.SAVED_REQUEST_KEY.equals(cookie.getName())) {
requestUri = cookie.getValue();
Cookie deleteCookie = createSavedRequestCookie(getContextPath());
deleteCookie.setMaxAge(0);
response.addCookie(deleteCookie);
if (cookies != null) for (Cookie cookie : cookies)
if (WebUtils.SAVED_REQUEST_KEY.equals(cookie.getName())) {
requestUri = cookie.getValue();
// delete cookie
cookie.setMaxAge(0);
servletResponse.addCookie(cookie);
break;
}
if (requestUri == null) requestUri = fallbackUrl;
WebUtils.issueRedirect(request, response, requestUri);
}
WebUtils.issueRedirect(servletRequest, servletResponse, requestUri);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import com.gargoylesoftware.htmlunit.html.HtmlElement;
import com.gargoylesoftware.htmlunit.html.HtmlInput;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
import com.gargoylesoftware.htmlunit.util.Cookie;

public class TapestrySecurityIntegrationTest extends AbstractContainerTest
{
Expand Down Expand Up @@ -105,11 +106,21 @@ public void testInterceptComponentMethodDeny() throws Exception
@Test(groups = {"notLoggedIn"})
public void testInterceptComponentMethodWithAjaxDeny() throws Exception
{
CookieManager cookieManager = webClient.getCookieManager();
cookieManager.clearCookies();
clickOnBasePage("componentMethodInterceptorWithAjax");
// this executes window.location.replace so we have to wait for it. is there an event we could listen instead?
webClient.waitForBackgroundJavaScript(500);
page = (HtmlPage) webClient.getCurrentWindow().getEnclosedPage();
assertLoginPage();

for (Cookie cookie : cookieManager.getCookies())
if (cookie.getName().equals("shiroSavedRequest")) {
// test we've stored a page render request, not the component event request. Could also just test for existence of a dot
assertEquals(cookie.getPath() + "/", APP_CONTEXT);
return;
}
fail();
}

@Test(groups = {"loggedIn"}, dependsOnMethods = {"testLogin"})
Expand Down Expand Up @@ -204,19 +215,20 @@ public void testUserFilterWithAjaxDeny() throws Exception
HtmlPage indexPage = webClient.getPage(BASEURI);
indexPage.getHtmlElementById("tynamoLogoutLink").click();

// Clicking on this link should make an ajax request, but HTMLUnit doesn't like it and sends a non-ajax request
// Clicking on this link should make an ajax request, but HTMLUnit doesn't like it and sends a non-ajax request
HtmlElement ajaxLink = (HtmlElement) page.getElementById("ajaxLink");
URL ajaxUrl = new URL(APP_HOST_PORT + ajaxLink.getAttribute("href"));
WebRequest ajaxRequest = new WebRequest(ajaxUrl);
ajaxRequest.setAdditionalHeader("X-Requested-With", "XMLHttpRequest");

Page jsonLoginResponse = webClient.getPage(ajaxRequest);
String ajaxLoginResp = jsonLoginResponse.getWebResponse().getContentAsString();
JSONObject jsonResp = new JSONObject(ajaxLoginResp);
String ajaxRedirectUrl = jsonResp.getJSONObject("_tapestry").getString("redirectURL");
assertTrue(ajaxRedirectUrl.contains(APP_CONTEXT), "The ajax redirect response '" + ajaxRedirectUrl + "' did not contain app context '" + APP_CONTEXT+"'");
page = webClient.getPage(APP_HOST_PORT+ajaxRedirectUrl);
assertLoginPage();
URL ajaxUrl = new URL(APP_HOST_PORT + ajaxLink.getAttribute("href"));
WebRequest ajaxRequest = new WebRequest(ajaxUrl);
ajaxRequest.setAdditionalHeader("X-Requested-With", "XMLHttpRequest");

Page jsonLoginResponse = webClient.getPage(ajaxRequest);
String ajaxLoginResp = jsonLoginResponse.getWebResponse().getContentAsString();
JSONObject jsonResp = new JSONObject(ajaxLoginResp);
String ajaxRedirectUrl = jsonResp.getJSONObject("_tapestry").getString("redirectURL");
assertTrue(ajaxRedirectUrl.contains(APP_CONTEXT), "The ajax redirect response '" + ajaxRedirectUrl
+ "' did not contain app context '" + APP_CONTEXT + "'");
page = webClient.getPage(APP_HOST_PORT + ajaxRedirectUrl);
assertLoginPage();
}

@Test(groups = {"notLoggedIn"})
Expand Down Expand Up @@ -814,11 +826,12 @@ public void testSaveRequestAnnotationHandler() throws Exception
assertLoginPage();
loginAction();
assertTrue(getLocation().startsWith(BASEURI + "about"), "Request wasn't redirected to the remembered url");
logoutAction();
}

@Test(dependsOnMethods = { "testLogout" })
public void testSaveRequestWithFallbackUri() throws Exception
{
logoutAction();
CookieManager cookieManager = webClient.getCookieManager();
cookieManager.clearCookies();
boolean original = cookieManager.isCookiesEnabled();
Expand All @@ -827,45 +840,46 @@ public void testSaveRequestWithFallbackUri() throws Exception
assertLoginPage();
loginAction();
assertTrue(getLocation().startsWith(BASEURI + "index"), "Request wasn't redirected to the default success url");
// note that all cookies are disabled so we don't have a session either and we are logged out in practice
cookieManager.setCookiesEnabled(original);
}


// the following test *does not* work because of deficiency in htmlunit itself. The request parameters however are saved
// see http://old.nabble.com/Problem-with-WebRequestSettings.getRequestParameters%28%29-td20167941.html
// htmlunit's current implementation only returns what's added with setRequestParameters()
// @Test(dependsOnMethods = {"testLogout"})
// public void testSaveRequestWithParameters() throws Exception
// {
// openPage("about?test=now");
// assertLoginPage();
// loginAction();
// assertTrue(getLocation().startsWith(BASEURI + "about"), "Request wasn't redirected to the remebered url");
// List<NameValuePair> valuePairs = page.getWebResponse().getRequestSettings().getRequestParameters();
// assertTrue(valuePairs.contains(new NameValuePair("test", "now")), "Request parameters weren't remebered");
// }

// @Test(dependsOnMethods = { "testLogout" })
// public void testSaveRequestWithParameters() throws Exception {
// openPage("about?test=now");
// assertLoginPage();
// loginAction();
// assertTrue(getLocation().startsWith(BASEURI + "about"), "Request wasn't redirected to the remebered url");
// List<NameValuePair> valuePairs = page.getWebResponse().getWebRequest().getRequestParameters();
// assertTrue(valuePairs.contains(new NameValuePair("test", "now")), "Request parameters weren't remebered");
// }

@Test(dependsOnMethods = { "testLogout" })
public void testSaveRequestFilter() throws Exception
{
logoutAction();
clickOnBasePage("authcCabinet");
assertLoginPage();
loginAction();
assertTrue(getLocation().startsWith(BASEURI + "authc/cabinet"), "Request wasn't redirected to the remebered url");
}

@Test(dependsOnMethods = {"testSaveRequestAnnotationHandler"})
public void testLoginWithSuccessURLAsContext() throws Exception
{
logoutAction();
clickOnBasePage("partlyAuthcCabinet");
clickOnLinkById("Login");
assertLoginPage();
loginAction();
assertTrue(getLocation().startsWith(BASEURI + "partlyauthc/cabinet"), "Request wasn't redirected to the configured success url");
assertSuccessInvoke();
}

// @Test(dependsOnMethods = {"testSaveRequestAnnotationHandler"})
// public void testLoginWithSuccessURLAsContext() throws Exception
// {
// logoutAction();
// clickOnBasePage("partlyAuthcCabinet");
// clickOnLinkById("Login");
// assertLoginPage();
// loginAction();
// assertTrue(getLocation().startsWith(BASEURI + "partlyauthc/cabinet"), "Request wasn't redirected to the configured success url");
// assertSuccessInvoke();
// }

@Test
public void testPort8180Filter() throws Exception
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,12 @@ public static void contributeApplicationDefaults(MappedConfiguration<String, Str
configuration.add(SymbolConstants.APPLICATION_VERSION, "0.0.1-SNAPSHOT");

configuration.add(SecuritySymbols.SUCCESS_URL, "/index");

// HTMLUnit gets confused with prototype's readAttribute:
// net.sourceforge.htmlunit.corejs.javascript.EcmaError: TypeError: Cannot find function readAttribute in object [object
// HTMLBodyElement]. (http://localhost:8180/test/modules.gz/t5/core/dom.js#137)
// and anyway, jquery is by far the more popular one nowadays
configuration.add(SymbolConstants.JAVASCRIPT_INFRASTRUCTURE_PROVIDER, "jquery");
}


Expand Down
Loading

0 comments on commit 8e5384a

Please sign in to comment.