[java-idp-plugin-duo] branch main updated: Unit test for passwordless opt-in management action, fixed an edge case.

Scott Cantor cantor.2 at osu.edu
Wed Apr 17 14:03:17 UTC 2024


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

scantor pushed a commit to branch main
in repository java-idp-plugin-duo.

View the commit online:
http://git.shibboleth.net/view/?p=java-idp-plugin-duo.git;a=commit;h=11519c6b493928173ed6b885cdaf70397ad692d0

The following commit(s) were added to refs/heads/main by this push:
     new 11519c6b Unit test for passwordless opt-in management action, fixed an edge case.
11519c6b is described below

commit 11519c6b493928173ed6b885cdaf70397ad692d0
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Wed Apr 17 10:03:11 2024 -0400

    Unit test for passwordless opt-in management action, fixed an edge case.
---
 .../impl/PostValidatePasswordlessEvaluation.java   |  88 +++++----
 .../duo/impl/PasswordlessCookieManagerTest.java    |   2 +-
 .../PostValidatePasswordlessEvaluationTest.java    | 213 +++++++++++++++++++++
 3 files changed, 262 insertions(+), 41 deletions(-)

diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/PostValidatePasswordlessEvaluation.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/PostValidatePasswordlessEvaluation.java
index 6dfcca55..8ffb356c 100644
--- a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/PostValidatePasswordlessEvaluation.java
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/PostValidatePasswordlessEvaluation.java
@@ -27,11 +27,9 @@ import org.slf4j.Logger;
 
 import net.shibboleth.idp.authn.AbstractAuthenticationAction;
 import net.shibboleth.idp.authn.context.AuthenticationContext;
-import net.shibboleth.idp.plugin.authn.duo.DuoOIDCIntegration;
 import net.shibboleth.idp.plugin.authn.duo.PasswordlessCookieManager;
 import net.shibboleth.idp.plugin.authn.duo.context.DuoOIDCAuthenticationContext;
 import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
-import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
 import net.shibboleth.shared.annotation.constraint.NotEmpty;
 import net.shibboleth.shared.component.ComponentInitializationException;
 import net.shibboleth.shared.logic.Constraint;
@@ -85,19 +83,16 @@ public class PostValidatePasswordlessEvaluation extends AbstractAuthenticationAc
     
     /** Condition governing "new" eligibility. */
     @Nonnull private Predicate<ProfileRequestContext> passwordlessCondition;
-    
+
+    /** Cookie manager to use. */
+    @NonnullAfterInit private PasswordlessCookieManager cookieManager;
+
     /** Whether to require the authentication be cacheable to allow this. */
     private boolean requireResultCacheable;
     
     /** Whether to detect mismatches between cookie and current username. */
     private boolean detectUsernameMismatch;
     
-    /** Cookie manager to use. */
-    @NonnullAfterInit private PasswordlessCookieManager cookieManager;
-    
-    /** Duo authentiction context. */
-    @NonnullBeforeExec private DuoOIDCAuthenticationContext duoContext;
-    
     /** Constructor. */
     public PostValidatePasswordlessEvaluation() {
         passwordlessCondition = PredicateSupport.alwaysFalse();
@@ -125,6 +120,17 @@ public class PostValidatePasswordlessEvaluation extends AbstractAuthenticationAc
         
         passwordlessCondition = Constraint.isNotNull(condition, "Passwordless eligibility condition cannot be null");
     }
+
+    /**
+     * Sets {@link PasswordlessCookieManager} to use.
+     * 
+     * @param manager cookie manager
+     */
+    public void setCookieManager(@Nullable final PasswordlessCookieManager manager) {
+        checkSetterPreconditions();
+        
+        cookieManager = Constraint.isNotNull(manager, "PasswordlessCookieManager cannot be null");
+    }
     
     /**
      * Sets whether a non-cacheable result should force the condition to return false.
@@ -151,17 +157,6 @@ public class PostValidatePasswordlessEvaluation extends AbstractAuthenticationAc
         checkSetterPreconditions();
         detectUsernameMismatch = flag;
     }
-
-    /**
-     * Sets {@link PasswordlessCookieManager} to use.
-     * 
-     * @param manager cookie manager
-     */
-    public void setCookieManager(@Nullable final PasswordlessCookieManager manager) {
-        checkSetterPreconditions();
-        
-        cookieManager = Constraint.isNotNull(manager, "PasswordlessCookieManager cannot be null");
-    }
     
     /** {@inheritDoc} */
     @Override
@@ -181,30 +176,12 @@ public class PostValidatePasswordlessEvaluation extends AbstractAuthenticationAc
         return true;
     }
     
-// Checkstyle: CyclomaticComplexity OFF
     /** {@inheritDoc} */
     @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
             @Nonnull final AuthenticationContext authenticationContext) {    
                  
-        
-        duoContext = authenticationContext.getSubcontext(DuoOIDCAuthenticationContext.class);
-        if (duoContext == null) {
-            log.error("{} No DuoAuthenticationContext available", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
-            return;
-        }
-        
-        final DuoOIDCIntegration integration = duoContext.getIntegration();
-        if (integration == null) {
-            log.error("{} No DuoOIDCIntegration available", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
-            return;
-        }
-        
-        final String username = duoContext.getUsername();
+        final String username = getUsername(profileRequestContext, authenticationContext);
         if (username == null) {
-            log.error("{} No username available", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
             return;
         }
 
@@ -231,6 +208,10 @@ public class PostValidatePasswordlessEvaluation extends AbstractAuthenticationAc
                 log.info("{} Clearing existing guard cookie for original username '{}'", getLogPrefix(),
                         cookie);
                 cookieManager.clearCookie();
+            } else {
+                log.info("{} Ignoring username mismatch, left guard cookie for original username '{}'", getLogPrefix(),
+                        cookie);
+                return;
             }
         }
         
@@ -242,7 +223,6 @@ public class PostValidatePasswordlessEvaluation extends AbstractAuthenticationAc
             log.debug("{} User '{}' not eligible for passwordless", getLogPrefix(), username);
         }
     }
- // Checkstyle: CyclomaticComplexity ON
 
     /** {@inheritDoc} */
     @Override
@@ -254,4 +234,32 @@ public class PostValidatePasswordlessEvaluation extends AbstractAuthenticationAc
         }
     }
     
+    /**
+     * Gets the username associated with this Duo run.
+     * 
+     * @param profileRequestContext profile request context
+     * @param authenticationContext authentication context
+     * 
+     * @return username from current transaction or null
+     */
+    @Nullable private String getUsername(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final AuthenticationContext authenticationContext) {
+        final DuoOIDCAuthenticationContext duoContext =
+                authenticationContext.getSubcontext(DuoOIDCAuthenticationContext.class);
+        if (duoContext == null) {
+            log.error("{} No DuoOIDCAuthenticationContext available", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return null;
+        }
+        
+        final String username = duoContext.getUsername();
+        if (username == null) {
+            log.error("{} No username available", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return null;
+        }
+        
+        return username;
+    }
+
 }
\ No newline at end of file
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/PasswordlessCookieManagerTest.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/PasswordlessCookieManagerTest.java
index 361cded3..5f9439b6 100644
--- a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/PasswordlessCookieManagerTest.java
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/PasswordlessCookieManagerTest.java
@@ -51,10 +51,10 @@ public class PasswordlessCookieManagerTest {
     protected PasswordlessCookieManager cookieManager;
     protected MockHttpServletRequest request;
     protected MockHttpServletResponse response;
+    protected DataSealer dataSealer;
 
     private Resource keystoreResource;
     private Resource versionResource;
-    private DataSealer dataSealer;
     
     @BeforeClass
     public void beforeClass() throws DataSealerException, ComponentInitializationException {
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/PostValidatePasswordlessEvaluationTest.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/PostValidatePasswordlessEvaluationTest.java
new file mode 100644
index 00000000..b5f62975
--- /dev/null
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/PostValidatePasswordlessEvaluationTest.java
@@ -0,0 +1,213 @@
+/*
+ * 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
+ *
+ *    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.plugin.authn.duo.impl;
+
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.springframework.webflow.execution.Event;
+import org.springframework.webflow.execution.RequestContext;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.google.common.net.UrlEscapers;
+
+import jakarta.servlet.http.Cookie;
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.plugin.authn.duo.PasswordlessCookieManager;
+import net.shibboleth.idp.plugin.authn.duo.context.DuoOIDCAuthenticationContext;
+import net.shibboleth.idp.profile.context.navigate.WebflowRequestContextProfileRequestContextLookup;
+import net.shibboleth.idp.profile.testing.ActionTestingSupport;
+import net.shibboleth.idp.profile.testing.RequestContextBuilder;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.PredicateSupport;
+import net.shibboleth.shared.net.URISupport;
+import net.shibboleth.shared.security.DataSealerException;
+
+/**
+ * Unit test for {@link PostValidatePasswordlessEvaluation} action.
+ */
+ at SuppressWarnings("javadoc")
+public class PostValidatePasswordlessEvaluationTest extends PasswordlessCookieManagerTest {
+
+    private PostValidatePasswordlessEvaluation action;
+    
+    private RequestContext src;
+    private ProfileRequestContext prc;
+    private AuthenticationContext ac;
+    private DuoOIDCAuthenticationContext dac;
+
+    @Override
+    @BeforeMethod
+    public void setUp() throws ComponentInitializationException {
+        super.setUp();
+        
+        action = new PostValidatePasswordlessEvaluation();
+        action.setCookieManager(cookieManager);
+        action.setPasswordlessCondition(PredicateSupport.alwaysTrue());
+        
+        src = new RequestContextBuilder().buildRequestContext();
+        prc = new WebflowRequestContextProfileRequestContextLookup().apply(src);
+        ac = prc.ensureSubcontext(AuthenticationContext.class);
+        dac = ac.ensureSubcontext(DuoOIDCAuthenticationContext.class);
+    }
+    
+    @Test
+    public void testNoUsername() throws ComponentInitializationException {
+        action.initialize();
+        
+        Event event = action.execute(src);
+        ActionTestingSupport.assertEvent(event, EventIds.INVALID_PROFILE_CTX);
+
+        ac.removeSubcontext(DuoOIDCAuthenticationContext.class);
+
+        event = action.execute(src);
+        ActionTestingSupport.assertEvent(event, EventIds.INVALID_PROFILE_CTX);
+    }
+ 
+    @Test
+    public void testNotCacheableCleared() throws ComponentInitializationException {
+        dac.setUsername("jdoe");
+        ac.setResultCacheable(false);
+
+        action.initialize();
+
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertProceedEvent(event);
+        
+        // Make sure cookie got cleared.
+        final Cookie cookie = response.getCookie(COOKIE_NAME);
+        assert cookie != null;
+        Assert.assertEquals(cookie.getMaxAge(), 0);
+    }
+
+    @Test
+    public void testNotCacheableNotCleared() throws ComponentInitializationException, DataSealerException {
+
+        String cookieValue = UrlEscapers.urlFormParameterEscaper().escape(dataSealer.wrap("jdoe"));
+        request.setCookies(new Cookie(COOKIE_NAME, cookieValue));
+
+        dac.setUsername("jdoe");
+        ac.setResultCacheable(false);
+
+        action.setRequireResultCacheable(false);
+        action.initialize();
+
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertProceedEvent(event);
+        
+        // Make sure cookie was refreshed.
+        final Cookie cookie = response.getCookie(COOKIE_NAME);
+        assert cookie != null;
+        Assert.assertEquals(cookie.getName(), COOKIE_NAME);
+        cookieValue = URISupport.doURLDecode(cookie.getValue());
+        assert cookieValue != null;
+        Assert.assertEquals(dataSealer.unwrap(cookieValue), "jdoe");
+    }
+
+    @Test
+    public void testOptOutSkips() throws ComponentInitializationException {
+        dac.setUsername("jdoe");
+        request.setCookies(new Cookie(COOKIE_NAME, PasswordlessCookieManager.NEGATIVE_VALUE));
+        
+        action.initialize();
+        
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertProceedEvent(event);
+        
+        // Make sure cookie wasn't updated.
+        final Cookie cookie = response.getCookie(COOKIE_NAME);
+        Assert.assertNull(cookie);
+    }
+
+    @Test
+    public void testNoCookieEligible() throws ComponentInitializationException {
+        dac.setUsername("jdoe");
+
+        action.initialize();
+        
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertEvent(event, PostValidatePasswordlessEvaluation.PROMPT_USER_EVENT);
+    }
+    
+    @Test
+    public void testNoCookieNotEligible() throws ComponentInitializationException {
+        dac.setUsername("jdoe");
+
+        action.setPasswordlessCondition(PredicateSupport.alwaysFalse());
+        action.initialize();
+        
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertProceedEvent(event);
+    }
+    
+    @Test
+    public void testCookieMatches() throws ComponentInitializationException, DataSealerException {
+        dac.setUsername("jdoe");
+
+        String cookieValue = UrlEscapers.urlFormParameterEscaper().escape(dataSealer.wrap("jdoe"));
+        request.setCookies(new Cookie(COOKIE_NAME, cookieValue));
+        
+        action.initialize();
+        
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertProceedEvent(event);
+        
+        // Check that cookie was refreshed.
+        final Cookie cookie = response.getCookie(COOKIE_NAME);
+        assert cookie != null;
+        Assert.assertEquals(cookie.getName(), COOKIE_NAME);
+        cookieValue = URISupport.doURLDecode(cookie.getValue());
+        assert cookieValue != null;
+        Assert.assertEquals(dataSealer.unwrap(cookieValue), "jdoe");
+    }
+ 
+    @Test
+    public void testCookieMismatch() throws ComponentInitializationException, DataSealerException {
+        dac.setUsername("jdoe");
+
+        String cookieValue = UrlEscapers.urlFormParameterEscaper().escape(dataSealer.wrap("jdoe2"));
+        request.setCookies(new Cookie(COOKIE_NAME, cookieValue));
+        
+        action.initialize();
+        
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertEvent(event, PostValidatePasswordlessEvaluation.PROMPT_USER_EVENT);
+        
+        // Check that cookie was cleared.
+        final Cookie cookie = response.getCookie(COOKIE_NAME);
+        assert cookie != null;
+        Assert.assertEquals(cookie.getMaxAge(), 0);
+    }
+    
+    @Test
+    public void testCookieMismatchIgnore() throws ComponentInitializationException, DataSealerException {
+        dac.setUsername("jdoe");
+
+        String cookieValue = UrlEscapers.urlFormParameterEscaper().escape(dataSealer.wrap("jdoe2"));
+        request.setCookies(new Cookie(COOKIE_NAME, cookieValue));
+        
+        action.setDetectUsernameMismatch(false);
+        action.initialize();
+        
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertProceedEvent(event);
+        
+        // Check that cookie wasn't cleared.
+        final Cookie cookie = response.getCookie(COOKIE_NAME);
+        Assert.assertNull(cookie);
+    }
+
+}
\ 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