This is an automated email from the ASF dual-hosted git repository. quantranhong1999 pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/james-project.git
commit d432868795d16163ecb6db8f10c92e2cec80d408 Author: Quan Tran <[email protected]> AuthorDate: Thu Jul 2 16:13:14 2026 +0700 JAMES-4195 Add JMAP OIDC token cache abstraction Adds OIDC token value objects, cache configuration, cache interface, Caffeine cache implementation, and cache contract tests. --- server/protocols/jmap/pom.xml | 17 ++ .../main/java/org/apache/james/jmap/oidc/Aud.java | 23 +++ .../james/jmap/oidc/CaffeineOidcTokenCache.java | 96 +++++++++ .../org/apache/james/jmap/oidc/OidcTokenCache.java | 28 +++ .../jmap/oidc/OidcTokenCacheConfiguration.java | 49 +++++ .../main/java/org/apache/james/jmap/oidc/Sid.java | 23 +++ .../java/org/apache/james/jmap/oidc/Token.java | 23 +++ .../java/org/apache/james/jmap/oidc/TokenInfo.java | 42 ++++ .../apache/james/jmap/oidc/TokenInfoResolver.java | 27 +++ .../jmap/oidc/CaffeineOidcTokenCacheTest.java | 44 ++++ .../james/jmap/oidc/OidcTokenCacheContract.java | 221 +++++++++++++++++++++ 11 files changed, 593 insertions(+) diff --git a/server/protocols/jmap/pom.xml b/server/protocols/jmap/pom.xml index 7854c215ff..32f57a72f5 100644 --- a/server/protocols/jmap/pom.xml +++ b/server/protocols/jmap/pom.xml @@ -68,6 +68,10 @@ <artifactId>testing-base</artifactId> <scope>test</scope> </dependency> + <dependency> + <groupId>com.github.ben-manes.caffeine</groupId> + <artifactId>caffeine</artifactId> + </dependency> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> @@ -80,6 +84,10 @@ <groupId>io.micrometer</groupId> <artifactId>micrometer-tracing</artifactId> </dependency> + <dependency> + <groupId>io.projectreactor</groupId> + <artifactId>reactor-core</artifactId> + </dependency> <dependency> <groupId>io.projectreactor.netty</groupId> <artifactId>reactor-netty</artifactId> @@ -93,10 +101,19 @@ <groupId>jakarta.annotation</groupId> <artifactId>jakarta.annotation-api</artifactId> </dependency> + <dependency> + <groupId>org.apache.commons</groupId> + <artifactId>commons-configuration2</artifactId> + </dependency> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpcore</artifactId> </dependency> + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-core</artifactId> + <scope>test</scope> + </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>jcl-over-slf4j</artifactId> diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/oidc/Aud.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/oidc/Aud.java new file mode 100644 index 0000000000..5037dbb143 --- /dev/null +++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/oidc/Aud.java @@ -0,0 +1,23 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you 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 * + * * + * http://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.apache.james.jmap.oidc; + +public record Aud(String value) { +} diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/oidc/CaffeineOidcTokenCache.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/oidc/CaffeineOidcTokenCache.java new file mode 100644 index 0000000000..eea435fc69 --- /dev/null +++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/oidc/CaffeineOidcTokenCache.java @@ -0,0 +1,96 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you 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 * + * * + * http://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.apache.james.jmap.oidc; + +import java.util.List; +import java.util.Optional; + +import jakarta.inject.Inject; + +import org.apache.james.core.Username; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.github.benmanes.caffeine.cache.AsyncCacheLoader; +import com.github.benmanes.caffeine.cache.AsyncLoadingCache; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.RemovalCause; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimaps; +import com.google.common.collect.SetMultimap; + +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; + +public class CaffeineOidcTokenCache implements OidcTokenCache { + + private static final Logger LOGGER = LoggerFactory.getLogger(CaffeineOidcTokenCache.class); + private static final long DEFAULT_TOKEN_CACHE_MAX_SIZE = 10_000; + + private final AsyncLoadingCache<Token, TokenInfo> cacheToken; + private final SetMultimap<Sid, Token> sidToTokens = Multimaps.synchronizedSetMultimap(HashMultimap.create()); + + @Inject + public CaffeineOidcTokenCache(TokenInfoResolver tokenInfoResolver, OidcTokenCacheConfiguration configuration) { + AsyncCacheLoader<Token, TokenInfo> cacheLoader = (token, executor) -> tokenInfoResolver.apply(token) + .map(tokenInfo -> { + tokenInfo.sid().ifPresentOrElse(sidValue -> sidToTokens.put(sidValue, token), + () -> LOGGER.warn("OIDC token of user {} does not have a sid, this will break backchannel logout. Please review OIDC configuration.", tokenInfo.email())); + return tokenInfo; + }) + .subscribeOn(Schedulers.fromExecutor(executor)) + .toFuture(); + + cacheToken = Caffeine.newBuilder() + .expireAfterWrite(configuration.expiration()) + .maximumSize(configuration.tokenCacheMaxSize().orElse(DEFAULT_TOKEN_CACHE_MAX_SIZE)) + .removalListener((Token token, TokenInfo tokenInfo, RemovalCause cause) -> { + if (cause.wasEvicted() && tokenInfo != null) { + tokenInfo.sid().ifPresent(sid -> sidToTokens.remove(sid, token)); + } + }) + .buildAsync(cacheLoader); + } + + @Override + public Mono<Void> invalidate(Sid sid) { + List<Token> snapshot; + synchronized (sidToTokens) { + snapshot = List.copyOf(sidToTokens.get(sid)); + } + return Mono.fromRunnable(() -> cacheToken.synchronous().invalidateAll(snapshot)) + .subscribeOn(Schedulers.boundedElastic()) + .then(Mono.fromRunnable(() -> sidToTokens.removeAll(sid))) + .then(); + } + + @Override + public Mono<TokenInfo> associatedInformation(Token token) { + return Mono.fromFuture(cacheToken.get(token)); + } + + @VisibleForTesting + Optional<Username> getUsernameFromCache(Token token) { + return Optional.ofNullable(cacheToken.synchronous().getIfPresent(token)) + .map(TokenInfo::email) + .map(Username::of); + } +} diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/oidc/OidcTokenCache.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/oidc/OidcTokenCache.java new file mode 100644 index 0000000000..84eb7a7150 --- /dev/null +++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/oidc/OidcTokenCache.java @@ -0,0 +1,28 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you 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 * + * * + * http://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.apache.james.jmap.oidc; + +import reactor.core.publisher.Mono; + +public interface OidcTokenCache { + Mono<Void> invalidate(Sid sid); + + Mono<TokenInfo> associatedInformation(Token token); +} diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/oidc/OidcTokenCacheConfiguration.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/oidc/OidcTokenCacheConfiguration.java new file mode 100644 index 0000000000..cd69435139 --- /dev/null +++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/oidc/OidcTokenCacheConfiguration.java @@ -0,0 +1,49 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you 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 * + * * + * http://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.apache.james.jmap.oidc; + +import java.time.Duration; +import java.util.Optional; + +import org.apache.commons.configuration2.Configuration; +import org.apache.james.util.DurationParser; + +import com.google.common.base.Preconditions; + +public record OidcTokenCacheConfiguration(Duration expiration, Optional<Long> tokenCacheMaxSize) { + + public static final String OIDC_TOKEN_CACHE_EXPIRATION = "oidc.token.cache.expiration"; + public static final String OIDC_TOKEN_CACHE_MAX_SIZE = "oidc.token.cache.maxSize"; + public static final Duration DEFAULT_EXPIRATION = Duration.ofMinutes(5); + + public static final OidcTokenCacheConfiguration DEFAULT = new OidcTokenCacheConfiguration(DEFAULT_EXPIRATION, Optional.empty()); + + public static OidcTokenCacheConfiguration parse(Configuration configuration) { + Optional<Long> tokenCacheMaxSize = Optional.ofNullable(configuration.getString(OIDC_TOKEN_CACHE_MAX_SIZE, null)) + .map(Long::parseLong); + + return new OidcTokenCacheConfiguration(Optional.ofNullable(configuration.getString(OIDC_TOKEN_CACHE_EXPIRATION)) + .map(DurationParser::parse).orElse(DEFAULT_EXPIRATION), tokenCacheMaxSize); + } + + public OidcTokenCacheConfiguration { + tokenCacheMaxSize.ifPresent(maxSize -> Preconditions.checkArgument(maxSize > 0, "maxSize must be greater than 0")); + } +} diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/oidc/Sid.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/oidc/Sid.java new file mode 100644 index 0000000000..06749a746c --- /dev/null +++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/oidc/Sid.java @@ -0,0 +1,23 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you 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 * + * * + * http://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.apache.james.jmap.oidc; + +public record Sid(String value) { +} diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/oidc/Token.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/oidc/Token.java new file mode 100644 index 0000000000..548214d0d7 --- /dev/null +++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/oidc/Token.java @@ -0,0 +1,23 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you 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 * + * * + * http://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.apache.james.jmap.oidc; + +public record Token(String value) { +} diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/oidc/TokenInfo.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/oidc/TokenInfo.java new file mode 100644 index 0000000000..ad2cb8d855 --- /dev/null +++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/oidc/TokenInfo.java @@ -0,0 +1,42 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you 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 * + * * + * http://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.apache.james.jmap.oidc; + +import java.time.Instant; +import java.util.List; +import java.util.Optional; + +import com.google.common.base.MoreObjects; + +public record TokenInfo(String email, Optional<Sid> sid, Instant exp, Optional<List<Aud>> aud) { + + public String asString() { + return MoreObjects.toStringHelper(this) + .add("email", email) + .add("sid", Optional.ofNullable(sid).flatMap(sidValue -> sidValue.map(Sid::value)).orElse(null)) + .add("exp", exp) + .add("aud", aud.map(audList -> audList + .stream() + .map(Aud::value) + .toList()) + .orElse(null)) + .toString(); + } +} diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/oidc/TokenInfoResolver.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/oidc/TokenInfoResolver.java new file mode 100644 index 0000000000..ebb885a5b0 --- /dev/null +++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/oidc/TokenInfoResolver.java @@ -0,0 +1,27 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you 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 * + * * + * http://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.apache.james.jmap.oidc; + +import java.util.function.Function; + +import reactor.core.publisher.Mono; + +public interface TokenInfoResolver extends Function<Token, Mono<TokenInfo>> { +} diff --git a/server/protocols/jmap/src/test/java/org/apache/james/jmap/oidc/CaffeineOidcTokenCacheTest.java b/server/protocols/jmap/src/test/java/org/apache/james/jmap/oidc/CaffeineOidcTokenCacheTest.java new file mode 100644 index 0000000000..12d23c6956 --- /dev/null +++ b/server/protocols/jmap/src/test/java/org/apache/james/jmap/oidc/CaffeineOidcTokenCacheTest.java @@ -0,0 +1,44 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you 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 * + * * + * http://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.apache.james.jmap.oidc; + +import java.util.Optional; + +import org.apache.james.core.Username; +import org.junit.jupiter.api.BeforeEach; + +public class CaffeineOidcTokenCacheTest extends OidcTokenCacheContract { + private CaffeineOidcTokenCache testee; + + @BeforeEach + void setUp() { + testee = new CaffeineOidcTokenCache(tokenInfoResolver, OidcTokenCacheConfiguration.DEFAULT); + } + + @Override + public CaffeineOidcTokenCache testee() { + return testee; + } + + @Override + public Optional<Username> getUsernameFromCache(Token token) { + return testee.getUsernameFromCache(token); + } +} diff --git a/server/protocols/jmap/src/test/java/org/apache/james/jmap/oidc/OidcTokenCacheContract.java b/server/protocols/jmap/src/test/java/org/apache/james/jmap/oidc/OidcTokenCacheContract.java new file mode 100644 index 0000000000..644592371d --- /dev/null +++ b/server/protocols/jmap/src/test/java/org/apache/james/jmap/oidc/OidcTokenCacheContract.java @@ -0,0 +1,221 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you 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 * + * * + * http://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.apache.james.jmap.oidc; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +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.when; + +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import org.apache.james.core.Username; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.google.common.collect.ImmutableList; + +import reactor.core.publisher.Mono; + +public abstract class OidcTokenCacheContract { + protected static final String EMAIL = "[email protected]"; + protected static final String SID_STRING = "sid-1"; + protected static final Sid SID = new Sid(SID_STRING); + protected static final Optional<List<Aud>> AUD = Optional.of(ImmutableList.of(new Aud("james"))); + protected static final Instant EXPIRES_AT = Instant.now().plus(Duration.ofMinutes(1)).truncatedTo(ChronoUnit.SECONDS); + protected static final TokenInfo TOKEN_INFO = new TokenInfo(EMAIL, Optional.of(SID), EXPIRES_AT, AUD); + + protected static final String EMAIL_2 = "[email protected]"; + protected static final String SID_STRING_2 = "sid-2"; + protected static final Sid SID_2 = new Sid(SID_STRING_2); + protected static final TokenInfo TOKEN_INFO_2 = new TokenInfo(EMAIL_2, Optional.of(SID_2), EXPIRES_AT, AUD); + + protected TokenInfoResolver tokenInfoResolver = mock(TokenInfoResolver.class); + + protected Token token; + protected Token token2; + + public abstract OidcTokenCache testee(); + + public abstract Optional<Username> getUsernameFromCache(Token token); + + @BeforeEach + void beforeEach() { + token = newToken(); + token2 = newToken(); + mockTokenInfoResolverSuccess(token, TOKEN_INFO); + } + + @AfterEach + void afterEach() { + reset(tokenInfoResolver); + } + + protected Token newToken() { + return new Token("token-" + UUID.randomUUID()); + } + + @Test + void invalidateShouldRemoveSidFromCache() { + testee().associatedInformation(token).block(); + + assertThat(getUsernameFromCache(token)).contains(Username.of(EMAIL)); + + testee().invalidate(SID).block(); + assertThat(getUsernameFromCache(token)).isEmpty(); + } + + @Test + void invalidateShouldRemoveTokenFromCache() { + testee().associatedInformation(token).block(); + verify(tokenInfoResolver, times(1)).apply(token); + testee().invalidate(SID).block(); + + testee().associatedInformation(token).block(); + verify(tokenInfoResolver, times(2)).apply(token); + } + + @Test + void invalidateShouldRemoveAllTokensForSid() { + TokenInfo tokenInfo1 = new TokenInfo(EMAIL, Optional.of(SID), EXPIRES_AT, AUD); + TokenInfo tokenInfo2 = new TokenInfo(EMAIL_2, Optional.of(SID), EXPIRES_AT, AUD); + + mockTokenInfoResolverSuccess(token, tokenInfo1); + mockTokenInfoResolverSuccess(token2, tokenInfo2); + + testee().associatedInformation(token).block(); + testee().associatedInformation(token2).block(); + + testee().invalidate(SID).block(); + + assertThat(getUsernameFromCache(token)).isEmpty(); + assertThat(getUsernameFromCache(token2)).isEmpty(); + } + + @Test + void invalidateShouldNotThrowWhenSidNotCached() { + assertThatCode(() -> testee().invalidate(new Sid(UUID.randomUUID().toString())).block()) + .doesNotThrowAnyException(); + } + + @Test + void invalidateShouldNotAffectOtherTokens() { + mockTokenInfoResolverSuccess(token, TOKEN_INFO); + mockTokenInfoResolverSuccess(token2, TOKEN_INFO_2); + testee().associatedInformation(token).block(); + testee().associatedInformation(token2).block(); + + verify(tokenInfoResolver, times(1)).apply(token2); + + testee().invalidate(SID).block(); + + testee().associatedInformation(token2).block(); + verify(tokenInfoResolver, times(1)).apply(token2); + } + + @Test + void associatedUsernameShouldReturnUsername() { + assertThat(testee().associatedInformation(token).block().email()) + .isEqualTo(EMAIL); + } + + @Test + void associatedUsernameShouldThrowErrorWhenTokenCouldNotBeResolved() { + Token token = new Token("token-" + UUID.randomUUID()); + mockTokenInfoResolverFailure(token, new RuntimeException("Token not found")); + + assertThatThrownBy(() -> testee().associatedInformation(token).block()) + .hasMessage("Token not found"); + } + + @Test + void associatedUsernameShouldPopulateCache() { + testee().associatedInformation(token).block(); + assertThat(getUsernameFromCache(token)) + .contains(Username.of(EMAIL)); + } + + @Test + void associatedUsernameShouldNotPopulateCacheWhenCacheHit() { + for (int i = 0; i < 5; i++) { + testee().associatedInformation(token).block(); + } + verify(tokenInfoResolver, times(1)).apply(token); + } + + @Test + void associatedUsernameShouldThrowWhenInvalidateRelatedSid() { + Token token = new Token("token-" + UUID.randomUUID()); + mockTokenInfoResolverSuccess(token, TOKEN_INFO); + testee().associatedInformation(token).block(); + testee().invalidate(SID).block(); + + mockTokenInfoResolverFailure(token, new RuntimeException("Token expired")); + + assertThatThrownBy(() -> testee().associatedInformation(token).block()) + .hasMessage("Token expired"); + } + + @Test + void associatedUsernameShouldCachedWhenAbsentSidInTokenInfo() { + Token token = new Token("token-" + UUID.randomUUID()); + mockTokenInfoResolverSuccess(token, new TokenInfo(EMAIL, Optional.empty(), EXPIRES_AT, AUD)); + + for (int i = 0; i < 5; i++) { + testee().associatedInformation(token).block(); + } + verify(tokenInfoResolver, times(1)).apply(token); + assertThat(getUsernameFromCache(token)).contains(Username.of(EMAIL)); + } + + @Test + void associatedUsernameShouldBeCachedWhenAbsentAudInTokenInfo() { + Token token = new Token("token-" + UUID.randomUUID()); + mockTokenInfoResolverSuccess(token, new TokenInfo(EMAIL, Optional.of(SID), EXPIRES_AT, Optional.empty())); + + for (int i = 0; i < 5; i++) { + assertThat(testee().associatedInformation(token).block().aud()) + .isEmpty(); + } + + verify(tokenInfoResolver, times(1)).apply(token); + assertThat(getUsernameFromCache(token)).contains(Username.of(EMAIL)); + } + + public void mockTokenInfoResolverSuccess(Token token, TokenInfo expected) { + when(tokenInfoResolver.apply(token)) + .thenReturn(Mono.just(expected)); + } + + public void mockTokenInfoResolverFailure(Token token, Exception expected) { + when(tokenInfoResolver.apply(token)) + .thenReturn(Mono.error(expected)); + } +} --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
