[java-identity-provider] branch main updated: IDP-1919 - CAS validation flows don't run consent checking interceptor

Scott Cantor cantor.2 at osu.edu
Wed Mar 16 17:16:27 UTC 2022


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

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

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

The following commit(s) were added to refs/heads/main by this push:
     new f38ca927c IDP-1919 - CAS validation flows don't run consent checking interceptor
f38ca927c is described below

commit f38ca927cd6d76dcbaf5523f828f517b1a330a12
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Wed Mar 16 13:16:23 2022 -0400

    IDP-1919 - CAS validation flows don't run consent checking interceptor
    
    https://shibboleth.atlassian.net/browse/IDP-1919
    
    Shore up by optionally storing consented IDs in TicketState.
---
 .../idp/attribute/context/AttributeContext.java    | 10 ++--
 .../idp/cas/config/LoginConfiguration.java         | 39 ++++++++++++++
 .../net/shibboleth/idp/cas/ticket/TicketState.java | 60 ++++++++++++++++------
 .../cas/flow/impl/GrantServiceTicketAction.java    | 49 +++++++++++++++++-
 .../PrepareTicketValidationResponseAction.java     | 16 +++++-
 .../impl/AbstractTicketSerializer.java             | 48 +++++++++++++----
 .../impl/ServiceTicketSerializerTest.java          | 43 ++++++++++++++++
 .../shibboleth/idp/conf/relying-party-mddriven.xml |  8 +++
 .../idp/test/flows/cas/SamlValidateFlowTest.java   | 34 ++++++++++++
 .../test/flows/cas/ServiceValidateFlowTest.java    | 42 ++++++++++++++-
 10 files changed, 314 insertions(+), 35 deletions(-)

diff --git a/idp-attribute-api/src/main/java/net/shibboleth/idp/attribute/context/AttributeContext.java b/idp-attribute-api/src/main/java/net/shibboleth/idp/attribute/context/AttributeContext.java
index bd5de7157..ec47185ea 100644
--- a/idp-attribute-api/src/main/java/net/shibboleth/idp/attribute/context/AttributeContext.java
+++ b/idp-attribute-api/src/main/java/net/shibboleth/idp/attribute/context/AttributeContext.java
@@ -121,12 +121,9 @@ public final class AttributeContext extends BaseContext {
     }
     
     /**
-     * Gets whether attribute release consent was obtained from the subject during this request.
+     * Gets whether attribute release consent was obtained from the subject during this request but not stored.
      * 
-     * <p>This may be false if consent was obtained during a prior request, but is a signal to later
-     * actions that consent may have been obtained but not stored.</p>
-     * 
-     * @return true iff consent was obtained during this request
+     * @return true iff consent was obtained during this request but not stored
      * 
      * @since 4.2.0
      */
@@ -135,7 +132,7 @@ public final class AttributeContext extends BaseContext {
     }
     
     /**
-     * Sets whether attribute release consent was obtained from the subject during this request.
+     * Sets whether attribute release consent was obtained from the subject during this request but not stored.
      * 
      * @param flag flag to set
      * 
@@ -148,4 +145,5 @@ public final class AttributeContext extends BaseContext {
         
         return this;
     }
+    
 }
\ No newline at end of file
diff --git a/idp-cas-api/src/main/java/net/shibboleth/idp/cas/config/LoginConfiguration.java b/idp-cas-api/src/main/java/net/shibboleth/idp/cas/config/LoginConfiguration.java
index c3fb73b09..b5ebdd196 100644
--- a/idp-cas-api/src/main/java/net/shibboleth/idp/cas/config/LoginConfiguration.java
+++ b/idp-cas-api/src/main/java/net/shibboleth/idp/cas/config/LoginConfiguration.java
@@ -74,6 +74,9 @@ public class LoginConfiguration extends AbstractProtocolConfiguration
     /** Whether to mandate forced authentication for the request. */
     @Nonnull private Predicate<ProfileRequestContext> forceAuthnPredicate;
 
+    /** Whether to store consent in service tickets. */
+    @Nonnull private Predicate<ProfileRequestContext> storeConsentInTicketsPredicate;
+
     /** Lookup function to supply proxyCount property. */
     @Nonnull private Function<ProfileRequestContext,Integer> proxyCountLookupStrategy;
     
@@ -85,6 +88,7 @@ public class LoginConfiguration extends AbstractProtocolConfiguration
         postAuthenticationFlowsLookupStrategy = FunctionSupport.constant(null);
         defaultAuthenticationContextsLookupStrategy = FunctionSupport.constant(null);
         forceAuthnPredicate = Predicates.alwaysFalse();
+        storeConsentInTicketsPredicate = Predicates.alwaysFalse();
         proxyCountLookupStrategy = FunctionSupport.constant(null);
     }
 
@@ -215,6 +219,41 @@ public class LoginConfiguration extends AbstractProtocolConfiguration
         forceAuthnPredicate = Constraint.isNotNull(condition, "Forced authentication predicate cannot be null");
     }
 
+    /**
+     * Get whether to store consent in service tickets.
+     * 
+     * @param profileRequestContext profile request context
+     * 
+     * @return whether to store consent in service tickets
+     * 
+     * @since 4.2.0
+     */
+    public boolean isStoreConsentInTickets(@Nullable final ProfileRequestContext profileRequestContext) {
+        return storeConsentInTicketsPredicate.test(profileRequestContext);
+    }
+
+    /**
+     * Set whether to store consent in service tickets.
+     * 
+     * @param flag flag to set
+     * 
+     * @since 4.2.0
+     */
+    public void setStoreConsentInTickets(final boolean flag) {
+        storeConsentInTicketsPredicate = flag ? Predicates.alwaysTrue() : Predicates.alwaysFalse();
+    }
+
+    /**
+     * Set condition for whether to store consent in service tickets.
+     * 
+     * @param condition condition to set
+     * 
+     * @since 4.2.0
+     */
+    public void setStoreConsentInTicketsPredicate(@Nonnull final Predicate<ProfileRequestContext> condition) {
+        storeConsentInTicketsPredicate = Constraint.isNotNull(condition, "Condition cannot be null");
+    }
+    
     /** {@inheritDoc} */
     @Nullable public Integer getProxyCount(@Nullable final ProfileRequestContext profileRequestContext) {
         final Integer count = proxyCountLookupStrategy.apply(profileRequestContext);
diff --git a/idp-cas-api/src/main/java/net/shibboleth/idp/cas/ticket/TicketState.java b/idp-cas-api/src/main/java/net/shibboleth/idp/cas/ticket/TicketState.java
index c79d4a7cc..4e78db55a 100644
--- a/idp-cas-api/src/main/java/net/shibboleth/idp/cas/ticket/TicketState.java
+++ b/idp-cas-api/src/main/java/net/shibboleth/idp/cas/ticket/TicketState.java
@@ -18,10 +18,18 @@
 package net.shibboleth.idp.cas.ticket;
 
 import java.time.Instant;
+import java.util.Collection;
 import java.util.Objects;
+import java.util.Set;
+
 import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
 
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotLive;
+import net.shibboleth.utilities.java.support.annotation.constraint.Unmodifiable;
 import net.shibboleth.utilities.java.support.logic.Constraint;
+import net.shibboleth.utilities.java.support.primitive.StringSupport;
 
 /**
  * Supplemental state data to be stored with a ticket.
@@ -31,21 +39,19 @@ import net.shibboleth.utilities.java.support.logic.Constraint;
 public class TicketState {
 
     /** ID of session in which ticket is created. */
-    @Nonnull
-    private String sessId;
+    @Nonnull private String sessId;
 
     /** Canonical authenticated principal name. */
-    @Nonnull
-    private String authenticatedPrincipalName;
+    @Nonnull private String authenticatedPrincipalName;
 
     /** Authentication instant. */
-    @Nonnull
-    private Instant authenticationInstant;
+    @Nonnull private Instant authenticationInstant;
 
     /** Authentication method ID/name/description. */
-    @Nonnull
-    private String authenticationMethod;
+    @Nonnull private String authenticationMethod;
 
+    /** Attribute IDs that were consented to during the ticket request. */
+    @Nonnull @NonnullElements private Set<String> consentedAttributeIds;
 
     /**
      * Creates a new instance with required fields.
@@ -71,8 +77,7 @@ public class TicketState {
      *
      * @return IdP session ID.
      */
-    @Nonnull
-    public String getSessionId() {
+    @Nonnull public String getSessionId() {
         return sessId;
     }
 
@@ -81,8 +86,7 @@ public class TicketState {
      *
      * @return Canonical principal.
      */
-    @Nonnull
-    public String getPrincipalName() {
+    @Nonnull public String getPrincipalName() {
         return authenticatedPrincipalName;
     }
 
@@ -91,8 +95,7 @@ public class TicketState {
      *
      * @return Principal authentication instant.
      */
-    @Nonnull
-    public Instant getAuthenticationInstant() {
+    @Nonnull public Instant getAuthenticationInstant() {
         return authenticationInstant;
     }
 
@@ -101,11 +104,36 @@ public class TicketState {
      *
      * @return Principal authentication method.
      */
-    @Nonnull
-    public String getAuthenticationMethod() {
+    @Nonnull public String getAuthenticationMethod() {
         return authenticationMethod;
     }
 
+    /**
+     * Get the attribute IDs that were consented to during the request.
+     * 
+     * @return immutable set of attribute IDs
+     * 
+     * @since 4.2.0
+     */
+    @Nullable @NonnullElements @Unmodifiable @NotLive public Set<String> getConsentedAttributeIds() {
+        return consentedAttributeIds;
+    }
+    
+    /**
+     * Set the attribute IDs that were consented to during the request.
+     * 
+     * @param attributeIds attribute IDs
+     * 
+     * @since 4.2.0
+     */
+    public void setConsentedAttributeIds(@Nullable @NonnullElements final Collection<String> attributeIds) {
+        if (attributeIds != null) {
+            consentedAttributeIds = Set.copyOf(StringSupport.normalizeStringCollection(attributeIds));
+        } else {
+            consentedAttributeIds = null;
+        }
+    }
+    
     @Override
     public boolean equals(final Object o) {
         if (o instanceof TicketState) {
diff --git a/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/flow/impl/GrantServiceTicketAction.java b/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/flow/impl/GrantServiceTicketAction.java
index 3003fff84..8fdd96f78 100644
--- a/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/flow/impl/GrantServiceTicketAction.java
+++ b/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/flow/impl/GrantServiceTicketAction.java
@@ -23,6 +23,7 @@ import java.util.function.Function;
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
+import net.shibboleth.idp.attribute.context.AttributeContext;
 import net.shibboleth.idp.authn.AuthenticationResult;
 import net.shibboleth.idp.authn.AuthnEventIds;
 import net.shibboleth.idp.authn.context.AuthenticationContext;
@@ -38,8 +39,10 @@ import net.shibboleth.idp.cas.ticket.TicketService;
 import net.shibboleth.idp.cas.ticket.TicketState;
 import net.shibboleth.idp.profile.IdPEventIds;
 import net.shibboleth.idp.profile.config.SecurityConfiguration;
+import net.shibboleth.idp.profile.context.RelyingPartyContext;
 import net.shibboleth.idp.session.IdPSession;
 import net.shibboleth.idp.session.context.SessionContext;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
 import net.shibboleth.utilities.java.support.logic.Constraint;
 import org.opensaml.messaging.context.navigate.ChildContextLookup;
 import org.opensaml.profile.action.ActionSupport;
@@ -75,6 +78,9 @@ public class GrantServiceTicketAction extends AbstractCASProtocolAction<ServiceT
     /** Function to retrieve subject principal name. */
     @Nonnull private final Function<ProfileRequestContext, String> principalLookupFunction;
 
+    /** Strategy used to locate the {@link AttributeContext} associated with a given {@link ProfileRequestContext}. */
+    @Nonnull private Function<ProfileRequestContext,AttributeContext> attributeContextLookupStrategy;
+
     /** Manages CAS tickets. */
     @Nonnull private final TicketService casTicketService;
 
@@ -89,7 +95,13 @@ public class GrantServiceTicketAction extends AbstractCASProtocolAction<ServiceT
     
     /** Authentication result. */
     @Nullable private AuthenticationResult authnResult;
+
+    /** Whether consent needs to be stored in ticket. */
+    private boolean storeConsent;
     
+    /** AttributeContext to use. */
+    @Nullable private AttributeContext attributeCtx;
+
     /** CAS request. */
     @Nullable private ServiceTicketRequest request;
 
@@ -106,6 +118,25 @@ public class GrantServiceTicketAction extends AbstractCASProtocolAction<ServiceT
         authnCtxLookupFunction = new ChildContextLookup<>(AuthenticationContext.class);
         principalLookupFunction = new SubjectContextPrincipalLookupFunction().compose(
                 new ChildContextLookup<>(SubjectContext.class));
+        attributeContextLookupStrategy = new ChildContextLookup<>(AttributeContext.class).compose(
+                new ChildContextLookup<>(RelyingPartyContext.class));
+    }
+    
+    /**
+     * Set the strategy used to locate the {@link AttributeContext} associated with a given
+     * {@link ProfileRequestContext}.
+     * 
+     * @param strategy strategy used to locate the {@link AttributeContext} associated with a given
+     *            {@link ProfileRequestContext}
+     *            
+     * @since 4.2.0
+     */
+    public void setAttributeContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, AttributeContext> strategy) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+        attributeContextLookupStrategy =
+                Constraint.isNotNull(strategy, "AttributeContext lookup strategy cannot be null");
     }
 
     @Override
@@ -155,7 +186,18 @@ public class GrantServiceTicketAction extends AbstractCASProtocolAction<ServiceT
             ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
             return false;
         }
-        
+
+        if (loginConfig.getPostAuthenticationFlows(profileRequestContext).contains("attribute-release")) {
+            attributeCtx = attributeContextLookupStrategy.apply(profileRequestContext);
+            if (attributeCtx != null) {
+                storeConsent = attributeCtx.isConsented() || loginConfig.isStoreConsentInTickets(profileRequestContext);
+                if (storeConsent) {
+                    log.debug("{} Storing consented attribute IDs into ticket: {}", getLogPrefix(),
+                            attributeCtx.getIdPAttributes().keySet());
+                }
+            }
+        }
+
         return true;
     }    
     
@@ -170,6 +212,11 @@ public class GrantServiceTicketAction extends AbstractCASProtocolAction<ServiceT
                     getPrincipalName(profileRequestContext),
                     authnResult.getAuthenticationInstant(),
                     authnResult.getAuthenticationFlowId());
+            
+            if (storeConsent) {
+                state.setConsentedAttributeIds(attributeCtx.getIdPAttributes().keySet());
+            }
+            
             ticket = casTicketService.createServiceTicket(
                     securityConfig.getIdGenerator().generateIdentifier(),
                     Instant.now().plus(loginConfig.getTicketValidityPeriod(profileRequestContext)),
diff --git a/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/flow/impl/PrepareTicketValidationResponseAction.java b/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/flow/impl/PrepareTicketValidationResponseAction.java
index f14c492c0..d4a2fb5c3 100644
--- a/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/flow/impl/PrepareTicketValidationResponseAction.java
+++ b/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/flow/impl/PrepareTicketValidationResponseAction.java
@@ -20,6 +20,7 @@ package net.shibboleth.idp.cas.flow.impl;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Set;
 import java.util.function.Function;
 
 import javax.annotation.Nonnull;
@@ -43,6 +44,7 @@ import net.shibboleth.idp.cas.protocol.ProtocolError;
 import net.shibboleth.idp.cas.protocol.TicketValidationRequest;
 import net.shibboleth.idp.cas.protocol.TicketValidationResponse;
 import net.shibboleth.idp.cas.ticket.TicketPrincipalLookupFunction;
+import net.shibboleth.idp.cas.ticket.TicketState;
 import net.shibboleth.idp.profile.IdPEventIds;
 import net.shibboleth.idp.profile.context.RelyingPartyContext;
 import net.shibboleth.utilities.java.support.annotation.constraint.Live;
@@ -94,6 +96,9 @@ public class PrepareTicketValidationResponseAction extends
     /** Stored off context from request. */
     @Nullable private AttributeContext attributeContext;
     
+    /** Stored consented attributes from ticket. */
+    @Nullable private Set<String> consentedAttributeIds;
+    
     /** Profile configuration. */
     @Nullable private ValidateConfiguration validateConfiguration;
     
@@ -157,6 +162,10 @@ public class PrepareTicketValidationResponseAction extends
         
         try {
             response = getCASResponse(profileRequestContext);
+            final TicketState state = getCASTicket(profileRequestContext).getTicketState();
+            if (state != null) {
+                consentedAttributeIds = state.getConsentedAttributeIds();
+            }
         } catch (final EventException e) {
             ActionSupport.buildEvent(profileRequestContext, e.getEventID());
             return false;
@@ -213,7 +222,12 @@ public class PrepareTicketValidationResponseAction extends
                 return;
             }
             for (final IdPAttribute attribute : inputAttributes) {
-                encodeAttribute(component.getComponent(), profileRequestContext, attribute, encodedAttributes);
+                if (consentedAttributeIds == null || consentedAttributeIds.contains(attribute.getId())) {
+                    encodeAttribute(component.getComponent(), profileRequestContext, attribute, encodedAttributes);
+                } else {
+                    log.info("{} Skipping attribute {} not in stored consent list from ticket", getLogPrefix(),
+                            attribute.getId());
+                }
             }
         } finally {
             if (null != component) {
diff --git a/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/ticket/serialization/impl/AbstractTicketSerializer.java b/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/ticket/serialization/impl/AbstractTicketSerializer.java
index 01e606c84..3dbccc5ad 100644
--- a/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/ticket/serialization/impl/AbstractTicketSerializer.java
+++ b/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/ticket/serialization/impl/AbstractTicketSerializer.java
@@ -21,14 +21,19 @@ import java.io.IOException;
 import java.io.StringReader;
 import java.io.StringWriter;
 import java.time.Instant;
+import java.util.HashSet;
+import java.util.Set;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 import javax.json.Json;
+import javax.json.JsonArray;
 import javax.json.JsonException;
 import javax.json.JsonObject;
 import javax.json.JsonReader;
 import javax.json.JsonReaderFactory;
+import javax.json.JsonString;
+import javax.json.JsonValue;
 import javax.json.stream.JsonGenerator;
 import javax.json.stream.JsonGeneratorFactory;
 
@@ -52,28 +57,31 @@ import org.slf4j.LoggerFactory;
 public abstract class AbstractTicketSerializer<T extends Ticket> implements StorageSerializer<T> {
 
     /** Service field name. */
-    private static final String SERVICE_FIELD = "rp";
+    @Nonnull @NotEmpty private static final String SERVICE_FIELD = "rp";
 
     /** Expiration instant field name. */
-    private static final String EXPIRATION_FIELD = "exp";
+    @Nonnull @NotEmpty private static final String EXPIRATION_FIELD = "exp";
 
     /** Supplemental ticket state field name. */
-    private static final String STATE_FIELD = "ts";
+    @Nonnull @NotEmpty private static final String STATE_FIELD = "ts";
 
     /** Session ID field name. */
-    private static final String SESSION_FIELD = "sid";
+    @Nonnull @NotEmpty private static final String SESSION_FIELD = "sid";
 
     /** Authenticated canonical principal name field. */
-    private static final String PRINCIPAL_FIELD = "p";
+    @Nonnull @NotEmpty private static final String PRINCIPAL_FIELD = "p";
 
     /** Authentication instant field name. */
-    private static final String AUTHN_INSTANT_FIELD = "ai";
+    @Nonnull @NotEmpty private static final String AUTHN_INSTANT_FIELD = "ai";
 
     /** Authentication method field name. */
-    private static final String AUTHN_METHOD_FIELD = "am";
+    @Nonnull @NotEmpty private static final String AUTHN_METHOD_FIELD = "am";
+    
+    /** Consented attribute IDs field name. */
+    @Nonnull @NotEmpty private static final String CONSENTED_ATTRS_FIELD = "con";
 
     /** Logger instance. */
-    private final Logger logger = LoggerFactory.getLogger(AbstractTicketSerializer.class);
+    @Nonnull private final Logger logger = LoggerFactory.getLogger(AbstractTicketSerializer.class);
 
     /** JSON generator factory. */
     @Nonnull
@@ -99,13 +107,23 @@ public abstract class AbstractTicketSerializer<T extends Ticket> implements Stor
             gen.writeStartObject()
                     .write(SERVICE_FIELD, ticket.getService())
                     .write(EXPIRATION_FIELD, ticket.getExpirationInstant().toEpochMilli());
+            
             if (ticket.getTicketState() != null) {
                 gen.writeStartObject(STATE_FIELD)
                         .write(SESSION_FIELD, ticket.getTicketState().getSessionId())
                         .write(PRINCIPAL_FIELD, ticket.getTicketState().getPrincipalName())
                         .write(AUTHN_INSTANT_FIELD, ticket.getTicketState().getAuthenticationInstant().toEpochMilli())
-                        .write(AUTHN_METHOD_FIELD, ticket.getTicketState().getAuthenticationMethod())
-                        .writeEnd();
+                        .write(AUTHN_METHOD_FIELD, ticket.getTicketState().getAuthenticationMethod());
+                
+                if (ticket.getTicketState().getConsentedAttributeIds() != null) {
+                    gen.writeStartArray(CONSENTED_ATTRS_FIELD);
+                    for (final String id : ticket.getTicketState().getConsentedAttributeIds()) {
+                        gen.write(id);
+                    }
+                    gen.writeEnd();
+                }
+                
+                gen.writeEnd();
             }
             serializeInternal(gen, ticket);
             gen.writeEnd();
@@ -137,6 +155,16 @@ public abstract class AbstractTicketSerializer<T extends Ticket> implements Stor
                         so.getString(PRINCIPAL_FIELD),
                         Instant.ofEpochMilli(so.getJsonNumber(AUTHN_INSTANT_FIELD).longValueExact()),
                         so.getString(AUTHN_METHOD_FIELD));
+                final JsonValue consent = so.get(CONSENTED_ATTRS_FIELD);
+                if (consent instanceof JsonArray) {
+                    final Set<String> idset = new HashSet<>();
+                    for (final JsonValue id : (JsonArray) consent) {
+                        if (id instanceof JsonString) {
+                            idset.add(((JsonString) id).getString());
+                        }
+                    }
+                    state.setConsentedAttributeIds(idset);
+                }
             } else {
                 state = null;
             }
diff --git a/idp-cas-impl/src/test/java/net/shibboleth/idp/cas/ticket/serialization/impl/ServiceTicketSerializerTest.java b/idp-cas-impl/src/test/java/net/shibboleth/idp/cas/ticket/serialization/impl/ServiceTicketSerializerTest.java
index 3692df1d6..21a13590c 100644
--- a/idp-cas-impl/src/test/java/net/shibboleth/idp/cas/ticket/serialization/impl/ServiceTicketSerializerTest.java
+++ b/idp-cas-impl/src/test/java/net/shibboleth/idp/cas/ticket/serialization/impl/ServiceTicketSerializerTest.java
@@ -25,6 +25,8 @@ import static org.testng.Assert.*;
 
 import java.time.Instant;
 import java.time.temporal.ChronoUnit;
+import java.util.Collections;
+import java.util.Set;
 
 /**
  * Unit test for {@link ServiceTicketSerializer}.
@@ -66,4 +68,45 @@ public class ServiceTicketSerializerTest {
         assertEquals(st2.isRenew(), st1.isRenew());
         assertEquals(st2.getTicketState(), st1.getTicketState());
     }
+
+    @Test
+    public void testSerializeWithConsent() throws Exception {
+        final ServiceTicket st1 = new ServiceTicket(
+                "ST-0123456789-e6342d467a4414e599aa3c323528e96f",
+                "https://nobody.example.org",
+                Instant.now().truncatedTo(ChronoUnit.MILLIS),
+                true);
+        final TicketState state = new TicketState("idpsess-d2db22058dc178d3b917363859e", "bob",
+                Instant.now().truncatedTo(ChronoUnit.MILLIS), "Password");
+        state.setConsentedAttributeIds(Set.of("foo", "bar"));
+        st1.setTicketState(state);
+        final String serialized = serializer.serialize(st1);
+        final ServiceTicket st2 = serializer.deserialize(1, "notused", st1.getId(), serialized, null);
+        assertEquals(st2.getId(), st1.getId());
+        assertEquals(st2.getService(), st1.getService());
+        assertEquals(st2.getExpirationInstant(), st1.getExpirationInstant());
+        assertEquals(st2.isRenew(), st1.isRenew());
+        assertEquals(st2.getTicketState(), st1.getTicketState());
+    }
+
+    @Test
+    public void testSerializeWithEmptyConsent() throws Exception {
+        final ServiceTicket st1 = new ServiceTicket(
+                "ST-0123456789-e6342d467a4414e599aa3c323528e96f",
+                "https://nobody.example.org",
+                Instant.now().truncatedTo(ChronoUnit.MILLIS),
+                true);
+        final TicketState state = new TicketState("idpsess-d2db22058dc178d3b917363859e", "bob",
+                Instant.now().truncatedTo(ChronoUnit.MILLIS), "Password");
+        state.setConsentedAttributeIds(Collections.emptySet());
+        st1.setTicketState(state);
+        final String serialized = serializer.serialize(st1);
+        final ServiceTicket st2 = serializer.deserialize(1, "notused", st1.getId(), serialized, null);
+        assertEquals(st2.getId(), st1.getId());
+        assertEquals(st2.getService(), st1.getService());
+        assertEquals(st2.getExpirationInstant(), st1.getExpirationInstant());
+        assertEquals(st2.isRenew(), st1.isRenew());
+        assertEquals(st2.getTicketState(), st1.getTicketState());
+    }
+
 }
\ No newline at end of file
diff --git a/idp-conf-impl/src/main/resources/net/shibboleth/idp/conf/relying-party-mddriven.xml b/idp-conf-impl/src/main/resources/net/shibboleth/idp/conf/relying-party-mddriven.xml
index 3f195fa9b..ada8951cc 100644
--- a/idp-conf-impl/src/main/resources/net/shibboleth/idp/conf/relying-party-mddriven.xml
+++ b/idp-conf-impl/src/main/resources/net/shibboleth/idp/conf/relying-party-mddriven.xml
@@ -668,6 +668,14 @@
                 <constructor-arg value="false" />
             </bean>
         </property>
+        <property name="storeConsentInTicketsPredicate">
+            <bean class="net.shibboleth.utilities.java.support.logic.PredicateSupport" factory-method="fromFunction">
+                <constructor-arg>
+                    <bean parent="shibboleth.MDDrivenBoolProperty" p:propertyName="storeConsentInTickets" />
+                </constructor-arg>
+                <constructor-arg value="false" />
+            </bean>
+        </property>
         <property name="proxyCountLookupStrategy">
             <bean parent="shibboleth.MDDrivenIntProperty" p:propertyName="proxyCount" />
         </property>
diff --git a/idp-conf/src/test/java/net/shibboleth/idp/test/flows/cas/SamlValidateFlowTest.java b/idp-conf/src/test/java/net/shibboleth/idp/test/flows/cas/SamlValidateFlowTest.java
index c05ac4a5e..3e274b316 100644
--- a/idp-conf/src/test/java/net/shibboleth/idp/test/flows/cas/SamlValidateFlowTest.java
+++ b/idp-conf/src/test/java/net/shibboleth/idp/test/flows/cas/SamlValidateFlowTest.java
@@ -38,6 +38,7 @@ import javax.annotation.Nonnull;
 import static org.testng.Assert.*;
 
 import java.time.Instant;
+import java.util.Set;
 
 /**
  * Tests the flow behind the <code>/samlValidate</code> endpoint.
@@ -100,6 +101,39 @@ public class SamlValidateFlowTest extends AbstractFlowTest {
         assertPopulatedAttributeContext((ProfileRequestContext) outcome.getOutput().get(END_STATE_OUTPUT_ATTR_NAME));
     }
 
+    @Test
+    public void testSuccessWithConsent() throws Exception {
+        final String principal = "john";
+        final IdPSession session = sessionManager.createSession(principal);
+        final TicketState state = new TicketState(session.getId(), principal, Instant.now(), "Password");
+        state.setConsentedAttributeIds(Set.of("uid", "eduPersonPrincipalName"));
+        final ServiceTicket ticket = ticketService.createServiceTicket(
+                "ST-1415133132-ompog68ygxKyX9BPwPuw0hESQBjuA",
+                Instant.now().plusSeconds(5),
+                "https://test.example.org/",
+                state,
+                false);
+        final String requestBody = SAML_REQUEST_TEMPLATE.replace("@@TICKET@@", ticket.getId());
+        request.setMethod("POST");
+        request.setContentType("text/xml");
+        request.setContent(requestBody.getBytes("UTF-8"));
+        externalContext.getMockRequestParameterMap().put("TARGET", ticket.getService());
+        overrideEndStateOutput(FLOW_ID, "ValidateSuccess");
+
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+
+        final String responseBody = response.getContentAsString();
+        final FlowExecutionOutcome outcome = result.getOutcome();
+        assertEquals(outcome.getId(), "ValidateSuccess");
+        assertTrue(responseBody.contains("<saml1p:StatusCode Value=\"saml1p:Success\"/>"));
+        assertTrue(responseBody.contains("<saml1:NameIdentifier>john</saml1:NameIdentifier>"));
+        assertTrue(responseBody.contains("<saml1:NameIdentifier>john</saml1:NameIdentifier>"));
+        assertTrue(responseBody.contains("<saml1:Attribute AttributeName=\"uid\" AttributeNamespace=\"http://www.ja-sig.org/products/cas/\"><saml1:AttributeValue xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:type=\"xsd:string\">john</saml1:AttributeValue></saml1:Attribute>"));
+        assertTrue(responseBody.contains("<saml1:Attribute AttributeName=\"eduPersonPrincipalName\" AttributeNamespace=\"http://www.ja-sig.org/products/cas/\"><saml1:AttributeValue xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:type=\"xsd:string\">john at example.org</saml1:AttributeValue></saml1:Attribute>"));
+        assertFalse(responseBody.contains("<saml1:Attribute AttributeName=\"mail\" AttributeNamespace=\"http://www.ja-sig.org/products/cas/\"><saml1:AttributeValue xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:type=\"xsd:string\">john at example.org</saml1:AttributeValue></saml1:Attribute>"));
+        assertPopulatedAttributeContext((ProfileRequestContext) outcome.getOutput().get(END_STATE_OUTPUT_ATTR_NAME));
+    }
+
     @Test
     public void testFailureTicketExpired() throws Exception {
         final String requestBody = SAML_REQUEST_TEMPLATE.replace("@@TICKET@@", "ST-123-abcdefg");
diff --git a/idp-conf/src/test/java/net/shibboleth/idp/test/flows/cas/ServiceValidateFlowTest.java b/idp-conf/src/test/java/net/shibboleth/idp/test/flows/cas/ServiceValidateFlowTest.java
index a23c3a664..6679f8a57 100644
--- a/idp-conf/src/test/java/net/shibboleth/idp/test/flows/cas/ServiceValidateFlowTest.java
+++ b/idp-conf/src/test/java/net/shibboleth/idp/test/flows/cas/ServiceValidateFlowTest.java
@@ -18,6 +18,7 @@
 package net.shibboleth.idp.test.flows.cas;
 
 import java.time.Instant;
+import java.util.Set;
 
 import javax.annotation.Nonnull;
 
@@ -102,11 +103,12 @@ public class ServiceValidateFlowTest extends AbstractFlowTest {
     public void testSuccess() throws Exception {
         final String principal = "john";
         final IdPSession session = sessionManager.createSession(principal);
+        final TicketState state = new TicketState(session.getId(), principal, Instant.now(), "Password");
         final ServiceTicket ticket = ticketService.createServiceTicket(
                 "ST-1415133132-ompog68ygxKyX9BPwPuw0hESQBjuA",
                 Instant.now().plusSeconds(5),
                 "https://test.example.org/",
-                new TicketState(session.getId(), principal, Instant.now(), "Password"),
+                state,
                 false);
 
         externalContext.getMockRequestParameterMap().put("service", ticket.getService());
@@ -134,6 +136,44 @@ public class ServiceValidateFlowTest extends AbstractFlowTest {
         assertEquals(updatedSession.getSPSessions().size(), 0);
     }
 
+    @Test
+    public void testSuccessWithConsent() throws Exception {
+        final String principal = "john";
+        final IdPSession session = sessionManager.createSession(principal);
+        final TicketState state = new TicketState(session.getId(), principal, Instant.now(), "Password");
+        state.setConsentedAttributeIds(Set.of("uid", "eduPersonPrincipalName"));
+        final ServiceTicket ticket = ticketService.createServiceTicket(
+                "ST-1415133132-ompog68ygxKyX9BPwPuw0hESQBjuA",
+                Instant.now().plusSeconds(5),
+                "https://test.example.org/",
+                state,
+                false);
+
+        externalContext.getMockRequestParameterMap().put("service", ticket.getService());
+        externalContext.getMockRequestParameterMap().put("ticket", ticket.getId());
+        overrideEndStateOutput(FLOW_ID, "ValidateSuccess");
+
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+
+        final String responseBody = response.getContentAsString();
+        final FlowExecutionOutcome outcome = result.getOutcome();
+        assertEquals(outcome.getId(), "ValidateSuccess");
+        assertTrue(responseBody.contains("<cas:authenticationSuccess>"));
+        assertTrue(responseBody.contains("<cas:user>john</cas:user>"));
+        assertTrue(responseBody.contains("<cas:attributes>"));
+        assertTrue(responseBody.contains("<cas:uid>john</cas:uid>"));
+        assertTrue(responseBody.contains("<cas:eduPersonPrincipalName>john at example.org</cas:eduPersonPrincipalName>"));
+        assertFalse(responseBody.contains("<cas:mail>john at example.org</cas:mail>"));
+        assertFalse(responseBody.contains("<cas:proxyGrantingTicket>"));
+        assertFalse(responseBody.contains("<cas:proxies>"));
+        assertPopulatedAttributeContext((ProfileRequestContext) outcome.getOutput().get(END_STATE_OUTPUT_ATTR_NAME));
+
+        final IdPSession updatedSession = sessionResolver.resolveSingle(
+                new CriteriaSet(new SessionIdCriterion(session.getId())));
+        assertNotNull(updatedSession);
+        assertEquals(updatedSession.getSPSessions().size(), 0);
+    }
+
     @Test
     public void testSuccessWithSLOParticipant() throws Exception {
         final String principal = "john";

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


More information about the commits mailing list