Clear sessions on password reset (#9955)

Signed-off-by: Shrikant Sharat Kandula <shrikant@appsmith.com>

Co-authored-by: Arpit Mohan <mohanarpit@users.noreply.github.com>
Co-authored-by: Nayan <nayan@appsmith.com>
This commit is contained in:
Shrikant Sharat Kandula 2022-01-28 16:49:00 +05:30 committed by GitHub
parent cc3ab115b5
commit d3d1f8bbf9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 62 additions and 2 deletions

View File

@ -9,7 +9,9 @@ import org.springframework.data.redis.core.ReactiveRedisOperations;
import org.springframework.data.redis.core.ReactiveRedisTemplate;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.session.data.redis.config.annotation.web.server.EnableRedisWebSession;
@ -42,4 +44,17 @@ public class RedisConfig {
return new ReactiveRedisTemplate<>(factory, context);
}
// Lifted from below and turned it into a bean. Wish Spring provided it as a bean.
// RedisWebSessionConfiguration.createReactiveRedisTemplate
@Bean
ReactiveRedisTemplate<String, Object> reactiveRedisTemplate(ReactiveRedisConnectionFactory factory) {
RedisSerializer<String> keySerializer = new StringRedisSerializer();
RedisSerializer<Object> defaultSerializer = new JdkSerializationRedisSerializer(getClass().getClassLoader());
RedisSerializationContext<String, Object> serializationContext = RedisSerializationContext
.<String, Object>newSerializationContext(defaultSerializer).key(keySerializer).hashKey(keySerializer)
.build();
return new ReactiveRedisTemplate<>(factory, serializationContext);
}
}

View File

@ -3,13 +3,16 @@ package com.appsmith.server.services;
import com.appsmith.server.repositories.UserRepository;
import com.appsmith.server.services.ce.SessionUserServiceCEImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.ReactiveRedisOperations;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class SessionUserServiceImpl extends SessionUserServiceCEImpl implements SessionUserService {
public SessionUserServiceImpl(UserRepository userRepository) {
super(userRepository);
public SessionUserServiceImpl(
UserRepository userRepository,
ReactiveRedisOperations<String, Object> redisOperations) {
super(userRepository, redisOperations);
}
}

View File

@ -10,4 +10,5 @@ public interface SessionUserServiceCE {
Mono<User> refreshCurrentUser(ServerWebExchange exchange);
Mono<Void> logoutAllSessions(String email);
}

View File

@ -3,9 +3,12 @@ package com.appsmith.server.services.ce;
import com.appsmith.server.domains.User;
import com.appsmith.server.exceptions.AppsmithError;
import com.appsmith.server.exceptions.AppsmithException;
import com.appsmith.server.helpers.CollectionUtils;
import com.appsmith.server.repositories.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.ReactiveRedisOperations;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
@ -14,6 +17,7 @@ import org.springframework.security.oauth2.client.authentication.OAuth2Authentic
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebSession;
import reactor.core.publisher.Mono;
import reactor.util.function.Tuple2;
import static org.springframework.security.web.server.context.WebSessionServerSecurityContextRepository.DEFAULT_SPRING_SECURITY_CONTEXT_ATTR_NAME;
@ -22,6 +26,7 @@ import static org.springframework.security.web.server.context.WebSessionServerSe
public class SessionUserServiceCEImpl implements SessionUserServiceCE {
private final UserRepository userRepository;
private final ReactiveRedisOperations<String, Object> redisOperations;
@Override
public Mono<User> getCurrentUser() {
@ -60,4 +65,33 @@ public class SessionUserServiceCEImpl implements SessionUserServiceCE {
});
}
@Override
public Mono<Void> logoutAllSessions(String email) {
// This pattern string comes from calling `ReactiveRedisSessionRepository.getSessionKey("*")` private method.
return redisOperations.keys("spring:session:sessions:*")
.flatMap(key -> Mono.zip(
Mono.just(key),
// The values are maps, containing various pieces of session related information.
// One of them, holds the serialized User object. We want just that.
redisOperations.opsForHash().entries(key)
.filter(e -> e.getValue() != null &&
("sessionAttr:" + DEFAULT_SPRING_SECURITY_CONTEXT_ATTR_NAME).equals(e.getKey())
)
.map(e -> (User) ((SecurityContext) e.getValue()).getAuthentication().getPrincipal())
.next()
))
// Now we have tuples of session keys, and the corresponding user objects.
// Filter the ones we need to clear out.
.filter(tuple -> StringUtils.equalsIgnoreCase(email, tuple.getT2().getEmail()))
.map(Tuple2::getT1)
.collectList()
.flatMap(keys ->
CollectionUtils.isNullOrEmpty(keys)
? Mono.just(0L)
: redisOperations.delete(keys.toArray(String[]::new))
)
.doOnError(error -> log.error("Error clearing user sessions", error))
.then();
}
}

View File

@ -60,6 +60,7 @@ import reactor.core.Exceptions;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Scheduler;
import reactor.core.scheduler.Schedulers;
import javax.validation.Validator;
import java.net.URLEncoder;
@ -368,6 +369,12 @@ public class UserServiceCEImpl extends BaseService<UserRepository, User, String>
)))
.flatMap(passwordResetTokenRepository::delete)
.then(repository.save(userFromDb))
.doOnSuccess(result ->
// In a separate thread, we delete all other sessions of this user.
sessionUserService.logoutAllSessions(userFromDb.getEmail())
.subscribeOn(Schedulers.boundedElastic())
.subscribe()
)
.thenReturn(true);
}));
}