[java-identity-provider] branch master updated: IDP-1124 - Function-driven login flow

Scott Cantor cantor.2 at osu.edu
Wed Sep 5 17:30:18 EDT 2018


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

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

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

The following commit(s) were added to refs/heads/master by this push:
       new  d9dd8a5   IDP-1124 - Function-driven login flow
d9dd8a5 is described below

commit d9dd8a5e1e8e5979c048e038a4be955d46877712
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Wed Sep 5 17:30:14 2018 -0400

    IDP-1124 - Function-driven login flow
    
    https://issues.shibboleth.net/jira/browse/IDP-1124
---
 .../idp/authn/impl/ValidateFunctionResult.java     | 170 +++++++++++++++++++++
 .../idp/authn/impl/ValidateFunctionResultTest.java | 127 +++++++++++++++
 .../resources/conf/authn/function-authn-config.xml |  37 +++++
 .../main/resources/conf/authn/general-authn.xml    |   2 +
 .../main/resources/system/conf/webflow-config.xml  |   1 +
 .../system/flows/authn/function-authn-beans.xml    |  32 ++++
 .../system/flows/authn/function-authn-flow.xml     |  33 ++++
 7 files changed, 402 insertions(+)

diff --git a/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/impl/ValidateFunctionResult.java b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/impl/ValidateFunctionResult.java
new file mode 100644
index 0000000..e9d86df
--- /dev/null
+++ b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/impl/ValidateFunctionResult.java
@@ -0,0 +1,170 @@
+/*
+ * 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.security.Principal;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.security.auth.Subject;
+
+import net.shibboleth.idp.authn.AbstractValidationAction;
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.authn.principal.UsernamePrincipal;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+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 net.shibboleth.utilities.java.support.logic.ConstraintViolationException;
+
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.base.Function;
+
+/**
+ * An action that executes a deployer-supplied function and produces an
+ * {@link net.shibboleth.idp.authn.AuthenticationResult} based on the function result.
+ * 
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_PROFILE_CTX}
+ * @event {@link AuthnEventIds#INVALID_CREDENTIALS}
+ * @event {@link AuthnEventIds#NO_CREDENTIALS}
+ * @pre <pre>ProfileRequestContext.getSubcontext(AuthenticationContext.class).getAttemptedFlow() != null</pre>
+ * @post If the function returns a String, Principal, or Subject, an
+ * {@link net.shibboleth.idp.authn.AuthenticationResult} is saved to the {@link AuthenticationContext}.
+ * 
+ * @since 3.4.0
+ */
+public class ValidateFunctionResult extends AbstractValidationAction {
+
+    /** Default prefix for metrics. */
+    @Nonnull @NotEmpty private static final String DEFAULT_METRIC_NAME = "net.shibboleth.idp.authn.function";
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(ValidateFunctionResult.class);
+    
+    /** Function to evaluate. */
+    @NonnullAfterInit private Function<ProfileRequestContext,?> resultLookupStrategy;
+    
+    /** Authentication result. */
+    @Nullable private Object result;
+    
+    /** Constructor. */
+    public ValidateFunctionResult() {
+        setMetricName(DEFAULT_METRIC_NAME);
+    }
+    
+    /**
+     * Set the function to execute to produce the authentication result.
+     * 
+     * <p>The function can return a {@link String}, a {@link Principal}, or a {@link Subject}.</p>
+     * 
+     * @param strategy result strategy
+     */
+    public void setResultLookupStrategy(@Nonnull final Function<ProfileRequestContext,?> strategy) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        
+        resultLookupStrategy = Constraint.isNotNull(strategy, "Result lookup strategy cannot be null");
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        
+        if (resultLookupStrategy == null) {
+            throw new ComponentInitializationException("Result lookup strategy cannot be null");
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final AuthenticationContext authenticationContext) {
+
+        if (!super.doPreExecute(profileRequestContext, authenticationContext)) {
+            return false;
+        }
+        
+        if (authenticationContext.getAttemptedFlow() == null) {
+            log.debug("{} No attempted flow within authentication context", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            recordFailure();
+            return false;
+        }
+        
+        return true;
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final AuthenticationContext authenticationContext) {
+
+        result = resultLookupStrategy.apply(profileRequestContext);
+
+        if (result == null) {
+            log.info("{} Authentication by function failed", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
+            recordFailure();
+        } else if (result instanceof String) {
+            log.info("{} Validated user via name '{}'", getLogPrefix(), result);
+            recordSuccess();
+            buildAuthenticationResult(profileRequestContext, authenticationContext);
+        } else if (result instanceof Principal) {
+            log.info("{} Validated user via Principal '{}'", getLogPrefix(), result);
+            recordSuccess();
+            buildAuthenticationResult(profileRequestContext, authenticationContext);
+        } else if (result instanceof Subject) {
+            log.info("{} Validated user via Subject", getLogPrefix());
+            recordSuccess();
+            buildAuthenticationResult(profileRequestContext, authenticationContext);
+        } else {
+            log.info("{} Authentication by function failed, result type was invalid", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_CREDENTIALS);
+            recordFailure();
+        }
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    @Nonnull protected Subject populateSubject(@Nonnull final Subject subject) {
+        
+        if (result instanceof String) {
+            subject.getPrincipals().add(new UsernamePrincipal((String) result));
+            return subject;
+        } else if (result instanceof Principal) {
+            subject.getPrincipals().add((Principal) result);
+            return subject;
+        } else if (result instanceof Subject) {
+            // Override supplied Subject with our own, after transferring over any custom Principals.
+            ((Subject) result).getPrincipals().addAll(subject.getPrincipals());
+            return (Subject) result;
+        }
+        
+        // Save my walrus!
+        throw new ConstraintViolationException("Result type was unexpected");
+    }
+    
+}
\ No newline at end of file
diff --git a/idp-authn-impl/src/test/java/net/shibboleth/idp/authn/impl/ValidateFunctionResultTest.java b/idp-authn-impl/src/test/java/net/shibboleth/idp/authn/impl/ValidateFunctionResultTest.java
new file mode 100644
index 0000000..6bf4ab7
--- /dev/null
+++ b/idp-authn-impl/src/test/java/net/shibboleth/idp/authn/impl/ValidateFunctionResultTest.java
@@ -0,0 +1,127 @@
+/*
+ * 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.security.Principal;
+
+import javax.security.auth.Subject;
+
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.authn.principal.TestPrincipal;
+import net.shibboleth.idp.authn.principal.UsernamePrincipal;
+import net.shibboleth.idp.profile.ActionTestingSupport;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.logic.FunctionSupport;
+
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.springframework.webflow.execution.Event;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.google.common.base.Functions;
+
+/** {@link ValidateFunctionResult} unit test. */
+public class ValidateFunctionResultTest extends BaseAuthenticationContextTest {
+    
+    private ValidateFunctionResult action;
+    
+    @BeforeMethod public void setUp() throws Exception {
+        super.setUp();
+        
+        prc.getSubcontext(AuthenticationContext.class).setAttemptedFlow(authenticationFlows.get(0));
+
+        action = new ValidateFunctionResult();
+    }
+
+    @Test public void testMissingFlow() throws ComponentInitializationException {
+        prc.getSubcontext(AuthenticationContext.class).setAttemptedFlow(null);
+        
+        action.setResultLookupStrategy(FunctionSupport.<ProfileRequestContext,Object>constant(null));
+        action.initialize();
+        
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertEvent(event, EventIds.INVALID_PROFILE_CTX);
+    }
+    
+    @Test public void testNoCredentials() throws ComponentInitializationException {
+        
+        action.setResultLookupStrategy(FunctionSupport.<ProfileRequestContext,Object>constant(null));
+        action.initialize();
+        
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertEvent(event, AuthnEventIds.NO_CREDENTIALS);
+    }
+
+    @Test public void testInvalidType() throws ComponentInitializationException {
+        
+        action.setResultLookupStrategy(Functions.<ProfileRequestContext>identity());
+        action.initialize();
+        
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertEvent(event, AuthnEventIds.INVALID_CREDENTIALS);
+    }
+
+    @Test public void testPrincipalName() throws ComponentInitializationException {
+        action.setResultLookupStrategy(FunctionSupport.<ProfileRequestContext,String>constant("foo"));
+        action.initialize();
+        
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertProceedEvent(event);
+        
+        final AuthenticationContext ac = prc.getSubcontext(AuthenticationContext.class);
+        Assert.assertNotNull(ac.getAuthenticationResult());
+        Assert.assertFalse(ac.getAuthenticationResult().isPreviousResult());
+        Assert.assertEquals(ac.getAuthenticationResult().getSubject().getPrincipals(
+                UsernamePrincipal.class).iterator().next().getName(), "foo");
+    }
+    
+    @Test public void testPrincipal() throws ComponentInitializationException {
+        action.setResultLookupStrategy(FunctionSupport.<ProfileRequestContext,Principal>constant(new TestPrincipal("foo")));
+        action.initialize();
+
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertProceedEvent(event);
+
+        final AuthenticationContext ac = prc.getSubcontext(AuthenticationContext.class);
+        Assert.assertNotNull(ac.getAuthenticationResult());
+        Assert.assertFalse(ac.getAuthenticationResult().isPreviousResult());
+        Assert.assertEquals(ac.getAuthenticationResult().getSubject().getPrincipals(
+                TestPrincipal.class).iterator().next().getName(), "foo");
+    }
+
+    @Test public void testSubject() throws ComponentInitializationException {
+        final Subject subject = new Subject();
+        subject.getPrincipals().add(new TestPrincipal("foo"));
+
+        action.setResultLookupStrategy(FunctionSupport.<ProfileRequestContext,Subject>constant(subject));
+        action.initialize();
+
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertProceedEvent(event);
+
+        final AuthenticationContext ac = prc.getSubcontext(AuthenticationContext.class);
+        Assert.assertNotNull(ac.getAuthenticationResult());
+        Assert.assertFalse(ac.getAuthenticationResult().isPreviousResult());
+        Assert.assertEquals(ac.getAuthenticationResult().getSubject().getPrincipals(
+                TestPrincipal.class).iterator().next().getName(), "foo");
+    }
+
+}
\ No newline at end of file
diff --git a/idp-conf/src/main/resources/conf/authn/function-authn-config.xml b/idp-conf/src/main/resources/conf/authn/function-authn-config.xml
new file mode 100644
index 0000000..cf7876a
--- /dev/null
+++ b/idp-conf/src/main/resources/conf/authn/function-authn-config.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+       xmlns:context="http://www.springframework.org/schema/context"
+       xmlns:util="http://www.springframework.org/schema/util"
+       xmlns:p="http://www.springframework.org/schema/p"
+       xmlns:c="http://www.springframework.org/schema/c"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
+                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
+                           http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
+                           
+       default-init-method="initialize"
+       default-destroy-method="destroy">
+
+    <!--
+    Add authentication flow descriptor's supportedPrincipals collection to the resulting Subject?
+    You would normally only unset this if you plan to return a completely constructed Subject from
+    your authentication function.
+    -->
+    <util:constant id="shibboleth.authn.Function.addDefaultPrincipals" static-field="java.lang.Boolean.TRUE" />
+    
+    <!--
+    The entire flow depends on the execution of a function bean you supply. A pathological script example
+    is below. The function may return a String, Principal, Subject, or a null to signal failure.
+    -->
+    
+    <bean id="shibboleth.authn.Function.ResultLookupStrategy"
+            parent="shibboleth.ContextFunctions.Scripted" factory-method="inlineScript">
+        <constructor-arg>
+            <value>
+            <![CDATA[
+                null;
+            ]]>
+            </value>
+        </constructor-arg>
+    </bean>    
+</beans>
diff --git a/idp-conf/src/main/resources/conf/authn/general-authn.xml b/idp-conf/src/main/resources/conf/authn/general-authn.xml
index ac55bbb..5699022 100644
--- a/idp-conf/src/main/resources/conf/authn/general-authn.xml
+++ b/idp-conf/src/main/resources/conf/authn/general-authn.xml
@@ -59,6 +59,8 @@
 
         <bean id="authn/RemoteUserInternal" parent="shibboleth.AuthenticationFlow" />
 
+        <bean id="authn/Function" parent="shibboleth.AuthenticationFlow" />
+
         <bean id="authn/X509" parent="shibboleth.AuthenticationFlow"
                 p:nonBrowserSupported="false">
             <property name="supportedPrincipals">
diff --git a/idp-conf/src/main/resources/system/conf/webflow-config.xml b/idp-conf/src/main/resources/system/conf/webflow-config.xml
index 6cb8c1d..df06b4c 100644
--- a/idp-conf/src/main/resources/system/conf/webflow-config.xml
+++ b/idp-conf/src/main/resources/system/conf/webflow-config.xml
@@ -84,6 +84,7 @@
                 <entry key="authn/External" value="../system/flows/authn/external-authn-flow.xml" />
                 <entry key="authn/Duo" value="../system/flows/authn/duo-authn-flow.xml" />
                 <entry key="authn/MFA" value="../system/flows/authn/mfa-authn-flow.xml" />
+                <entry key="authn/Function" value="../system/flows/authn/function-authn-flow.xml" />
           
                 <!-- Master flow for subject c14n. -->
                 <entry key="c14n.events" value="../conf/c14n/subject-c14n-events-flow.xml" />
diff --git a/idp-conf/src/main/resources/system/flows/authn/function-authn-beans.xml b/idp-conf/src/main/resources/system/flows/authn/function-authn-beans.xml
new file mode 100644
index 0000000..7b2cc61
--- /dev/null
+++ b/idp-conf/src/main/resources/system/flows/authn/function-authn-beans.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+       xmlns:context="http://www.springframework.org/schema/context"
+       xmlns:util="http://www.springframework.org/schema/util"
+       xmlns:p="http://www.springframework.org/schema/p"
+       xmlns:c="http://www.springframework.org/schema/c"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
+                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
+                           http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
+                           
+       default-init-method="initialize"
+       default-destroy-method="destroy">
+    
+    <bean class="org.springframework.context.support.PropertySourcesPlaceholderConfigurer"
+        p:placeholderPrefix="%{" p:placeholderSuffix="}" />
+
+    <bean class="net.shibboleth.ext.spring.config.IdentifiableBeanPostProcessor" />
+    <bean class="net.shibboleth.idp.profile.impl.ProfileActionBeanPostProcessor" />
+
+    <import resource="../../../conf/authn/function-authn-config.xml" />
+            
+    <bean id="ValidateFunctionResult" class="net.shibboleth.idp.authn.impl.ValidateFunctionResult" scope="prototype"
+        p:addDefaultPrincipals="#{getObject('shibboleth.authn.Function.addDefaultPrincipals') ?: true}"
+        p:resultCachingPredicate="#{getObject('shibboleth.authn.Function.resultCachingPredicate')}"
+        p:resultLookupStrategy-ref="shibboleth.authn.Function.ResultLookupStrategy" />
+
+    <bean id="PopulateSubjectCanonicalizationContext"
+        class="net.shibboleth.idp.authn.impl.PopulateSubjectCanonicalizationContext" scope="prototype"
+        p:availableFlows-ref="shibboleth.PostLoginSubjectCanonicalizationFlows" />
+
+</beans>
diff --git a/idp-conf/src/main/resources/system/flows/authn/function-authn-flow.xml b/idp-conf/src/main/resources/system/flows/authn/function-authn-flow.xml
new file mode 100644
index 0000000..e6c4072
--- /dev/null
+++ b/idp-conf/src/main/resources/system/flows/authn/function-authn-flow.xml
@@ -0,0 +1,33 @@
+<flow xmlns="http://www.springframework.org/schema/webflow"
+      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+      xsi:schemaLocation="http://www.springframework.org/schema/webflow http://www.springframework.org/schema/webflow/spring-webflow.xsd"
+      parent="authn.abstract">
+
+    <!-- This is a login flow for function-driven authentication. -->
+    
+    <action-state id="CallFunction">
+        <evaluate expression="ValidateFunctionResult" />
+        <evaluate expression="PopulateSubjectCanonicalizationContext" />
+        <evaluate expression="'proceed'" />
+        
+        <transition on="proceed" to="CallSubjectCanonicalization" />
+    </action-state>
+
+    <!-- This runs a c14n step on the result of the authentication. -->
+    <subflow-state id="CallSubjectCanonicalization" subflow="c14n">
+        <input name="calledAsSubflow" value="true" />
+        <transition on="proceed" to="proceed" />
+        
+        <!-- This shouldn't generally happen, but if c14n fails, it's allowable to fall through. -->
+        <transition on="SubjectCanonicalizationError" to="ReselectFlow" />
+    </subflow-state>
+
+    <!-- As a "fall-through" method, remap selected events to select a different flow. -->
+    <global-transitions>
+        <transition on="NoCredentials" to="ReselectFlow" />
+        <transition on="InvalidCredentials" to="ReselectFlow" />
+    </global-transitions>
+
+    <bean-import resource="function-authn-beans.xml" />
+
+</flow>

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


More information about the commits mailing list