[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