[java-opensaml] branch main updated: Move OIDC revocation cache into main storage module.
Scott Cantor
cantor.2 at osu.edu
Wed Jan 26 00:37:50 UTC 2022
This is an automated email from the git hooks/post-receive script.
scantor pushed a commit to branch main
in repository java-opensaml.
View the commit online:
http://git.shibboleth.net/view/?p=java-opensaml.git;a=commit;h=428b8dbc2d3bc608aff37eda6444252abe2277e9
The following commit(s) were added to refs/heads/main by this push:
new 428b8dbc2 Move OIDC revocation cache into main storage module.
428b8dbc2 is described below
commit 428b8dbc2d3bc608aff37eda6444252abe2277e9
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Tue Jan 25 19:37:47 2022 -0500
Move OIDC revocation cache into main storage module.
---
.../java/org/opensaml/storage/RevocationCache.java | 224 +++++++++++++++++++++
.../opensaml/storage/impl/RevocationCacheTest.java | 165 +++++++++++++++
2 files changed, 389 insertions(+)
diff --git a/opensaml-storage-api/src/main/java/org/opensaml/storage/RevocationCache.java b/opensaml-storage-api/src/main/java/org/opensaml/storage/RevocationCache.java
new file mode 100644
index 000000000..9da28035c
--- /dev/null
+++ b/opensaml-storage-api/src/main/java/org/opensaml/storage/RevocationCache.java
@@ -0,0 +1,224 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID 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.opensaml.storage;
+
+import java.io.IOException;
+import java.time.Duration;
+import java.time.Instant;
+
+import javax.annotation.Nonnull;
+
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+import net.shibboleth.utilities.java.support.annotation.constraint.Positive;
+import net.shibboleth.utilities.java.support.annotation.constraint.ThreadSafeAfterInit;
+import net.shibboleth.utilities.java.support.component.AbstractIdentifiableInitializableComponent;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
+import org.apache.commons.codec.digest.DigestUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Stores and checks for revocation entries.
+ *
+ * <p>
+ * This class is thread-safe and uses a synchronized method to prevent race conditions within the underlying store
+ * (lacking an atomic "check and insert" operation).
+ * </p>
+ *
+ * @since 4.2.0
+ */
+ at ThreadSafeAfterInit
+public class RevocationCache extends AbstractIdentifiableInitializableComponent {
+
+ /** Logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(RevocationCache.class);
+
+ /** Backing storage for the replay cache. */
+ @NonnullAfterInit private StorageService storage;
+
+ /** Flag controlling behavior on storage failure. */
+ private boolean strict;
+
+ /** Default lifetime of revocation entry. Default value: 6 hours */
+ @Nonnull @Positive private Duration expires;
+
+ /**
+ * Constructor.
+ */
+ public RevocationCache() {
+ expires = Duration.ofHours(6);
+ }
+
+ /**
+ * Set the default revocation entry expiration.
+ *
+ * @param entryExpiration lifetime of an revocation entry in milliseconds
+ */
+ public void setEntryExpiration(@Positive final Duration entryExpiration) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+ Constraint.isTrue(entryExpiration != null && !entryExpiration.isNegative() && !entryExpiration.isZero(),
+ "Revocation cache default entry expiration must be greater than 0");
+ expires = entryExpiration;
+ }
+
+ /**
+ * Get the backing store for the cache.
+ *
+ * @return the backing store.
+ */
+ @NonnullAfterInit public StorageService getStorage() {
+ return storage;
+ }
+
+ /**
+ * Set the backing store for the cache.
+ *
+ * @param storageService backing store to use
+ */
+ public void setStorage(@Nonnull final StorageService storageService) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+ storage = Constraint.isNotNull(storageService, "StorageService cannot be null");
+ final StorageCapabilities caps = storage.getCapabilities();
+ if (caps instanceof StorageCapabilitiesEx) {
+ Constraint.isTrue(((StorageCapabilitiesEx) caps).isServerSide(), "StorageService cannot be client-side");
+ }
+ }
+
+ /**
+ * Get the strictness flag.
+ *
+ * @return true iff we should treat storage failures as a replay
+ */
+ public boolean isStrict() {
+ return strict;
+ }
+
+ /**
+ * Set the strictness flag.
+ *
+ * @param flag true iff we should treat storage failures as a replay
+ */
+ public void setStrict(final boolean flag) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+ strict = flag;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void doInitialize() throws ComponentInitializationException {
+ if (storage == null) {
+ throw new ComponentInitializationException("StorageService cannot be null");
+ }
+ }
+
+
+ /**
+ * Invokes {@link #revoke(String, String, Duration)} with a default expiration parameter.
+ *
+ * @param context a context label to subdivide the cache
+ * @param s value to revoke
+ *
+ * @return true if value has successfully been listed as revoked in the cache.
+ */
+ public synchronized boolean revoke(@Nonnull @NotEmpty final String context, @Nonnull @NotEmpty final String s) {
+ return revoke(context, s, expires);
+ }
+
+ /**
+ * Returns true if the value is successfully revoked. If value has already been revoked, expiration is updated.
+ *
+ * @param context a context label to subdivide the cache
+ * @param s value to revoke
+ * @param exp entry expiration
+ *
+ * @return true if value has successfully been listed as revoked in the cache.
+ */
+ public synchronized boolean revoke(@Nonnull @NotEmpty final String context, @Nonnull @NotEmpty final String s,
+ @Nonnull final Duration exp) {
+ final String key;
+
+ final StorageCapabilities caps = storage.getCapabilities();
+ if (context.length() > caps.getContextSize()) {
+ log.error("context {} too long for StorageService (limit {})", context, caps.getContextSize());
+ return false;
+ } else if (s.length() > caps.getKeySize()) {
+ key = DigestUtils.sha1Hex(s);
+ } else {
+ key = s;
+ }
+ try {
+ final StorageRecord<?> entry = storage.read(context, key);
+ if (entry == null) {
+ log.debug("Entry '{}' of context '{}' is not yet on list of revoked entries,"
+ + " adding to cache with expiration time {}", key, context, expires);
+ storage.create(context, key, "y", Instant.now().plus(exp).toEpochMilli());
+ return true;
+ }
+
+ storage.update(context, key, "y", Instant.now().plus(exp).toEpochMilli());
+ log.debug("Entry '{}' of context '{}' was already revoked, updating expiration", key, context);
+ return true;
+ } catch (final IOException e) {
+ log.error("Exception reading/writing to storage service, returning {}", strict ? "failure" : "success", e);
+ return !strict;
+ }
+ }
+
+ /**
+ * Returns false if the value has successfully been confirmed as not revoked.
+ *
+ * @param context a context label to subdivide the cache
+ * @param s value to revoke
+ *
+ * @return false if the check value is not found in the cache
+ */
+ public synchronized boolean isRevoked(@Nonnull @NotEmpty final String context, @Nonnull @NotEmpty final String s) {
+ final String key;
+ final StorageCapabilities caps = storage.getCapabilities();
+ if (context.length() > caps.getContextSize()) {
+ log.error("context {} too long for StorageService (limit {})", context, caps.getContextSize());
+ return true;
+ } else if (s.length() > caps.getKeySize()) {
+ key = DigestUtils.sha1Hex(s);
+ } else {
+ key = s;
+ }
+
+ try {
+ final StorageRecord<?> entry = storage.read(context, key);
+ if (entry == null) {
+ log.debug("Entry '{}' is not revoked", key);
+ return false;
+ }
+
+ log.debug("Entry '{}' is revoked", s);
+ return true;
+ } catch (final IOException e) {
+ log.error("Exception reading/writing to storage service, returning {}", strict ? "failure" : "success", e);
+ return !strict;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/opensaml-storage-impl/src/test/java/org/opensaml/storage/impl/RevocationCacheTest.java b/opensaml-storage-impl/src/test/java/org/opensaml/storage/impl/RevocationCacheTest.java
new file mode 100644
index 000000000..b2e27fa76
--- /dev/null
+++ b/opensaml-storage-impl/src/test/java/org/opensaml/storage/impl/RevocationCacheTest.java
@@ -0,0 +1,165 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID 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.opensaml.storage.impl;
+
+import java.nio.charset.Charset;
+import java.time.Duration;
+import java.util.Random;
+
+import org.opensaml.storage.impl.client.ClientStorageService;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.Test;
+
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.logic.ConstraintViolationException;
+
+import org.testng.annotations.BeforeMethod;
+import org.testng.Assert;
+import org.opensaml.storage.RevocationCache;
+
+/**
+ * Tests for {@link RevocationCache}
+ */
+public class RevocationCacheTest {
+
+
+ private MemoryStorageService storageService;
+
+ private RevocationCache revocationCache;
+
+ @BeforeMethod
+ protected void setUp() throws Exception {
+
+ storageService = new MemoryStorageService();
+ storageService.setId("test");
+ storageService.initialize();
+
+ revocationCache = new RevocationCache();
+ revocationCache.setEntryExpiration(Duration.ofMillis(500));
+ revocationCache.setStorage(storageService);
+ revocationCache.initialize();
+ }
+
+ @AfterMethod
+ protected void tearDown() {
+ revocationCache.destroy();
+ revocationCache = null;
+
+ storageService.destroy();
+ storageService = null;
+ }
+
+ @Test
+ public void testInit() {
+ revocationCache = new RevocationCache();
+ try {
+ revocationCache.setStorage(null);
+ Assert.fail("Null StorageService should have caused constraint violation");
+ } catch (final Exception e) {
+ }
+
+ try {
+ revocationCache.setStorage(new ClientStorageService());
+
+ Assert.fail("ClientStorageService should have caused constraint violation");
+ } catch (final Exception e) {
+ }
+ }
+
+
+ @Test
+ public void testStrictSetter() throws ComponentInitializationException {
+ Assert.assertFalse(revocationCache.isStrict());
+ revocationCache = new RevocationCache();
+ revocationCache.setStorage(storageService);
+ revocationCache.setStrict(true);
+ revocationCache.initialize();
+ Assert.assertTrue(revocationCache.isStrict());
+ }
+
+ @Test (expectedExceptions = ConstraintViolationException.class)
+ public void testExpirationSetter() throws ComponentInitializationException {
+ //Must be positive
+ revocationCache = new RevocationCache();
+ revocationCache.setEntryExpiration(Duration.ZERO);
+ }
+
+ @Test
+ public void testStorageGetter() throws ComponentInitializationException {
+ Assert.assertEquals(storageService, revocationCache.getStorage());
+ }
+
+ @Test
+ public void testRevocationSuccess() throws ComponentInitializationException {
+ Assert.assertFalse(revocationCache.isRevoked("context", "item"));
+ Assert.assertTrue(revocationCache.revoke("context", "item"));
+ Assert.assertTrue(revocationCache.isRevoked("context", "item"));
+ }
+
+ @Test
+ public void testRevocationSuccessLongContext() throws ComponentInitializationException {
+ storageService = new MemoryStorageService();
+ storageService.setId("test");
+ storageService.setContextSize(50);
+ storageService.initialize();
+
+ revocationCache = new RevocationCache();
+ revocationCache.setStorage(storageService);
+ revocationCache.initialize();
+
+ final byte[] array = new byte[storageService.getCapabilities().getContextSize()*2];
+ new Random().nextBytes(array);
+ final String context = new String(array, Charset.forName("UTF-8"));
+ Assert.assertTrue(context.length()>storageService.getCapabilities().getContextSize());
+ Assert.assertTrue(revocationCache.isRevoked(context, "item"));
+ Assert.assertFalse(revocationCache.revoke(context, "item"));
+ }
+
+ @Test
+ public void testRevocationSuccessLongLongItem() throws ComponentInitializationException {
+ storageService = new MemoryStorageService();
+ storageService.setId("test");
+ storageService.setKeySize(50);
+ storageService.initialize();
+ revocationCache = new RevocationCache();
+ revocationCache.setStorage(storageService);
+ revocationCache.initialize();
+ final byte[] array = new byte[storageService.getCapabilities().getKeySize()*2];
+ new Random().nextBytes(array);
+ final String item = new String(array, Charset.forName("UTF-8"));
+ Assert.assertTrue(item.length()>storageService.getCapabilities().getKeySize());
+ Assert.assertFalse(revocationCache.isRevoked("context", item));
+ Assert.assertTrue(revocationCache.revoke("context", item));
+ Assert.assertTrue(revocationCache.isRevoked("context", item));
+ }
+
+ @Test
+ public void testRevocationExpirationSuccess() throws ComponentInitializationException, InterruptedException {
+ //Test expiration of entry (500ms)
+ Assert.assertFalse(revocationCache.isRevoked("context", "item"));
+ Assert.assertTrue(revocationCache.revoke("context", "item"));
+ Thread.sleep(600L);
+ Assert.assertFalse(revocationCache.isRevoked("context", "item"));
+ //Test rolling window, second revoke updates expiration past original 500ms
+ Assert.assertTrue(revocationCache.revoke("context", "item"));
+ Thread.sleep(300L);
+ Assert.assertTrue(revocationCache.revoke("context", "item"));
+ Thread.sleep(300L);
+ Assert.assertTrue(revocationCache.isRevoked("context", "item"));
+ }
+}
\ No newline at end of file
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list