Skip to content

Commit d89ace2

Browse files
committed
SEC-2002: Added events to notify of session ID change
Session fixation protection, whether by clean new session or migrated session, now publishes an event when a session is migrated or its ID is changed. This enables application developers to keep track of the session ID of a particular authentication from the time the authentication is successful until the time of logout. Previously this was not possible since session migration changed the session ID and there was no way to reliably detect that. Revised changes per Rob Winch's suggestions.
1 parent 743960d commit d89ace2

File tree

4 files changed

+252
-10
lines changed

4 files changed

+252
-10
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Copyright 2002-2013 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.web.authentication.session;
18+
19+
import org.springframework.security.authentication.event.AbstractAuthenticationEvent;
20+
import org.springframework.security.core.Authentication;
21+
import org.springframework.util.Assert;
22+
23+
/**
24+
* Indicates a session ID was changed for the purposes of session fixation protection.
25+
*
26+
* @author Nicholas Williams
27+
* @since 3.2
28+
* @see SessionFixationProtectionStrategy
29+
*/
30+
public class SessionFixationProtectionEvent extends AbstractAuthenticationEvent {
31+
//~ Instance fields ================================================================================================
32+
33+
private final String oldSessionId;
34+
35+
private final String newSessionId;
36+
37+
//~ Constructors ===================================================================================================
38+
39+
/**
40+
* Constructs a new session fixation protection event.
41+
*
42+
* @param authentication The authentication object
43+
* @param oldSessionId The old session ID before it was changed
44+
* @param newSessionId The new session ID after it was changed
45+
*/
46+
public SessionFixationProtectionEvent(Authentication authentication, String oldSessionId, String newSessionId) {
47+
super(authentication);
48+
Assert.hasLength(oldSessionId);
49+
Assert.hasLength(newSessionId);
50+
this.oldSessionId = oldSessionId;
51+
this.newSessionId = newSessionId;
52+
}
53+
54+
//~ Methods ========================================================================================================
55+
56+
/**
57+
* Getter for the session ID before it was changed.
58+
*
59+
* @return the old session ID.
60+
*/
61+
public String getOldSessionId() {
62+
return this.oldSessionId;
63+
}
64+
65+
/**
66+
* Getter for the session ID after it was changed.
67+
*
68+
* @return the new session ID.
69+
*/
70+
public String getNewSessionId() {
71+
return this.newSessionId;
72+
}
73+
}

web/src/main/java/org/springframework/security/web/authentication/session/SessionFixationProtectionStrategy.java

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,37 @@
1+
/*
2+
* Copyright 2002-2013 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
117
package org.springframework.security.web.authentication.session;
218

3-
import org.apache.commons.logging.Log;
4-
import org.apache.commons.logging.LogFactory;
5-
import org.springframework.security.core.Authentication;
6-
import org.springframework.util.Assert;
19+
import java.util.Enumeration;
20+
import java.util.HashMap;
21+
import java.util.List;
22+
import java.util.Map;
723

824
import javax.servlet.http.HttpServletRequest;
925
import javax.servlet.http.HttpServletResponse;
1026
import javax.servlet.http.HttpSession;
11-
import java.util.*;
27+
28+
import org.apache.commons.logging.Log;
29+
import org.apache.commons.logging.LogFactory;
30+
import org.springframework.context.ApplicationEvent;
31+
import org.springframework.context.ApplicationEventPublisher;
32+
import org.springframework.context.ApplicationEventPublisherAware;
33+
import org.springframework.security.core.Authentication;
34+
import org.springframework.util.Assert;
1235

1336
/**
1437
* The default implementation of {@link SessionAuthenticationStrategy}.
@@ -36,9 +59,14 @@
3659
* @author Luke Taylor
3760
* @since 3.0
3861
*/
39-
public class SessionFixationProtectionStrategy implements SessionAuthenticationStrategy {
62+
public class SessionFixationProtectionStrategy implements SessionAuthenticationStrategy, ApplicationEventPublisherAware {
4063
protected final Log logger = LogFactory.getLog(this.getClass());
4164

65+
/**
66+
* Used for publishing events related to session fixation protection, such as {@link SessionFixationProtectionEvent}.
67+
*/
68+
private ApplicationEventPublisher applicationEventPublisher = new NullEventPublisher();
69+
4270
/**
4371
* Indicates that the session attributes of an existing session
4472
* should be migrated to the new session. Defaults to <code>true</code>.
@@ -112,12 +140,19 @@ public void onAuthentication(Authentication authentication, HttpServletRequest r
112140
/**
113141
* Called when the session has been changed and the old attributes have been migrated to the new session.
114142
* Only called if a session existed to start with. Allows subclasses to plug in additional behaviour.
143+
* * <p>
144+
* The default implementation of this method publishes a {@link SessionFixationProtectionEvent} to notify
145+
* the application that the session ID has changed. If you override this method and still wish these events to be
146+
* published, you should call {@code super.onSessionChange()} within your overriding method.
115147
*
116148
* @param originalSessionId the original session identifier
117149
* @param newSession the newly created session
118150
* @param auth the token for the newly authenticated principal
119151
*/
120152
protected void onSessionChange(String originalSessionId, HttpSession newSession, Authentication auth) {
153+
applicationEventPublisher.publishEvent(new SessionFixationProtectionEvent(
154+
auth, originalSessionId, newSession.getId()
155+
));
121156
}
122157

123158
/**
@@ -179,6 +214,10 @@ private HashMap<String, Object> createMigratedAttributeMap(HttpSession session)
179214
return attributesToMigrate;
180215
}
181216

217+
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
218+
this.applicationEventPublisher = applicationEventPublisher;
219+
}
220+
182221
/**
183222
* Defines whether attributes should be migrated to a new session or not. Has no effect if you
184223
* override the {@code extractAttributes} method.
@@ -206,4 +245,8 @@ public void setRetainedAttributes(List<String> retainedAttributes) {
206245
public void setAlwaysCreateSession(boolean alwaysCreateSession) {
207246
this.alwaysCreateSession = alwaysCreateSession;
208247
}
248+
249+
private static final class NullEventPublisher implements ApplicationEventPublisher {
250+
public void publishEvent(ApplicationEvent event) { }
251+
}
209252
}

web/src/test/java/org/springframework/security/web/authentication/session/ConcurrentSessionControlStrategyTests.java

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,36 @@
1+
/*
2+
* Copyright 2002-2013 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
117
package org.springframework.security.web.authentication.session;
218

19+
import static org.junit.Assert.*;
320
import static org.mockito.AdditionalMatchers.not;
421
import static org.mockito.Matchers.anyObject;
522
import static org.mockito.Matchers.anyString;
623
import static org.mockito.Matchers.eq;
7-
import static org.mockito.Mockito.times;
8-
import static org.mockito.Mockito.verify;
24+
import static org.mockito.Mockito.*;
925

1026
import org.junit.Before;
1127
import org.junit.Test;
1228
import org.junit.runner.RunWith;
29+
import org.mockito.ArgumentCaptor;
1330
import org.mockito.Mock;
1431
import org.mockito.runners.MockitoJUnitRunner;
32+
import org.springframework.context.ApplicationEvent;
33+
import org.springframework.context.ApplicationEventPublisher;
1534
import org.springframework.mock.web.MockHttpServletRequest;
1635
import org.springframework.mock.web.MockHttpServletResponse;
1736
import org.springframework.security.core.Authentication;
@@ -60,4 +79,28 @@ public void onAuthenticationChangeSession() {
6079
verify(sessionRegistry,times(0)).removeSessionInformation(anyString());
6180
verify(sessionRegistry).registerNewSession(not(eq(originalSessionId)), anyObject());
6281
}
82+
83+
// SEC-2002
84+
@Test
85+
public void onAuthenticationChangeSessionWithEventPublisher() {
86+
String originalSessionId = request.getSession().getId();
87+
88+
ApplicationEventPublisher eventPublisher = mock(ApplicationEventPublisher.class);
89+
strategy.setApplicationEventPublisher(eventPublisher);
90+
91+
strategy.onAuthentication(authentication, request, response);
92+
93+
verify(sessionRegistry,times(0)).removeSessionInformation(anyString());
94+
verify(sessionRegistry).registerNewSession(not(eq(originalSessionId)), anyObject());
95+
96+
ArgumentCaptor<ApplicationEvent> eventArgumentCaptor = ArgumentCaptor.forClass(ApplicationEvent.class);
97+
verify(eventPublisher).publishEvent(eventArgumentCaptor.capture());
98+
99+
assertNotNull(eventArgumentCaptor.getValue());
100+
assertTrue(eventArgumentCaptor.getValue() instanceof SessionFixationProtectionEvent);
101+
SessionFixationProtectionEvent event = (SessionFixationProtectionEvent)eventArgumentCaptor.getValue();
102+
assertEquals(originalSessionId, event.getOldSessionId());
103+
assertEquals(request.getSession().getId(), event.getNewSessionId());
104+
assertSame(authentication, event.getAuthentication());
105+
}
63106
}

web/src/test/java/org/springframework/security/web/session/DefaultSessionAuthenticationStrategyTests.java

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,36 @@
1+
/*
2+
* Copyright 2002-2013 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
117
package org.springframework.security.web.session;
218

319
import static org.junit.Assert.*;
4-
import static org.mockito.Mockito.mock;
20+
import static org.mockito.Mockito.*;
521

622
import javax.servlet.http.HttpServletRequest;
723
import javax.servlet.http.HttpSession;
824

925
import org.junit.Test;
26+
import org.mockito.ArgumentCaptor;
27+
import org.springframework.context.ApplicationEvent;
28+
import org.springframework.context.ApplicationEventPublisher;
1029
import org.springframework.mock.web.MockHttpServletRequest;
1130
import org.springframework.mock.web.MockHttpServletResponse;
1231
import org.springframework.security.core.Authentication;
32+
import org.springframework.security.web.authentication.session.SessionFixationProtectionEvent;
1333
import org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy;
14-
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
1534

1635
/**
1736
*
@@ -40,6 +59,38 @@ public void newSessionIsCreatedIfSessionAlreadyExists() throws Exception {
4059
assertFalse(sessionId.equals(request.getSession().getId()));
4160
}
4261

62+
// SEC-2002
63+
@Test
64+
public void newSessionIsCreatedIfSessionAlreadyExistsWithEventPublisher() throws Exception {
65+
SessionFixationProtectionStrategy strategy = new SessionFixationProtectionStrategy();
66+
HttpServletRequest request = new MockHttpServletRequest();
67+
HttpSession session = request.getSession();
68+
session.setAttribute("blah", "blah");
69+
session.setAttribute("SPRING_SECURITY_SAVED_REQUEST_KEY", "DefaultSavedRequest");
70+
String oldSessionId = session.getId();
71+
72+
ApplicationEventPublisher eventPublisher = mock(ApplicationEventPublisher.class);
73+
strategy.setApplicationEventPublisher(eventPublisher);
74+
75+
Authentication mockAuthentication = mock(Authentication.class);
76+
77+
strategy.onAuthentication(mockAuthentication, request, new MockHttpServletResponse());
78+
79+
ArgumentCaptor<ApplicationEvent> eventArgumentCaptor = ArgumentCaptor.forClass(ApplicationEvent.class);
80+
verify(eventPublisher).publishEvent(eventArgumentCaptor.capture());
81+
82+
assertFalse(oldSessionId.equals(request.getSession().getId()));
83+
assertNotNull(request.getSession().getAttribute("blah"));
84+
assertNotNull(request.getSession().getAttribute("SPRING_SECURITY_SAVED_REQUEST_KEY"));
85+
86+
assertNotNull(eventArgumentCaptor.getValue());
87+
assertTrue(eventArgumentCaptor.getValue() instanceof SessionFixationProtectionEvent);
88+
SessionFixationProtectionEvent event = (SessionFixationProtectionEvent)eventArgumentCaptor.getValue();
89+
assertEquals(oldSessionId, event.getOldSessionId());
90+
assertEquals(request.getSession().getId(), event.getNewSessionId());
91+
assertSame(mockAuthentication, event.getAuthentication());
92+
}
93+
4394
// See SEC-1077
4495
@Test
4596
public void onlySavedRequestAttributeIsMigratedIfMigrateAttributesIsFalse() throws Exception {
@@ -56,6 +107,38 @@ public void onlySavedRequestAttributeIsMigratedIfMigrateAttributesIsFalse() thro
56107
assertNotNull(request.getSession().getAttribute("SPRING_SECURITY_SAVED_REQUEST_KEY"));
57108
}
58109

110+
// SEC-2002
111+
@Test
112+
public void onlySavedRequestAttributeIsMigratedIfMigrateAttributesIsFalseWithEventPublisher() throws Exception {
113+
SessionFixationProtectionStrategy strategy = new SessionFixationProtectionStrategy();
114+
strategy.setMigrateSessionAttributes(false);
115+
HttpServletRequest request = new MockHttpServletRequest();
116+
HttpSession session = request.getSession();
117+
session.setAttribute("blah", "blah");
118+
session.setAttribute("SPRING_SECURITY_SAVED_REQUEST_KEY", "DefaultSavedRequest");
119+
String oldSessionId = session.getId();
120+
121+
ApplicationEventPublisher eventPublisher = mock(ApplicationEventPublisher.class);
122+
strategy.setApplicationEventPublisher(eventPublisher);
123+
124+
Authentication mockAuthentication = mock(Authentication.class);
125+
126+
strategy.onAuthentication(mockAuthentication, request, new MockHttpServletResponse());
127+
128+
ArgumentCaptor<ApplicationEvent> eventArgumentCaptor = ArgumentCaptor.forClass(ApplicationEvent.class);
129+
verify(eventPublisher).publishEvent(eventArgumentCaptor.capture());
130+
131+
assertNull(request.getSession().getAttribute("blah"));
132+
assertNotNull(request.getSession().getAttribute("SPRING_SECURITY_SAVED_REQUEST_KEY"));
133+
134+
assertNotNull(eventArgumentCaptor.getValue());
135+
assertTrue(eventArgumentCaptor.getValue() instanceof SessionFixationProtectionEvent);
136+
SessionFixationProtectionEvent event = (SessionFixationProtectionEvent)eventArgumentCaptor.getValue();
137+
assertEquals(oldSessionId, event.getOldSessionId());
138+
assertEquals(request.getSession().getId(), event.getNewSessionId());
139+
assertSame(mockAuthentication, event.getAuthentication());
140+
}
141+
59142
@Test
60143
public void sessionIsCreatedIfAlwaysCreateTrue() throws Exception {
61144
SessionFixationProtectionStrategy strategy = new SessionFixationProtectionStrategy();

0 commit comments

Comments
 (0)