diff --git a/src/main/java/org/springframework/session/data/mongo/JacksonMongoSessionConverter.java b/src/main/java/org/springframework/session/data/mongo/JacksonMongoSessionConverter.java index eb96b0b..5ac731f 100644 --- a/src/main/java/org/springframework/session/data/mongo/JacksonMongoSessionConverter.java +++ b/src/main/java/org/springframework/session/data/mongo/JacksonMongoSessionConverter.java @@ -20,6 +20,8 @@ import java.util.Collections; import java.util.Date; import java.util.HashMap; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.bson.Document; @@ -126,6 +128,7 @@ public class JacksonMongoSessionConverter extends AbstractMongoSessionConverter protected MongoSession convert(Document source) { Date expireAt = (Date) source.remove(EXPIRE_AT_FIELD_NAME); + source.remove("originalSessionId"); String json = source.toJson(JsonWriterSettings.builder().outputMode(JsonMode.RELAXED).build()); try { @@ -142,7 +145,10 @@ public class JacksonMongoSessionConverter extends AbstractMongoSessionConverter * Used to whitelist {@link MongoSession} for {@link SecurityJackson2Modules}. */ private static class MongoSessionMixin { - // Nothing special + @JsonCreator + public MongoSessionMixin(@JsonProperty("_id") String id, + @JsonProperty("intervalSeconds") long maxInactiveIntervalInSeconds) { + } } /** diff --git a/src/test/java/org/springframework/session/data/mongo/integration/MongoDbDeleteJacksonSessionVerificationTest.java b/src/test/java/org/springframework/session/data/mongo/integration/MongoDbDeleteJacksonSessionVerificationTest.java new file mode 100644 index 0000000..4fba99d --- /dev/null +++ b/src/test/java/org/springframework/session/data/mongo/integration/MongoDbDeleteJacksonSessionVerificationTest.java @@ -0,0 +1,192 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.session.data.mongo.integration; + +import com.mongodb.reactivestreams.client.MongoClient; +import com.mongodb.reactivestreams.client.MongoClients; +import de.flapdoodle.embed.mongo.MongodExecutable; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.mongodb.core.ReactiveMongoOperations; +import org.springframework.data.mongodb.core.ReactiveMongoTemplate; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; +import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.core.userdetails.MapReactiveUserDetailsService; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.web.server.SecurityWebFilterChain; +import org.springframework.session.data.mongo.AbstractMongoSessionConverter; +import org.springframework.session.data.mongo.JacksonMongoSessionConverter; +import org.springframework.session.data.mongo.config.annotation.web.reactive.EnableMongoWebSession; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.web.reactive.server.FluxExchangeResult; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.util.SocketUtils; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.reactive.config.EnableWebFlux; +import org.springframework.web.reactive.function.BodyInserters; +import reactor.test.StepVerifier; + +import java.io.IOException; +import java.net.URI; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +/** + * @author Boris Finkelshteyn + */ +@ExtendWith(SpringExtension.class) +@ContextConfiguration +public class MongoDbDeleteJacksonSessionVerificationTest { + + @Autowired ApplicationContext ctx; + + WebTestClient client; + + @BeforeEach + void setUp() { + this.client = WebTestClient.bindToApplicationContext(this.ctx).build(); + } + + @Test + void logoutShouldDeleteOldSessionFromMongoDB() { + + // 1. Login and capture the SESSION cookie value. + + FluxExchangeResult loginResult = this.client.post().uri("/login") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) // + .body(BodyInserters // + .fromFormData("username", "admin") // + .with("password", "password")) // + .exchange() // + .returnResult(String.class); + + assertThat(loginResult.getResponseHeaders().getLocation()).isEqualTo(URI.create("/")); + + String originalSessionId = loginResult.getResponseCookies().getFirst("SESSION").getValue(); + + // 2. Fetch a protected resource using the SESSION cookie. + + this.client.get().uri("/hello") // + .cookie("SESSION", originalSessionId) // + .exchange() // + .expectStatus().isOk() // + .returnResult(String.class).getResponseBody() // + .as(StepVerifier::create) // + .expectNext("HelloWorld") // + .verifyComplete(); + + // 3. Logout using the SESSION cookie, and capture the new SESSION cookie. + + String newSessionId = this.client.post().uri("/logout") // + .cookie("SESSION", originalSessionId) // + .exchange() // + .expectStatus().isFound() // + .returnResult(String.class) + .getResponseCookies().getFirst("SESSION").getValue(); + + assertThat(newSessionId).isNotEqualTo(originalSessionId); + + // 4. Verify the new SESSION cookie is not yet authorized. + + this.client.get().uri("/hello") // + .cookie("SESSION", newSessionId) // + .exchange() // + .expectStatus().isFound() // + .expectHeader().value(HttpHeaders.LOCATION, value -> assertThat(value).isEqualTo("/login")); + + // 5. Verify the original SESSION cookie no longer works. + + this.client.get().uri("/hello") // + .cookie("SESSION", originalSessionId) // + .exchange() // + .expectStatus().isFound() // + .expectHeader().value(HttpHeaders.LOCATION, value -> assertThat(value).isEqualTo("/login")); + } + + @RestController + static class TestController { + + @GetMapping("/hello") + public ResponseEntity hello() { + return ResponseEntity.ok("HelloWorld"); + } + + } + + @EnableWebFluxSecurity + static class SecurityConfig { + @Bean + public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { + return http // + .logout()// + /**/.and() // + .formLogin() // + /**/.and() // + .csrf().disable() // + .authorizeExchange() // + .anyExchange().authenticated() // + /**/.and() // + .build(); + } + + @Bean + public MapReactiveUserDetailsService userDetailsService() { + return new MapReactiveUserDetailsService(User.withDefaultPasswordEncoder() // + .username("admin") // + .password("password") // + .roles("USER,ADMIN") // + .build()); + } + + @Bean + public AbstractMongoSessionConverter mongoSessionConverter() { + return new JacksonMongoSessionConverter(); + } + } + + @Configuration + @EnableWebFlux + @EnableMongoWebSession + static class Config { + private int embeddedMongoPort = SocketUtils.findAvailableTcpPort(); + + @Bean(initMethod = "start", destroyMethod = "stop") + public MongodExecutable embeddedMongoServer() throws IOException { + return MongoITestUtils.embeddedMongoServer(this.embeddedMongoPort); + } + + @Bean + public ReactiveMongoOperations mongoOperations(MongodExecutable embeddedMongoServer) { + MongoClient mongo = MongoClients.create("mongodb://localhost:" + this.embeddedMongoPort); + return new ReactiveMongoTemplate(mongo, "DB_Name_DeleteJacksonSessionVerificationTest"); + } + + @Bean + TestController controller() { + return new TestController(); + } + } +}