[java-plugin-shibd-oidc] branch main updated: Make some progress to agent driven authn request features

Phil Smart philip.smart at jisc.ac.uk
Fri Sep 12 16:34:04 UTC 2025


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

philsmart pushed a commit to branch main
in repository java-plugin-shibd-oidc.

View the commit online:
http://git.shibboleth.net/view/?p=java-plugin-shibd-oidc.git;a=commit;h=5782e7f9f446ab7bb4446af9bcb7819832960e52

The following commit(s) were added to refs/heads/main by this push:
     new 5782e7f  Make some progress to agent driven authn request features
5782e7f is described below

commit 5782e7f9f446ab7bb4446af9bcb7819832960e52
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Fri Sep 12 17:34:02 2025 +0100

    Make some progress to agent driven authn request features
    
     - Just trying this out.
---
 .../messaging/AbstractAgentMessageHandler.java     | 111 +++++++++++++++++++++
 .../AbstractAgentRequestMessageHandler.java        | 101 +++++++++++++++++++
 .../AbstractApplicationMessageHandler.java         |  69 +++++++++++++
 .../sp/oidc/profile/OIDCInitiatorConstants.java    |  57 +++++++++++
 .../sp/oidc/flows/OIDCAuthenticationFlowTest.java  |  25 +++++
 ...CAuthenticationRequestActionMessageHandler.java |  33 +++++-
 .../impl/AddForceAuthenticationHandler.java        |  16 +++
 7 files changed, 407 insertions(+), 5 deletions(-)

diff --git a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/AbstractAgentMessageHandler.java b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/AbstractAgentMessageHandler.java
new file mode 100644
index 0000000..deac1fe
--- /dev/null
+++ b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/AbstractAgentMessageHandler.java
@@ -0,0 +1,111 @@
+/*
+ * 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.sp.oidc.messaging;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.messaging.handler.MessageHandlerException;
+
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.sp.Agent;
+import net.shibboleth.sp.context.AgentRequestContext;
+import net.shibboleth.sp.ddf.DDF;
+import net.shibboleth.sp.messaging.RemotedHttpServletResponse;
+
+/**
+ * TODO javadoc and should we use as a message handler
+ */
+public abstract class AbstractAgentMessageHandler extends AbstractAgentRequestMessageHandler {
+    
+    /** Cached agent from context. */
+    @NonnullBeforeExec private Agent agent;
+
+    /** Whether to create the output objects into which the message will be encoded. */
+    private boolean createOutputObjects;
+    
+    /**
+     * Gets whether to create an output {@link DDF} and {@link RemotedHttpServletResponse}.
+     *
+     * @return whether to create output objects
+     */
+    public boolean isCreateOutputObjects() {
+        return createOutputObjects;
+    }
+    
+    /**
+     * Sets whether to create an output {@link DDF} and {@link RemotedHttpServletResponse}.
+     * 
+     * <p>Defaults to false.</p>
+     * 
+     * @param flag flag to set
+     */
+    public void setCreateOutputObjects(final boolean flag) {
+        checkSetterPreconditions();
+        
+        createOutputObjects = flag;
+    }
+    
+    /** {@inheritDoc}  */
+    @Override
+    protected boolean doPreInvoke(@Nonnull final MessageContext messageContext) throws MessageHandlerException {
+        
+        if (!super.doPreInvoke(messageContext)) {
+            return false;
+        }
+        agent = ensureAgentRequestContext().getAgent();
+        if (agent == null) {
+            throw new MessageHandlerException("No Agent found in context");
+        }
+        
+        return true;
+    }
+    
+    /**
+     * Gets the {@link Agent} for this request.
+     * 
+     * @return the agent, or null
+     */
+    @Nullable public Agent getAgent() {
+        return agent;
+    }
+    
+    /**
+     * Gets the {@link Agent} for this request.
+     * 
+     * @return the agent
+     */
+    @Nonnull public Agent ensureAgent() {
+        return Constraint.isNotNull(agent, "Agent was null");
+    }
+    
+    /**
+     * If {@link #isCreateOutputObjects()} is true, then this ensures an output
+     * {@link DDF} is in place and if creating one, adds the structure and installs
+     * the wrapper for a {@link RemotedHttpServletResponse}.
+     */
+    protected void ensureOutputObjects() {
+        final AgentRequestContext agentRequestContext = ensureAgentRequestContext();
+        if (isCreateOutputObjects() && agentRequestContext.getOutput() == null) {
+            final DDF output = new DDF(null);
+            agentRequestContext.setOutput(output);
+            agentRequestContext.setRemotedHttpServletResponse(new RemotedHttpServletResponse(
+                    output.structure().addmember(RemotedHttpServletResponse.STRUCTURE_NAME)));
+        }
+    }
+
+}
diff --git a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/AbstractAgentRequestMessageHandler.java b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/AbstractAgentRequestMessageHandler.java
new file mode 100644
index 0000000..243ff3d
--- /dev/null
+++ b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/AbstractAgentRequestMessageHandler.java
@@ -0,0 +1,101 @@
+/*
+ * 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.sp.oidc.messaging;
+
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.messaging.context.navigate.ParentContextLookup;
+import org.opensaml.messaging.handler.AbstractMessageHandler;
+import org.opensaml.messaging.handler.MessageHandlerException;
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.sp.context.AgentRequestContext;
+
+/**
+ * TODO javadoc and should we use as a message handler
+ */
+public abstract class AbstractAgentRequestMessageHandler extends AbstractMessageHandler {
+    
+    /** Lookup strategy for {@link AgentRequestContext}. */
+    @Nonnull private Function<MessageContext,AgentRequestContext> agentRequestContextLookupStrategy;
+
+    /** Cached context to populate. */
+    @NonnullBeforeExec private AgentRequestContext agentRequestContext;
+    
+    /**
+     * Gets the cached request context located by the lookup strategy.
+     * 
+     * @return cached request context or null
+     */
+    @Nullable public AgentRequestContext getAgentRequestContext() {
+        return agentRequestContext;
+    }
+
+    /**
+     * Gets the cached request context located by the lookup strategy.
+     * 
+     * @return cached request context
+     */
+    @Nonnull public AgentRequestContext ensureAgentRequestContext() {
+        return Constraint.isNotNull(agentRequestContext, "AgentRequestContext was null");
+    }
+
+    /** Constructor. */
+    protected AbstractAgentRequestMessageHandler() {
+        // By default msgCtx (up)-> ProfileRequestContext (down)-> AgentRequestContext 
+        agentRequestContextLookupStrategy = new ChildContextLookup<>(AgentRequestContext.class)
+                .compose(new ParentContextLookup<>(ProfileRequestContext.class));
+        
+    }
+    
+    /**
+     * Sets the lookup strategy for the {@link AgentRequestContext}.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setAgentRequestContextLookupStrategy(
+            @Nonnull final Function<MessageContext,AgentRequestContext> strategy) {
+        checkSetterPreconditions();
+        
+        agentRequestContextLookupStrategy = Constraint.isNotNull(strategy,
+                "AgentRequestContext lookup strategy cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doPreInvoke(@Nonnull final MessageContext messageContext) throws MessageHandlerException {
+        
+        if (!super.doPreInvoke(messageContext)) {
+            return false;
+        }
+        
+        agentRequestContext = agentRequestContextLookupStrategy.apply(messageContext);
+        if (agentRequestContext == null) {
+            throw new MessageHandlerException("No AgentRequestContext found");
+        }
+        
+        return true;
+    }
+
+
+
+}
diff --git a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/AbstractApplicationMessageHandler.java b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/AbstractApplicationMessageHandler.java
new file mode 100644
index 0000000..2796923
--- /dev/null
+++ b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/AbstractApplicationMessageHandler.java
@@ -0,0 +1,69 @@
+/*
+ * 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.sp.oidc.messaging;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.messaging.handler.MessageHandlerException;
+
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.sp.Application;
+
+/**
+ * TODO javadoc and should we use as a message handler
+ */
+public abstract class AbstractApplicationMessageHandler extends AbstractAgentMessageHandler {
+    
+    /** Cached agent from context. */
+    @NonnullBeforeExec private Application application;
+
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doPreInvoke(@Nonnull final MessageContext messageContext) throws MessageHandlerException {
+        
+        if (!super.doPreInvoke(messageContext)) {
+            return false;
+        }
+        
+        application = ensureAgentRequestContext().getApplication();
+        if (application == null) {
+            throw new MessageHandlerException("No Application found in context");
+        }
+        
+        return true;
+    }
+    
+    /**
+     * Gets the {@link Application} for this request.
+     * 
+     * @return the application, or null
+     */
+    @Nullable public Application getApplication() {
+        return application;
+    }
+    
+    /**
+     * Gets the {@link Application} for this request.
+     * 
+     * @return the application
+     */
+    @Nonnull public Application ensureApplication() {
+        return Constraint.isNotNull(application, "Application was null");
+    }
+
+}
diff --git a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/profile/OIDCInitiatorConstants.java b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/profile/OIDCInitiatorConstants.java
new file mode 100644
index 0000000..46ade91
--- /dev/null
+++ b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/profile/OIDCInitiatorConstants.java
@@ -0,0 +1,57 @@
+/*
+ * 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.sp.oidc.profile;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.saml.saml2.core.AuthnContextClassRef;
+import org.opensaml.saml.saml2.core.NameIDPolicy;
+import org.opensaml.saml.saml2.metadata.NameIDFormat;
+
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+
+/**
+ * Constants for OIDC session initiator operations.
+ * 
+ * TODO these are not all correct for OIDC
+ */
+public final class OIDCInitiatorConstants {
+
+    /** ForceAuthn input parameter. */
+    @Nonnull @NotEmpty public static final String FORCE_AUTHN = "forceAuthn";
+
+    /** IsPassive input parameter. */
+    @Nonnull @NotEmpty public static final String IS_PASSIVE = "isPassive";
+
+    /** authnContextClassRef input parameter. */
+    @Nonnull @NotEmpty public static final String AUTHN_CONTEXT_CLASS_REF = AuthnContextClassRef.DEFAULT_ELEMENT_LOCAL_NAME;
+
+    /** authnContextComparison input parameter. */
+    @Nonnull @NotEmpty public static final String AUTHN_CONTEXT_COMPARISON = "authnContextComparison";
+
+    /** AttributeConsumingServiceIndex input parameter. */
+    @Nonnull @NotEmpty public static final String ATTRIBUTE_INDEX = "attributeIndex";
+
+    /** NameIDFormat input parameter. */
+    @Nonnull @NotEmpty public static final String NAMEID_FORMAT = NameIDFormat.DEFAULT_ELEMENT_LOCAL_NAME;
+
+    /** SPNameQualifier input parameter. */
+    @Nonnull @NotEmpty public static final String SP_NAME_QUALIFIER = NameIDPolicy.SP_NAME_QUALIFIER_ATTRIB_NAME;
+
+    /** Private constructor. */
+    private OIDCInitiatorConstants() {
+     
+    }
+}
diff --git a/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/OIDCAuthenticationFlowTest.java b/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/OIDCAuthenticationFlowTest.java
index cb77560..6b13737 100644
--- a/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/OIDCAuthenticationFlowTest.java
+++ b/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/OIDCAuthenticationFlowTest.java
@@ -40,6 +40,7 @@ import net.shibboleth.sp.ddf.DDF;
 import net.shibboleth.sp.flows.AbstractSPFlowTest;
 import net.shibboleth.sp.messaging.RemotedHttpServletRequest;
 import net.shibboleth.sp.messaging.RemotedHttpServletResponse;
+import net.shibboleth.sp.oidc.profile.OIDCInitiatorConstants;
 import net.shibboleth.sp.profile.InitiatorConstants;
 import net.shibboleth.sp.profile.SPConstants;
 import net.shibboleth.sp.profile.impl.IssueCorrelationCookie;
@@ -95,6 +96,30 @@ public class OIDCAuthenticationFlowTest extends AbstractSPFlowTest {
         final AuthorizationRequest req = validateOutputMessage(result);
     }
     
+    /**
+     * Basic flow test with forceAuthn.
+     * 
+     * @throws IOException on error
+     * @throws MessageDecodingException 
+     */
+    @Test
+    public void testForceAuthnFromAgent() throws IOException, MessageDecodingException {
+        setDefaultAuth();
+        
+        final DDF input = new DDF(null).structure();
+        input.addmember(RemotedHttpServletRequest.STRUCTURE_NAME).structure();        
+        input.addmember(InitiatorConstants.RESPONSE_URL).string(RESPONSE_URL);
+        input.addmember(SPConstants.TARGET).unsafe_string(RESOURCE_URL); 
+        input.addmember(OIDCInitiatorConstants.FORCE_AUTHN).integer(1);
+        setApplicationRequest("test-oidc-application", input);
+
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertFlowExecutionResult(result, FLOW_ID);
+        assertFlowExecutionOutcome(result.getOutcome());
+        
+        final AuthorizationRequest req = validateOutputMessage(result);
+    }
+    
     /**
      * Decode an encoded response and run sanity checks against it.
      * 
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/messaging/impl/AbstractOIDCAuthenticationRequestActionMessageHandler.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/messaging/impl/AbstractOIDCAuthenticationRequestActionMessageHandler.java
index fd97208..1c1fb0f 100644
--- a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/messaging/impl/AbstractOIDCAuthenticationRequestActionMessageHandler.java
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/messaging/impl/AbstractOIDCAuthenticationRequestActionMessageHandler.java
@@ -22,7 +22,6 @@ import javax.annotation.Nullable;
 
 import org.opensaml.messaging.context.MessageContext;
 import org.opensaml.messaging.context.navigate.ChildContextLookup;
-import org.opensaml.messaging.handler.AbstractMessageHandler;
 import org.opensaml.messaging.handler.MessageHandlerException;
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.opensaml.profile.context.navigate.ParentProfileRequestContextLookup;
@@ -41,6 +40,8 @@ import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
 import net.shibboleth.shared.logic.Constraint;
 import net.shibboleth.shared.primitive.LoggerFactory;
 import net.shibboleth.shared.primitive.NonnullSupplier;
+import net.shibboleth.sp.ddf.DDF;
+import net.shibboleth.sp.oidc.messaging.AbstractApplicationMessageHandler;
 
 /** 
  * An abstract message handler that runs inside an {@link WebFlowMessageHandlerAdaptor}
@@ -49,7 +50,7 @@ import net.shibboleth.shared.primitive.NonnullSupplier;
  * <p>The {@link MessageContext} will either be INBOUND or OUTBOUND depending on the direction defined
  * by the calling {@link WebFlowMessageHandlerAdaptor} action.</p>
  */
-public abstract class AbstractOIDCAuthenticationRequestActionMessageHandler extends AbstractMessageHandler {    
+public abstract class AbstractOIDCAuthenticationRequestActionMessageHandler extends AbstractApplicationMessageHandler {    
     
     /** Lookup function for parent ProfileRequestContext. */
     @Nonnull private static final ParentProfileRequestContextLookup<MessageContext> PRC_LOOKUP
@@ -77,11 +78,13 @@ public abstract class AbstractOIDCAuthenticationRequestActionMessageHandler exte
     /** Current HTTP request, if available. */
     @Nullable private NonnullSupplier<HttpServletRequest> httpServletRequestSupplier;
     
+    /** Input message from agent. */
+    @NonnullBeforeExec private DDF input;
+    
     /** Constructor.*/
     protected AbstractOIDCAuthenticationRequestActionMessageHandler() {
         providerMetadataLookupStrategy = new ChildContextLookup<>(OIDCProviderMetadataContext.class).compose(
-                new ChildContextLookup<>(OIDCPeerEntityContext.class));
-        
+                new ChildContextLookup<>(OIDCPeerEntityContext.class));        
         relyingPartyContextLookupStrategy = new ChildContextLookup<>(RelyingPartyContext.class);
     }
     
@@ -219,8 +222,23 @@ public abstract class AbstractOIDCAuthenticationRequestActionMessageHandler exte
         return Predicates.compose(predicate::test, PRC_LOOKUP::apply);
     }
     
+
+    /**
+     * Get the input message from the agent.
+     * 
+     * @return input message
+     */
+    @NonnullBeforeExec public DDF getInput() {
+        return input;
+    }
+    
+    
     @Override
     protected boolean doPreInvoke(@Nonnull final MessageContext messageContext) throws MessageHandlerException {
+        
+        if (!super.doPreInvoke(messageContext)) {
+            return false;
+        }
 
         if (!(messageContext.getMessage() instanceof OIDCAuthenticationRequest)) {
             throw new MessageHandlerException("Message was not an authentication request");
@@ -247,7 +265,12 @@ public abstract class AbstractOIDCAuthenticationRequestActionMessageHandler exte
             throw new MessageHandlerException("Profile configuration could not found");
         }
         
-        return super.doPreInvoke(messageContext);
+        input = ensureAgentRequestContext().getInput();
+        if (input == null) {
+            throw new MessageHandlerException("No input message");
+        }
+        
+        return true;
     }
     
 
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/messaging/impl/AddForceAuthenticationHandler.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/messaging/impl/AddForceAuthenticationHandler.java
index 6b4f0a6..6e1025c 100644
--- a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/messaging/impl/AddForceAuthenticationHandler.java
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/messaging/impl/AddForceAuthenticationHandler.java
@@ -26,6 +26,7 @@ import com.nimbusds.oauth2.sdk.ParseException;
 import com.nimbusds.openid.connect.sdk.Prompt;
 
 import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.sp.oidc.profile.OIDCInitiatorConstants;
 
 /** 
  * An action that sets the 'prompt' parameter to 'login' and max_age to 0 seconds, iff force authn was requested by the 
@@ -39,6 +40,21 @@ public class AddForceAuthenticationHandler extends AbstractOIDCAuthenticationReq
     @Override protected void doInvoke(@Nonnull final MessageContext messageContext) 
             throws MessageHandlerException {
         
+        final Integer forceAuthn = getInput().getmember(OIDCInitiatorConstants.FORCE_AUTHN).integer();
+        
+        if (forceAuthn != null) {
+            // TODO disallow feature if set but not allowed
+            try {
+                getAuthenticationRequest().setPrompt(Prompt.parse(Prompt.Type.LOGIN.toString()));
+                getAuthenticationRequest().setMaxAge(Duration.ofSeconds(0));
+            } catch (final ParseException e) {
+                // This should never happen
+                throw new MessageHandlerException("Unable to honour force-authn, "
+                        + "setting prompt to force-login as failed", e);
+            }
+            return;
+        }
+        // Else try from Profile config
         if (getProfileConfiguration().isForceAuthn(lookupProfileRequestContext(messageContext))) {
             log.trace("{} Setting prompt=login and max_age=0 (ForceAuthn) for OIDC AuthnRequest", getLogPrefix());
             try {

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


More information about the commits mailing list