[java-identity-provider] branch maint-4 updated: IDP-1981 - Build Predicate based on RevocationCache

Scott Cantor cantor.2 at osu.edu
Thu Jul 28 11:56:16 UTC 2022


This is an automated email from the git hooks/post-receive script.

scantor pushed a commit to branch maint-4
in repository java-identity-provider.

View the commit online:
http://git.shibboleth.net/view/?p=java-identity-provider.git;a=commit;h=ff62ec54b189cad1a5d8a38c46f79ebe97075bc9

The following commit(s) were added to refs/heads/maint-4 by this push:
     new ff62ec54b IDP-1981 - Build Predicate based on RevocationCache
ff62ec54b is described below

commit ff62ec54b189cad1a5d8a38c46f79ebe97075bc9
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Thu Jul 28 07:56:09 2022 -0400

    IDP-1981 - Build Predicate based on RevocationCache
    
    https://shibboleth.atlassian.net/browse/IDP-1981
    
    Implemented as a new feature for login flows.
    Bumped IdP to OpenSAML 4.3.0 to pick up RevocationCache changes.
---
 .../idp/authn/AuthenticationFlowDescriptor.java    |  35 +++-
 .../shibboleth/idp/authn/AuthenticationResult.java |  34 +++-
 .../idp/authn/context/RevocationContext.java       |  54 ++++++
 .../authn/context/logic/RevocationCondition.java   | 188 +++++++++++++++++++++
 .../idp/authn/context/logic/package-info.java      |  22 +++
 .../idp/authn/impl/SelectAuthenticationFlow.java   |   9 +-
 .../impl/TransitionMultiFactorAuthentication.java  |   2 +-
 .../idp/authn/impl/RevocationConditionTest.java    | 111 ++++++++++++
 .../net/shibboleth/idp/conf/authn-system.xml       |  19 ++-
 .../src/main/resources/conf/authn/authn.properties |  14 ++
 idp-parent/pom.xml                                 |   2 +-
 11 files changed, 478 insertions(+), 12 deletions(-)

diff --git a/idp-authn-api/src/main/java/net/shibboleth/idp/authn/AuthenticationFlowDescriptor.java b/idp-authn-api/src/main/java/net/shibboleth/idp/authn/AuthenticationFlowDescriptor.java
index 49eaad7f4..1f5ba2007 100644
--- a/idp-authn-api/src/main/java/net/shibboleth/idp/authn/AuthenticationFlowDescriptor.java
+++ b/idp-authn-api/src/main/java/net/shibboleth/idp/authn/AuthenticationFlowDescriptor.java
@@ -28,6 +28,7 @@ import java.util.Comparator;
 import java.util.Map;
 import java.util.Set;
 import java.util.function.BiConsumer;
+import java.util.function.BiPredicate;
 import java.util.function.Predicate;
 
 import javax.annotation.Nonnull;
@@ -98,6 +99,9 @@ public class AuthenticationFlowDescriptor extends AbstractIdentifiableInitializa
 
     /** Whether this flow allows reuse of its results. */
     @Nonnull private Predicate<ProfileRequestContext> reuseCondition;
+    
+    /** Whether a result from this flow should be considered revoked. */
+    @Nullable private BiPredicate<ProfileRequestContext,AuthenticationResult> revocationCondition;
 
     /** Maximum amount of time since first usage that a flow should be considered active. */
     @Nullable private Duration lifetime;
@@ -303,7 +307,7 @@ public class AuthenticationFlowDescriptor extends AbstractIdentifiableInitializa
     @Nonnull public Predicate<ProfileRequestContext> getReuseCondition() {
         return reuseCondition;
     }
-
+    
     /**
      * Set condition controlling whether results from this flow should be reused for SSO.
      * 
@@ -322,6 +326,31 @@ public class AuthenticationFlowDescriptor extends AbstractIdentifiableInitializa
                 Constraint.isNotNull(condition, "Predicate cannot be null"));
     }
 
+    /**
+     * Get condition controlling whether a result from this flow should be considered revoked.
+     * 
+     * @return condition
+     * 
+     * @since 4.3.0
+     */
+    @Nonnull public BiPredicate<ProfileRequestContext,AuthenticationResult> getRevocationCondition() {
+        return revocationCondition;
+    }
+    
+    /**
+     * Set condition controlling whether a result from this flow should be considered revoked.
+     * 
+     * @param condition condition to set
+     * 
+     * @since 4.3.0
+     */
+    public void setRevocationCondition(
+            @Nullable final BiPredicate<ProfileRequestContext,AuthenticationResult> condition) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+        revocationCondition = condition;
+    }
+
     /**
      * Gets a subject decorating component called prior to completing authentication and passing
      * control to subject canonicalization.
@@ -565,6 +594,8 @@ public class AuthenticationFlowDescriptor extends AbstractIdentifiableInitializa
         } else {
             result.setReuseCondition(reuseCondition);
         }
+        
+        result.setRevocationCondition(revocationCondition);
         return result;
     }
     
@@ -593,6 +624,8 @@ public class AuthenticationFlowDescriptor extends AbstractIdentifiableInitializa
         } else {
             result.setReuseCondition(reuseCondition);
         }
+        
+        result.setRevocationCondition(revocationCondition);
         return result;
     }
 
diff --git a/idp-authn-api/src/main/java/net/shibboleth/idp/authn/AuthenticationResult.java b/idp-authn-api/src/main/java/net/shibboleth/idp/authn/AuthenticationResult.java
index c471955c3..0faf02887 100644
--- a/idp-authn-api/src/main/java/net/shibboleth/idp/authn/AuthenticationResult.java
+++ b/idp-authn-api/src/main/java/net/shibboleth/idp/authn/AuthenticationResult.java
@@ -24,6 +24,7 @@ import java.util.HashMap;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
+import java.util.function.BiPredicate;
 import java.util.function.Predicate;
 
 import javax.annotation.Nonnull;
@@ -54,7 +55,7 @@ import com.google.common.base.MoreObjects;
  * that make up a single overall result, but the IdP always acts on a single result as the
  * product of a given request for a login.</p>
  */
-public class AuthenticationResult implements PrincipalSupportingComponent {
+public class AuthenticationResult implements PrincipalSupportingComponent, Predicate<ProfileRequestContext> {
     
     /** The Subject established by the authentication result. */
     @Nonnull private final Subject subject;
@@ -77,6 +78,9 @@ public class AuthenticationResult implements PrincipalSupportingComponent {
     /** Whether this result can be reused. */
     @Nonnull private Predicate<ProfileRequestContext> reuseCondition;
     
+    /** Whether this result should be considered revoked. */
+    @Nonnull private BiPredicate<ProfileRequestContext,AuthenticationResult> revocationCondition;
+    
     /**
      * Constructor.
      * 
@@ -109,7 +113,7 @@ public class AuthenticationResult implements PrincipalSupportingComponent {
     }
     
     /**
-     * Get condition controlling whether this result should be reused for SSO.
+     * Gets condition controlling whether this result should be reused for SSO.
      * 
      * @return condition controlling whether result should be reused for SSO
      * 
@@ -120,7 +124,7 @@ public class AuthenticationResult implements PrincipalSupportingComponent {
     }
     
     /**
-     * Set condition controlling whether this result should be reused for SSO.
+     * Sets condition controlling whether this result should be reused for SSO.
      * 
      * @param condition condition to set
      * 
@@ -131,7 +135,28 @@ public class AuthenticationResult implements PrincipalSupportingComponent {
     }
     
     /**
-     * Get the Subject identifying the authenticated entity.
+     * Sets condition controlling whether this result has been revoked subsequent to creation.
+     * 
+     * @param condition condition to set
+     * 
+     * @since 4.3.0
+     */
+    public void setRevocationCondition(
+            @Nullable final BiPredicate<ProfileRequestContext,AuthenticationResult> condition) {
+        revocationCondition = condition;
+    }
+    
+    /** {@inheritDoc} */
+    public boolean test(@Nullable final ProfileRequestContext input) {
+        if (reuseCondition.test(input)) {
+            return revocationCondition != null ? !revocationCondition.test(input, this) : true;
+        }
+        
+        return false;
+    }
+    
+    /**
+     * Gets the Subject identifying the authenticated entity.
      * 
      * @return a Subject identifying the authenticated entity
      */
@@ -341,4 +366,5 @@ public class AuthenticationResult implements PrincipalSupportingComponent {
         }
         
     }
+
 }
\ No newline at end of file
diff --git a/idp-authn-api/src/main/java/net/shibboleth/idp/authn/context/RevocationContext.java b/idp-authn-api/src/main/java/net/shibboleth/idp/authn/context/RevocationContext.java
new file mode 100644
index 000000000..f62bd07e5
--- /dev/null
+++ b/idp-authn-api/src/main/java/net/shibboleth/idp/authn/context/RevocationContext.java
@@ -0,0 +1,54 @@
+/*
+ * 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 net.shibboleth.idp.authn.context;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.BaseContext;
+
+import net.shibboleth.utilities.java.support.annotation.constraint.Live;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
+
+/**
+ * Context for caching information about revocation of authentication results.
+ * 
+ * @since 4.3.0
+ */
+public class RevocationContext extends BaseContext {
+
+    /** Revocation records. */
+    @Nonnull @NonnullElements private final Collection<String> revocationRecords;
+    
+    /** Constructor. */
+    public RevocationContext() {
+        revocationRecords = new ArrayList<>(2);
+    }
+    
+    /**
+     * Gets the modifiable collection of revocation records.
+     * 
+     * @return modifiable revocation record collection
+     */
+    @Nonnull @NonnullElements @Live public Collection<String> getRevocationRecords() {
+        return revocationRecords;
+    }
+    
+}
\ No newline at end of file
diff --git a/idp-authn-api/src/main/java/net/shibboleth/idp/authn/context/logic/RevocationCondition.java b/idp-authn-api/src/main/java/net/shibboleth/idp/authn/context/logic/RevocationCondition.java
new file mode 100644
index 000000000..dbe54cbb1
--- /dev/null
+++ b/idp-authn-api/src/main/java/net/shibboleth/idp/authn/context/logic/RevocationCondition.java
@@ -0,0 +1,188 @@
+/*
+ * 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 net.shibboleth.idp.authn.context.logic;
+
+import java.io.IOException;
+import java.time.Instant;
+import java.util.Collection;
+import java.util.function.BiPredicate;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.servlet.http.HttpServletRequest;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.storage.RevocationCache;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import net.shibboleth.idp.authn.AuthenticationResult;
+import net.shibboleth.idp.authn.context.RevocationContext;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+import net.shibboleth.utilities.java.support.component.AbstractInitializableComponent;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
+/**
+ * A condition for login flows that checks for revocation.
+ * 
+ * @since 4.3.0
+ */
+public class RevocationCondition extends AbstractInitializableComponent
+        implements BiPredicate<ProfileRequestContext,AuthenticationResult> {
+
+    /** Revocation context. */
+    @Nonnull @NotEmpty public static final String REVOCATION_CONTEXT = "LoginFlowRevocation";
+
+    /** Prefix of keys for principal-based revocation. */
+    @Nonnull @NotEmpty public static final String PRINCIPAL_REVOCATION_PREFIX = "prin:";
+
+    /** Prefix of keys for address-based revocation. */
+    @Nonnull @NotEmpty public static final String ADDRESS_REVOCATION_PREFIX = "addr:";
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(RevocationCondition.class);
+    
+    /** Cache to use. */
+    @NonnullAfterInit private RevocationCache revocationCache;
+    
+    /** Lookup strategy for principal name. */
+    @NonnullAfterInit private Function<ProfileRequestContext,String> principalNameLookupStrategy;
+
+    /** Servlet request. */
+    @Nullable private HttpServletRequest httpServletRequest; 
+    
+    /**
+     * Set {@link RevocationCache} to use.
+     * 
+     * @param cache cache to use
+     */
+    public void setRevocationCache(@Nonnull final RevocationCache cache) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+        
+        revocationCache = Constraint.isNotNull(cache, "RevocationCache cannot be null");
+    }
+    
+    /**
+     * Set lookup strategy for principal name.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setPrincipalNameLookupStrategy(@Nonnull final Function<ProfileRequestContext,String> strategy) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+        
+        principalNameLookupStrategy = Constraint.isNotNull(strategy, "Principal name lookup strategy cannot be null");
+    }
+    
+    /**
+     * Set {@link HttpServletRequest} in order to obtain client address.
+     * 
+     * @param request servlet request interface
+     */
+    public void setHttpServletRequest(@Nullable final HttpServletRequest request) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+        
+        httpServletRequest = request;
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        
+        if (revocationCache == null) {
+            throw new ComponentInitializationException("RevocationCache cannot be null");
+        } else if (principalNameLookupStrategy == null) {
+            throw new ComponentInitializationException("Principal name lookup strategy cannot be null");
+        }
+    }
+
+    /** {@inheritDoc} */
+    public boolean test(@Nullable final ProfileRequestContext input,  @Nullable final AuthenticationResult input2) {
+        
+        if (input == null || input2 == null) {
+            log.error("Called with null inputs");
+            return true;
+        }
+        
+        final String principal = principalNameLookupStrategy.apply(input);
+        if (principal == null) {
+            log.error("Principal lookup strategy returned null value");
+            return true;
+        }
+        
+        log.debug("Checking revocation for principal name {} for {} result", principal,
+                input2.getAuthenticationFlowId());
+        
+        RevocationContext context = input.getSubcontext(RevocationContext.class);
+        if (context == null) {
+            try {
+                final String principalRecord = revocationCache.getRevocationRecord(REVOCATION_CONTEXT,
+                        PRINCIPAL_REVOCATION_PREFIX + principal);
+                final String addressRecord = httpServletRequest != null ?
+                        revocationCache.getRevocationRecord(REVOCATION_CONTEXT,
+                                ADDRESS_REVOCATION_PREFIX + httpServletRequest.getRemoteAddr()) :
+                            null;
+                context = input.getSubcontext(RevocationContext.class, true);
+                if (principalRecord != null) {
+                    context.getRevocationRecords().add(principalRecord);
+                }
+                if (addressRecord != null) {
+                    context.getRevocationRecords().add(addressRecord);
+                }
+            } catch (final IOException e) {
+                log.error("Error checking revocation cache for principal {}, treating as revoked",
+                        principal, e);
+                return true;
+            }
+        }
+        
+        return isRevoked(principal, input2, context.getRevocationRecords());
+    }
+
+    /**
+     * Check the revocation records' timestamps for applicability.
+     * 
+     * @param principal name of principal
+     * @param result active result being checked 
+     * @param revocationRecords the records from the cache
+     * 
+     * @return true iff the revocation applies to this result
+     */
+    protected boolean isRevoked(@Nonnull @NotEmpty final String principal, @Nonnull final AuthenticationResult result,
+            @Nonnull @NonnullElements final Collection<String> revocationRecords) {
+        
+        for (final String r : revocationRecords) {
+            if (result.getAuthenticationInstant().isBefore(Instant.ofEpochSecond(Long.valueOf(r)))) {
+                log.info("Authentication result {} for principal {} has been revoked", result.getAuthenticationFlowId(),
+                        principal);
+                return true;
+            }
+        }
+        
+        return false;
+    }
+    
+}
\ No newline at end of file
diff --git a/idp-authn-api/src/main/java/net/shibboleth/idp/authn/context/logic/package-info.java b/idp-authn-api/src/main/java/net/shibboleth/idp/authn/context/logic/package-info.java
new file mode 100644
index 000000000..6237f69a5
--- /dev/null
+++ b/idp-authn-api/src/main/java/net/shibboleth/idp/authn/context/logic/package-info.java
@@ -0,0 +1,22 @@
+/*
+ * 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.
+ */
+
+/**
+ * Predicates related to authentication.
+ */
+
+package net.shibboleth.idp.authn.context.logic;
\ No newline at end of file
diff --git a/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/impl/SelectAuthenticationFlow.java b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/impl/SelectAuthenticationFlow.java
index c530a1a04..be7925218 100644
--- a/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/impl/SelectAuthenticationFlow.java
+++ b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/impl/SelectAuthenticationFlow.java
@@ -201,7 +201,7 @@ public class SelectAuthenticationFlow extends AbstractAuthenticationAction {
         AuthenticationResult activeResult = null;
         if (!authenticationContext.isForceAuthn()) {
             activeResult = authenticationContext.getActiveResults().get(flow.getId());
-            if (!activeResult.getReuseCondition().test(profileRequestContext)) {
+            if (!activeResult.test(profileRequestContext)) {
                 log.debug("{} Active result for flow {} not reusable, ignoring", getLogPrefix(),
                         activeResult.getAuthenticationFlowId());
                 activeResult = null;
@@ -293,7 +293,7 @@ public class SelectAuthenticationFlow extends AbstractAuthenticationAction {
         AuthenticationResult resultToSelect = null;
         
         for (final AuthenticationResult activeResult : authenticationContext.getActiveResults().values()) {
-            if (activeResult.getReuseCondition().test(profileRequestContext)) {
+            if (activeResult.test(profileRequestContext)) {
                 resultToSelect = activeResult;
                 if (preferredPrincipalCtx == null || preferredPrincipalCtx.isAcceptable(activeResult)) {
                     break;
@@ -482,7 +482,8 @@ public class SelectAuthenticationFlow extends AbstractAuthenticationAction {
                 final PrincipalEvalPredicate predicate = requestedPrincipalCtx.getPredicate(p);
                 if (predicate != null) {
                     for (final AuthenticationResult result : activeResults.values()) {
-                        if (result.getReuseCondition().test(profileRequestContext) && predicate.test(result)) {
+                        if (result.test(profileRequestContext) &&
+                                predicate.test(result)) {
                             selectActiveResult(profileRequestContext, authenticationContext, result);
                             return;
                         }
@@ -520,7 +521,7 @@ public class SelectAuthenticationFlow extends AbstractAuthenticationAction {
                         // Now check for an active result we can use from this flow. Not all results from a flow
                         // will necessarily match the request just because the flow might.
                         final AuthenticationResult result = activeResults.get(descriptor.getId());
-                        if (result == null || !result.getReuseCondition().test(profileRequestContext)
+                        if (result == null || !result.test(profileRequestContext)
                                 || !predicate.test(result)) {
                             if (result != null) {
                                 log.debug("{} Active result for flow {} not usable, ignoring", getLogPrefix(),
diff --git a/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/impl/TransitionMultiFactorAuthentication.java b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/impl/TransitionMultiFactorAuthentication.java
index cb1b87659..c8ba2a46f 100644
--- a/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/impl/TransitionMultiFactorAuthentication.java
+++ b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/impl/TransitionMultiFactorAuthentication.java
@@ -272,7 +272,7 @@ public class TransitionMultiFactorAuthentication extends AbstractAuthenticationA
         // infinite recursion is the configuration of transitions supplied by the deployer.
         final AuthenticationResult activeResult = mfaContext.getActiveResults().get(flowId);
         if (activeResult != null) {
-            if (activeResult.getReuseCondition().test(profileRequestContext)) {
+            if (activeResult.test(profileRequestContext)) {
                 log.debug("{} Reusing active result for flow {}", getLogPrefix(), flowId);
                 activeResult.setLastActivityInstantToNow();
                 previousEvent = EventIds.PROCEED_EVENT_ID;
diff --git a/idp-authn-impl/src/test/java/net/shibboleth/idp/authn/impl/RevocationConditionTest.java b/idp-authn-impl/src/test/java/net/shibboleth/idp/authn/impl/RevocationConditionTest.java
new file mode 100644
index 000000000..5afc5f8c1
--- /dev/null
+++ b/idp-authn-impl/src/test/java/net/shibboleth/idp/authn/impl/RevocationConditionTest.java
@@ -0,0 +1,111 @@
+/*
+ * 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 net.shibboleth.idp.authn.impl;
+
+import java.time.Duration;
+import java.util.Arrays;
+
+import javax.security.auth.Subject;
+
+import net.shibboleth.idp.authn.AuthenticationResult;
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.authn.context.logic.RevocationCondition;
+import net.shibboleth.idp.authn.impl.testing.BaseAuthenticationContextTest;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.logic.FunctionSupport;
+
+import org.joda.time.Instant;
+import org.opensaml.storage.RevocationCache;
+import org.opensaml.storage.impl.MemoryStorageService;
+import org.testng.Assert;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+/** {@link RevocationCondition} unit test. */
+public class RevocationConditionTest extends BaseAuthenticationContextTest {
+    
+    private MemoryStorageService storageService;
+    private RevocationCache revocationCache;
+    private RevocationCondition condition; 
+
+    @BeforeMethod
+    public void setUp() throws ComponentInitializationException {
+        super.setUp();
+        
+        storageService = new MemoryStorageService();
+        storageService.setId("test");
+        storageService.setCleanupInterval(Duration.ZERO);
+        storageService.initialize();
+        
+        revocationCache = new RevocationCache();
+        revocationCache.setStorage(storageService);
+        revocationCache.setId("test");
+        revocationCache.initialize();
+
+        condition = new RevocationCondition();
+        condition.setRevocationCache(revocationCache);
+        condition.setPrincipalNameLookupStrategy(FunctionSupport.constant("jdoe"));
+        condition.initialize();
+        
+        authenticationFlows.get(1).setRevocationCondition(condition);
+    }
+    
+    @AfterMethod
+    public void tearDown() {
+        condition.destroy();
+        revocationCache.destroy();
+        storageService.destroy();
+    }
+    
+    
+    @Test public void testNotRevoked() {
+        final AuthenticationResult active = authenticationFlows.get(1).newAuthenticationResult(new Subject());
+        final AuthenticationContext authCtx = prc.getSubcontext(AuthenticationContext.class);
+        authCtx.setActiveResults(Arrays.asList(active));
+
+        Assert.assertTrue(active.test(prc));
+    }
+    
+    @Test public void testRevoked() {
+        final AuthenticationResult active = authenticationFlows.get(1).newAuthenticationResult(new Subject());
+        final AuthenticationContext authCtx = prc.getSubcontext(AuthenticationContext.class);
+        authCtx.setActiveResults(Arrays.asList(active));
+
+        revocationCache.revoke(RevocationCondition.REVOCATION_CONTEXT,
+                RevocationCondition.PRINCIPAL_REVOCATION_PREFIX + "jdoe",
+                Long.toString(Instant.now().getMillis() / 1000 + 3600L),
+                Duration.ofDays(1));
+        
+        Assert.assertFalse(active.test(prc));
+    }
+
+    @Test public void testPastRevoked() {
+        final AuthenticationResult active = authenticationFlows.get(1).newAuthenticationResult(new Subject());
+        final AuthenticationContext authCtx = prc.getSubcontext(AuthenticationContext.class);
+        authCtx.setActiveResults(Arrays.asList(active));
+
+        revocationCache.revoke(RevocationCondition.REVOCATION_CONTEXT,
+                RevocationCondition.PRINCIPAL_REVOCATION_PREFIX + "jdoe",
+                Long.toString(Instant.now().getMillis() / 1000 - 3600L),
+                Duration.ofDays(1));
+        
+        Assert.assertTrue(active.test(prc));
+    }
+
+}
\ No newline at end of file
diff --git a/idp-conf-impl/src/main/resources/net/shibboleth/idp/conf/authn-system.xml b/idp-conf-impl/src/main/resources/net/shibboleth/idp/conf/authn-system.xml
index c495e30e3..918cafb22 100644
--- a/idp-conf-impl/src/main/resources/net/shibboleth/idp/conf/authn-system.xml
+++ b/idp-conf-impl/src/main/resources/net/shibboleth/idp/conf/authn-system.xml
@@ -27,7 +27,8 @@
             p:lifetime="%{idp.authn.defaultLifetime:PT60M}"
             p:inactivityTimeout="%{idp.authn.defaultTimeout:PT30M}"
             p:principalWeightMap="#{getObject('shibboleth.AuthenticationPrincipalWeightMap') ?: getObject('shibboleth.DefaultAuthenticationPrincipalWeightMap')}"
-            p:principalServiceManager-ref="shibboleth.PrincipalServiceManager">
+            p:principalServiceManager-ref="shibboleth.PrincipalServiceManager"
+            p:revocationCondition="#{%{idp.authn.revocation:false} ? getObject('%{idp.authn.revocation.Condition:shibboleth.RevocationCondition}'.trim()) : null}">
         <property name="supportedPrincipals">
             <list>
                 <bean parent="shibboleth.SAML2AuthnContextClassRef"
@@ -533,4 +534,20 @@
     <bean id="shibboleth.SAMLACDeclRefBetter" parent="shibboleth.Pair"
         p:first="#{ T(net.shibboleth.idp.saml.authn.principal.AuthnContextDeclRefPrincipal) }" p:second="better"/>
     
+    <!-- Revocation feature. -->
+    
+    <bean id="shibboleth.RevocationCondition" class="net.shibboleth.idp.authn.context.logic.RevocationCondition" lazy-init="true"
+        p:revocationCache-ref="%{idp.authn.revocation.cache:shibboleth.AuthnRevocationCache}"
+        p:httpServletRequest="#{%{idp.authn.revocation.addressBased:false} ? getObject('shibboleth.HttpServletRequest') : null}"
+        p:principalNameLookupStrategy-ref="shibboleth.RevocationPrincipalLookupStrategy" />
+        
+    <bean id="shibboleth.RevocationPrincipalLookupStrategy" parent="shibboleth.Functions.Compose" lazy-init="true"
+        c:g-ref="shibboleth.PrincipalNameLookup.Session"
+        c:f-ref="shibboleth.ChildLookup.SessionContext" />
+    
+    <bean id="shibboleth.AuthnRevocationCache" class="org.opensaml.storage.RevocationCache" lazy-init="true"
+        p:entryExpiration="#{'%{idp.authn.revocation.lifetime:%{idp.authn.defaultLifetime:PT12H}}'}"
+        p:storage-ref="#{'%{idp.authn.revocation.StorageService:shibboleth.StorageService}'.trim()}"
+        p:strict="%{idp.authn.revocation.strict:false}" />
+
 </beans>
diff --git a/idp-conf/src/main/resources/conf/authn/authn.properties b/idp-conf/src/main/resources/conf/authn/authn.properties
index 97a75257e..86de8ee7f 100644
--- a/idp-conf/src/main/resources/conf/authn/authn.properties
+++ b/idp-conf/src/main/resources/conf/authn/authn.properties
@@ -24,6 +24,20 @@
 # If using IdP discovery feature, provides a discovery location to use.
 #idp.authn.discoveryURL = https://ds.example.org/shibboleth-ds/index.html
 
+# Revocation (administrative logout)
+#idp.authn.revocation = false
+#idp.authn.revocation.lifetime = %{idp.authn.defaultAuthnLifetime:PT12H}
+# Name of BiCondition to apply for check
+#idp.authn.revocation.Condition = shibboleth.RevocationCondition
+# Set to true to treat lookup failures as being revoked.
+#idp.authn.revocation.strict = false
+# Set to true to check for address-based revocation.
+#idp.authn.revocation.addressBased = false
+# Default implementation based on a StorageService bean.
+#idp.authn.revocation.cache = shibboleth.AuthnRevocationCache
+#idp.authn.revocation.StorageService = shibboleth.StorageService
+
+
 # Properties below override specific method behavior, as an alternative
 # to defining Spring beans in XML. Refer to the documentation for a complete
 # list. Many of the properties below are mentioned only because they are
diff --git a/idp-parent/pom.xml b/idp-parent/pom.xml
index d804f1970..c46a3ad77 100644
--- a/idp-parent/pom.xml
+++ b/idp-parent/pom.xml
@@ -70,7 +70,7 @@
         <idwsfconsumer.version>2.1.0</idwsfconsumer.version>
         <java-support.version>8.3.2-SNAPSHOT</java-support.version>
         <opensaml.groupId>org.opensaml</opensaml.groupId>
-        <opensaml.version>4.2.1-SNAPSHOT</opensaml.version>
+        <opensaml.version>4.3.0-SNAPSHOT</opensaml.version>
         <spring-extensions.version>6.2.1-SNAPSHOT</spring-extensions.version>
         <checkstyle.configLocation>${project.basedir}/../idp-parent/resources/checkstyle/checkstyle.xml</checkstyle.configLocation>
         <idp-parent.site.url>${shibboleth.site.deploy.url}java-identity-provider/${project.version}/</idp-parent.site.url>

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.


More information about the commits mailing list