[java-identity-provider] branch main updated: IDP-2351 - Allow per-AuthenticationResult lifetime/timeout policies
Scott Cantor
cantor.2 at osu.edu
Thu Mar 6 20:24:46 UTC 2025
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=4a105c40f3571761664f8aa79c21021b60d20ac6
The following commit(s) were added to refs/heads/main by this push:
new 4a105c40f IDP-2351 - Allow per-AuthenticationResult lifetime/timeout policies
4a105c40f is described below
commit 4a105c40f3571761664f8aa79c21021b60d20ac6
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Thu Mar 6 15:24:38 2025 -0500
IDP-2351 - Allow per-AuthenticationResult lifetime/timeout policies
https://shibboleth.atlassian.net/browse/IDP-2351
Implement the basic machinery to allow for this.
---
.../idp/authn/AuthenticationFlowDescriptor.java | 42 +++++++---
.../shibboleth/idp/authn/AuthenticationResult.java | 90 ++++++++++++++++++++++
.../DefaultAuthenticationResultSerializer.java | 26 +++++++
.../DefaultAuthenticationResultSerializerTest.java | 37 +++++++++
.../simpleAuthenticationResultWithOptional.json | 1 +
.../idp/session/impl/StorageBackedIdPSession.java | 8 +-
6 files changed, 191 insertions(+), 13 deletions(-)
diff --git a/idp-authn-api/src/main/java/net/shibboleth/idp/authn/AuthenticationFlowDescriptor.java b/idp-authn-api/src/main/java/net/shibboleth/idp/authn/AuthenticationFlowDescriptor.java
index f005edf2f..62c6ec7a8 100644
--- a/idp-authn-api/src/main/java/net/shibboleth/idp/authn/AuthenticationFlowDescriptor.java
+++ b/idp-authn-api/src/main/java/net/shibboleth/idp/authn/AuthenticationFlowDescriptor.java
@@ -370,8 +370,10 @@ public class AuthenticationFlowDescriptor extends AbstractIdentifiableInitializa
}
/**
- * Get the maximum amount of time, since first usage, a flow should be considered active. A null
- * indicates that there is no upper limit on the lifetime on an active flow.
+ * Get the maximum amount of time, since first usage, a flow result should be considered active, if not
+ * overridden.
+ *
+ * <p>A null indicates that there is no upper limit on the lifetime.</p>
*
* @return maximum amount of time a flow should be considered active
*/
@@ -380,8 +382,10 @@ public class AuthenticationFlowDescriptor extends AbstractIdentifiableInitializa
}
/**
- * Set the maximum amount of time, since first usage, a flow should be considered active. A null value
- * indicates that there is no upper limit on the lifetime on an active flow.
+ * Set the maximum amount of time, since first usage, a flow result should be considered active, if not
+ * overridden.
+ *
+ * <p>A null value indicates that there is no upper limit on the lifetime.</p>
*
* @param flowLifetime the lifetime for the flow
*/
@@ -394,7 +398,8 @@ public class AuthenticationFlowDescriptor extends AbstractIdentifiableInitializa
}
/**
- * Get the maximum amount of time, since the last usage, a flow should be considered active.
+ * Get the maximum amount of time, since the last usage, a flow result should be considered active,
+ * if not overridden.
*
* <p>
* Defaults to 30 minutes.
@@ -407,7 +412,8 @@ public class AuthenticationFlowDescriptor extends AbstractIdentifiableInitializa
}
/**
- * Set the maximum amount of time, since the last usage, a flow should be considered active.
+ * Set the maximum amount of time, since the last usage, a flow result should be considered active,
+ * if not overridden.
*
* @param timeout the flow inactivity timeout, must be greater than zero
*/
@@ -432,9 +438,15 @@ public class AuthenticationFlowDescriptor extends AbstractIdentifiableInitializa
"AuthenticationResult was not produced by this flow");
final Instant now = Instant.now();
- if (getLifetime() != null && now.isAfter(result.getAuthenticationInstant().plus(getLifetime()))) {
+
+ // Lifetime comes from result, falling back to flow default.
+ final Duration effectiveLifetime = result.getResultLifetime(this);
+ if (effectiveLifetime != null && now.isAfter(result.getAuthenticationInstant().plus(effectiveLifetime))) {
return false;
- } else if (now.isAfter(result.getLastActivityInstant().plus(getInactivityTimeout()))) {
+ }
+
+ // Timeout comes from result, falling back to flow default.
+ if (now.isAfter(result.getLastActivityInstant().plus(result.getResultTimeout(this)))) {
return false;
}
@@ -614,6 +626,16 @@ public class AuthenticationFlowDescriptor extends AbstractIdentifiableInitializa
(expiration != null) ?
expiration - inactivityTimeout.toMillis() - STORAGE_EXPIRATION_OFFSET.toMillis() :
null);
+
+ if (expiration != null) {
+ // Check for a result-specific timeout to possibly recompute the last activity timestamp based on it.
+ final Duration overriddenTimeout = result.getResultTimeout();
+ if (overriddenTimeout != null) {
+ result.setLastActivityInstant(Instant.ofEpochMilli(
+ expiration - overriddenTimeout.toMillis() - STORAGE_EXPIRATION_OFFSET.toMillis()));
+ }
+ }
+
if (proxyRestrictionsEnforced) {
result.setReuseCondition(PredicateSupport.and(reuseCondition, result.new ProxyRestrictionReusePredicate()));
} else {
@@ -681,7 +703,7 @@ public class AuthenticationFlowDescriptor extends AbstractIdentifiableInitializa
*
* @param <T> object type
*/
- private class WeightedComparator<T> implements Comparator<T> {
+ private final class WeightedComparator<T> implements Comparator<T> {
/** {@inheritDoc} */
public int compare(final T o1, final T o2) {
@@ -703,7 +725,7 @@ public class AuthenticationFlowDescriptor extends AbstractIdentifiableInitializa
* A {@link Predicate} that implements a cross-check between an effective proxy count of zero and
* whether a descriptor is honoring the limit.
*/
- private class ProxyCountPredicate implements Predicate<ProfileRequestContext> {
+ private final class ProxyCountPredicate implements Predicate<ProfileRequestContext> {
/** {@inheritDoc} */
public boolean test(@Nullable final ProfileRequestContext input) {
diff --git a/idp-authn-api/src/main/java/net/shibboleth/idp/authn/AuthenticationResult.java b/idp-authn-api/src/main/java/net/shibboleth/idp/authn/AuthenticationResult.java
index f62953163..51f164277 100644
--- a/idp-authn-api/src/main/java/net/shibboleth/idp/authn/AuthenticationResult.java
+++ b/idp-authn-api/src/main/java/net/shibboleth/idp/authn/AuthenticationResult.java
@@ -15,6 +15,7 @@
package net.shibboleth.idp.authn;
import java.security.Principal;
+import java.time.Duration;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
@@ -65,6 +66,12 @@ public class AuthenticationResult implements PrincipalSupportingComponent, Predi
/** The last time this result was used to bypass authentication. */
@Nonnull private Instant lastActivityInstant;
+ /** Optional override of flow's lifetime policy for this result. */
+ @Nullable private Duration resultLifetime;
+
+ /** Optional override of flow's inactivity timeout policy for this result. */
+ @Nullable private Duration resultTimeout;
+
/** Tracks whether a result was loaded from a previous session or created as part of the current request. */
private boolean previousResult;
@@ -228,6 +235,89 @@ public class AuthenticationResult implements PrincipalSupportingComponent, Predi
assert now != null;
lastActivityInstant = now;
}
+
+ /**
+ * Get the effective lifetime policy for the result based on the provided flow descriptor.
+ *
+ * @param flow correspnding flow descriptor
+ *
+ * @return effective lifetime policy combining result and flow
+ *
+ * @since 5.2.0
+ */
+ @Nullable public Duration getResultLifetime(@Nonnull final AuthenticationFlowDescriptor flow) {
+ return resultLifetime != null ? resultLifetime : flow.getLifetime();
+ }
+
+ /**
+ * Get the per-result lifetime policy if it exists.
+ *
+ * <p>If set, this overrides the more typical flow-based lifetime setting for the result.</p>
+ *
+ * @return per-result lifetime policy
+ *
+ * @since 5.2.0
+ */
+ @Nullable public Duration getResultLifetime() {
+ return resultLifetime;
+ }
+
+ /**
+ * Set the per-result lifetime policy.
+ *
+ * <p>If null, the underlying flow definition's lifetime applies.</p>
+ *
+ * @param lifetime lifetime to set
+ *
+ * @since 5.2.0
+ */
+ public void setResultLifetime(@Nullable final Duration lifetime) {
+ Constraint.isFalse(lifetime != null && (lifetime.isNegative() || lifetime.isZero()),
+ "Lifetime must be null or greater than 0");
+ resultLifetime = lifetime;
+ }
+
+ /**
+ * Get the effective timeout policy for the result based on the provided flow descriptor.
+ *
+ * @param flow correspnding flow descriptor
+ *
+ * @return effective timeout policy combining result and flow
+ *
+ * @since 5.2.0
+ */
+ @Nonnull public Duration getResultTimeout(@Nonnull final AuthenticationFlowDescriptor flow) {
+ return resultTimeout != null ? resultTimeout : flow.getInactivityTimeout();
+ }
+
+ /**
+ * Get the per-result inactivity timeout policy if it exists.
+ *
+ * <p>If set, this overrides the more typical flow-based timeout setting for the result.</p>
+ *
+ * @return per-result inactivity timeout policy
+ *
+ * @since 5.2.0
+ */
+ @Nullable public Duration getResultTimeout() {
+ return resultTimeout;
+ }
+
+ /**
+ * Set the per-result inactivity timeout policy.
+ *
+ * <p>If null, the underlying flow definition's inactivity timeout applies.</p>
+ *
+ * @param timeout timeout to set
+ *
+ * @since 5.2.0
+ */
+ public void setResultTimeout(@Nullable final Duration timeout) {
+ if (timeout != null) {
+ Constraint.isFalse(timeout.isNegative() || timeout.isZero(), "Inactivity timeout must be greater than 0");
+ }
+ resultTimeout = timeout;
+ }
/**
* Get whether this result was loaded from a session as the product of a previous request.
diff --git a/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/impl/DefaultAuthenticationResultSerializer.java b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/impl/DefaultAuthenticationResultSerializer.java
index 63c5d08c2..49ae07cbd 100644
--- a/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/impl/DefaultAuthenticationResultSerializer.java
+++ b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/impl/DefaultAuthenticationResultSerializer.java
@@ -21,6 +21,7 @@ import java.security.Principal;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
+import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
@@ -34,6 +35,7 @@ import javax.annotation.Nullable;
import jakarta.json.Json;
import jakarta.json.JsonArray;
import jakarta.json.JsonException;
+import jakarta.json.JsonNumber;
import jakarta.json.JsonObject;
import jakarta.json.JsonReader;
import jakarta.json.JsonReaderFactory;
@@ -77,6 +79,12 @@ public class DefaultAuthenticationResultSerializer extends AbstractInitializable
/** Field name of authentication instant. */
@Nonnull @NotEmpty private static final String AUTHN_INSTANT_FIELD = "ts";
+ /** Field name of optional result-specific lifetime. */
+ @Nonnull @NotEmpty private static final String RESULT_LIFETIME_FIELD = "rl";
+
+ /** Field name of optional result-specific inactivity timeout. */
+ @Nonnull @NotEmpty private static final String RESULT_TIMEOUT_FIELD = "rt";
+
/** Field name of principal array. */
@Nonnull @NotEmpty private static final String PRINCIPAL_ARRAY_FIELD = "princ";
@@ -196,6 +204,16 @@ public class DefaultAuthenticationResultSerializer extends AbstractInitializable
gen.writeStartObject().write(FLOW_ID_FIELD, instance.getAuthenticationFlowId())
.write(AUTHN_INSTANT_FIELD, instance.getAuthenticationInstant().toEpochMilli());
+ Duration dur = instance.getResultLifetime();
+ if (dur != null) {
+ gen.write(RESULT_LIFETIME_FIELD, dur.getSeconds());
+ }
+
+ dur = instance.getResultTimeout();
+ if (dur != null) {
+ gen.write(RESULT_TIMEOUT_FIELD, dur.getSeconds());
+ }
+
final Map<String,String> addtlData = instance.getAdditionalData();
if (!addtlData.isEmpty()) {
gen.writeStartObject(ADDTL_DATA_FIELD);
@@ -277,6 +295,14 @@ public class DefaultAuthenticationResultSerializer extends AbstractInitializable
result.setLastActivityInstant(Instant.ofEpochMilli(expiration != null ? expiration : authnInstant));
result.setPreviousResult(true);
+ if (obj.get(RESULT_LIFETIME_FIELD) instanceof JsonNumber lifetime) {
+ result.setResultLifetime(Duration.ofSeconds(lifetime.longValueExact()));
+ }
+
+ if (obj.get(RESULT_TIMEOUT_FIELD) instanceof JsonNumber timeout) {
+ result.setResultTimeout(Duration.ofSeconds(timeout.longValueExact()));
+ }
+
final JsonObject addtlData = obj.getJsonObject(ADDTL_DATA_FIELD);
if (addtlData != null) {
final Map<String,String> dataMap = result.getAdditionalData();
diff --git a/idp-authn-impl/src/test/java/net/shibboleth/idp/authn/impl/DefaultAuthenticationResultSerializerTest.java b/idp-authn-impl/src/test/java/net/shibboleth/idp/authn/impl/DefaultAuthenticationResultSerializerTest.java
index 6f55cd9fc..0ff45e119 100644
--- a/idp-authn-impl/src/test/java/net/shibboleth/idp/authn/impl/DefaultAuthenticationResultSerializerTest.java
+++ b/idp-authn-impl/src/test/java/net/shibboleth/idp/authn/impl/DefaultAuthenticationResultSerializerTest.java
@@ -22,6 +22,7 @@ import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.URISyntaxException;
+import java.time.Duration;
import java.time.Instant;
import java.util.List;
@@ -236,6 +237,42 @@ public class DefaultAuthenticationResultSerializerTest {
assertEquals(result.getAuthenticationFlowId(), result2.getAuthenticationFlowId());
assertEquals(result.getAuthenticationInstant(), result2.getAuthenticationInstant());
+ assertEquals(result.getResultLifetime(), result2.getResultLifetime());
+ assertEquals(result.getResultTimeout(), result2.getResultTimeout());
+ assertEquals(result.getLastActivityInstant(), result2.getLastActivityInstant());
+ assertEquals(result.getSubject(), result2.getSubject());
+ assertEquals(result.getAdditionalData(), result2.getAdditionalData());
+ assertTrue(result2.getReuseCondition().test(prc));
+ }
+
+ @Test public void testSimpleWithOptionalFields() throws Exception {
+ serializer.initialize();
+ flowDescriptor.initialize();
+
+ final AuthenticationResult result = createResult(flowDescriptor, new Subject());
+ result.getAdditionalData().put("foo", "bar");
+ result.getAdditionalData().put("frobnitz", "zorkmid");
+ result.getSubject().getPrincipals().add(new UsernamePrincipal("bob"));
+ result.setResultLifetime(Duration.ofHours(1));
+ result.setResultTimeout(Duration.ofMinutes(5));
+
+ final ProfileRequestContext prc = getProfileRequestContext(CollectionSupport.singletonList(flowDescriptor));
+ assertTrue(result.getReuseCondition().test(prc));
+
+ flowDescriptor.serialize(result);
+ final String s2 = fileToString(DATAPATH + "simpleAuthenticationResultWithOptional.json");
+ // assertEquals(s, s2);
+
+ final AuthenticationResult result2 = flowDescriptor.deserialize(1, CONTEXT, KEY, s2,
+ Instant.ofEpochMilli(ACTIVITY)
+ .plus(result.getResultTimeout())
+ .plus(AuthenticationFlowDescriptor.STORAGE_EXPIRATION_OFFSET)
+ .toEpochMilli());
+
+ assertEquals(result.getAuthenticationFlowId(), result2.getAuthenticationFlowId());
+ assertEquals(result.getAuthenticationInstant(), result2.getAuthenticationInstant());
+ assertEquals(result.getResultLifetime(), result2.getResultLifetime());
+ assertEquals(result.getResultTimeout(), result2.getResultTimeout());
assertEquals(result.getLastActivityInstant(), result2.getLastActivityInstant());
assertEquals(result.getSubject(), result2.getSubject());
assertEquals(result.getAdditionalData(), result2.getAdditionalData());
diff --git a/idp-authn-impl/src/test/resources/net/shibboleth/idp/authn/impl/simpleAuthenticationResultWithOptional.json b/idp-authn-impl/src/test/resources/net/shibboleth/idp/authn/impl/simpleAuthenticationResultWithOptional.json
new file mode 100644
index 000000000..bbbc854ff
--- /dev/null
+++ b/idp-authn-impl/src/test/resources/net/shibboleth/idp/authn/impl/simpleAuthenticationResultWithOptional.json
@@ -0,0 +1 @@
+{"id":"test","ts":1378827849463,"rl":3600,"rt":300,"props":{"foo":"bar","frobnitz":"zorkmid"},"princ":[{"U":"bob"}]}
\ No newline at end of file
diff --git a/idp-session-impl/src/main/java/net/shibboleth/idp/session/impl/StorageBackedIdPSession.java b/idp-session-impl/src/main/java/net/shibboleth/idp/session/impl/StorageBackedIdPSession.java
index fd75f916a..b9d546148 100644
--- a/idp-session-impl/src/main/java/net/shibboleth/idp/session/impl/StorageBackedIdPSession.java
+++ b/idp-session-impl/src/main/java/net/shibboleth/idp/session/impl/StorageBackedIdPSession.java
@@ -15,6 +15,7 @@
package net.shibboleth.idp.session.impl;
import java.io.IOException;
+import java.time.Duration;
import java.time.Instant;
import java.util.Iterator;
import java.util.Map;
@@ -262,7 +263,7 @@ public class StorageBackedIdPSession extends AbstractIdPSession {
if (flow != null) {
try {
if (!sessionManager.getStorageService().updateExpiration(getId(), result.getAuthenticationFlowId(),
- result.getLastActivityInstant().plus(flow.getInactivityTimeout()).plus(
+ result.getLastActivityInstant().plus(result.getResultTimeout(flow)).plus(
AuthenticationFlowDescriptor.STORAGE_EXPIRATION_OFFSET).toEpochMilli())) {
log.warn("Skipping update, AuthenticationResult for flow {} in session {} not found in storage",
flowId, getId());
@@ -584,13 +585,14 @@ public class StorageBackedIdPSession extends AbstractIdPSession {
int attempts = 10;
boolean success = false;
do {
+ final Duration effectiveTimeout = result.getResultTimeout(flow);
success = sessionManager.getStorageService().create(getId(), flowId, result, flow,
- result.getLastActivityInstant().plus(flow.getInactivityTimeout()).plus(
+ result.getLastActivityInstant().plus(effectiveTimeout).plus(
AuthenticationFlowDescriptor.STORAGE_EXPIRATION_OFFSET).toEpochMilli());
if (!success) {
// The record already exists, so we need to overwrite via an update.
success = sessionManager.getStorageService().update(getId(), flowId, result, flow,
- result.getLastActivityInstant().plus(flow.getInactivityTimeout()).plus(
+ result.getLastActivityInstant().plus(effectiveTimeout).plus(
AuthenticationFlowDescriptor.STORAGE_EXPIRATION_OFFSET).toEpochMilli());
}
} while (!success && attempts-- > 0);
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list