Skip to content

Commit

Permalink
Implement cookie based CSRF for login form
Browse files Browse the repository at this point in the history
  • Loading branch information
fhanik committed Aug 7, 2015
1 parent 8a3c0d9 commit 41dba9d
Show file tree
Hide file tree
Showing 24 changed files with 680 additions and 147 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/*
* ******************************************************************************
* * Cloud Foundry
* * Copyright (c) [2009-2015] Pivotal Software, Inc. All Rights Reserved.
* *
* * This product is licensed to you under the Apache License, Version 2.0 (the "License").
* * You may not use this product except in compliance with the License.
* *
* * This product includes a number of subcomponents with
* * separate copyright notices and license terms. Your use of these
* * subcomponents is subject to the terms and conditions of the
* * subcomponent's license, as noted in the LICENSE file.
* ******************************************************************************
*/

package org.cloudfoundry.identity.uaa.web;

import org.springframework.security.oauth2.common.util.RandomValueStringGenerator;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.security.web.csrf.CsrfTokenRepository;
import org.springframework.security.web.csrf.DefaultCsrfToken;

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

public class CookieBasedCsrfTokenRepository implements CsrfTokenRepository {

public static final String DEFAULT_CSRF_HEADER_NAME = "X-CSRF-TOKEN";
public static final String DEFAULT_CSRF_COOKIE_NAME = "X-Uaa-Csrf";
public static final int DEFAULT_COOKIE_MAX_AGE = 300;

private RandomValueStringGenerator generator = new RandomValueStringGenerator(6);
private String parameterName = DEFAULT_CSRF_COOKIE_NAME;
private String headerName = DEFAULT_CSRF_HEADER_NAME;
private int cookieMaxAge = DEFAULT_COOKIE_MAX_AGE;

public int getCookieMaxAge() {
return cookieMaxAge;
}

public void setCookieMaxAge(int cookieMaxAge) {
this.cookieMaxAge = cookieMaxAge;
}

public String getHeaderName() {
return headerName;
}

public void setHeaderName(String headerName) {
this.headerName = headerName;
}

public String getParameterName() {
return parameterName;
}

public void setParameterName(String parameterName) {
this.parameterName = parameterName;
}

public void setGenerator(RandomValueStringGenerator generator) {
this.generator = generator;
}

public RandomValueStringGenerator getGenerator() {
return generator;
}

@Override
public CsrfToken generateToken(HttpServletRequest request) {
String token = generator.generate();
return new DefaultCsrfToken(getHeaderName(), getParameterName(), token);
}

@Override
public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) {
boolean expire = false;
if (token==null) {
token = generateToken(request);
expire = true;
}
Cookie csrfCookie = new Cookie(token.getParameterName(), token.getToken());
csrfCookie.setHttpOnly(true);
if (expire) {
csrfCookie.setMaxAge(0);
} else {
csrfCookie.setMaxAge(getCookieMaxAge());
}
response.addCookie(csrfCookie);
}

@Override
public CsrfToken loadToken(HttpServletRequest request) {
Cookie[] cookies = request.getCookies();
if (cookies!=null) {
for (Cookie cookie : request.getCookies()) {
if (getParameterName().equals(cookie.getName())) {
return new DefaultCsrfToken(getHeaderName(), getParameterName(), cookie.getValue());
}
}
}
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -272,9 +272,11 @@ public ResponseEntity<Void> postForRedirect(String path, HttpHeaders headers, Mu
throw new IllegalStateException("Expected 302 but server returned status code " + exchange.getStatusCode());
}

headers.remove("Cookie");
if (exchange.getHeaders().containsKey("Set-Cookie")) {
String cookie = exchange.getHeaders().getFirst("Set-Cookie");
headers.set("Cookie", cookie);
for (String cookie : exchange.getHeaders().get("Set-Cookie")) {
headers.add("Cookie", cookie);
}
}

String location = exchange.getHeaders().getLocation().toString();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* ******************************************************************************
* * Cloud Foundry
* * Copyright (c) [2009-2015] Pivotal Software, Inc. All Rights Reserved.
* *
* * This product is licensed to you under the Apache License, Version 2.0 (the "License").
* * You may not use this product except in compliance with the License.
* *
* * This product includes a number of subcomponents with
* * separate copyright notices and license terms. Your use of these
* * subcomponents is subject to the terms and conditions of the
* * subcomponent's license, as noted in the LICENSE file.
* ******************************************************************************
*/

package org.cloudfoundry.identity.uaa.web;

import org.junit.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.oauth2.common.util.RandomValueStringGenerator;
import org.springframework.security.web.csrf.CsrfToken;

import javax.servlet.http.Cookie;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;

public class CookieBasedCsrfTokenRepositoryTests {

@Test
public void testGetHeader_and_Parameter_Name() throws Exception {
CookieBasedCsrfTokenRepository repo = new CookieBasedCsrfTokenRepository();
assertEquals(CookieBasedCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME, repo.getParameterName());
repo.setParameterName("testcookie");
assertEquals("testcookie", repo.getParameterName());

assertEquals(CookieBasedCsrfTokenRepository.DEFAULT_CSRF_HEADER_NAME, repo.getHeaderName());
repo.setHeaderName("testheader");
assertEquals("testheader", repo.getHeaderName());

repo.setGenerator(new RandomValueStringGenerator() {
@Override
public String generate() {
return "token-id";
}
});

CsrfToken token = repo.generateToken(new MockHttpServletRequest());
assertEquals("testheader", token.getHeaderName());
assertEquals("testcookie", token.getParameterName());
assertEquals("token-id", token.getToken());
}

@Test
public void testSave_and_Load_Token() throws Exception {
CookieBasedCsrfTokenRepository repo = new CookieBasedCsrfTokenRepository();
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
CsrfToken token = repo.generateToken(request);
repo.saveToken(token, request, response);

Cookie cookie = response.getCookie(token.getParameterName());
assertNotNull(cookie);
assertEquals(token.getToken(), cookie.getValue());
assertEquals(true, cookie.isHttpOnly());

request.setCookies(cookie);

CsrfToken saved = repo.loadToken(request);
assertEquals(token.getToken(), saved.getToken());
assertEquals(token.getHeaderName(), saved.getHeaderName());
assertEquals(token.getParameterName(), saved.getParameterName());
}

}
31 changes: 26 additions & 5 deletions login/src/main/resources/login-ui.xml
Original file line number Diff line number Diff line change
Expand Up @@ -124,16 +124,37 @@
<access-denied-handler ref="loginEntryPoint"/>
</http>

<bean id="uiLoginRequestMatcher" class="org.springframework.security.web.util.matcher.AntPathRequestMatcher">
<constructor-arg value="/login.do" />
</bean>

<bean id="uiLogoutRequestMatcher" class="org.springframework.security.web.util.matcher.AntPathRequestMatcher">
<constructor-arg value="/logout.do" />
</bean>


<bean id="loginCookieCsrfRepository" class="org.cloudfoundry.identity.uaa.web.CookieBasedCsrfTokenRepository"/>
<bean id="logoutFilter" class="org.springframework.security.web.authentication.logout.LogoutFilter">
<constructor-arg ref="logoutHandler"/>
<constructor-arg>
<bean class="org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler"/>
</constructor-arg>
<property name="logoutRequestMatcher" ref="uiLogoutRequestMatcher"/>
</bean>
<http name="uiSecurity" request-matcher-ref="uiRequestMatcher" use-expressions="false"
authentication-manager-ref="zoneAwareAuthzAuthenticationManager" xmlns="http://www.springframework.org/schema/security">
<access-denied-handler error-page="/"/>
<intercept-url pattern="/login**" access="IS_AUTHENTICATED_ANONYMOUSLY" />
<intercept-url pattern="/**" access="IS_AUTHENTICATED_FULLY" />
<form-login login-page="/login" username-parameter="username" password-parameter="password"
login-processing-url="/login.do" authentication-failure-handler-ref="loginAuthenticationFailureHandler"
<form-login login-page="/login"
username-parameter="username"
password-parameter="password"
login-processing-url="/login.do"
authentication-failure-handler-ref="loginAuthenticationFailureHandler"
authentication-details-source-ref="authenticationDetailsSource"/>
<logout logout-url="/logout.do" success-handler-ref="logoutHandler" />
<csrf disabled="true"/>
<!--<logout logout-url="/logout.do" success-handler-ref="logoutHandler" invalidate-session="true"/>-->
<custom-filter ref="logoutFilter" before="LOGOUT_FILTER"/>
<csrf disabled="false" token-repository-ref="loginCookieCsrfRepository" request-matcher-ref="uiLoginRequestMatcher"/>
<access-denied-handler error-page="/login?error=invalid_login_request"/>
</http>


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,13 @@
import static org.junit.Assert.assertTrue;

import java.util.Arrays;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.cloudfoundry.identity.uaa.test.IntegrationTestContextLoader;
import org.cloudfoundry.identity.uaa.test.TestAccountSetup;
import org.cloudfoundry.identity.uaa.test.UaaTestAccounts;
import org.cloudfoundry.identity.uaa.web.CookieBasedCsrfTokenRepository;
import org.junit.Rule;
import org.junit.Test;
import org.springframework.http.HttpHeaders;
Expand Down Expand Up @@ -49,55 +53,65 @@ public class AuthenticationIntegrationTests {
@Test
public void formLoginSucceeds() throws Exception {

ResponseEntity<Void> result;
ResponseEntity<String> result;
String location;
String cookie;

HttpHeaders uaaHeaders = new HttpHeaders();
HttpHeaders appHeaders = new HttpHeaders();
uaaHeaders.setAccept(Arrays.asList(MediaType.TEXT_HTML));
appHeaders.setAccept(Arrays.asList(MediaType.TEXT_HTML));

// *** GET /app/id
result = serverRunning.getForResponse("/id", appHeaders);
result = serverRunning.getForString("/id", appHeaders);
assertEquals(HttpStatus.FOUND, result.getStatusCode());
location = result.getHeaders().getLocation().toString();

cookie = result.getHeaders().getFirst("Set-Cookie");
assertNotNull("Expected cookie in " + result.getHeaders(), cookie);
appHeaders.set("Cookie", cookie);
for (String cookie : result.getHeaders().get("Set-Cookie")) {
assertNotNull("Expected cookie in " + result.getHeaders(), cookie);
appHeaders.add("Cookie", cookie);
}

assertTrue("Wrong location: " + location, location.contains("/oauth/authorize"));
// *** GET /uaa/oauth/authorize
result = serverRunning.getForResponse(location, uaaHeaders);
result = serverRunning.getForString(location, uaaHeaders);
assertEquals(HttpStatus.FOUND, result.getStatusCode());
location = result.getHeaders().getLocation().toString();

cookie = result.getHeaders().getFirst("Set-Cookie");
assertNotNull("Expected cookie in " + result.getHeaders(), cookie);
uaaHeaders.set("Cookie", cookie);
for (String cookie : result.getHeaders().get("Set-Cookie")) {
assertNotNull("Expected cookie in " + result.getHeaders(), cookie);
uaaHeaders.add("Cookie", cookie);
}

assertTrue("Wrong location: " + location, location.contains("/login"));

result = serverRunning.getForString(location, uaaHeaders);
for (String cookie : result.getHeaders().get("Set-Cookie")) {
assertNotNull("Expected cookie in " + result.getHeaders(), cookie);
uaaHeaders.add("Cookie", cookie);
}

location = serverRunning.getAuthServerUrl("/login.do");

MultiValueMap<String, String> formData;
formData = new LinkedMultiValueMap<String, String>();
formData = new LinkedMultiValueMap<>();
formData.add("username", testAccounts.getUserName());
formData.add("password", testAccounts.getPassword());
formData.add(CookieBasedCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME, extractCookieCsrf(result.getBody()));

// *** POST /uaa/login.do
result = serverRunning.postForResponse(location, uaaHeaders, formData);
result = serverRunning.postForString(location, formData, uaaHeaders);

cookie = result.getHeaders().getFirst("Set-Cookie");
assertNotNull("Expected cookie in " + result.getHeaders(), cookie);
uaaHeaders.set("Cookie", cookie);
for (String cookie : result.getHeaders().get("Set-Cookie")) {
assertNotNull("Expected cookie in " + result.getHeaders(), cookie);
uaaHeaders.add("Cookie", cookie);
}

assertEquals(HttpStatus.FOUND, result.getStatusCode());
location = result.getHeaders().getLocation().toString();

assertTrue("Wrong location: " + location, location.contains("/oauth/authorize"));
// *** GET /uaa/oauth/authorize
result = serverRunning.getForResponse(location, uaaHeaders);
result = serverRunning.getForString(location, uaaHeaders);

// If there is no token in place already for this client we get the
// approval page.
Expand All @@ -109,7 +123,7 @@ public void formLoginSucceeds() throws Exception {
formData.add("user_oauth_approval", "true");

// *** POST /uaa/oauth/authorize
result = serverRunning.postForResponse(location, uaaHeaders, formData);
result = serverRunning.postForString(location, formData, uaaHeaders);
}

location = result.getHeaders().getLocation().toString();
Expand All @@ -118,8 +132,19 @@ public void formLoginSucceeds() throws Exception {
assertTrue("Wrong location: " + location, location.contains("/id"));

// *** GET /app/id
result = serverRunning.getForResponse(location, appHeaders);
result = serverRunning.getForString(location, appHeaders);
// System.err.println(result.getHeaders());
assertEquals(HttpStatus.OK, result.getStatusCode());
}

public static String extractCookieCsrf(String body) {
String pattern = "\\<input type=\\\"hidden\\\" name=\\\"X-Uaa-Csrf\\\" value=\\\"(.*?)\\\"";

Pattern linkPattern = Pattern.compile(pattern);
Matcher matcher = linkPattern.matcher(body);
if (matcher.find()) {
return matcher.group(1);
}
return null;
}
}
1 change: 1 addition & 0 deletions uaa/src/main/resources/messages.properties
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ login.password_expired=Your current password has expired. Please reset your pass
login.invalid_idp=The application is not authorized for your account.
login.account_not_verified=Your account is not verified. Please check your email for a verification link.
login.account_locked=Your account has been locked because of too many failed attempts to login.
login.invalid_login_request=Invalid login attempt, request does not meet our security standards, please try again.
new_invite.invalid_email=Please enter a valid email address.
new_invite.existing_user=There is already a user with that username.

Expand Down
2 changes: 1 addition & 1 deletion uaa/src/main/webapp/WEB-INF/spring-servlet.xml
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@
<constructor-arg value="/login**" />
</bean>
<bean class="org.springframework.security.web.util.matcher.AntPathRequestMatcher">
<constructor-arg value="/logout.do*" />
<constructor-arg value="/logout.do**" />
</bean>
</list>
</constructor-arg>
Expand Down
Loading

0 comments on commit 41dba9d

Please sign in to comment.