Skip to content

Commit

Permalink
Merge branch 'feature/stateless_csrf_upon_login' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
fhanik committed Aug 7, 2015
2 parents 8a3c0d9 + bdb1a39 commit a54f3fb
Show file tree
Hide file tree
Showing 25 changed files with 719 additions and 212 deletions.

This file was deleted.

@@ -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;
}
}
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
@@ -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());
}

}
72 changes: 66 additions & 6 deletions login/src/main/resources/login-ui.xml
Expand Up @@ -124,16 +124,76 @@
<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="uiAuthorizeRequestMatcher" class="org.springframework.security.web.util.matcher.AntPathRequestMatcher">
<constructor-arg value="/oauth/authorize**" />
</bean>

<bean id="uiRequestMatcher" class="org.springframework.security.web.util.matcher.OrRequestMatcher">
<constructor-arg>
<list>
<bean class="org.springframework.security.web.util.matcher.AntPathRequestMatcher">
<constructor-arg value="/" />
</bean>
<bean class="org.springframework.security.web.util.matcher.AntPathRequestMatcher">
<constructor-arg value="/oauth/**" />
</bean>
<bean class="org.springframework.security.web.util.matcher.AntPathRequestMatcher">
<constructor-arg value="/login**" />
</bean>
<bean class="org.springframework.security.web.util.matcher.AntPathRequestMatcher">
<constructor-arg value="/logout.do**" />
</bean>
</list>
</constructor-arg>
</bean>



<bean id="loginCookieCsrfRepository" class="org.cloudfoundry.identity.uaa.web.CookieBasedCsrfTokenRepository"/>
<bean id="logoutFilter" class="org.springframework.security.web.authentication.logout.LogoutFilter">
<constructor-arg index="0" ref="logoutHandler"/>
<constructor-arg index="1">
<util:list>
<bean class="org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler"/>
<bean class="org.springframework.security.web.csrf.CsrfLogoutHandler">
<constructor-arg ref="loginCookieCsrfRepository"/>
</bean>
<bean class="org.springframework.security.web.authentication.logout.CookieClearingLogoutHandler">
<constructor-arg index="0">
<util:list><value>JSESSIONID</value></util:list>
</constructor-arg>
</bean>
</util:list>
</constructor-arg>
<property name="logoutRequestMatcher" ref="uiLogoutRequestMatcher"/>
</bean>
<bean id="savedRequestCache" class="org.springframework.security.web.savedrequest.HttpSessionRequestCache">
<property name="requestMatcher" ref="uiAuthorizeRequestMatcher"/>
</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"
authentication-details-source-ref="authenticationDetailsSource"/>
<logout logout-url="/logout.do" success-handler-ref="logoutHandler" />
<csrf disabled="true"/>
<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"
default-target-url="/"/>
<!--<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"/>
<request-cache ref="savedRequestCache"/>
</http>


Expand Down

0 comments on commit a54f3fb

Please sign in to comment.