Skip to content

Commit

Permalink
Upon a password change we will expire all HTTP sessions for that user
Browse files Browse the repository at this point in the history
  • Loading branch information
fhanik committed Aug 5, 2015
1 parent cad9bdd commit 9730cd6
Show file tree
Hide file tree
Showing 13 changed files with 425 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ public class AuthzAuthenticationFilter implements Filter {
* @param methods the methods to set (defaults to POST)
*/
public void setMethods(Set<String> methods) {
this.methods = new HashSet<String>();
this.methods = new HashSet<>();
for (String method : methods) {
this.methods.add(method.toUpperCase());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* ******************************************************************************
* * 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.authentication;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.cloudfoundry.identity.uaa.user.UaaUser;
import org.cloudfoundry.identity.uaa.user.UaaUserDatabase;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;

public class SessionResetFilter extends OncePerRequestFilter {

private static Log logger = LogFactory.getLog(SessionResetFilter.class);

private final RedirectStrategy strategy;
private final String redirectUrl;
private final UaaUserDatabase userDatabase;

public SessionResetFilter(RedirectStrategy strategy, String redirectUrl, UaaUserDatabase userDatabase) {
this.strategy = strategy;
this.redirectUrl = redirectUrl;
this.userDatabase = userDatabase;
}

public String getRedirectUrl() {
return redirectUrl;
}

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
SecurityContext context = SecurityContextHolder.getContext();
if (context!=null && context.getAuthentication()!=null && context.getAuthentication() instanceof UaaAuthentication) {
UaaAuthentication authentication = (UaaAuthentication)context.getAuthentication();
if (authentication.isAuthenticated() &&
Origin.UAA.equals(authentication.getPrincipal().getOrigin()) &&
null != request.getSession(false)) {

boolean redirect = false;
String userId = authentication.getPrincipal().getId();
try {
logger.debug("Evaluating user-id for session reset:"+userId);
UaaUser user = userDatabase.retrieveUserById(userId);
long lastAuthTime = authentication.getAuthenticatedTime();
long passwordModTime = user.getPasswordLastModified().getTime() ;
//if the password has changed after authentication time
if (hasPasswordChangedAfterAuthentication(lastAuthTime, passwordModTime)) {
logger.debug(String.format("Resetting user session for user ID: %s Auth Time: %s Password Change Time: %s",userId, lastAuthTime, passwordModTime));
redirect = true;
}
} catch (UsernameNotFoundException x) {
logger.info("Authenticated user ["+userId+"] was not found in DB.");
redirect = true;
}
if (redirect) {
handleRedirect(request, response);
return;
}
}
}
filterChain.doFilter(request,response);
}

protected boolean hasPasswordChangedAfterAuthentication(long lastAuthTime, long passwordModTime) {
return passwordModTime > lastAuthTime;
}

protected void handleRedirect(HttpServletRequest request, HttpServletResponse response) throws IOException {
HttpSession session = request.getSession(false);
if (session!=null) {
session.invalidate();
}
strategy.sendRedirect(request, response, getRedirectUrl());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
package org.cloudfoundry.identity.uaa.authentication;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
Expand All @@ -30,6 +31,7 @@ public class UaaAuthentication implements Authentication, Serializable {
private UaaPrincipal principal;
private UaaAuthenticationDetails details;
private boolean authenticated;
private long authenticatedTime = -1l;

/**
* Creates a token with the supplied array of authorities.
Expand All @@ -40,15 +42,16 @@ public class UaaAuthentication implements Authentication, Serializable {
public UaaAuthentication(UaaPrincipal principal,
List<? extends GrantedAuthority> authorities,
UaaAuthenticationDetails details) {
this(principal, null, authorities, details, true);
this(principal, null, authorities, details, true, System.currentTimeMillis());
}

@JsonCreator
public UaaAuthentication(@JsonProperty("principal") UaaPrincipal principal,
@JsonProperty("credentials") Object credentials,
@JsonProperty("authorities") List<? extends GrantedAuthority> authorities,
@JsonProperty("details") UaaAuthenticationDetails details,
@JsonProperty("authenticated") boolean authenticated) {
@JsonProperty("authenticated") boolean authenticated,
@JsonProperty(value = "authenticatedTime", defaultValue = "-1") long authenticatedTime) {
if (principal == null || authorities == null) {
throw new IllegalArgumentException("principal and authorities must not be null");
}
Expand All @@ -57,9 +60,15 @@ public UaaAuthentication(@JsonProperty("principal") UaaPrincipal principal,
this.details = details;
this.credentials = credentials;
this.authenticated = authenticated;
this.authenticatedTime = authenticatedTime == 0 ? -1 : authenticatedTime;
}

public long getAuthenticatedTime() {
return authenticatedTime;
}

@Override
@JsonIgnore
public String getName() {
// Should we return the ID for the principal name? (No, because the
// UaaUserDatabase retrieves users by name.)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,11 @@ public Authentication authenticate(Authentication req) throws AuthenticationExce
}
}

Authentication success = new UaaAuthentication(new UaaPrincipal(user),
user.getAuthorities(), (UaaAuthenticationDetails) req.getDetails());
Authentication success = new UaaAuthentication(
new UaaPrincipal(user),
user.getAuthorities(),
(UaaAuthenticationDetails) req.getDetails());

publish(new UserAuthenticationSuccessEvent(user, success));

return success;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ protected void stripScopesFromAuthentication(String identityZoneId, HttpServletR
null,
UaaStringUtils.getAuthoritiesFromStrings(clientScopes),
new UaaAuthenticationDetails(servletRequest),
true);
true, userAuthentication.getAuthenticatedTime());
}
oa = new OAuth2Authentication(request, userAuthentication);
oa.setDetails(oaDetails);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
/*
* ******************************************************************************
* * 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.authentication;

import org.cloudfoundry.identity.uaa.user.InMemoryUaaUserDatabase;
import org.cloudfoundry.identity.uaa.user.UaaUser;
import org.cloudfoundry.identity.uaa.user.UaaUserDatabase;
import org.cloudfoundry.identity.uaa.zone.IdentityZone;
import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.util.ReflectionUtils;

import javax.servlet.FilterChain;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.lang.reflect.Field;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.Map;

import static org.mockito.Matchers.anyBoolean;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;

public class SessionResetFilterTests {

SessionResetFilter filter;
HttpServletResponse response;
HttpServletRequest request;
HttpSession session;
FilterChain chain;
UaaUserDatabase userDatabase;
UaaAuthentication authentication;
Date yesterday;
UaaUser user;
Map<String,UaaUser> users;

@Before
public void setUpFilter() throws Exception {

yesterday = new Date(System.currentTimeMillis()-(1000*60*60*24));

user = new UaaUser(
"user-id",
"username",
"password",
"email",
Collections.EMPTY_LIST,
"given name",
"family name",
yesterday,
yesterday,
Origin.UAA,
null,
true,
IdentityZone.getUaa().getId(),
"salt",
yesterday
);

UaaPrincipal principal = new UaaPrincipal(user);

authentication = new UaaAuthentication(principal, null, Collections.EMPTY_LIST, null, true, System.currentTimeMillis());

users = new HashMap<>();
users.put(user.getId(), user);
userDatabase = new InMemoryUaaUserDatabase(users);

chain = mock(FilterChain.class);
request = mock(HttpServletRequest.class);
response = mock(HttpServletResponse.class);
session = mock(HttpSession.class);
when(request.getSession(anyBoolean())).thenReturn(session);
filter = new SessionResetFilter(new DefaultRedirectStrategy(),"/login", userDatabase);
}

@After
public void clearThingsUp() {
SecurityContextHolder.clearContext();
IdentityZoneHolder.clear();
}


@Test
public void test_No_Authentication_Present() throws Exception {
filter.doFilterInternal(request, response, chain);
verify(chain, times(1)).doFilter(request, response);
}

@Test
public void test_No_UAA_Authentication_Present() throws Exception {
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken("test","test");
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
filter.doFilterInternal(request, response, chain);
verify(chain, times(1)).doFilter(request, response);
verifyZeroInteractions(request);
verifyZeroInteractions(response);
}

@Test
public void test_User_Modified_After_Authentication() throws Exception {
setFieldValue("authenticatedTime", (yesterday.getTime() - (1000 * 60 * 60 * 24)), authentication);
SecurityContextHolder.getContext().setAuthentication(authentication);
filter.doFilterInternal(request, response, chain);

//user is not forwarded, and error response is generated right away
Mockito.verifyZeroInteractions(chain);
//user redirect
verify(response, times(1)).sendRedirect(anyString());
//session was requested
verify(request, times(2)).getSession(false);
//session was invalidated
verify(session, times(1)).invalidate();
}

protected long dropMilliSeconds(long time) {
return ( time / 1000l ) * 1000l;
}

@Test
public void test_User_Not_Modified() throws Exception {
SecurityContextHolder.getContext().setAuthentication(authentication);
filter.doFilterInternal(request, response, chain);
verify(chain, times(1)).doFilter(request, response);
verifyZeroInteractions(response);
}

@Test
public void test_User_Not_Originated_In_Uaa() throws Exception {
SecurityContextHolder.getContext().setAuthentication(authentication);
setFieldValue("origin", Origin.LDAP, authentication.getPrincipal());
filter.doFilterInternal(request, response, chain);
verify(chain, times(1)).doFilter(request, response);
verifyZeroInteractions(request);
verifyZeroInteractions(response);
}

protected void setFieldValue(String fieldname, Object value, Object object) {
Field f = ReflectionUtils.findField(object.getClass(), fieldname);
ReflectionUtils.makeAccessible(f);
ReflectionUtils.setField(f, object, value);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* ******************************************************************************
* * 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.authentication;

import org.cloudfoundry.identity.uaa.util.JsonUtils;
import org.junit.Assert;
import org.junit.Test;

public class UaaAuthenticationSerializationTests {

@Test
public void testDeserializationWithoutAuthenticatedTime() throws Exception {
String data ="{\"principal\":{\"id\":\"user-id\",\"name\":\"username\",\"email\":\"email\",\"origin\":\"uaa\",\"externalId\":null,\"zoneId\":\"uaa\"},\"credentials\":null,\"authorities\":[],\"details\":null,\"authenticated\":true,\"authenticatedTime\":1438649464353,\"name\":\"username\"}";
UaaAuthentication authentication1 = JsonUtils.readValue(data, UaaAuthentication.class);
Assert.assertEquals(1438649464353l, authentication1.getAuthenticatedTime());

String dataWithoutTime ="{\"principal\":{\"id\":\"user-id\",\"name\":\"username\",\"email\":\"email\",\"origin\":\"uaa\",\"externalId\":null,\"zoneId\":\"uaa\"},\"credentials\":null,\"authorities\":[],\"details\":null,\"authenticated\":true,\"name\":\"username\"}";
UaaAuthentication authentication2 = JsonUtils.readValue(dataWithoutTime, UaaAuthentication.class);
Assert.assertEquals(-1, authentication2.getAuthenticatedTime());

}
}
Loading

0 comments on commit 9730cd6

Please sign in to comment.