[java-identity-provider] branch master updated: JSPT-79 - Review date and time handling for Java 8

Scott Cantor cantor.2 at osu.edu
Fri Mar 1 13:10:30 EST 2019


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=b2c421cf65a46de82b9857ff6a27dbbada271a31

The following commit(s) were added to refs/heads/master by this push:
       new  b2c421c   JSPT-79 - Review date and time handling for Java 8
b2c421c is described below

commit b2c421cf65a46de82b9857ff6a27dbbada271a31
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Fri Mar 1 13:10:25 2019 -0500

    JSPT-79 - Review date and time handling for Java 8
    
    https://issues.shibboleth.net/jira/browse/JSPT-79
---
 idp-admin-impl/pom.xml                             |   4 +
 .../idp/admin/impl/DoStorageOperation.java         |   4 +-
 .../shibboleth/idp/admin/impl/OutputMetrics.java   |   9 +-
 .../saml/impl/AttributeInMetadataMatcher.java      |   6 +-
 .../attribute/resolver/AbstractDataConnector.java  |  29 +-
 .../idp/attribute/resolver/DataConnector.java      |   7 +-
 .../provider/V2SAMLProfileRequestContext.java      |  10 +-
 .../resolver/impl/AttributeResolverImpl.java       |  11 +-
 .../impl/AttributeResolverServiceGaugeSet.java     |  14 +-
 .../resolver/impl/AttributeResolverImplTest.java   |   3 +-
 idp-attribute-resolver-spring/pom.xml              |   6 -
 .../spring/dc/AbstractDataConnectorParser.java     |  22 +-
 .../spring/dc/impl/DataConnectorFactoryBean.java   |   8 +-
 .../dc/http/HTTPDataConnectorParserTest.java       |   3 +-
 .../dc/ldap/LDAPDataConnectorParserTest.java       | 312 +++++++++++----------
 .../dc/rdbms/RDBMSDataConnectorParserTest.java     |   6 +-
 idp-authn-api/pom.xml                              |   4 -
 .../idp/authn/AuthenticationFlowDescriptor.java    |   5 +-
 .../shibboleth/idp/authn/AuthenticationResult.java |  52 ++--
 .../idp/authn/ExternalAuthentication.java          |   3 -
 .../idp/authn/context/AuthenticationContext.java   |  26 +-
 .../context/ExternalAuthenticationContext.java     |  13 +-
 .../idp/authn/AuthenticationResultTest.java        |   6 +-
 .../authn/context/AuthenticationContextTest.java   |  11 +-
 idp-authn-impl/pom.xml                             |   6 +
 .../shibboleth/idp/authn/duo/impl/DuoSupport.java  |   9 +-
 .../DefaultAuthenticationResultSerializer.java     |   9 +-
 .../idp/authn/impl/ExternalAuthenticationImpl.java |  20 +-
 .../PopulateMultiFactorAuthenticationContext.java  |   5 +-
 .../impl/StorageBackedAccountLockoutManager.java   |  38 ++-
 .../authn/impl/ValidateExternalAuthentication.java |   3 +-
 .../DefaultAuthenticationResultSerializerTest.java |   5 +-
 ...pulateMultiFactorAuthenticationContextTest.java |   7 +-
 .../StorageBackedAccountLockoutManagerTest.java    |   6 +-
 .../impl/ValidateExternalAuthenticationTest.java   |   6 +-
 .../idp/cas/ticket/ProxyGrantingTicket.java        |   4 +-
 .../net/shibboleth/idp/cas/ticket/ProxyTicket.java |   4 +-
 .../shibboleth/idp/cas/ticket/ServiceTicket.java   |   4 +-
 .../java/net/shibboleth/idp/cas/ticket/Ticket.java |   4 +-
 .../shibboleth/idp/cas/ticket/TicketService.java   |   2 +-
 .../shibboleth/idp/cas/ticket/TicketServiceEx.java |   4 +-
 .../net/shibboleth/idp/cas/ticket/TicketState.java |   2 +-
 .../BuildSamlValidationFailureMessageAction.java   |   5 +-
 .../BuildSamlValidationSuccessMessageAction.java   |   9 +-
 .../idp/cas/flow/impl/GrantProxyTicketAction.java  |   8 +-
 .../cas/flow/impl/GrantServiceTicketAction.java    |   9 +-
 .../impl/UpdateIdPSessionWithSPSessionAction.java  |  33 +--
 .../cas/flow/impl/ValidateProxyCallbackAction.java |   5 +-
 .../idp/cas/flow/impl/ValidateTicketAction.java    |   4 +-
 .../cas/service/impl/ServiceEntityDescriptor.java  |  21 +-
 .../idp/cas/session/impl/CASSPSession.java         |  19 +-
 .../cas/session/impl/CASSPSessionSerializer.java   |  24 +-
 .../idp/cas/ticket/impl/AbstractTicketService.java |   4 +-
 .../idp/cas/ticket/impl/EncodingTicketService.java |   5 +-
 .../idp/cas/ticket/impl/SimpleTicketService.java   |   3 +-
 .../impl/AbstractTicketSerializer.java             |  10 +-
 .../impl/ProxyGrantingTicketSerializer.java        |   3 +-
 .../serialization/impl/ProxyTicketSerializer.java  |   3 +-
 .../impl/ServiceTicketSerializer.java              |   3 +-
 .../idp/cas/flow/impl/AbstractFlowActionTest.java  |   5 +-
 .../flow/impl/ValidateProxyCallbackActionTest.java |   2 +-
 .../session/impl/CASSPSessionSerializerTest.java   |  12 +-
 .../cas/ticket/impl/EncodingTicketServiceTest.java |  22 +-
 .../cas/ticket/impl/SimpleTicketServiceTest.java   |   9 +-
 .../impl/ProxyGrantingTicketSerializerTest.java    |  17 +-
 .../impl/ProxyTicketSerializerTest.java            |  11 +-
 .../impl/ServiceTicketSerializerTest.java          |  11 +-
 idp-cas-impl/src/test/resources/logback-test.xml   |   2 +-
 .../src/test/resources/spring/test-flow-beans.xml  |   1 +
 .../main/resources/system/conf/global-system.xml   |   5 +-
 .../idp/test/flows/cas/ProxyFlowTest.java          |   9 +-
 .../idp/test/flows/cas/ProxyValidateFlowTest.java  |  10 +-
 .../idp/test/flows/cas/SamlValidateFlowTest.java   |   8 +-
 .../test/flows/cas/ServiceValidateFlowTest.java    |  15 +-
 .../idp/test/flows/cas/ThrowingTicketService.java  |  12 +-
 .../idp/test/flows/cas/ValidateFlowTest.java       |   6 +-
 .../flows/saml1/SAML1AttributeQueryFlowTest.java   |   4 +-
 .../test/flows/saml2/AbstractSAML2SSOFlowTest.java |  10 +-
 .../flows/saml2/SAML2AttributeQueryFlowTest.java   |   4 +-
 .../consent/flow/impl/ConsentFlowDescriptor.java   |  26 +-
 .../storage/impl/CreateGlobalConsentResult.java    |   4 +-
 .../consent/flow/storage/impl/CreateResult.java    |   4 +-
 .../flow/impl/ConsentFlowDescriptorTest.java       |  13 +-
 .../storage/impl/ConsentSerializerTest.java        |   3 -
 idp-core/pom.xml                                   |   6 +-
 .../shibboleth/idp/log/LogbackLoggingService.java  |   6 +-
 .../idp/metrics/ReloadableServiceGaugeSet.java     |  11 +-
 .../shibboleth/idp/metrics/impl/HTTPReporter.java  |   7 +-
 .../shibboleth/idp/metrics/impl/IdPGaugeSet.java   |   8 +-
 .../idp/installer/TestMetadataGenerator.java       |   8 +-
 idp-profile-api/pom.xml                            |   6 +
 .../idp/profile/logic/DateAttributePredicate.java  |  85 +++++-
 .../profile/logic/DateAttributePredicateTest.java  | 107 ++++++-
 .../profile/audit/impl/PopulateAuditContext.java   |  38 ++-
 .../idp/profile/audit/impl/WriteAuditLog.java      |  45 +--
 ...WriteProfileInterceptorResultToStorageTest.java |   7 +-
 .../metadata/ReloadServiceConfigurationTest.java   |   6 +-
 .../credential/X509FilesystemParserTest.java       |  10 +-
 .../security/credential/X509InlineParserTest.java  |   8 +-
 .../trustengine/SignatureChainingParserTest.java   |   4 +-
 .../StaticExplicitKeyFactoryBeanTest.java          |   6 +-
 .../trustengine/StaticExplicitKeyParserTest.java   |   4 +-
 .../StaticExplicitKeySignatureParserTest.java      |   4 +-
 idp-saml-api/pom.xml                               |   4 -
 .../mapping/AbstractSAMLAttributeValueMapper.java  |   6 +-
 .../metadata/RelyingPartyMetadataProvider.java     |   7 +-
 ...tractCollectionConfigurationLookupStrategy.java |   7 +-
 .../config/LongConfigurationLookupStrategy.java    |   6 +-
 .../config/StringConfigurationLookupStrategy.java  |   6 +-
 .../idp/saml/session/SAML1SPSession.java           |   8 +-
 .../idp/saml/session/SAML2SPSession.java           |   7 +-
 .../idp/saml/session/SAML2SPSessionTest.java       |  22 +-
 idp-saml-impl/pom.xml                              |   4 -
 .../SAML1AttributeDesignatorsMapperService.java    |   6 +-
 .../mapping/impl/SAML2AttributesMapperService.java |   6 +-
 .../audit/impl/AssertionInstantAuditExtractor.java |   6 +-
 .../audit/impl/AuthnInstantAuditExtractor.java     |   6 +-
 .../impl/AttributeMappingNodeProcessor.java        |   6 +-
 .../impl/MetadataResolverServiceGaugeSet.java      |  26 +-
 .../BaseIdPInitiatedSSORequestMessageDecoder.java  |   6 +-
 .../saml/profile/impl/IdPInitiatedSSORequest.java  |  21 +-
 .../AddAuthenticationStatementToAssertion.java     |   3 +-
 .../impl/IdPInitiatedSSORequestMessageDecoder.java |   4 +-
 .../impl/AddDelegationRestrictionToAssertions.java |   6 +-
 .../impl/PopulateOutboundMessageContext.java       |   5 +-
 .../profile/impl/AddAuthnStatementToAssertion.java |   6 +-
 .../impl/IdPInitiatedSSORequestMessageDecoder.java |   4 +-
 .../idp/saml/session/impl/AddLogoutRequest.java    |   5 +-
 .../impl/SAML1SPSessionCreationStrategy.java       |  16 +-
 .../session/impl/SAML1SPSessionSerializer.java     |   8 +-
 .../impl/SAML2SPSessionCreationStrategy.java       |  25 +-
 .../session/impl/SAML2SPSessionSerializer.java     |   9 +-
 .../AddAuthenticationStatementToAssertionTest.java |   6 +-
 .../IdPInitiatedSSORequestMessageDecoderTest.java  |  24 +-
 .../impl/PopulateOutboundMessageContextTest.java   |   7 +-
 .../impl/LibertyHTTPSOAP11EncoderTest.java         |   5 +-
 .../impl/AddAuthnStatementToAssertionTest.java     |   5 +-
 .../IdPInitiatedSSORequestMessageDecoderTest.java  |  25 +-
 .../profile/impl/ProcessLogoutRequestTest.java     |  45 +--
 .../impl/PrepareInboundMessageContextTest.java     |   6 +-
 .../session/impl/SAML2SPSessionSerializerTest.java |  18 +-
 .../shibboleth/idp/session/AbstractIdPSession.java |  39 +--
 .../idp/session/AbstractSPSessionSerializer.java   |  27 +-
 .../net/shibboleth/idp/session/BasicSPSession.java |  30 +-
 .../net/shibboleth/idp/session/IdPSession.java     |   6 +-
 .../java/net/shibboleth/idp/session/SPSession.java |   8 +-
 .../shibboleth/idp/session/BasicSPSessionTest.java |  31 +-
 .../net/shibboleth/idp/session/IdPSessionTest.java |  22 +-
 idp-session-impl/pom.xml                           |   4 -
 .../impl/BasicSPSessionCreationStrategy.java       |  14 +-
 .../idp/session/impl/BasicSPSessionSerializer.java |  11 +-
 .../impl/ExtractActiveAuthenticationResults.java   |   7 +-
 .../idp/session/impl/StorageBackedIdPSession.java  |  66 +++--
 .../impl/StorageBackedIdPSessionSerializer.java    |  11 +-
 .../session/impl/StorageBackedSessionManager.java  |  48 ++--
 .../session/impl/BasicSPSessionSerializerTest.java |  10 +-
 .../idp/session/impl/DestroySessionsTest.java      |   5 +-
 .../idp/session/impl/ProcessLogoutTest.java        |  11 +-
 .../session/impl/SessionManagerBaseTestCase.java   |   4 +-
 .../StorageBackedIdPSessionSerializerTest.java     |  13 +-
 .../impl/StorageBackedSessionManagerTest.java      |  47 ++--
 .../UpdateSessionWithAuthenticationResultTest.java |   4 +-
 .../impl/UpdateSessionWithSPSessionTest.java       |  14 +-
 163 files changed, 1223 insertions(+), 1082 deletions(-)

diff --git a/idp-admin-impl/pom.xml b/idp-admin-impl/pom.xml
index eaa3f5c..a0bdba7 100644
--- a/idp-admin-impl/pom.xml
+++ b/idp-admin-impl/pom.xml
@@ -26,6 +26,10 @@
           <groupId>com.github.jasminb</groupId>
           <artifactId>jsonapi-converter</artifactId>
         </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.datatype</groupId>
+            <artifactId>jackson-datatype-jsr310</artifactId>
+        </dependency>    
         
         <dependency>
             <groupId>${project.groupId}</groupId>
diff --git a/idp-admin-impl/src/main/java/net/shibboleth/idp/admin/impl/DoStorageOperation.java b/idp-admin-impl/src/main/java/net/shibboleth/idp/admin/impl/DoStorageOperation.java
index 680edee..c89d1da 100644
--- a/idp-admin-impl/src/main/java/net/shibboleth/idp/admin/impl/DoStorageOperation.java
+++ b/idp-admin-impl/src/main/java/net/shibboleth/idp/admin/impl/DoStorageOperation.java
@@ -18,6 +18,7 @@
 package net.shibboleth.idp.admin.impl;
 
 import java.io.IOException;
+import java.time.Instant;
 import java.util.Collections;
 
 import javax.annotation.Nonnull;
@@ -33,7 +34,6 @@ import net.shibboleth.utilities.java.support.component.ComponentInitializationEx
 import net.shibboleth.utilities.java.support.component.ComponentSupport;
 import net.shibboleth.utilities.java.support.logic.Constraint;
 
-import org.joda.time.DateTime;
 import org.opensaml.profile.action.ActionSupport;
 import org.opensaml.profile.action.EventIds;
 import org.opensaml.profile.context.ProfileRequestContext;
@@ -190,7 +190,7 @@ public class DoStorageOperation extends AbstractProfileAction {
                         g.writeNumberField("version", record.getVersion());
                         if (record.getExpiration() != null) {
                             g.writeFieldName("expiration");
-                            g.writeObject(new DateTime(record.getExpiration()));
+                            g.writeObject(Instant.ofEpochMilli(record.getExpiration()));
                         }
                         g.close();
                     } else {
diff --git a/idp-admin-impl/src/main/java/net/shibboleth/idp/admin/impl/OutputMetrics.java b/idp-admin-impl/src/main/java/net/shibboleth/idp/admin/impl/OutputMetrics.java
index 2d51109..0f054f6 100644
--- a/idp-admin-impl/src/main/java/net/shibboleth/idp/admin/impl/OutputMetrics.java
+++ b/idp-admin-impl/src/main/java/net/shibboleth/idp/admin/impl/OutputMetrics.java
@@ -38,7 +38,6 @@ import net.shibboleth.utilities.java.support.component.ComponentSupport;
 import net.shibboleth.utilities.java.support.logic.Constraint;
 import net.shibboleth.utilities.java.support.primitive.StringSupport;
 
-import org.joda.time.DateTime;
 import org.opensaml.profile.action.ActionSupport;
 import org.opensaml.profile.action.EventIds;
 import org.opensaml.profile.context.ProfileRequestContext;
@@ -51,8 +50,9 @@ import com.codahale.metrics.MetricFilter;
 import com.codahale.metrics.MetricRegistry;
 import com.codahale.metrics.json.MetricsModule;
 import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
 import com.fasterxml.jackson.databind.util.JSONPObject;
-import com.fasterxml.jackson.datatype.joda.JodaModule;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
 
 /**
  * Action that outputs one or more {@link Metric} objects.
@@ -148,7 +148,7 @@ public class OutputMetrics extends AbstractProfileAction {
     }
     
     /**
-     * Set the {@link DateTime} formatting string to apply when writing {@link DateTime}-valued fields.
+     * Set the formatting string to apply when writing date/time fields.
      * 
      * @param format formatting string
      */
@@ -254,8 +254,9 @@ public class OutputMetrics extends AbstractProfileAction {
             
             final ObjectMapper mapper = new ObjectMapper().registerModule(
                     new MetricsModule(TimeUnit.SECONDS, TimeUnit.SECONDS, true, filter));
-            mapper.registerModule(new JodaModule());
+            mapper.registerModule(new JavaTimeModule());
             mapper.setDateFormat(new SimpleDateFormat(dateTimeFormat != null ? dateTimeFormat : DEFAULT_DT_FORMAT));
+            mapper.configure(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS, false);
             
             if (jsonpCallbackName != null) {
                 response.setContentType("application/javascript");
diff --git a/idp-attribute-filter-impl/src/main/java/net/shibboleth/idp/attribute/filter/matcher/saml/impl/AttributeInMetadataMatcher.java b/idp-attribute-filter-impl/src/main/java/net/shibboleth/idp/attribute/filter/matcher/saml/impl/AttributeInMetadataMatcher.java
index da94118..8309e26 100644
--- a/idp-attribute-filter-impl/src/main/java/net/shibboleth/idp/attribute/filter/matcher/saml/impl/AttributeInMetadataMatcher.java
+++ b/idp-attribute-filter-impl/src/main/java/net/shibboleth/idp/attribute/filter/matcher/saml/impl/AttributeInMetadataMatcher.java
@@ -17,6 +17,7 @@
 
 package net.shibboleth.idp.attribute.filter.matcher.saml.impl;
 
+import java.time.Instant;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
@@ -38,7 +39,6 @@ import net.shibboleth.utilities.java.support.component.AbstractIdentifiableIniti
 import net.shibboleth.utilities.java.support.component.ComponentSupport;
 import net.shibboleth.utilities.java.support.primitive.StringSupport;
 
-import org.joda.time.DateTime;
 import org.opensaml.core.xml.XMLObject;
 import org.opensaml.core.xml.schema.XSAny;
 import org.opensaml.core.xml.schema.XSBase64Binary;
@@ -353,9 +353,9 @@ public class AttributeInMetadataMatcher extends AbstractIdentifiableInitializabl
         } else if (xmlObj instanceof XSInteger) {
             toMatch = ((XSInteger) xmlObj).getValue().toString();
         } else if (xmlObj instanceof XSDateTime) {
-            final DateTime dt = ((XSDateTime) xmlObj).getValue();
+            final Instant dt = ((XSDateTime) xmlObj).getValue();
             if (dt != null) {
-                toMatch = ((XSDateTime) xmlObj).getDateTimeFormatter().print(dt);
+                toMatch = ((XSDateTime) xmlObj).getDateTimeFormatter().format(dt);
             }
         } else if (xmlObj instanceof XSBase64Binary) {
             toMatch = ((XSBase64Binary) xmlObj).getValue();
diff --git a/idp-attribute-resolver-api/src/main/java/net/shibboleth/idp/attribute/resolver/AbstractDataConnector.java b/idp-attribute-resolver-api/src/main/java/net/shibboleth/idp/attribute/resolver/AbstractDataConnector.java
index 4ba2acd..04a24ea 100644
--- a/idp-attribute-resolver-api/src/main/java/net/shibboleth/idp/attribute/resolver/AbstractDataConnector.java
+++ b/idp-attribute-resolver-api/src/main/java/net/shibboleth/idp/attribute/resolver/AbstractDataConnector.java
@@ -17,6 +17,8 @@
 
 package net.shibboleth.idp.attribute.resolver;
 
+import java.time.Duration;
+import java.time.Instant;
 import java.util.Map;
 
 import javax.annotation.Nonnull;
@@ -26,7 +28,6 @@ import javax.annotation.concurrent.ThreadSafe;
 import net.shibboleth.idp.attribute.IdPAttribute;
 import net.shibboleth.idp.attribute.resolver.context.AttributeResolutionContext;
 import net.shibboleth.idp.attribute.resolver.context.AttributeResolverWorkContext;
-import net.shibboleth.utilities.java.support.annotation.Duration;
 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;
@@ -50,11 +51,16 @@ public abstract class AbstractDataConnector extends AbstractResolverPlugin<Map<S
     @Nullable private String logPrefix;
 
     /** When did this connector last fail? */
-    private long lastFail;
+    @Nullable private Instant lastFail;
 
     /** How long to wait until we declare the connector live again. */
-    @Duration private long noRetryDelay;
+    @Nonnull private Duration noRetryDelay;
 
+    /** Constructor. */
+    public AbstractDataConnector() {
+        noRetryDelay = Duration.ZERO; 
+    }
+    
     /**
      * Gets the ID of the {@link AbstractDataConnector} whose values will be used in the event that this data connector
      * experiences an error.
@@ -62,7 +68,7 @@ public abstract class AbstractDataConnector extends AbstractResolverPlugin<Map<S
      * @return ID of the {@link AbstractDataConnector} whose values will be used in the event that this data connector
      *         experiences an error
      */
-    @Override @Nullable public String getFailoverDataConnectorId() {
+    @Nullable public String getFailoverDataConnectorId() {
         return failoverDataConnectorId;
     }
 
@@ -85,14 +91,14 @@ public abstract class AbstractDataConnector extends AbstractResolverPlugin<Map<S
      *
      * @param time what to set
      */
-    public void setLastFail(final long time) {
+    public void setLastFail(@Nullable final Instant time) {
         lastFail = time;
     }
 
     /**
      * {@inheritDoc}
      */
-    @Override public long getLastFail() {
+    @Nullable public Instant getLastFail() {
         return lastFail;
     }
 
@@ -101,12 +107,12 @@ public abstract class AbstractDataConnector extends AbstractResolverPlugin<Map<S
      *
      * @param delay what to set
      */
-    @Duration public void setNoRetryDelay(@Duration final long delay) {
+    public void setNoRetryDelay(@Nonnull final Duration delay) {
         noRetryDelay = delay;
     }
 
     /** {@inheritDoc} */
-    @Override @Duration public long getNoRetryDelay() {
+    @Nonnull public Duration getNoRetryDelay() {
         return noRetryDelay;
     }
 
@@ -117,7 +123,8 @@ public abstract class AbstractDataConnector extends AbstractResolverPlugin<Map<S
      * {@link #doDataConnectorResolve(AttributeResolutionContext, AttributeResolverWorkContext)}. It serves as a future
      * extension point for introducing new common behavior.
      */
-    @Override @Nullable public final Map<String, IdPAttribute> doResolve(
+    @Override
+    @Nullable public final Map<String, IdPAttribute> doResolve(
             @Nonnull final AttributeResolutionContext resolutionContext,
             @Nonnull final AttributeResolverWorkContext workContext) throws ResolutionException {
 
@@ -128,7 +135,7 @@ public abstract class AbstractDataConnector extends AbstractResolverPlugin<Map<S
             // Do not record these failures, they are 'expected'
             throw e;
         } catch (final Exception e) {
-            setLastFail(System.currentTimeMillis());
+            setLastFail(Instant.now());
             throw e;
         }
 
@@ -176,7 +183,7 @@ public abstract class AbstractDataConnector extends AbstractResolverPlugin<Map<S
      * @return "Data connector '<definitionID>' :"
      */
     @Nonnull @NotEmpty protected String getLogPrefix() {
-        // local cache of cached entry to allow unsynchronised clearing of per class cache.
+        // local cache of cached entry to allow unsynchronized clearing of per class cache.
         String prefix = logPrefix;
         if (null == prefix) {
             final StringBuilder builder = new StringBuilder("Data Connector '").append(getId()).append("':");
diff --git a/idp-attribute-resolver-api/src/main/java/net/shibboleth/idp/attribute/resolver/DataConnector.java b/idp-attribute-resolver-api/src/main/java/net/shibboleth/idp/attribute/resolver/DataConnector.java
index e8e97d2..e8fc731 100644
--- a/idp-attribute-resolver-api/src/main/java/net/shibboleth/idp/attribute/resolver/DataConnector.java
+++ b/idp-attribute-resolver-api/src/main/java/net/shibboleth/idp/attribute/resolver/DataConnector.java
@@ -17,8 +17,11 @@
 
 package net.shibboleth.idp.attribute.resolver;
 
+import java.time.Duration;
+import java.time.Instant;
 import java.util.Map;
 
+import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.ThreadSafe;
 
@@ -35,7 +38,7 @@ public interface DataConnector extends ResolverPlugin<Map<String, IdPAttribute>>
      *
      * @return how long to wait.
      */
-     long getNoRetryDelay();
+     @Nonnull Duration getNoRetryDelay();
 
      /**
       * Get the time when this connector last failed. This will be set for any exception regardless of the setting of
@@ -43,7 +46,7 @@ public interface DataConnector extends ResolverPlugin<Map<String, IdPAttribute>>
       *
       * @return when it last failed
       */
-     long getLastFail();
+     @Nullable Instant getLastFail();
 
     /**
      * Gets the ID of the {@link DataConnector} whose values will be used in the event that this data connector
diff --git a/idp-attribute-resolver-impl/src/main/java/edu/internet2/middleware/shibboleth/common/attribute/provider/V2SAMLProfileRequestContext.java b/idp-attribute-resolver-impl/src/main/java/edu/internet2/middleware/shibboleth/common/attribute/provider/V2SAMLProfileRequestContext.java
index 25a7055..0f6e1c7 100644
--- a/idp-attribute-resolver-impl/src/main/java/edu/internet2/middleware/shibboleth/common/attribute/provider/V2SAMLProfileRequestContext.java
+++ b/idp-attribute-resolver-impl/src/main/java/edu/internet2/middleware/shibboleth/common/attribute/provider/V2SAMLProfileRequestContext.java
@@ -17,6 +17,7 @@
 
 package edu.internet2.middleware.shibboleth.common.attribute.provider;
 
+import java.time.Instant;
 import java.util.Collection;
 import java.util.Map;
 
@@ -30,7 +31,6 @@ import net.shibboleth.utilities.java.support.primitive.DeprecationSupport;
 import net.shibboleth.utilities.java.support.primitive.StringSupport;
 import net.shibboleth.utilities.java.support.primitive.DeprecationSupport.ObjectType;
 
-import org.joda.time.DateTime;
 import org.opensaml.core.xml.XMLObject;
 import org.opensaml.saml.common.SAMLObject;
 import org.opensaml.saml.metadata.resolver.MetadataResolver;
@@ -136,7 +136,7 @@ public class V2SAMLProfileRequestContext {
      * 
      * @return null
      */
-    public DateTime getInboundSAMLMessageIssueInstant() {
+    public Instant getInboundSAMLMessageIssueInstant() {
         unsupportedMethod("getInboundSAMLMessageIssueInstant");
         return null;
     }
@@ -236,7 +236,7 @@ public class V2SAMLProfileRequestContext {
      * 
      * @return null
      */
-    public DateTime getOutboundSAMLMessageIssueInstant() {
+    public Instant getOutboundSAMLMessageIssueInstant() {
         unsupportedMethod("getOutboundSAMLMessageIssueInstant");
         return null;
     }
@@ -353,7 +353,7 @@ public class V2SAMLProfileRequestContext {
      * 
      * @param param ignored.
      */
-    public void setInboundSAMLMessageIssueInstant(final DateTime param) {
+    public void setInboundSAMLMessageIssueInstant(final Instant param) {
         unsupportedMethod("setInboundSAMLMessageIssueInstant");
     }
 
@@ -447,7 +447,7 @@ public class V2SAMLProfileRequestContext {
      * 
      * @param param ignored.
      */
-    public void setOutboundSAMLMessageIssueInstant(final DateTime param) {
+    public void setOutboundSAMLMessageIssueInstant(final Instant param) {
         unsupportedMethod("setOutboundSAMLMessageIssueInstant");
 
     }
diff --git a/idp-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/impl/AttributeResolverImpl.java b/idp-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/impl/AttributeResolverImpl.java
index 85b0563..f5219f0 100644
--- a/idp-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/impl/AttributeResolverImpl.java
+++ b/idp-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/impl/AttributeResolverImpl.java
@@ -17,6 +17,7 @@
 
 package net.shibboleth.idp.attribute.resolver.impl;
 
+import java.time.Instant;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
@@ -337,8 +338,7 @@ public class AttributeResolverImpl extends AbstractServiceableComponent<Attribut
         Constraint.isNotNull(resolutionContext, "Attribute resolution context cannot be null");
         final AttributeResolverWorkContext workContext =
                 resolutionContext.getSubcontext(AttributeResolverWorkContext.class, false);
-        final long resolveTime = System.currentTimeMillis();
-
+        
         if (workContext.getResolvedDataConnectors().containsKey(connectorId)) {
             log.trace("{} Data connector '{}' was already resolved, nothing to do", logPrefix, connectorId);
             return;
@@ -350,8 +350,11 @@ public class AttributeResolverImpl extends AbstractServiceableComponent<Attribut
             return;
         }
 
-        if (resolveTime < connector.getLastFail() + connector.getNoRetryDelay()) {
-            log.debug("{} Data connector '{}' failed to resolve previously.  Still waiting", logPrefix, 
+        final Instant resolveTime = Instant.now();
+        
+        if (connector.getLastFail() != null
+                && resolveTime.isBefore(connector.getLastFail().plus(connector.getNoRetryDelay()))) {
+            log.debug("{} Data connector '{}' failed to resolve previously, still waiting", logPrefix, 
                     connectorId);
             final String failoverDataConnectorId = connector.getFailoverDataConnectorId();
             if (null != failoverDataConnectorId) {
diff --git a/idp-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/impl/AttributeResolverServiceGaugeSet.java b/idp-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/impl/AttributeResolverServiceGaugeSet.java
index 1dc2497..86aee3f 100644
--- a/idp-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/impl/AttributeResolverServiceGaugeSet.java
+++ b/idp-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/impl/AttributeResolverServiceGaugeSet.java
@@ -16,13 +16,12 @@
  */
 package net.shibboleth.idp.attribute.resolver.impl;
 
+import java.time.Instant;
 import java.util.Collection;
 import java.util.Map;
 
 import javax.annotation.Nonnull;
 
-import org.joda.time.DateTime;
-
 import com.codahale.metrics.Gauge;
 import com.codahale.metrics.MetricFilter;
 import com.codahale.metrics.MetricRegistry;
@@ -55,9 +54,9 @@ public class AttributeResolverServiceGaugeSet extends ReloadableServiceGaugeSet
 // Checkstyle: AnonInnerLength OFF
         getMetricMap().put(
                 MetricRegistry.name(DEFAULT_METRIC_NAME, metricName, "failure"),
-                new Gauge<Map<String,DateTime>>() {
-                    public Map<String,DateTime> getValue() {
-                        final Builder mapBuilder = ImmutableMap.<String,DateTime>builder();
+                new Gauge<Map<String,Instant>>() {
+                    public Map<String,Instant> getValue() {
+                        final Builder mapBuilder = ImmutableMap.<String,Instant>builder();
                         final ServiceableComponent<AttributeResolver> component =
                                 getService().getServiceableComponent();
                         if (component != null) {
@@ -66,9 +65,8 @@ public class AttributeResolverServiceGaugeSet extends ReloadableServiceGaugeSet
                                 final Collection<DataConnector> connectors = resolver.getDataConnectors().values();
                                 
                                 for (final DataConnector connector: connectors) {
-                                    final long lastFail = connector.getLastFail();
-                                    if (lastFail > 0) {
-                                        mapBuilder.put(connector.getId(), new DateTime(lastFail));
+                                    if (connector.getLastFail() != null) {
+                                        mapBuilder.put(connector.getId(), connector.getLastFail());
                                     }
                                 }
                             } finally {
diff --git a/idp-attribute-resolver-impl/src/test/java/net/shibboleth/idp/attribute/resolver/impl/AttributeResolverImplTest.java b/idp-attribute-resolver-impl/src/test/java/net/shibboleth/idp/attribute/resolver/impl/AttributeResolverImplTest.java
index 05b134f..3d5301a 100644
--- a/idp-attribute-resolver-impl/src/test/java/net/shibboleth/idp/attribute/resolver/impl/AttributeResolverImplTest.java
+++ b/idp-attribute-resolver-impl/src/test/java/net/shibboleth/idp/attribute/resolver/impl/AttributeResolverImplTest.java
@@ -17,6 +17,7 @@
 
 package net.shibboleth.idp.attribute.resolver.impl;
 
+import java.time.Duration;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -340,7 +341,7 @@ public class AttributeResolverImplTest {
         final MockDataConnector dc2 = new MockDataConnector("dc2", Collections.singletonMap("Atr", i2Val));
         dc2.initialize();
         dc1.setFailoverDataConnectorId("dc2");
-        dc1.setNoRetryDelay(3000);
+        dc1.setNoRetryDelay(Duration.ofSeconds(3));
         dc1.initialize();
         
         final HashSet<DataConnector> connectors = new HashSet<>(2);
diff --git a/idp-attribute-resolver-spring/pom.xml b/idp-attribute-resolver-spring/pom.xml
index 72b4a4a..ad381e1 100644
--- a/idp-attribute-resolver-spring/pom.xml
+++ b/idp-attribute-resolver-spring/pom.xml
@@ -157,12 +157,6 @@
             <artifactId>opensaml-storage-impl</artifactId>
             <scope>test</scope>
         </dependency>
-
-        <dependency>
-            <groupId>org.ldaptive</groupId>
-            <artifactId>ldaptive-unboundid</artifactId>
-            <scope>test</scope>
-        </dependency>
         <dependency>
             <groupId>com.unboundid</groupId>
             <artifactId>unboundid-ldapsdk</artifactId>
diff --git a/idp-attribute-resolver-spring/src/main/java/net/shibboleth/idp/attribute/resolver/spring/dc/AbstractDataConnectorParser.java b/idp-attribute-resolver-spring/src/main/java/net/shibboleth/idp/attribute/resolver/spring/dc/AbstractDataConnectorParser.java
index 863fd00..eff04c2 100644
--- a/idp-attribute-resolver-spring/src/main/java/net/shibboleth/idp/attribute/resolver/spring/dc/AbstractDataConnectorParser.java
+++ b/idp-attribute-resolver-spring/src/main/java/net/shibboleth/idp/attribute/resolver/spring/dc/AbstractDataConnectorParser.java
@@ -28,7 +28,6 @@ import net.shibboleth.idp.attribute.resolver.spring.dc.impl.DataConnectorFactory
 import net.shibboleth.idp.attribute.resolver.spring.impl.AttributeResolverNamespaceHandler;
 import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
 import net.shibboleth.utilities.java.support.primitive.StringSupport;
-import net.shibboleth.utilities.java.support.xml.DOMTypeSupport;
 import net.shibboleth.utilities.java.support.xml.ElementSupport;
 
 import org.slf4j.Logger;
@@ -127,12 +126,8 @@ public abstract class AbstractDataConnectorParser extends BaseResolverPluginPars
         }
 
         if (config.hasAttributeNS(null, ATTR_NORETRYDELAY)) {
-            final String noRetryDelay = StringSupport.trimOrNull(config.getAttributeNS(null, ATTR_NORETRYDELAY));
-            final BeanDefinitionBuilder duration =
-                    BeanDefinitionBuilder.rootBeanDefinition(AbstractDataConnectorParser.class, "buildDuration");
-            duration.addConstructorArgValue(noRetryDelay);
-            duration.addConstructorArgValue(1);
-            builder.addPropertyValue("noRetryDelay", duration.getBeanDefinition());
+            builder.addPropertyValue("noRetryDelay",
+                    StringSupport.trimOrNull(config.getAttributeNS(null, ATTR_NORETRYDELAY)));
         }
 
         if (isNative(config)) {
@@ -187,19 +182,6 @@ public abstract class AbstractDataConnectorParser extends BaseResolverPluginPars
     protected abstract void doV2Parse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder);
 
     /**
-     * Converts the supplied duration to milliseconds and divides it by the divisor. Useful for modifying durations
-     * while resolving property replacement.
-     * 
-     * @param duration string format
-     * @param divisor to modify the duration with
-     * 
-     * @return result of the division
-     */
-    public static long buildDuration(final String duration, final long divisor) {
-        return DOMTypeSupport.durationToLong(duration) / divisor;
-    }
-
-    /**
      * Return a string which is to be prepended to all log messages.
      * 
      * @return "Data Connector: '<definitionID>' :"
diff --git a/idp-attribute-resolver-spring/src/main/java/net/shibboleth/idp/attribute/resolver/spring/dc/impl/DataConnectorFactoryBean.java b/idp-attribute-resolver-spring/src/main/java/net/shibboleth/idp/attribute/resolver/spring/dc/impl/DataConnectorFactoryBean.java
index 83e37a4..1148e9e 100644
--- a/idp-attribute-resolver-spring/src/main/java/net/shibboleth/idp/attribute/resolver/spring/dc/impl/DataConnectorFactoryBean.java
+++ b/idp-attribute-resolver-spring/src/main/java/net/shibboleth/idp/attribute/resolver/spring/dc/impl/DataConnectorFactoryBean.java
@@ -20,6 +20,7 @@ package net.shibboleth.idp.attribute.resolver.spring.dc.impl;
 import java.beans.Introspector;
 import java.beans.PropertyDescriptor;
 import java.lang.reflect.Constructor;
+import java.time.Duration;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
@@ -31,7 +32,6 @@ import javax.annotation.Nullable;
 import net.shibboleth.ext.spring.util.ApplicationContextBuilder;
 import net.shibboleth.idp.attribute.resolver.AbstractDataConnector;
 import net.shibboleth.idp.attribute.resolver.spring.impl.AbstractResolverPluginFactoryBean;
-import net.shibboleth.utilities.java.support.annotation.Duration;
 import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
 import net.shibboleth.utilities.java.support.logic.Constraint;
 
@@ -84,7 +84,7 @@ public class DataConnectorFactoryBean extends AbstractResolverPluginFactoryBean<
     @Nullable private String failoverDataConnectorId;
 
     /** Data Connector property "noRetryDelay". */
-    @Nullable @Duration private Long noRetryDelay;
+    @Nullable private Duration noRetryDelay;
 
     /**
      * Data Connector property "failoverDataConnectorId".
@@ -109,7 +109,7 @@ public class DataConnectorFactoryBean extends AbstractResolverPluginFactoryBean<
      * 
      * @return the value of property to set or null if never set
      */
-    @Nullable @Duration public Long getNoRetryDelay() {
+    @Nullable public Duration getNoRetryDelay() {
         return noRetryDelay;
     }
     
@@ -118,7 +118,7 @@ public class DataConnectorFactoryBean extends AbstractResolverPluginFactoryBean<
      * 
      * @param delay the value to set
      */
-    @Duration public void setNoRetryDelay(@Nullable @Duration final Long delay) {
+    public void setNoRetryDelay(@Nullable final Duration delay) {
         noRetryDelay = delay;
     }
 
diff --git a/idp-attribute-resolver-spring/src/test/java/net/shibboleth/idp/attribute/resolver/spring/dc/http/HTTPDataConnectorParserTest.java b/idp-attribute-resolver-spring/src/test/java/net/shibboleth/idp/attribute/resolver/spring/dc/http/HTTPDataConnectorParserTest.java
index d17278d..c7df46f 100644
--- a/idp-attribute-resolver-spring/src/test/java/net/shibboleth/idp/attribute/resolver/spring/dc/http/HTTPDataConnectorParserTest.java
+++ b/idp-attribute-resolver-spring/src/test/java/net/shibboleth/idp/attribute/resolver/spring/dc/http/HTTPDataConnectorParserTest.java
@@ -32,6 +32,7 @@ import org.testng.annotations.AfterMethod;
 import org.testng.annotations.Test;
 
 import net.shibboleth.ext.spring.config.DurationToLongConverter;
+import net.shibboleth.ext.spring.config.StringToDurationConverter;
 import net.shibboleth.ext.spring.config.StringToIPRangeConverter;
 import net.shibboleth.ext.spring.config.StringToResourceConverter;
 import net.shibboleth.ext.spring.util.SchemaTypeAwareXMLBeanDefinitionReader;
@@ -346,7 +347,7 @@ public class HTTPDataConnectorParserTest {
 
         final ConversionServiceFactoryBean service = new ConversionServiceFactoryBean();
         service.setConverters(new HashSet<>(Arrays.asList(new DurationToLongConverter(), new StringToIPRangeConverter(),
-                new StringToResourceConverter())));
+                new StringToResourceConverter(), new StringToDurationConverter())));
         service.afterPropertiesSet();
 
         context.getBeanFactory().setConversionService(service.getObject());
diff --git a/idp-attribute-resolver-spring/src/test/java/net/shibboleth/idp/attribute/resolver/spring/dc/ldap/LDAPDataConnectorParserTest.java b/idp-attribute-resolver-spring/src/test/java/net/shibboleth/idp/attribute/resolver/spring/dc/ldap/LDAPDataConnectorParserTest.java
index c415d37..b914645 100644
--- a/idp-attribute-resolver-spring/src/test/java/net/shibboleth/idp/attribute/resolver/spring/dc/ldap/LDAPDataConnectorParserTest.java
+++ b/idp-attribute-resolver-spring/src/test/java/net/shibboleth/idp/attribute/resolver/spring/dc/ldap/LDAPDataConnectorParserTest.java
@@ -19,6 +19,7 @@ package net.shibboleth.idp.attribute.resolver.spring.dc.ldap;
 
 import java.io.IOException;
 import java.security.GeneralSecurityException;
+import java.time.Duration;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -44,8 +45,6 @@ import org.springframework.core.env.StandardEnvironment;
 import org.springframework.core.io.ClassPathResource;
 import org.springframework.core.io.Resource;
 import org.springframework.core.io.support.ResourcePropertySource;
-import org.testng.Assert;
-import org.testng.AssertJUnit;
 import org.testng.annotations.AfterMethod;
 import org.testng.annotations.AfterTest;
 import org.testng.annotations.BeforeTest;
@@ -62,6 +61,7 @@ import com.unboundid.util.ssl.SSLUtil;
 import com.unboundid.util.ssl.TrustStoreTrustManager;
 
 import net.shibboleth.ext.spring.config.DurationToLongConverter;
+import net.shibboleth.ext.spring.config.StringToDurationConverter;
 import net.shibboleth.ext.spring.config.StringToIPRangeConverter;
 import net.shibboleth.ext.spring.config.StringToResourceConverter;
 import net.shibboleth.ext.spring.context.FilesystemGenericApplicationContext;
@@ -76,6 +76,9 @@ import net.shibboleth.idp.attribute.resolver.dc.ldap.impl.StringAttributeValueMa
 import net.shibboleth.idp.attribute.resolver.spring.dc.ldap.impl.LDAPDataConnectorParser;
 import net.shibboleth.idp.saml.impl.TestSources;
 
+import static org.testng.Assert.*;
+
+
 /** Test for {@link LDAPDataConnectorParser}. */
 public class LDAPDataConnectorParserTest {
 
@@ -104,7 +107,6 @@ public class LDAPDataConnectorParserTest {
      * @throws GeneralSecurityException if the startTLS keystore or truststore cannot be loaded
      */
     @BeforeTest public void setupDirectoryServer() throws LDAPException, GeneralSecurityException {
-
         final InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig("dc=shibboleth,dc=net");
         final SSLUtil sslUtil =
                 new SSLUtil(new KeyStoreKeyManager(
@@ -131,47 +133,47 @@ public class LDAPDataConnectorParserTest {
     @Test public void v2Config() throws Exception {
         final LDAPDataConnector dataConnector =
                 getLdapDataConnector(new String[] {"net/shibboleth/idp/attribute/resolver/spring/dc/ldap/resolver/ldap-attribute-resolver-v2.xml"});
-        Assert.assertNotNull(dataConnector);
+        assertNotNull(dataConnector);
         doTest(dataConnector);
         final StringAttributeValueMappingStrategy mappingStrategy =
                 (StringAttributeValueMappingStrategy) dataConnector.getMappingStrategy();
-        Assert.assertEquals(mappingStrategy.getResultRenamingMap().size(), 1);
-        Assert.assertEquals(mappingStrategy.getResultRenamingMap().get("homephone"), "phonenumber");
+        assertEquals(mappingStrategy.getResultRenamingMap().size(), 1);
+        assertEquals(mappingStrategy.getResultRenamingMap().get("homephone"), "phonenumber");
 
         dataConnector.initialize();
         final AttributeResolutionContext context =
                 TestSources.createResolutionContext(TestSources.PRINCIPAL_ID, TestSources.IDP_ENTITY_ID,
                         TestSources.SP_ENTITY_ID);
         final Map<String, IdPAttribute> attrs = dataConnector.resolve(context);
-        Assert.assertNotNull(attrs);
-        Assert.assertNotNull(attrs.get("entryDN"));
+        assertNotNull(attrs);
+        assertNotNull(attrs.get("entryDN"));
     }
 
     
     @Test public void v2NoSec() throws Exception {
         final LDAPDataConnector dataConnector =
                 getLdapDataConnector(new String[] {"net/shibboleth/idp/attribute/resolver/spring/dc/ldap/resolver/ldap-attribute-resolver-v2-nosec.xml"});
-        Assert.assertNotNull(dataConnector);
+        assertNotNull(dataConnector);
         doTest(dataConnector);
         final StringAttributeValueMappingStrategy mappingStrategy =
                 (StringAttributeValueMappingStrategy) dataConnector.getMappingStrategy();
-        Assert.assertEquals(mappingStrategy.getResultRenamingMap().size(), 1);
-        Assert.assertEquals(mappingStrategy.getResultRenamingMap().get("homephone"), "phonenumber");
+        assertEquals(mappingStrategy.getResultRenamingMap().size(), 1);
+        assertEquals(mappingStrategy.getResultRenamingMap().get("homephone"), "phonenumber");
 
         dataConnector.initialize();
         final AttributeResolutionContext context =
                 TestSources.createResolutionContext(TestSources.PRINCIPAL_ID, TestSources.IDP_ENTITY_ID,
                         TestSources.SP_ENTITY_ID);
         final Map<String, IdPAttribute> attrs = dataConnector.resolve(context);
-        Assert.assertNotNull(attrs);
-        Assert.assertNotNull(attrs.get("entryDN"));
+        assertNotNull(attrs);
+        assertNotNull(attrs.get("entryDN"));
     }
 
     @Test public void v2ResolverOtherDups() throws Exception {
         final LDAPDataConnector dataConnector =
                 getLdapDataConnector(new String[] {"net/shibboleth/idp/attribute/resolver/spring/dc/ldap/resolver/ldap-attribute-resolver-v2-multi.xml",
                         "net/shibboleth/idp/attribute/resolver/spring/dc/ldap/ldap-attribute-resolver-spring-context.xml"});
-        Assert.assertNotNull(dataConnector);
+        assertNotNull(dataConnector);
         doTest(dataConnector);
     }
 
@@ -180,134 +182,134 @@ public class LDAPDataConnectorParserTest {
     @Test public void v2MinimalConfig() throws Exception {
         final LDAPDataConnector dataConnector =
                 getLdapDataConnector(new String[] {"net/shibboleth/idp/attribute/resolver/spring/dc/ldap/resolver/ldap-attribute-resolver-v2-minimal.xml"});
-        Assert.assertNotNull(dataConnector);
-        AssertJUnit.assertEquals(0, dataConnector.getNoRetryDelay());
+        assertNotNull(dataConnector);
+        assertEquals(Duration.ZERO, dataConnector.getNoRetryDelay());
         final DefaultConnectionFactory connFactory = (DefaultConnectionFactory) dataConnector.getConnectionFactory();
-        AssertJUnit.assertNotNull(connFactory);
+        assertNotNull(connFactory);
 
         final ConnectionConfig connConfig = connFactory.getConnectionConfig();
-        AssertJUnit.assertNotNull(connConfig);
-        AssertJUnit.assertEquals("ldap://localhost:10389", connConfig.getLdapUrl());
-        AssertJUnit.assertEquals(false, connConfig.getUseSSL());
-        AssertJUnit.assertEquals(false, connConfig.getUseStartTLS());
+        assertNotNull(connConfig);
+        assertEquals("ldap://localhost:10389", connConfig.getLdapUrl());
+        assertEquals(false, connConfig.getUseSSL());
+        assertEquals(false, connConfig.getUseStartTLS());
         final BindConnectionInitializer connInitializer = (BindConnectionInitializer) connConfig.getConnectionInitializer();
-        AssertJUnit.assertEquals("cn=Directory Manager", connInitializer.getBindDn());
-        AssertJUnit.assertEquals("password", connInitializer.getBindCredential().getString());
-        AssertJUnit.assertEquals(3000, connConfig.getConnectTimeout());
-        AssertJUnit.assertEquals(3000, connConfig.getResponseTimeout());
+        assertEquals("cn=Directory Manager", connInitializer.getBindDn());
+        assertEquals("password", connInitializer.getBindCredential().getString());
+        assertEquals(3000, connConfig.getConnectTimeout());
+        assertEquals(3000, connConfig.getResponseTimeout());
 
         final SslConfig sslConfig = connFactory.getConnectionConfig().getSslConfig();
-        AssertJUnit.assertNotNull(sslConfig);
+        assertNotNull(sslConfig);
         final CredentialConfig credentialConfig = sslConfig.getCredentialConfig();
-        AssertJUnit.assertNotNull(credentialConfig);
+        assertNotNull(credentialConfig);
 
         final ProviderConfig providerConfig = connFactory.getProvider().getProviderConfig();
-        AssertJUnit.assertNotNull(providerConfig);
-        AssertJUnit.assertTrue(providerConfig.getProperties().isEmpty());
+        assertNotNull(providerConfig);
+        assertTrue(providerConfig.getProperties().isEmpty());
 
         final SearchExecutor searchExecutor = dataConnector.getSearchExecutor();
-        AssertJUnit.assertNotNull(searchExecutor);
-        AssertJUnit.assertEquals("", searchExecutor.getBaseDn());
-        AssertJUnit.assertNull(searchExecutor.getSearchFilter());
-        AssertJUnit.assertEquals(3000, searchExecutor.getTimeLimit());
+        assertNotNull(searchExecutor);
+        assertEquals("", searchExecutor.getBaseDn());
+        assertNull(searchExecutor.getSearchFilter());
+        assertEquals(3000, searchExecutor.getTimeLimit());
 
         final ConnectionFactoryValidator validator = (ConnectionFactoryValidator) dataConnector.getValidator();
-        AssertJUnit.assertNotNull(validator);
-        AssertJUnit.assertTrue(validator.isThrowValidateError());
-        AssertJUnit.assertNotNull(validator.getConnectionFactory());
+        assertNotNull(validator);
+        assertTrue(validator.isThrowValidateError());
+        assertNotNull(validator.getConnectionFactory());
 
         final ExecutableSearchBuilder searchBuilder = dataConnector.getExecutableSearchBuilder();
-        AssertJUnit.assertNotNull(searchBuilder);
+        assertNotNull(searchBuilder);
 
         final StringAttributeValueMappingStrategy mappingStrategy = (StringAttributeValueMappingStrategy) dataConnector.getMappingStrategy();
-        AssertJUnit.assertNotNull(mappingStrategy);
-        AssertJUnit.assertFalse(mappingStrategy.isNoResultAnError());
-        AssertJUnit.assertFalse(mappingStrategy.isMultipleResultsAnError());
+        assertNotNull(mappingStrategy);
+        assertFalse(mappingStrategy.isNoResultAnError());
+        assertFalse(mappingStrategy.isMultipleResultsAnError());
 
-        AssertJUnit.assertNull(dataConnector.getResultsCache());
+        assertNull(dataConnector.getResultsCache());
         
         dataConnector.initialize();
         final AttributeResolutionContext context =
                 TestSources.createResolutionContext(TestSources.PRINCIPAL_ID, TestSources.IDP_ENTITY_ID,
                         TestSources.SP_ENTITY_ID);
         final Map<String, IdPAttribute> attrs = dataConnector.resolve(context);
-        Assert.assertNotNull(attrs);
-        Assert.assertNotNull(attrs.get("entryDN"));
+        assertNotNull(attrs);
+        assertNotNull(attrs.get("entryDN"));
     }
 
     @Test public void v2MinimalPoolConfig() throws Exception {
         final LDAPDataConnector dataConnector =
                 getLdapDataConnector(new String[] {"net/shibboleth/idp/attribute/resolver/spring/dc/ldap/resolver/ldap-attribute-resolver-v2-minimal-pool.xml"});
-        Assert.assertNotNull(dataConnector);
-        AssertJUnit.assertEquals(0, dataConnector.getNoRetryDelay());
+        assertNotNull(dataConnector);
+        assertEquals(Duration.ZERO, dataConnector.getNoRetryDelay());
         final PooledConnectionFactory connFactory = (PooledConnectionFactory) dataConnector.getConnectionFactory();
-        AssertJUnit.assertNotNull(connFactory);
+        assertNotNull(connFactory);
         final BlockingConnectionPool connPool = (BlockingConnectionPool) connFactory.getConnectionPool();
-        AssertJUnit.assertNotNull(connPool);
-        AssertJUnit.assertEquals(0, connPool.getBlockWaitTime());
-        AssertJUnit.assertEquals("resolver-pool", connPool.getName());
+        assertNotNull(connPool);
+        assertEquals(0, connPool.getBlockWaitTime());
+        assertEquals("resolver-pool", connPool.getName());
         final PoolConfig poolConfig = connPool.getPoolConfig();
-        AssertJUnit.assertNotNull(poolConfig);
-        AssertJUnit.assertEquals(0, poolConfig.getMinPoolSize());
-        AssertJUnit.assertEquals(3, poolConfig.getMaxPoolSize());
-        AssertJUnit.assertFalse(poolConfig.isValidatePeriodically());
-        AssertJUnit.assertEquals(1800, poolConfig.getValidatePeriod());
-        AssertJUnit.assertTrue(connPool.getFailFastInitialize());
-        AssertJUnit.assertNull(connPool.getValidator());
+        assertNotNull(poolConfig);
+        assertEquals(0, poolConfig.getMinPoolSize());
+        assertEquals(3, poolConfig.getMaxPoolSize());
+        assertFalse(poolConfig.isValidatePeriodically());
+        assertEquals(1800, poolConfig.getValidatePeriod());
+        assertTrue(connPool.getFailFastInitialize());
+        assertNull(connPool.getValidator());
 
         final IdlePruneStrategy pruneStrategy = (IdlePruneStrategy) connPool.getPruneStrategy();
-        AssertJUnit.assertNotNull(pruneStrategy);
-        AssertJUnit.assertEquals(300, pruneStrategy.getPrunePeriod());
-        AssertJUnit.assertEquals(600, pruneStrategy.getIdleTime());
+        assertNotNull(pruneStrategy);
+        assertEquals(300, pruneStrategy.getPrunePeriod());
+        assertEquals(600, pruneStrategy.getIdleTime());
 
         final ConnectionConfig connConfig = connPool.getConnectionFactory().getConnectionConfig();
-        AssertJUnit.assertNotNull(connConfig);
-        AssertJUnit.assertEquals("ldap://localhost:10389", connConfig.getLdapUrl());
-        AssertJUnit.assertEquals(false, connConfig.getUseSSL());
-        AssertJUnit.assertEquals(false, connConfig.getUseStartTLS());
+        assertNotNull(connConfig);
+        assertEquals("ldap://localhost:10389", connConfig.getLdapUrl());
+        assertEquals(false, connConfig.getUseSSL());
+        assertEquals(false, connConfig.getUseStartTLS());
         final BindConnectionInitializer connInitializer = (BindConnectionInitializer) connConfig.getConnectionInitializer();
-        AssertJUnit.assertEquals("cn=Directory Manager", connInitializer.getBindDn());
-        AssertJUnit.assertEquals("password", connInitializer.getBindCredential().getString());
-        AssertJUnit.assertEquals(3000, connConfig.getConnectTimeout());
-        AssertJUnit.assertEquals(3000, connConfig.getResponseTimeout());
+        assertEquals("cn=Directory Manager", connInitializer.getBindDn());
+        assertEquals("password", connInitializer.getBindCredential().getString());
+        assertEquals(3000, connConfig.getConnectTimeout());
+        assertEquals(3000, connConfig.getResponseTimeout());
 
         final SslConfig sslConfig = connPool.getConnectionFactory().getConnectionConfig().getSslConfig();
-        AssertJUnit.assertNotNull(sslConfig);
+        assertNotNull(sslConfig);
         final CredentialConfig credentialConfig = sslConfig.getCredentialConfig();
-        AssertJUnit.assertNotNull(credentialConfig);
+        assertNotNull(credentialConfig);
 
         final ProviderConfig providerConfig = connPool.getConnectionFactory().getProvider().getProviderConfig();
-        AssertJUnit.assertNotNull(providerConfig);
-        AssertJUnit.assertTrue(providerConfig.getProperties().isEmpty());
+        assertNotNull(providerConfig);
+        assertTrue(providerConfig.getProperties().isEmpty());
 
         final SearchExecutor searchExecutor = dataConnector.getSearchExecutor();
-        AssertJUnit.assertNotNull(searchExecutor);
-        AssertJUnit.assertEquals("", searchExecutor.getBaseDn());
-        AssertJUnit.assertNull(searchExecutor.getSearchFilter());
-        AssertJUnit.assertEquals(3000, searchExecutor.getTimeLimit());
+        assertNotNull(searchExecutor);
+        assertEquals("", searchExecutor.getBaseDn());
+        assertNull(searchExecutor.getSearchFilter());
+        assertEquals(3000, searchExecutor.getTimeLimit());
 
         final ConnectionFactoryValidator validator = (ConnectionFactoryValidator) dataConnector.getValidator();
-        AssertJUnit.assertNotNull(validator);
-        AssertJUnit.assertTrue(validator.isThrowValidateError());
-        AssertJUnit.assertNotNull(validator.getConnectionFactory());
+        assertNotNull(validator);
+        assertTrue(validator.isThrowValidateError());
+        assertNotNull(validator.getConnectionFactory());
 
         final ExecutableSearchBuilder searchBuilder = dataConnector.getExecutableSearchBuilder();
-        AssertJUnit.assertNotNull(searchBuilder);
+        assertNotNull(searchBuilder);
 
         final StringAttributeValueMappingStrategy mappingStrategy = (StringAttributeValueMappingStrategy) dataConnector.getMappingStrategy();
-        AssertJUnit.assertNotNull(mappingStrategy);
-        AssertJUnit.assertFalse(mappingStrategy.isNoResultAnError());
-        AssertJUnit.assertFalse(mappingStrategy.isMultipleResultsAnError());
+        assertNotNull(mappingStrategy);
+        assertFalse(mappingStrategy.isNoResultAnError());
+        assertFalse(mappingStrategy.isMultipleResultsAnError());
 
-        AssertJUnit.assertNull(dataConnector.getResultsCache());
+        assertNull(dataConnector.getResultsCache());
         
         dataConnector.initialize();
         final AttributeResolutionContext context =
                 TestSources.createResolutionContext(TestSources.PRINCIPAL_ID, TestSources.IDP_ENTITY_ID,
                         TestSources.SP_ENTITY_ID);
         final Map<String, IdPAttribute> attrs = dataConnector.resolve(context);
-        Assert.assertNotNull(attrs);
-        Assert.assertNotNull(attrs.get("entryDN"));
+        assertNotNull(attrs);
+        assertNotNull(attrs.get("entryDN"));
     }
 
     @Test public void v2PropsConfig() throws Exception {
@@ -315,7 +317,7 @@ public class LDAPDataConnectorParserTest {
         final LDAPDataConnector dataConnector =
                 getLdapDataConnector(props, new String[] {
                         "net/shibboleth/idp/attribute/resolver/spring/dc/ldap/resolver/ldap-attribute-resolver-v2-props.xml",});
-        Assert.assertNotNull(dataConnector);
+        assertNotNull(dataConnector);
         doTest(dataConnector);
 
         dataConnector.initialize();
@@ -323,18 +325,18 @@ public class LDAPDataConnectorParserTest {
                 TestSources.createResolutionContext(TestSources.PRINCIPAL_ID, TestSources.IDP_ENTITY_ID,
                         TestSources.SP_ENTITY_ID);
         final Map<String, IdPAttribute> attrs = dataConnector.resolve(context);
-        Assert.assertNotNull(attrs);
-        Assert.assertEquals(attrs.size(), 4);
-        Assert.assertNotNull(attrs.get("uid"));
-        Assert.assertNotNull(attrs.get("homephone"));
-        Assert.assertNotNull(attrs.get("mail"));
-        Assert.assertNotNull(attrs.get("entryDN"));
+        assertNotNull(attrs);
+        assertEquals(attrs.size(), 4);
+        assertNotNull(attrs.get("uid"));
+        assertNotNull(attrs.get("homephone"));
+        assertNotNull(attrs.get("mail"));
+        assertNotNull(attrs.get("entryDN"));
     }
 
     @Test public void springConfig() throws Exception {
         final LDAPDataConnector dataConnector =
                 getLdapDataConnector(new String[] {"net/shibboleth/idp/attribute/resolver/spring/dc/ldap/resolver/ldap-attribute-resolver-spring.xml"});
-        Assert.assertNotNull(dataConnector);
+        assertNotNull(dataConnector);
         doTest(dataConnector);
 
         dataConnector.initialize();
@@ -342,7 +344,7 @@ public class LDAPDataConnectorParserTest {
                 TestSources.createResolutionContext(TestSources.PRINCIPAL_ID, TestSources.IDP_ENTITY_ID,
                         TestSources.SP_ENTITY_ID);
         final Map<String, IdPAttribute> attrs = dataConnector.resolve(context);
-        Assert.assertNotNull(attrs);
+        assertNotNull(attrs);
     }
 
     @Test public void springPropsConfig() throws Exception,
@@ -350,7 +352,7 @@ public class LDAPDataConnectorParserTest {
         final Resource props = new ClassPathResource("net/shibboleth/idp/attribute/resolver/spring/dc/ldap/ldap-v3.properties");
         final LDAPDataConnector dataConnector =
                 getLdapDataConnector(props, new String[] {"net/shibboleth/idp/attribute/resolver/spring/dc/ldap/resolver/ldap-attribute-resolver-spring-props.xml"});
-        Assert.assertNotNull(dataConnector);
+        assertNotNull(dataConnector);
         doTest(dataConnector);
 
         dataConnector.initialize();
@@ -358,11 +360,11 @@ public class LDAPDataConnectorParserTest {
                 TestSources.createResolutionContext(TestSources.PRINCIPAL_ID, TestSources.IDP_ENTITY_ID,
                         TestSources.SP_ENTITY_ID);
         final Map<String, IdPAttribute> attrs = dataConnector.resolve(context);
-        Assert.assertNotNull(attrs);
-        Assert.assertEquals(attrs.size(), 3);
-        Assert.assertNotNull(attrs.get("uid"));
-        Assert.assertNotNull(attrs.get("phonenumber"));
-        Assert.assertNotNull(attrs.get("mail"));
+        assertNotNull(attrs);
+        assertEquals(attrs.size(), 3);
+        assertNotNull(attrs.get("uid"));
+        assertNotNull(attrs.get("phonenumber"));
+        assertNotNull(attrs.get("mail"));
     }
 
     /**
@@ -387,11 +389,11 @@ public class LDAPDataConnectorParserTest {
         c = context.getBean("cache");
         final Object ccc = context.getBean(Cache.class);
 
-        Assert.assertNotNull(cb);
-        Assert.assertNotNull(c);
-        Assert.assertNotNull(cbc);
-        Assert.assertNotNull(ccc);
-        Assert.assertNotNull(cc,
+        assertNotNull(cb);
+        assertNotNull(c);
+        assertNotNull(cbc);
+        assertNotNull(ccc);
+        assertNotNull(cc,
                 "The Spring bug described in https://issues.shibboleth.net/jira/browse/IDP-338 has come back");
 
     }
@@ -401,22 +403,22 @@ public class LDAPDataConnectorParserTest {
                 getLdapDataConnector(new String[] {
                         "net/shibboleth/idp/attribute/resolver/spring/dc/ldap/resolver/ldap-attribute-resolver-v2-hybrid.xml",
                         "net/shibboleth/idp/attribute/resolver/spring/dc/ldap/ldap-attribute-resolver-spring-context.xml"});
-        Assert.assertNotNull(dataConnector);
+        assertNotNull(dataConnector);
         doTest(dataConnector);
 
         dataConnector.initialize();
         final StringAttributeValueMappingStrategy mappingStrategy =
                 (StringAttributeValueMappingStrategy) dataConnector.getMappingStrategy();
-        Assert.assertEquals(mappingStrategy.getResultRenamingMap().size(), 1);
-        Assert.assertEquals(mappingStrategy.getResultRenamingMap().get("homephone"), "phonenumber");
+        assertEquals(mappingStrategy.getResultRenamingMap().size(), 1);
+        assertEquals(mappingStrategy.getResultRenamingMap().get("homephone"), "phonenumber");
         final AttributeResolutionContext context =
                 TestSources.createResolutionContext(TestSources.PRINCIPAL_ID, TestSources.IDP_ENTITY_ID,
                         TestSources.SP_ENTITY_ID);
         final Map<String, IdPAttribute> attrs = dataConnector.resolve(context);
-        Assert.assertNotNull(attrs);
-        Assert.assertNull(attrs.get("homephone"));
-        Assert.assertNotNull(attrs.get("phonenumber"));
-        Assert.assertNotNull(attrs.get("entryDN"));
+        assertNotNull(attrs);
+        assertNull(attrs.get("homephone"));
+        assertNotNull(attrs.get("phonenumber"));
+        assertNotNull(attrs.get("entryDN"));
     }
 
     protected LDAPDataConnector getLdapDataConnector(final Resource properties, final String[] beanDefinitions) throws IOException {
@@ -427,7 +429,7 @@ public class LDAPDataConnectorParserTest {
         
         final ConversionServiceFactoryBean service = new ConversionServiceFactoryBean();
         service.setConverters(new HashSet<>(Arrays.asList(new DurationToLongConverter(), new StringToIPRangeConverter(),
-                new StringToResourceConverter())));
+                new StringToResourceConverter(), new StringToDurationConverter())));
         service.afterPropertiesSet();
 
         context.getBeanFactory().setConversionService(service.getObject());
@@ -462,76 +464,76 @@ public class LDAPDataConnectorParserTest {
     protected void doTest(final LDAPDataConnector dataConnector) throws ResolutionException {
 
         final String id = dataConnector.getId();
-        AssertJUnit.assertEquals("myLDAP", id);
-        AssertJUnit.assertEquals(300000, dataConnector.getNoRetryDelay());
+        assertEquals("myLDAP", id);
+        assertEquals(Duration.ofMinutes(5), dataConnector.getNoRetryDelay());
 
         final PooledConnectionFactory connFactory = (PooledConnectionFactory) dataConnector.getConnectionFactory();
-        AssertJUnit.assertNotNull(connFactory);
+        assertNotNull(connFactory);
         final BlockingConnectionPool connPool = (BlockingConnectionPool) connFactory.getConnectionPool();
-        AssertJUnit.assertNotNull(connPool);
-        AssertJUnit.assertEquals(5000, connPool.getBlockWaitTime());
-        AssertJUnit.assertEquals("resolver-pool", connPool.getName());
+        assertNotNull(connPool);
+        assertEquals(5000, connPool.getBlockWaitTime());
+        assertEquals("resolver-pool", connPool.getName());
         final PoolConfig poolConfig = connPool.getPoolConfig();
-        AssertJUnit.assertNotNull(poolConfig);
-        AssertJUnit.assertEquals(5, poolConfig.getMinPoolSize());
-        AssertJUnit.assertEquals(10, poolConfig.getMaxPoolSize());
-        AssertJUnit.assertTrue(poolConfig.isValidatePeriodically());
-        AssertJUnit.assertEquals(900, poolConfig.getValidatePeriod());
-        AssertJUnit.assertFalse(connPool.getFailFastInitialize());
+        assertNotNull(poolConfig);
+        assertEquals(5, poolConfig.getMinPoolSize());
+        assertEquals(10, poolConfig.getMaxPoolSize());
+        assertTrue(poolConfig.isValidatePeriodically());
+        assertEquals(900, poolConfig.getValidatePeriod());
+        assertFalse(connPool.getFailFastInitialize());
 
         final SearchValidator searchValidator = (SearchValidator) connPool.getValidator();
-        AssertJUnit.assertNotNull(searchValidator);
-        AssertJUnit.assertEquals("dc=shibboleth,dc=net", searchValidator.getSearchRequest().getBaseDn());
-        AssertJUnit.assertEquals("(ou=people)", searchValidator.getSearchRequest().getSearchFilter().getFilter());
+        assertNotNull(searchValidator);
+        assertEquals("dc=shibboleth,dc=net", searchValidator.getSearchRequest().getBaseDn());
+        assertEquals("(ou=people)", searchValidator.getSearchRequest().getSearchFilter().getFilter());
 
         final IdlePruneStrategy pruneStrategy = (IdlePruneStrategy) connPool.getPruneStrategy();
-        AssertJUnit.assertNotNull(pruneStrategy);
-        AssertJUnit.assertEquals(300, pruneStrategy.getPrunePeriod());
-        AssertJUnit.assertEquals(600, pruneStrategy.getIdleTime());
+        assertNotNull(pruneStrategy);
+        assertEquals(300, pruneStrategy.getPrunePeriod());
+        assertEquals(600, pruneStrategy.getIdleTime());
 
         final ConnectionConfig connConfig = connPool.getConnectionFactory().getConnectionConfig();
-        AssertJUnit.assertNotNull(connConfig);
-        AssertJUnit.assertEquals("ldap://localhost:10389", connConfig.getLdapUrl());
-        AssertJUnit.assertEquals(false, connConfig.getUseSSL());
-        AssertJUnit.assertEquals(true, connConfig.getUseStartTLS());
+        assertNotNull(connConfig);
+        assertEquals("ldap://localhost:10389", connConfig.getLdapUrl());
+        assertEquals(false, connConfig.getUseSSL());
+        assertEquals(true, connConfig.getUseStartTLS());
         final BindConnectionInitializer connInitializer = (BindConnectionInitializer) connConfig.getConnectionInitializer();
-        AssertJUnit.assertEquals("cn=Directory Manager", connInitializer.getBindDn());
-        AssertJUnit.assertEquals("password", connInitializer.getBindCredential().getString());
-        AssertJUnit.assertEquals(2000, connConfig.getConnectTimeout());
-        AssertJUnit.assertEquals(4000, connConfig.getResponseTimeout());
+        assertEquals("cn=Directory Manager", connInitializer.getBindDn());
+        assertEquals("password", connInitializer.getBindCredential().getString());
+        assertEquals(2000, connConfig.getConnectTimeout());
+        assertEquals(4000, connConfig.getResponseTimeout());
 
         final SslConfig sslConfig = connPool.getConnectionFactory().getConnectionConfig().getSslConfig();
-        AssertJUnit.assertNotNull(sslConfig);
+        assertNotNull(sslConfig);
         final CredentialConfig credentialConfig = sslConfig.getCredentialConfig();
-        AssertJUnit.assertNotNull(credentialConfig);
+        assertNotNull(credentialConfig);
 
         final Map<String, Object> providerProps = new HashMap<>();
         providerProps.put("name1", "value1");
         providerProps.put("name2", "value2");
         final ProviderConfig providerConfig = connPool.getConnectionFactory().getProvider().getProviderConfig();
-        AssertJUnit.assertNotNull(providerConfig);
-        AssertJUnit.assertEquals(providerProps, providerConfig.getProperties());
+        assertNotNull(providerConfig);
+        assertEquals(providerProps, providerConfig.getProperties());
 
         final SearchExecutor searchExecutor = dataConnector.getSearchExecutor();
-        AssertJUnit.assertNotNull(searchExecutor);
-        AssertJUnit.assertEquals("ou=people,dc=shibboleth,dc=net", searchExecutor.getBaseDn());
-        AssertJUnit.assertNull(searchExecutor.getSearchFilter());
-        AssertJUnit.assertEquals(7000, searchExecutor.getTimeLimit());
+        assertNotNull(searchExecutor);
+        assertEquals("ou=people,dc=shibboleth,dc=net", searchExecutor.getBaseDn());
+        assertNull(searchExecutor.getSearchFilter());
+        assertEquals(7000, searchExecutor.getTimeLimit());
 
         final ConnectionFactoryValidator validator = (ConnectionFactoryValidator) dataConnector.getValidator();
-        AssertJUnit.assertNotNull(validator);
-        AssertJUnit.assertTrue(validator.isThrowValidateError());
-        AssertJUnit.assertNotNull(validator.getConnectionFactory());
+        assertNotNull(validator);
+        assertTrue(validator.isThrowValidateError());
+        assertNotNull(validator.getConnectionFactory());
 
         final ExecutableSearchBuilder searchBuilder = dataConnector.getExecutableSearchBuilder();
-        AssertJUnit.assertNotNull(searchBuilder);
+        assertNotNull(searchBuilder);
 
         final StringAttributeValueMappingStrategy mappingStrategy = (StringAttributeValueMappingStrategy) dataConnector.getMappingStrategy();
-        AssertJUnit.assertNotNull(mappingStrategy);
-        AssertJUnit.assertTrue(mappingStrategy.isNoResultAnError());
-        AssertJUnit.assertTrue(mappingStrategy.isMultipleResultsAnError());
+        assertNotNull(mappingStrategy);
+        assertTrue(mappingStrategy.isNoResultAnError());
+        assertTrue(mappingStrategy.isMultipleResultsAnError());
 
         final Cache<String, Map<String, IdPAttribute>> resultCache = dataConnector.getResultsCache();
-        AssertJUnit.assertNotNull(resultCache);
+        assertNotNull(resultCache);
     }
 }
diff --git a/idp-attribute-resolver-spring/src/test/java/net/shibboleth/idp/attribute/resolver/spring/dc/rdbms/RDBMSDataConnectorParserTest.java b/idp-attribute-resolver-spring/src/test/java/net/shibboleth/idp/attribute/resolver/spring/dc/rdbms/RDBMSDataConnectorParserTest.java
index 2bdfc12..52bf1a4 100644
--- a/idp-attribute-resolver-spring/src/test/java/net/shibboleth/idp/attribute/resolver/spring/dc/rdbms/RDBMSDataConnectorParserTest.java
+++ b/idp-attribute-resolver-spring/src/test/java/net/shibboleth/idp/attribute/resolver/spring/dc/rdbms/RDBMSDataConnectorParserTest.java
@@ -19,6 +19,7 @@ package net.shibboleth.idp.attribute.resolver.spring.dc.rdbms;
 
 import java.io.IOException;
 import java.sql.SQLException;
+import java.time.Duration;
 import java.util.Arrays;
 import java.util.HashSet;
 import java.util.Map;
@@ -43,6 +44,7 @@ import org.testng.annotations.Test;
 import com.google.common.cache.Cache;
 
 import net.shibboleth.ext.spring.config.DurationToLongConverter;
+import net.shibboleth.ext.spring.config.StringToDurationConverter;
 import net.shibboleth.ext.spring.config.StringToIPRangeConverter;
 import net.shibboleth.ext.spring.config.StringToResourceConverter;
 import net.shibboleth.ext.spring.util.SchemaTypeAwareXMLBeanDefinitionReader;
@@ -170,7 +172,7 @@ public class RDBMSDataConnectorParserTest {
 
         final ConversionServiceFactoryBean service = new ConversionServiceFactoryBean();
         service.setConverters(new HashSet<>(Arrays.asList(new DurationToLongConverter(), new StringToIPRangeConverter(),
-                new StringToResourceConverter())));
+                new StringToResourceConverter(), new StringToDurationConverter())));
         service.afterPropertiesSet();
 
         context.getBeanFactory().setConversionService(service.getObject());
@@ -193,7 +195,7 @@ public class RDBMSDataConnectorParserTest {
 
         final String id = dataConnector.getId();
         Assert.assertEquals("myDatabase", id);
-        Assert.assertEquals(300000, dataConnector.getNoRetryDelay());
+        Assert.assertEquals(Duration.ofMinutes(5), dataConnector.getNoRetryDelay());
 
         final BasicDataSource dataSource = (BasicDataSource) dataConnector.getDataSource();
         Assert.assertNotNull(dataSource);
diff --git a/idp-authn-api/pom.xml b/idp-authn-api/pom.xml
index 871a97e..072b6f2 100644
--- a/idp-authn-api/pom.xml
+++ b/idp-authn-api/pom.xml
@@ -54,10 +54,6 @@
             <artifactId>javax.json-api</artifactId>
         </dependency>
         <dependency>
-            <groupId>joda-time</groupId>
-            <artifactId>joda-time</artifactId>
-        </dependency>
-        <dependency>
             <groupId>eu.bitwalker</groupId>
             <artifactId>UserAgentUtils</artifactId>
             <version>1.18</version>
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 1680062..c861c61 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
@@ -258,9 +258,10 @@ public class AuthenticationFlowDescriptor extends AbstractIdentifiableInitializa
                 "AuthenticationResult was not produced by this flow");
 
         final long now = System.currentTimeMillis();
-        if (getLifetime() > 0 && result.getAuthenticationInstant() + getLifetime() <= now) {
+        if (getLifetime() > 0 && result.getAuthenticationInstant().plusMillis(getLifetime()).toEpochMilli() <= now) {
             return false;
-        } else if (getInactivityTimeout() > 0 && result.getLastActivityInstant() + getInactivityTimeout() <= now) {
+        } else if (getInactivityTimeout() > 0
+                && result.getLastActivityInstant().plusMillis(getInactivityTimeout()).toEpochMilli() <= now) {
             return false;
         }
 
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 0cc210f..52f42e3 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
@@ -18,6 +18,7 @@
 package net.shibboleth.idp.authn;
 
 import java.security.Principal;
+import java.time.Instant;
 import java.util.Collections;
 import java.util.Objects;
 import java.util.Set;
@@ -31,13 +32,10 @@ import net.shibboleth.idp.authn.principal.PrincipalSupportingComponent;
 import net.shibboleth.idp.authn.principal.UsernamePrincipal;
 import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
 import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
-import net.shibboleth.utilities.java.support.annotation.constraint.Positive;
 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;
 
-import org.joda.time.DateTime;
-
 import com.google.common.base.MoreObjects;
 import com.google.common.collect.ImmutableSet;
 
@@ -58,11 +56,11 @@ public class AuthenticationResult implements PrincipalSupportingComponent {
     /** The identifier of the flow used to produce this result. */
     @Nonnull @NotEmpty private final String authenticationFlowId;
     
-    /** The time, in milliseconds since the epoch, that the authentication completed. */
-    @Positive private long authenticationInstant;
+    /** The time that the authentication completed. */
+    @Nonnull private Instant authenticationInstant;
 
-    /** The last time, in milliseconds since the epoch, this result was used to bypass authentication. */
-    @Positive private long lastActivityInstant;
+    /** The last time this result was used to bypass authentication. */
+    @Nonnull private Instant lastActivityInstant;
     
     /** Tracks whether a result was loaded from a previous session or created as part of the current request. */
     private boolean previousResult;
@@ -80,7 +78,7 @@ public class AuthenticationResult implements PrincipalSupportingComponent {
         authenticationFlowId = Constraint.isNotNull(StringSupport.trimOrNull(flowId),
                 "Authentication flow ID cannot be null nor empty");
         subject = Constraint.isNotNull(newSubject, "Subject list cannot be null or empty");
-        authenticationInstant = System.currentTimeMillis();
+        authenticationInstant = Instant.now();
         lastActivityInstant = authenticationInstant;
     }
 
@@ -121,46 +119,46 @@ public class AuthenticationResult implements PrincipalSupportingComponent {
     }
 
     /**
-     * Get the time, in milliseconds since the epoch, that the authentication completed.
+     * Get the time that the authentication completed.
      * 
-     * @return time, in milliseconds since the epoch, that the authentication completed, never non-positive
+     * @return time that the authentication completed
      */
-    @Positive public long getAuthenticationInstant() {
+    @Nonnull public Instant getAuthenticationInstant() {
         return authenticationInstant;
     }
 
     /**
-     * Set the time, in milliseconds since the epoch, that the authentication completed.
+     * Set the time that the authentication completed.
      * 
-     * @param instant time, in milliseconds since the epoch, that the authentication completed, never non-positive
+     * @param instant time that the authentication completed, never non-positive
      */
-    public void setAuthenticationInstant(@Positive final long instant) {
-        authenticationInstant = Constraint.isGreaterThan(0, instant, "Authentication instant must be greater than 0");
+    public void setAuthenticationInstant(@Nonnull final Instant instant) {
+        authenticationInstant = Constraint.isNotNull(instant, "Authentication instant cannot be null");
     }
     
     /**
-     * Get the last time, in milliseconds since the epoch, this result was used for authentication.
+     * Get the last time this result was used for authentication.
      * 
-     * @return last time, in milliseconds since the epoch, this result was used for authentication
+     * @return last time this result was used for authentication
      */
-    @Positive public long getLastActivityInstant() {
+    @Nonnull public Instant getLastActivityInstant() {
         return lastActivityInstant;
     }
     
     /**
-     * Set the last time, in milliseconds since the epoch, result was used for authentication.
+     * Set the last time result was used for authentication.
      * 
-     * @param instant last time, in milliseconds since the epoch, result was used to bypass authentication
+     * @param instant last time result was used to bypass authentication
      */
-    public void setLastActivityInstant(@Positive final long instant) {
-        lastActivityInstant = Constraint.isGreaterThan(0, instant, "Last activity instant must be greater than 0");
+    public void setLastActivityInstant(@Nonnull final Instant instant) {
+        lastActivityInstant = Constraint.isNotNull(instant, "Last activity instant cannot be null");
     }
 
     /**
-     * Set the last activity instant, in milliseconds since the epoch, for this result to the current time.
+     * Set the last activity instant for this result to the current time.
      */
     public void setLastActivityInstantToNow() {
-        lastActivityInstant = System.currentTimeMillis();
+        lastActivityInstant = Instant.now();
     }
     
     /**
@@ -204,7 +202,7 @@ public class AuthenticationResult implements PrincipalSupportingComponent {
 
         if (obj instanceof AuthenticationResult) {
             return Objects.equals(getAuthenticationFlowId(), ((AuthenticationResult) obj).getAuthenticationFlowId())
-                    && getAuthenticationInstant() == ((AuthenticationResult) obj).getAuthenticationInstant();
+                    && getAuthenticationInstant().equals(((AuthenticationResult) obj).getAuthenticationInstant());
         }
 
         return false;
@@ -215,8 +213,8 @@ public class AuthenticationResult implements PrincipalSupportingComponent {
     public String toString() {
         return MoreObjects.toStringHelper(this).add("authenticationFlowId", authenticationFlowId)
                 .add("authenticatedPrincipal", getSubjectName())
-                .add("authenticationInstant", new DateTime(authenticationInstant))
-                .add("lastActivityInstant", new DateTime(lastActivityInstant))
+                .add("authenticationInstant", authenticationInstant)
+                .add("lastActivityInstant", lastActivityInstant)
                 .add("previousResult", previousResult).toString();
     }
     
diff --git a/idp-authn-api/src/main/java/net/shibboleth/idp/authn/ExternalAuthentication.java b/idp-authn-api/src/main/java/net/shibboleth/idp/authn/ExternalAuthentication.java
index 586911f..1f92f0b 100644
--- a/idp-authn-api/src/main/java/net/shibboleth/idp/authn/ExternalAuthentication.java
+++ b/idp-authn-api/src/main/java/net/shibboleth/idp/authn/ExternalAuthentication.java
@@ -85,9 +85,6 @@ public class ExternalAuthentication {
     /** Request attribute that indicates whether the authentication requires passive authentication. */
     @Nonnull @NotEmpty public static final String PASSIVE_AUTHN_PARAM = "isPassive";
 
-    /** Request attribute that provides which authentication method should be attempted. */
-    @Deprecated @Nonnull @NotEmpty public static final String AUTHN_METHOD_PARAM = "authnMethod";
-
     /** Request attribute that provides the entity ID of the relying party that is requesting authentication. */
     @Nonnull @NotEmpty public static final String RELYING_PARTY_PARAM = "relyingParty";
 
diff --git a/idp-authn-api/src/main/java/net/shibboleth/idp/authn/context/AuthenticationContext.java b/idp-authn-api/src/main/java/net/shibboleth/idp/authn/context/AuthenticationContext.java
index e67ec58..db9378b 100644
--- a/idp-authn-api/src/main/java/net/shibboleth/idp/authn/context/AuthenticationContext.java
+++ b/idp-authn-api/src/main/java/net/shibboleth/idp/authn/context/AuthenticationContext.java
@@ -19,6 +19,7 @@ package net.shibboleth.idp.authn.context;
 
 import java.lang.reflect.Constructor;
 import java.security.Principal;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -40,11 +41,9 @@ import net.shibboleth.utilities.java.support.annotation.constraint.Live;
 import net.shibboleth.utilities.java.support.annotation.constraint.NonNegative;
 import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
 import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
-import net.shibboleth.utilities.java.support.annotation.constraint.Positive;
 import net.shibboleth.utilities.java.support.logic.Constraint;
 import net.shibboleth.utilities.java.support.primitive.StringSupport;
 
-import org.joda.time.DateTime;
 import org.opensaml.messaging.context.BaseContext;
 import org.opensaml.profile.context.ProfileRequestContext;
 
@@ -67,8 +66,8 @@ import com.google.common.collect.Collections2;
  */
 public final class AuthenticationContext extends BaseContext {
 
-    /** Time, in milliseconds since the epoch, when the authentication process started. */
-    @Positive private final long initiationInstant;
+    /** Time when the authentication process started. */
+    @Nonnull private final Instant initiationInstant;
 
     /** Whether to require fresh subject interaction to succeed. */
     private boolean forceAuthn;
@@ -115,12 +114,12 @@ public final class AuthenticationContext extends BaseContext {
     /** Result may be cached for reuse in the normal way. */
     private boolean resultCacheable;
     
-    /** Time, in milliseconds since the epoch, when authentication process completed. */
-    @NonNegative private long completionInstant;
+    /** Time when authentication process completed. */
+    @Nullable private Instant completionInstant;
 
     /** Constructor. */
     public AuthenticationContext() {
-        initiationInstant = System.currentTimeMillis();
+        initiationInstant = Instant.now();
         
         availableFlows = new HashMap<>();
         potentialFlows = new LinkedHashMap<>();
@@ -137,7 +136,7 @@ public final class AuthenticationContext extends BaseContext {
      * 
      * @return time when the authentication process started
      */
-    @Positive public long getInitiationInstant() {
+    @Nonnull public Instant getInitiationInstant() {
         return initiationInstant;
     }
 
@@ -502,12 +501,11 @@ public final class AuthenticationContext extends BaseContext {
     }
         
     /**
-     * Get the time, in milliseconds since the epoch, when the authentication process ended. A value of 0 indicates
-     * that authentication has not yet completed.
+     * Get the time when the authentication process ended.
      * 
      * @return time when the authentication process ended
      */
-    @NonNegative public long getCompletionInstant() {
+    @Nullable public Instant getCompletionInstant() {
         return completionInstant;
     }
 
@@ -517,7 +515,7 @@ public final class AuthenticationContext extends BaseContext {
      * @return this authentication context
      */
     @Nonnull public AuthenticationContext setCompletionInstant() {
-        completionInstant = System.currentTimeMillis();
+        completionInstant = Instant.now();
         return this;
     }
 
@@ -680,7 +678,7 @@ public final class AuthenticationContext extends BaseContext {
     @Override
     public String toString() {
         return MoreObjects.toStringHelper(this)
-                .add("initiationInstant", new DateTime(initiationInstant))
+                .add("initiationInstant", initiationInstant)
                 .add("isPassive", isPassive)
                 .add("forceAuthn", forceAuthn)
                 .add("hintedName", hintedName)
@@ -692,7 +690,7 @@ public final class AuthenticationContext extends BaseContext {
                 .add("authenticationStateMap", stateMap)
                 .add("resultCacheable", resultCacheable)
                 .add("authenticationResult", authenticationResult)
-                .add("completionInstant", new DateTime(completionInstant))
+                .add("completionInstant", completionInstant)
                 .toString();
     }
 
diff --git a/idp-authn-api/src/main/java/net/shibboleth/idp/authn/context/ExternalAuthenticationContext.java b/idp-authn-api/src/main/java/net/shibboleth/idp/authn/context/ExternalAuthenticationContext.java
index 5eb5f06..48a7533 100644
--- a/idp-authn-api/src/main/java/net/shibboleth/idp/authn/context/ExternalAuthenticationContext.java
+++ b/idp-authn-api/src/main/java/net/shibboleth/idp/authn/context/ExternalAuthenticationContext.java
@@ -18,6 +18,7 @@
 package net.shibboleth.idp.authn.context;
 
 import java.security.Principal;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Collection;
 
@@ -25,10 +26,10 @@ import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 import javax.security.auth.Subject;
 
-import org.joda.time.DateTime;
 import org.opensaml.messaging.context.BaseContext;
 
 import net.shibboleth.utilities.java.support.annotation.constraint.Live;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
 
 /**
  * A context representing the state of an externalized authentication attempt,
@@ -52,10 +53,10 @@ public final class ExternalAuthenticationContext extends BaseContext {
     @Nullable private Subject subject;
 
     /** Time of authentication. */
-    @Nullable private DateTime authnInstant;
+    @Nullable private Instant authnInstant;
     
     /** Proxied authenticating sources. */
-    @Nonnull private Collection<String> authenticatingAuthorities;
+    @Nonnull @NonnullElements private Collection<String> authenticatingAuthorities;
     
     /** Error message. */
     @Nullable private String authnError;
@@ -168,7 +169,7 @@ public final class ExternalAuthenticationContext extends BaseContext {
      * 
      * @return time of authentication
      */
-    @Nullable public DateTime getAuthnInstant() {
+    @Nullable public Instant getAuthnInstant() {
         return authnInstant;
     }
 
@@ -179,7 +180,7 @@ public final class ExternalAuthenticationContext extends BaseContext {
      * 
      * @return this context
      */
-    @Nonnull public ExternalAuthenticationContext setAuthnInstant(@Nullable final DateTime instant) {
+    @Nonnull public ExternalAuthenticationContext setAuthnInstant(@Nullable final Instant instant) {
         authnInstant = instant;
         
         return this;
@@ -192,7 +193,7 @@ public final class ExternalAuthenticationContext extends BaseContext {
      * 
      * @since 3.4.0
      */
-    @Nonnull @Live public Collection<String> getAuthenticatingAuthorities() {
+    @Nonnull @NonnullElements @Live public Collection<String> getAuthenticatingAuthorities() {
         return authenticatingAuthorities;
     }
 
diff --git a/idp-authn-api/src/test/java/net/shibboleth/idp/authn/AuthenticationResultTest.java b/idp-authn-api/src/test/java/net/shibboleth/idp/authn/AuthenticationResultTest.java
index 7cb760c..5f09dde 100644
--- a/idp-authn-api/src/test/java/net/shibboleth/idp/authn/AuthenticationResultTest.java
+++ b/idp-authn-api/src/test/java/net/shibboleth/idp/authn/AuthenticationResultTest.java
@@ -17,6 +17,8 @@
 
 package net.shibboleth.idp.authn;
 
+import java.time.Instant;
+
 import javax.security.auth.Subject;
 
 import net.shibboleth.idp.authn.AuthenticationResult;
@@ -31,12 +33,12 @@ public class AuthenticationResultTest {
 
     /** Tests that everything is properly initialized during object construction. */
     @Test public void testInstantiation() throws Exception {
-        long start = System.currentTimeMillis();
+        Instant start = Instant.now();
         // this is here to allow the event's creation time to deviate from the 'start' time
         Thread.sleep(50);
 
         AuthenticationResult event = new AuthenticationResult("test", new UsernamePrincipal("bob"));
-        Assert.assertTrue(event.getAuthenticationInstant() > start);
+        Assert.assertTrue(event.getAuthenticationInstant().isAfter(start));
         Assert.assertEquals(event.getAuthenticationFlowId(), "test");
         
         Assert.assertTrue(event.getSubject().getPrincipals(UsernamePrincipal.class).contains(new UsernamePrincipal("bob")));
diff --git a/idp-authn-api/src/test/java/net/shibboleth/idp/authn/context/AuthenticationContextTest.java b/idp-authn-api/src/test/java/net/shibboleth/idp/authn/context/AuthenticationContextTest.java
index e7cb816..ababe2e 100644
--- a/idp-authn-api/src/test/java/net/shibboleth/idp/authn/context/AuthenticationContextTest.java
+++ b/idp-authn-api/src/test/java/net/shibboleth/idp/authn/context/AuthenticationContextTest.java
@@ -17,6 +17,7 @@
 
 package net.shibboleth.idp.authn.context;
 
+import java.time.Instant;
 import java.util.Arrays;
 import java.util.Collections;
 
@@ -36,12 +37,12 @@ public class AuthenticationContextTest {
 
     /** Tests initiation instant instantiation. */
     public void testInitiationInstant() throws Exception {
-        long start = System.currentTimeMillis();
+        Instant start = Instant.now();
         // this is here to allow the event's creation time to deviate from the 'start' time
         Thread.sleep(50);
 
         AuthenticationContext ctx = new AuthenticationContext();
-        Assert.assertTrue(ctx.getInitiationInstant() > start);
+        Assert.assertTrue(ctx.getInitiationInstant().isAfter(start));
     }
 
     /** Tests mutating forcing authentication. */
@@ -93,14 +94,14 @@ public class AuthenticationContextTest {
     /** Tests setting completion instant. */
     public void testCompletionInstant() throws Exception {
         final AuthenticationContext ctx = new AuthenticationContext();
-        Assert.assertEquals(ctx.getCompletionInstant(), 0);
+        Assert.assertNull(ctx.getCompletionInstant());
 
-        long now = System.currentTimeMillis();
+        Instant now = Instant.now();
         // this is here to allow the event's creation time to deviate from the 'start' time
         Thread.sleep(50);
 
         ctx.setCompletionInstant();
-        Assert.assertTrue(ctx.getCompletionInstant() > now);
+        Assert.assertTrue(ctx.getCompletionInstant().isAfter(now));
     }
     
     /** Tests RequestedPrincipalContext helpers. */
diff --git a/idp-authn-impl/pom.xml b/idp-authn-impl/pom.xml
index dda28ce..02b0e8b 100644
--- a/idp-authn-impl/pom.xml
+++ b/idp-authn-impl/pom.xml
@@ -95,6 +95,12 @@
             <version>${duoweb.version}</version>
         </dependency>
 
+        <!-- DEPRECATED - remove in V5 -->
+        <dependency>
+            <groupId>joda-time</groupId>
+            <artifactId>joda-time</artifactId>
+        </dependency>
+
         <!-- Provided Dependencies -->
 
         <!-- Runtime Dependencies -->
diff --git a/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/duo/impl/DuoSupport.java b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/duo/impl/DuoSupport.java
index cda0c7e..1ebcd1a 100644
--- a/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/duo/impl/DuoSupport.java
+++ b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/duo/impl/DuoSupport.java
@@ -33,6 +33,8 @@ import java.io.UnsupportedEncodingException;
 import java.net.URI;
 import java.security.InvalidKeyException;
 import java.security.NoSuchAlgorithmException;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
@@ -42,9 +44,6 @@ import javax.annotation.Nonnull;
 
 import org.apache.http.NameValuePair;
 import org.apache.http.client.methods.RequestBuilder;
-import org.joda.time.DateTime;
-import org.joda.time.format.DateTimeFormat;
-import org.joda.time.format.DateTimeFormatter;
 
 /**
  * Helpers for DuoWeb and Duo AuthAPI operations.
@@ -131,7 +130,7 @@ public final class DuoSupport {
         final String ikey = duo.getIntegrationKey();
         final String skey = duo.getSecretKey();
         final int sigVersion = 2;
-        final String date = new DateTime().toString(RFC_2822_DATE_FORMAT);
+        final String date = RFC_2822_DATE_FORMAT.format(LocalDateTime.now());
         final String canon = canonRequest(request, date, sigVersion);
         final String sig = Util.hmacSign(skey, canon);
 
@@ -204,7 +203,7 @@ public final class DuoSupport {
     }
     
     static {
-        RFC_2822_DATE_FORMAT = DateTimeFormat.forPattern("EEE', 'dd' 'MMM' 'yyyy' 'HH:mm:ss' 'Z");
+        RFC_2822_DATE_FORMAT = DateTimeFormatter.ofPattern("EEE', 'dd' 'MMM' 'yyyy' 'HH:mm:ss' 'Z");
     }
     
 }
\ No newline at end of file
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 7805021..eef54aa 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.io.IOException;
 import java.io.StringReader;
 import java.io.StringWriter;
 import java.security.Principal;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -162,7 +163,7 @@ public class DefaultAuthenticationResultSerializer extends AbstractInitializable
             final StringWriter sink = new StringWriter(128);
             final JsonGenerator gen = generatorFactory.createGenerator(sink);
             gen.writeStartObject().write(FLOW_ID_FIELD, instance.getAuthenticationFlowId())
-                    .write(AUTHN_INSTANT_FIELD, instance.getAuthenticationInstant())
+                    .write(AUTHN_INSTANT_FIELD, instance.getAuthenticationInstant().toEpochMilli())
                     .writeStartArray(PRINCIPAL_ARRAY_FIELD);
 
             for (final Principal p : instance.getSubject().getPrincipals()) {
@@ -195,7 +196,6 @@ public class DefaultAuthenticationResultSerializer extends AbstractInitializable
 
             return sink.toString();
         } catch (final JsonException e) {
-            log.error("Exception while serializing AuthenticationResult", e);
             throw new IOException("Exception while serializing AuthenticationResult", e);
         }
     }
@@ -225,8 +225,8 @@ public class DefaultAuthenticationResultSerializer extends AbstractInitializable
             final long authnInstant = obj.getJsonNumber(AUTHN_INSTANT_FIELD).longValueExact();
 
             final AuthenticationResult result = new AuthenticationResult(flowId, new Subject());
-            result.setAuthenticationInstant(authnInstant);
-            result.setLastActivityInstant(expiration != null ? expiration : authnInstant);
+            result.setAuthenticationInstant(Instant.ofEpochMilli(authnInstant));
+            result.setLastActivityInstant(Instant.ofEpochMilli(expiration != null ? expiration : authnInstant));
             result.setPreviousResult(true);
 
             final JsonArray principals = obj.getJsonArray(PRINCIPAL_ARRAY_FIELD);
@@ -264,7 +264,6 @@ public class DefaultAuthenticationResultSerializer extends AbstractInitializable
             return result;
 
         } catch (final NullPointerException | ClassCastException | ArithmeticException | JsonException e) {
-            log.error("Exception while parsing AuthenticationResult", e);
             throw new IOException("Found invalid data structure while parsing AuthenticationResult", e);
         }
     }
diff --git a/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/impl/ExternalAuthenticationImpl.java b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/impl/ExternalAuthenticationImpl.java
index b8cdbf3..e69d393 100644
--- a/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/impl/ExternalAuthenticationImpl.java
+++ b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/impl/ExternalAuthenticationImpl.java
@@ -19,6 +19,7 @@ package net.shibboleth.idp.authn.impl;
 
 import java.io.IOException;
 import java.security.Principal;
+import java.time.Instant;
 import java.util.Collection;
 import java.util.function.Function;
 
@@ -40,6 +41,8 @@ import net.shibboleth.idp.authn.context.ExternalAuthenticationContext;
 import net.shibboleth.idp.consent.context.ConsentManagementContext;
 import net.shibboleth.idp.profile.context.RelyingPartyContext;
 import net.shibboleth.utilities.java.support.logic.Constraint;
+import net.shibboleth.utilities.java.support.primitive.DeprecationSupport;
+import net.shibboleth.utilities.java.support.primitive.DeprecationSupport.ObjectType;
 
 /**
  * Implementation of the {@link ExternalAuthentication} API that handles moving information in and out
@@ -94,7 +97,6 @@ public class ExternalAuthenticationImpl extends ExternalAuthentication {
     }
 
     /** {@inheritDoc} */
-    @SuppressWarnings("deprecation")
     @Override
     protected void doStart(@Nonnull final HttpServletRequest request) throws ExternalAuthenticationException {
         final AuthenticationContext authnContext = profileRequestContext.getSubcontext(AuthenticationContext.class);
@@ -110,18 +112,13 @@ public class ExternalAuthenticationImpl extends ExternalAuthentication {
         request.setAttribute(PASSIVE_AUTHN_PARAM, authnContext.isPassive());
         request.setAttribute(FORCE_AUTHN_PARAM, authnContext.isForceAuthn());
                 
-        final Collection<Principal> principals = authnContext.getAttemptedFlow().getSupportedPrincipals();
-        if (!principals.isEmpty()) {
-            request.setAttribute(AUTHN_METHOD_PARAM, principals.iterator().next().getName());
-        }
-        
         final RelyingPartyContext rpCtx = relyingPartyContextLookupStrategy.apply(profileRequestContext);
         if (rpCtx != null) {
             request.setAttribute(RELYING_PARTY_PARAM, rpCtx.getRelyingPartyId());
         }
     }
 
- // Checkstyle: CyclomaticComplexity OFF
+ // Checkstyle: CyclomaticComplexity|MethodLength OFF
     /** {@inheritDoc} */
     @Override
     protected void doFinish(@Nonnull final HttpServletRequest request, @Nonnull final HttpServletResponse response)
@@ -156,7 +153,12 @@ public class ExternalAuthenticationImpl extends ExternalAuthentication {
         
         attr = request.getAttribute(AUTHENTICATION_INSTANT_KEY);
         if (attr != null && attr instanceof DateTime) {
-            extContext.setAuthnInstant((DateTime) attr);
+            // This is a V4 deprecation, do not remove until V5.
+            DeprecationSupport.warn(ObjectType.CLASS, DateTime.class.getName(), "ExternalAuthentication",
+                    Instant.class.getName());
+            extContext.setAuthnInstant(Instant.ofEpochMilli(((DateTime) attr).getMillis()));
+        } else if (attr != null && attr instanceof Instant) {
+            extContext.setAuthnInstant((Instant) attr);
         }
         
         attr = request.getAttribute(AUTHENTICATING_AUTHORITIES_KEY);
@@ -193,7 +195,7 @@ public class ExternalAuthenticationImpl extends ExternalAuthentication {
         
         response.sendRedirect(extContext.getFlowExecutionUrl());
     }
-// Checkstyle: CyclomaticComplexity OFF
+// Checkstyle: CyclomaticComplexity|MethodLength OFF
 
     /** {@inheritDoc} */
     @Override
diff --git a/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/impl/PopulateMultiFactorAuthenticationContext.java b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/impl/PopulateMultiFactorAuthenticationContext.java
index 97271fe..abaae08 100644
--- a/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/impl/PopulateMultiFactorAuthenticationContext.java
+++ b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/impl/PopulateMultiFactorAuthenticationContext.java
@@ -17,6 +17,7 @@
 
 package net.shibboleth.idp.authn.impl;
 
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Map;
@@ -231,8 +232,8 @@ public class PopulateMultiFactorAuthenticationContext extends AbstractAuthentica
                 if (descriptor.test(profileRequestContext)) {
                     if (descriptor.isResultActive(candidate)) {
                         if (authenticationContext.getMaxAge() > 0
-                                && candidate.getAuthenticationInstant() + authenticationContext.getMaxAge()
-                                    < System.currentTimeMillis()) {
+                                && candidate.getAuthenticationInstant().plusMillis(
+                                        authenticationContext.getMaxAge()).isBefore(Instant.now())) {
                             log.debug("{} Ignoring active result from login flow {} due to maxAge on request",
                                     getLogPrefix(), candidate.getAuthenticationFlowId());
                         } else {
diff --git a/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/impl/StorageBackedAccountLockoutManager.java b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/impl/StorageBackedAccountLockoutManager.java
index f6f7c8d..4b43bf1 100644
--- a/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/impl/StorageBackedAccountLockoutManager.java
+++ b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/impl/StorageBackedAccountLockoutManager.java
@@ -18,13 +18,14 @@
 package net.shibboleth.idp.authn.impl;
 
 import java.io.IOException;
+import java.time.Duration;
+import java.time.Instant;
 import java.util.function.Function;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 import javax.servlet.http.HttpServletRequest;
 
-import org.joda.time.DateTime;
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.opensaml.storage.StorageCapabilities;
 import org.opensaml.storage.StorageCapabilitiesEx;
@@ -37,7 +38,6 @@ import net.shibboleth.idp.authn.AccountLockoutManager;
 import net.shibboleth.idp.authn.context.AuthenticationContext;
 import net.shibboleth.idp.authn.context.LockoutManagerContext;
 import net.shibboleth.idp.authn.context.UsernamePasswordContext;
-import net.shibboleth.utilities.java.support.annotation.Duration;
 import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
 import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
 import net.shibboleth.utilities.java.support.annotation.constraint.Positive;
@@ -67,10 +67,10 @@ public class StorageBackedAccountLockoutManager extends AbstractIdentifiableInit
     @Nonnull private Function<ProfileRequestContext,Integer> maxAttemptsLookupStrategy;
 
     /** Lookup function for interval after which counter is reset. */
-    @Nonnull private Function<ProfileRequestContext,Long> counterIntervalLookupStrategy;
+    @Nonnull private Function<ProfileRequestContext,Duration> counterIntervalLookupStrategy;
 
     /** Lookup function for duration of lockout. */
-    @Nonnull private Function<ProfileRequestContext,Long> lockoutDurationLookupStrategy;
+    @Nonnull private Function<ProfileRequestContext,Duration> lockoutDurationLookupStrategy;
     
     /** Controls whether attempts against locked accounts extend duration. */
     private boolean extendLockoutDuration;
@@ -78,8 +78,8 @@ public class StorageBackedAccountLockoutManager extends AbstractIdentifiableInit
     /** Constructor. */
     public StorageBackedAccountLockoutManager() {
         setMaxAttempts(5);
-        setCounterInterval(5 * 60 * 1000);
-        setLockoutDuration(5 * 60 * 1000);
+        setCounterInterval(Duration.ofMinutes(5));
+        setLockoutDuration(Duration.ofMinutes(5));
     }
 
     /**
@@ -147,12 +147,11 @@ public class StorageBackedAccountLockoutManager extends AbstractIdentifiableInit
      * 
      * @param window counter window
      */
-    @Duration
-    public void setCounterInterval(@Duration @Positive final long window) {
+    public void setCounterInterval(@Nonnull final Duration window) {
         ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
         
         counterIntervalLookupStrategy = FunctionSupport.constant(
-                Constraint.isGreaterThan(0, window, "Counter interval must be greater than 0"));
+                Constraint.isNotNull(window, "Counter interval cannot be null"));
     }
     
     /**
@@ -162,7 +161,7 @@ public class StorageBackedAccountLockoutManager extends AbstractIdentifiableInit
      * 
      * @param strategy lookup function
      */
-    public void setCounterIntervalLookupStrategy(@Nonnull final Function<ProfileRequestContext,Long> strategy) {
+    public void setCounterIntervalLookupStrategy(@Nonnull final Function<ProfileRequestContext,Duration> strategy) {
         ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
 
         counterIntervalLookupStrategy = Constraint.isNotNull(strategy,
@@ -176,12 +175,11 @@ public class StorageBackedAccountLockoutManager extends AbstractIdentifiableInit
      * 
      * @param duration lockout duration
      */
-    @Duration
-    public void setLockoutDuration(@Duration @Positive final long duration) {
+    public void setLockoutDuration(@Nonnull final Duration duration) {
         ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
         
         lockoutDurationLookupStrategy = FunctionSupport.constant(
-                Constraint.isGreaterThan(0, duration, "Lockout duration must be greater than 0"));
+                Constraint.isNotNull(duration, "Lockout duration cannot be null"));
     }
     
     /**
@@ -191,7 +189,7 @@ public class StorageBackedAccountLockoutManager extends AbstractIdentifiableInit
      * 
      * @param strategy lookup function
      */
-    public void setLockoutDurationLookupStrategy(@Nonnull final Function<ProfileRequestContext,Long> strategy) {
+    public void setLockoutDurationLookupStrategy(@Nonnull final Function<ProfileRequestContext,Duration> strategy) {
         ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
 
         lockoutDurationLookupStrategy = Constraint.isNotNull(strategy,
@@ -248,8 +246,8 @@ public class StorageBackedAccountLockoutManager extends AbstractIdentifiableInit
             if (counter >= maxAttemptsLookupStrategy.apply(profileRequestContext)) {
                 // Recover time of last attempt from the record expiration and find the time elapsed since.
                 // If that's under the lockout duration, we're locked out.
-                final long lockoutDuration = lockoutDurationLookupStrategy.apply(profileRequestContext);
-                final long counterInterval = counterIntervalLookupStrategy.apply(profileRequestContext);
+                final long lockoutDuration = lockoutDurationLookupStrategy.apply(profileRequestContext).toMillis();
+                final long counterInterval = counterIntervalLookupStrategy.apply(profileRequestContext).toMillis();
                 final long lastAttempt = sr.getExpiration() - Math.max(lockoutDuration, counterInterval);
                 final long timeDifference = System.currentTimeMillis() - lastAttempt;
                 if (timeDifference <= lockoutDuration) {
@@ -319,7 +317,6 @@ public class StorageBackedAccountLockoutManager extends AbstractIdentifiableInit
         }
         
         // Read back account record, initializing counter to zero otherwise.
-
         log.debug("Reading account lockout data for '{}'", key);
         
         int counter = 0;
@@ -340,8 +337,8 @@ public class StorageBackedAccountLockoutManager extends AbstractIdentifiableInit
         }
         
         final long now = System.currentTimeMillis();
-        final long lockoutDuration = lockoutDurationLookupStrategy.apply(profileRequestContext);
-        final long counterInterval = counterIntervalLookupStrategy.apply(profileRequestContext);
+        final long lockoutDuration = lockoutDurationLookupStrategy.apply(profileRequestContext).toMillis();
+        final long counterInterval = counterIntervalLookupStrategy.apply(profileRequestContext).toMillis();
         
         // Compute last access time by backing off from record expiration.
         long lastAccess = now;
@@ -358,7 +355,8 @@ public class StorageBackedAccountLockoutManager extends AbstractIdentifiableInit
         ++counter;
         final long expiration = System.currentTimeMillis() + Math.max(lockoutDuration, counterInterval);
 
-        log.debug("Invalid login count for '{}' will be {}, expiring at {}", key, counter, new DateTime(expiration));
+        log.debug("Invalid login count for '{}' will be {}, expiring at {}", key, counter,
+                Instant.ofEpochMilli(expiration));
 
         // Create or update as required. Retry on errors.
         if (sr == null) {
diff --git a/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/impl/ValidateExternalAuthentication.java b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/impl/ValidateExternalAuthentication.java
index d0a4819..bcb5f01 100644
--- a/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/impl/ValidateExternalAuthentication.java
+++ b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/impl/ValidateExternalAuthentication.java
@@ -180,8 +180,7 @@ public class ValidateExternalAuthentication extends AbstractValidationAction {
         
         if (authenticationContext.getAuthenticationResult() != null) {
             if (extContext.getAuthnInstant() != null) {
-                authenticationContext.getAuthenticationResult().setAuthenticationInstant(
-                        extContext.getAuthnInstant().getMillis());
+                authenticationContext.getAuthenticationResult().setAuthenticationInstant(extContext.getAuthnInstant());
             }
             if (extContext.isPreviousResult()) {
                 authenticationContext.getAuthenticationResult().setPreviousResult(true);
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 8670045..13b9b91 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
@@ -21,6 +21,7 @@ import java.io.File;
 import java.io.FileInputStream;
 import java.io.IOException;
 import java.net.URISyntaxException;
+import java.time.Instant;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
@@ -67,7 +68,7 @@ public class DefaultAuthenticationResultSerializerTest {
     
     private static final String KEY = "_key";
     
-    private static final long INSTANT = 1378827849463L;
+    private static final Instant INSTANT = Instant.ofEpochMilli(1378827849463L);
     
     private static final long ACTIVITY = 1378827556778L;
     
@@ -326,7 +327,7 @@ public class DefaultAuthenticationResultSerializerTest {
     private AuthenticationResult createResult(String flowId, Subject subject) {
         final AuthenticationResult result = new AuthenticationResult(flowId, subject);
         result.setAuthenticationInstant(INSTANT);
-        result.setLastActivityInstant(ACTIVITY);
+        result.setLastActivityInstant(Instant.ofEpochMilli(ACTIVITY));
         return result;
     }
     
diff --git a/idp-authn-impl/src/test/java/net/shibboleth/idp/authn/impl/PopulateMultiFactorAuthenticationContextTest.java b/idp-authn-impl/src/test/java/net/shibboleth/idp/authn/impl/PopulateMultiFactorAuthenticationContextTest.java
index 3f64bae..27ee20d 100644
--- a/idp-authn-impl/src/test/java/net/shibboleth/idp/authn/impl/PopulateMultiFactorAuthenticationContextTest.java
+++ b/idp-authn-impl/src/test/java/net/shibboleth/idp/authn/impl/PopulateMultiFactorAuthenticationContextTest.java
@@ -17,6 +17,7 @@
 
 package net.shibboleth.idp.authn.impl;
 
+import java.time.Instant;
 import java.util.Collections;
 import java.util.Map;
 
@@ -90,17 +91,17 @@ public class PopulateMultiFactorAuthenticationContextTest {
     @Test public void testResults() throws ComponentInitializationException {
         final Subject subject = new Subject();
         AuthenticationResult result = new AuthenticationResult("foo", new Subject());
-        result.setAuthenticationInstant(System.currentTimeMillis() - 7200 * 1000);
+        result.setAuthenticationInstant(Instant.now().minusSeconds(7200));
         subject.getPrincipals().add(new AuthenticationResultPrincipal(result));
         result = new AuthenticationResult("bar", new Subject());
         subject.getPrincipals().add(new AuthenticationResultPrincipal(result));
         result = new AuthenticationResult("baz", new Subject());
         subject.getPrincipals().add(new AuthenticationResultPrincipal(result));
         result = new AuthenticationResult("bav", new Subject());
-        result.setAuthenticationInstant(System.currentTimeMillis() - 1000 * 1000);
+        result.setAuthenticationInstant(Instant.now().minusSeconds(1000));
         subject.getPrincipals().add(new AuthenticationResultPrincipal(result));
         result = new AuthenticationResult("bag", new Subject());
-        result.setAuthenticationInstant(System.currentTimeMillis() - 2000 * 1000);
+        result.setAuthenticationInstant(Instant.now().minusSeconds(2000));
         subject.getPrincipals().add(new AuthenticationResultPrincipal(result));
         
         ac.getActiveResults().put("authn/MFA", new AuthenticationResult("authn/MFA", subject));
diff --git a/idp-authn-impl/src/test/java/net/shibboleth/idp/authn/impl/StorageBackedAccountLockoutManagerTest.java b/idp-authn-impl/src/test/java/net/shibboleth/idp/authn/impl/StorageBackedAccountLockoutManagerTest.java
index 92c3170..68a3e35 100644
--- a/idp-authn-impl/src/test/java/net/shibboleth/idp/authn/impl/StorageBackedAccountLockoutManagerTest.java
+++ b/idp-authn-impl/src/test/java/net/shibboleth/idp/authn/impl/StorageBackedAccountLockoutManagerTest.java
@@ -18,6 +18,8 @@
 package net.shibboleth.idp.authn.impl;
 
 
+import java.time.Duration;
+
 import javax.servlet.http.HttpServletRequest;
 
 import net.shibboleth.idp.authn.context.AuthenticationContext;
@@ -50,8 +52,8 @@ public class StorageBackedAccountLockoutManagerTest extends BaseAuthenticationCo
         manager.setStorageService(ss);
         manager.setLockoutKeyStrategy(keyStrategy);
         manager.setMaxAttempts(3);
-        manager.setCounterInterval(3000);
-        manager.setLockoutDuration(5000);
+        manager.setCounterInterval(Duration.ofSeconds(3));
+        manager.setLockoutDuration(Duration.ofSeconds(5));
         manager.initialize();
         
         ((MockHttpServletRequest) src.getExternalContext().getNativeRequest()).setRemoteAddr("192.168.1.1");
diff --git a/idp-authn-impl/src/test/java/net/shibboleth/idp/authn/impl/ValidateExternalAuthenticationTest.java b/idp-authn-impl/src/test/java/net/shibboleth/idp/authn/impl/ValidateExternalAuthenticationTest.java
index a73aba0..3ee4c8c 100644
--- a/idp-authn-impl/src/test/java/net/shibboleth/idp/authn/impl/ValidateExternalAuthenticationTest.java
+++ b/idp-authn-impl/src/test/java/net/shibboleth/idp/authn/impl/ValidateExternalAuthenticationTest.java
@@ -17,6 +17,7 @@
 
 package net.shibboleth.idp.authn.impl;
 
+import java.time.Instant;
 import java.util.Arrays;
 import java.util.Set;
 
@@ -33,7 +34,6 @@ import net.shibboleth.idp.authn.principal.TestPrincipal;
 import net.shibboleth.idp.authn.principal.UsernamePrincipal;
 import net.shibboleth.idp.profile.ActionTestingSupport;
 
-import org.joda.time.DateTime;
 import org.opensaml.profile.action.EventIds;
 import org.springframework.webflow.execution.Event;
 import org.testng.Assert;
@@ -120,7 +120,7 @@ public class ValidateExternalAuthenticationTest extends BaseAuthenticationContex
         final AuthenticationContext ac = prc.getSubcontext(AuthenticationContext.class);
         final ExternalAuthenticationContext eac = ac.getSubcontext(ExternalAuthenticationContext.class, true);
         eac.setPrincipalName("foo");
-        final DateTime ts = DateTime.now().minus(3600);
+        final Instant ts = Instant.now().minusSeconds(3600);
         eac.setAuthnInstant(ts);
         eac.setPreviousResult(true);
         
@@ -128,7 +128,7 @@ public class ValidateExternalAuthenticationTest extends BaseAuthenticationContex
         ActionTestingSupport.assertProceedEvent(event);
         Assert.assertNotNull(ac.getAuthenticationResult());
         Assert.assertTrue(ac.getAuthenticationResult().isPreviousResult());
-        Assert.assertEquals(ts.getMillis(), ac.getAuthenticationResult().getAuthenticationInstant());
+        Assert.assertEquals(ts, ac.getAuthenticationResult().getAuthenticationInstant());
     }
 
     @Test public void testAuthnAuthorities() {
diff --git a/idp-cas-api/src/main/java/net/shibboleth/idp/cas/ticket/ProxyGrantingTicket.java b/idp-cas-api/src/main/java/net/shibboleth/idp/cas/ticket/ProxyGrantingTicket.java
index 3f61cc1..d081e22 100644
--- a/idp-cas-api/src/main/java/net/shibboleth/idp/cas/ticket/ProxyGrantingTicket.java
+++ b/idp-cas-api/src/main/java/net/shibboleth/idp/cas/ticket/ProxyGrantingTicket.java
@@ -17,11 +17,11 @@
 
 package net.shibboleth.idp.cas.ticket;
 
+import java.time.Instant;
+
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
-import org.joda.time.Instant;
-
 import net.shibboleth.utilities.java.support.primitive.DeprecationSupport;
 import net.shibboleth.utilities.java.support.primitive.DeprecationSupport.ObjectType;
 import net.shibboleth.utilities.java.support.primitive.StringSupport;
diff --git a/idp-cas-api/src/main/java/net/shibboleth/idp/cas/ticket/ProxyTicket.java b/idp-cas-api/src/main/java/net/shibboleth/idp/cas/ticket/ProxyTicket.java
index a3e1315..25b430d 100644
--- a/idp-cas-api/src/main/java/net/shibboleth/idp/cas/ticket/ProxyTicket.java
+++ b/idp-cas-api/src/main/java/net/shibboleth/idp/cas/ticket/ProxyTicket.java
@@ -17,11 +17,11 @@
 
 package net.shibboleth.idp.cas.ticket;
 
+import java.time.Instant;
+
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
-import org.joda.time.Instant;
-
 import net.shibboleth.utilities.java.support.logic.Constraint;
 import net.shibboleth.utilities.java.support.primitive.DeprecationSupport;
 import net.shibboleth.utilities.java.support.primitive.DeprecationSupport.ObjectType;
diff --git a/idp-cas-api/src/main/java/net/shibboleth/idp/cas/ticket/ServiceTicket.java b/idp-cas-api/src/main/java/net/shibboleth/idp/cas/ticket/ServiceTicket.java
index 9c91ecc..47c326b 100644
--- a/idp-cas-api/src/main/java/net/shibboleth/idp/cas/ticket/ServiceTicket.java
+++ b/idp-cas-api/src/main/java/net/shibboleth/idp/cas/ticket/ServiceTicket.java
@@ -17,11 +17,11 @@
 
 package net.shibboleth.idp.cas.ticket;
 
+import java.time.Instant;
+
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
-import org.joda.time.Instant;
-
 import net.shibboleth.utilities.java.support.primitive.DeprecationSupport;
 import net.shibboleth.utilities.java.support.primitive.DeprecationSupport.ObjectType;
 
diff --git a/idp-cas-api/src/main/java/net/shibboleth/idp/cas/ticket/Ticket.java b/idp-cas-api/src/main/java/net/shibboleth/idp/cas/ticket/Ticket.java
index 9e7966d..7439bcd 100644
--- a/idp-cas-api/src/main/java/net/shibboleth/idp/cas/ticket/Ticket.java
+++ b/idp-cas-api/src/main/java/net/shibboleth/idp/cas/ticket/Ticket.java
@@ -17,11 +17,11 @@
 
 package net.shibboleth.idp.cas.ticket;
 
+import java.time.Instant;
+
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
-import org.joda.time.Instant;
-
 import net.shibboleth.utilities.java.support.logic.Constraint;
 import net.shibboleth.utilities.java.support.primitive.DeprecationSupport;
 import net.shibboleth.utilities.java.support.primitive.DeprecationSupport.ObjectType;
diff --git a/idp-cas-api/src/main/java/net/shibboleth/idp/cas/ticket/TicketService.java b/idp-cas-api/src/main/java/net/shibboleth/idp/cas/ticket/TicketService.java
index f4046ef..d0977ec 100644
--- a/idp-cas-api/src/main/java/net/shibboleth/idp/cas/ticket/TicketService.java
+++ b/idp-cas-api/src/main/java/net/shibboleth/idp/cas/ticket/TicketService.java
@@ -17,7 +17,7 @@
 
 package net.shibboleth.idp.cas.ticket;
 
-import org.joda.time.Instant;
+import java.time.Instant;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
diff --git a/idp-cas-api/src/main/java/net/shibboleth/idp/cas/ticket/TicketServiceEx.java b/idp-cas-api/src/main/java/net/shibboleth/idp/cas/ticket/TicketServiceEx.java
index c1c4b62..54b7d2c 100644
--- a/idp-cas-api/src/main/java/net/shibboleth/idp/cas/ticket/TicketServiceEx.java
+++ b/idp-cas-api/src/main/java/net/shibboleth/idp/cas/ticket/TicketServiceEx.java
@@ -17,9 +17,9 @@
 
 package net.shibboleth.idp.cas.ticket;
 
-import javax.annotation.Nonnull;
+import java.time.Instant;
 
-import org.joda.time.Instant;
+import javax.annotation.Nonnull;
 
 /**
  * Extended CAS ticket management service.
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 311bd82..c79d4a7 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
@@ -17,11 +17,11 @@
 
 package net.shibboleth.idp.cas.ticket;
 
+import java.time.Instant;
 import java.util.Objects;
 import javax.annotation.Nonnull;
 
 import net.shibboleth.utilities.java.support.logic.Constraint;
-import org.joda.time.Instant;
 
 /**
  * Supplemental state data to be stored with a ticket.
diff --git a/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/flow/impl/BuildSamlValidationFailureMessageAction.java b/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/flow/impl/BuildSamlValidationFailureMessageAction.java
index c7587ff..bb242fb 100644
--- a/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/flow/impl/BuildSamlValidationFailureMessageAction.java
+++ b/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/flow/impl/BuildSamlValidationFailureMessageAction.java
@@ -17,12 +17,13 @@
 
 package net.shibboleth.idp.cas.flow.impl;
 
+import java.time.Instant;
+
 import javax.annotation.Nonnull;
 import javax.xml.namespace.QName;
 
 import net.shibboleth.idp.cas.protocol.TicketValidationRequest;
 import net.shibboleth.idp.cas.protocol.TicketValidationResponse;
-import org.joda.time.DateTime;
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.opensaml.saml.common.SAMLObject;
 import org.opensaml.saml.saml1.core.Response;
@@ -48,7 +49,7 @@ public class BuildSamlValidationFailureMessageAction extends AbstractOutgoingSam
         final TicketValidationResponse validationResponse = getCASResponse(profileRequestContext);
         final Response response = newSAMLObject(Response.class, Response.DEFAULT_ELEMENT_NAME);
         response.setID(request.getTicket());
-        response.setIssueInstant(DateTime.now());
+        response.setIssueInstant(Instant.now());
         final Status status = newSAMLObject(Status.class, Status.DEFAULT_ELEMENT_NAME);
         final StatusCode statusCode = newSAMLObject(StatusCode.class, StatusCode.DEFAULT_ELEMENT_NAME);
         statusCode.setValue(new QName(NAMESPACE, validationResponse.getErrorCode()));
diff --git a/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/flow/impl/BuildSamlValidationSuccessMessageAction.java b/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/flow/impl/BuildSamlValidationSuccessMessageAction.java
index 39b3302..4f6aabd 100644
--- a/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/flow/impl/BuildSamlValidationSuccessMessageAction.java
+++ b/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/flow/impl/BuildSamlValidationSuccessMessageAction.java
@@ -17,6 +17,8 @@
 
 package net.shibboleth.idp.cas.flow.impl;
 
+import java.time.Instant;
+
 import javax.annotation.Nonnull;
 
 import net.shibboleth.idp.cas.protocol.TicketValidationRequest;
@@ -27,7 +29,6 @@ import net.shibboleth.utilities.java.support.logic.Constraint;
 import net.shibboleth.utilities.java.support.primitive.StringSupport;
 import net.shibboleth.utilities.java.support.security.IdentifierGenerationStrategy;
 
-import org.joda.time.DateTime;
 import org.opensaml.core.xml.XMLObjectBuilder;
 import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
 import org.opensaml.core.xml.schema.XSString;
@@ -101,7 +102,7 @@ public class BuildSamlValidationSuccessMessageAction extends AbstractOutgoingSam
             @Nonnull final RequestContext springRequestContext,
             @Nonnull final ProfileRequestContext<SAMLObject, SAMLObject> profileRequestContext) {
 
-        final DateTime now = DateTime.now();
+        final Instant now = Instant.now();
 
         final TicketValidationRequest request = getCASRequest(profileRequestContext);
         final TicketValidationResponse ticketResponse = getCASResponse(profileRequestContext);
@@ -114,7 +115,7 @@ public class BuildSamlValidationSuccessMessageAction extends AbstractOutgoingSam
 
         final Response response = newSAMLObject(Response.class, Response.DEFAULT_ELEMENT_NAME);
         response.setID(request.getTicket());
-        response.setIssueInstant(DateTime.now());
+        response.setIssueInstant(now);
         final Status status = newSAMLObject(Status.class, Status.DEFAULT_ELEMENT_NAME);
         final StatusCode code = newSAMLObject(StatusCode.class, StatusCode.DEFAULT_ELEMENT_NAME);
         code.setValue(StatusCode.SUCCESS);
@@ -188,7 +189,7 @@ public class BuildSamlValidationSuccessMessageAction extends AbstractOutgoingSam
      * @return new authentication statement
      */
     private AuthenticationStatement newAuthenticationStatement(
-            final DateTime authnInstant, final String authnMethod, final String principal) {
+            final Instant authnInstant, final String authnMethod, final String principal) {
         final AuthenticationStatement authnStatement = newSAMLObject(
                 AuthenticationStatement.class, AuthenticationStatement.DEFAULT_ELEMENT_NAME);
         authnStatement.setAuthenticationInstant(authnInstant);
diff --git a/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/flow/impl/GrantProxyTicketAction.java b/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/flow/impl/GrantProxyTicketAction.java
index 4cd05b5..eef41d3 100644
--- a/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/flow/impl/GrantProxyTicketAction.java
+++ b/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/flow/impl/GrantProxyTicketAction.java
@@ -17,6 +17,7 @@
 
 package net.shibboleth.idp.cas.flow.impl;
 
+import java.time.Instant;
 import java.util.function.Predicate;
 
 import javax.annotation.Nonnull;
@@ -37,7 +38,6 @@ import net.shibboleth.idp.session.criterion.SessionIdCriterion;
 import net.shibboleth.utilities.java.support.logic.Constraint;
 import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
 import net.shibboleth.utilities.java.support.resolver.ResolverException;
-import org.joda.time.DateTime;
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -103,14 +103,14 @@ public class GrantProxyTicketAction extends AbstractCASProtocolAction<ProxyTicke
     }
 
     /** {@inheritDoc} */
-    @Nonnull
     @Override
+    @Nonnull
     protected Event doExecute(
             final @Nonnull RequestContext springRequestContext,
             final @Nonnull ProfileRequestContext profileRequestContext) {
 
         final ProxyGrantingTicket pgt = (ProxyGrantingTicket) getCASTicket(profileRequestContext);
-        if (pgt == null || pgt.getExpirationInstant().isBeforeNow()) {
+        if (pgt == null || pgt.getExpirationInstant().isBefore(Instant.now())) {
             return ProtocolError.TicketExpired.event(this);
         }
         final ProxyConfiguration config = configLookupFunction.apply(profileRequestContext);
@@ -151,7 +151,7 @@ public class GrantProxyTicketAction extends AbstractCASProtocolAction<ProxyTicke
             log.debug("Granting proxy ticket for {}", request.getTargetService());
             pt = casTicketService.createProxyTicket(
                     config.getSecurityConfiguration().getIdGenerator().generateIdentifier(),
-                    DateTime.now().plus(config.getTicketValidityPeriod()).toInstant(),
+                    Instant.now().plusMillis(config.getTicketValidityPeriod()),
                     pgt,
                     request.getTargetService());
         } catch (final RuntimeException e) {
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 e013e7f..81a0d38 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
@@ -17,6 +17,7 @@
 
 package net.shibboleth.idp.cas.flow.impl;
 
+import java.time.Instant;
 import java.util.function.Function;
 
 import javax.annotation.Nonnull;
@@ -36,8 +37,6 @@ import net.shibboleth.idp.cas.ticket.TicketState;
 import net.shibboleth.idp.session.IdPSession;
 import net.shibboleth.idp.session.context.SessionContext;
 import net.shibboleth.utilities.java.support.logic.Constraint;
-import org.joda.time.DateTime;
-import org.joda.time.Instant;
 import org.opensaml.messaging.context.navigate.ChildContextLookup;
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.slf4j.Logger;
@@ -123,11 +122,11 @@ public class GrantServiceTicketAction extends AbstractCASProtocolAction<ServiceT
             final TicketState state = new TicketState(
                     session.getId(),
                     getPrincipalName(profileRequestContext),
-                    new Instant(authnResult.getAuthenticationInstant()),
+                    authnResult.getAuthenticationInstant(),
                     authnResult.getAuthenticationFlowId());
             ticket = ticketServiceEx.createServiceTicket(
                     config.getSecurityConfiguration().getIdGenerator().generateIdentifier(),
-                    DateTime.now().plus(config.getTicketValidityPeriod()).toInstant(),
+                    Instant.now().plusMillis(config.getTicketValidityPeriod()),
                     request.getService(),
                     state,
                     request.isRenew());
@@ -186,7 +185,7 @@ public class GrantServiceTicketAction extends AbstractCASProtocolAction<ServiceT
     private AuthenticationResult getLatestAuthenticationResult(final IdPSession session) {
         AuthenticationResult latest = null;
         for (final AuthenticationResult result : session.getAuthenticationResults()) {
-            if (latest == null || result.getAuthenticationInstant() > latest.getAuthenticationInstant()) {
+            if (latest == null || result.getAuthenticationInstant().isAfter(latest.getAuthenticationInstant())) {
                 latest = result;
             }
         }
diff --git a/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/flow/impl/UpdateIdPSessionWithSPSessionAction.java b/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/flow/impl/UpdateIdPSessionWithSPSessionAction.java
index 6ab88aa..5cb3b4c 100644
--- a/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/flow/impl/UpdateIdPSessionWithSPSessionAction.java
+++ b/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/flow/impl/UpdateIdPSessionWithSPSessionAction.java
@@ -17,6 +17,9 @@
 
 package net.shibboleth.idp.cas.flow.impl;
 
+import java.time.Duration;
+import java.time.Instant;
+
 import javax.annotation.Nonnull;
 
 import net.shibboleth.idp.cas.service.Service;
@@ -27,8 +30,6 @@ import net.shibboleth.idp.session.SPSession;
 import net.shibboleth.idp.session.SessionException;
 import net.shibboleth.idp.session.SessionResolver;
 import net.shibboleth.idp.session.criterion.SessionIdCriterion;
-import net.shibboleth.utilities.java.support.annotation.Duration;
-import net.shibboleth.utilities.java.support.annotation.constraint.Positive;
 import net.shibboleth.utilities.java.support.logic.Constraint;
 import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
 import net.shibboleth.utilities.java.support.resolver.ResolverException;
@@ -50,36 +51,30 @@ import org.springframework.webflow.execution.RequestContext;
 public class UpdateIdPSessionWithSPSessionAction extends AbstractCASProtocolAction {
 
     /** Class logger. */
-    private final Logger log = LoggerFactory.getLogger(UpdateIdPSessionWithSPSessionAction.class);
+    @Nonnull private final Logger log = LoggerFactory.getLogger(UpdateIdPSessionWithSPSessionAction.class);
 
     /** Looks up IdP sessions. */
-    @Nonnull
-    private final SessionResolver sessionResolver;
+    @Nonnull private final SessionResolver sessionResolver;
 
     /** Lifetime of sessions to create. */
-    @Positive
-    @Duration
-    private final long sessionLifetime;
+    @Nonnull private final Duration sessionLifetime;
 
 
     /**
      * Creates a new instance with given parameters.
      *
      * @param resolver Session resolver component
-     * @param lifetime lifetime in milliseconds, determines upper bound for expiration of the
-     * {@link CASSPSession} to be created
+     * @param lifetime determines upper bound for expiration of the {@link CASSPSession} to be created
      */
-    public UpdateIdPSessionWithSPSessionAction(
-            @Nonnull final SessionResolver resolver,
-            @Positive at Duration final long lifetime) {
+    public UpdateIdPSessionWithSPSessionAction(@Nonnull final SessionResolver resolver,
+            @Nonnull final Duration lifetime) {
         sessionResolver = Constraint.isNotNull(resolver, "Session resolver cannot be null.");
-        sessionLifetime = Constraint.isGreaterThan(0, lifetime, "Lifetime must be greater than 0");
+        sessionLifetime = Constraint.isNotNull(lifetime, "Lifetime cannot be null");
     }
 
-    @Nonnull
+    /** {@inheritDoc} */
     @Override
-    protected Event doExecute(
-            final @Nonnull RequestContext springRequestContext,
+    @Nonnull protected Event doExecute(final @Nonnull RequestContext springRequestContext,
             final @Nonnull ProfileRequestContext profileRequestContext) {
 
         final Ticket ticket = getCASTicket(profileRequestContext);
@@ -95,11 +90,11 @@ public class UpdateIdPSessionWithSPSessionAction extends AbstractCASProtocolActi
             log.warn("Possible sign of misconfiguration, IdPSession resolution error: {}", e);
         }
         if (session != null) {
-            final long now = System.currentTimeMillis();
+            final Instant now = Instant.now();
             final SPSession sps = new CASSPSession(
                     ticket.getService(),
                     now,
-                    now + sessionLifetime,
+                    now.plus(sessionLifetime),
                     ticket.getId());
             log.debug("Created SP session {}", sps);
             try {
diff --git a/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/flow/impl/ValidateProxyCallbackAction.java b/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/flow/impl/ValidateProxyCallbackAction.java
index 5a97d1d..c5e7355 100644
--- a/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/flow/impl/ValidateProxyCallbackAction.java
+++ b/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/flow/impl/ValidateProxyCallbackAction.java
@@ -19,12 +19,11 @@ package net.shibboleth.idp.cas.flow.impl;
 
 import java.net.URI;
 import java.net.URISyntaxException;
+import java.time.Instant;
 
 import javax.annotation.Nonnull;
 
 import org.apache.http.client.utils.URIBuilder;
-import org.joda.time.DateTime;
-import org.joda.time.Instant;
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -125,7 +124,7 @@ public class ValidateProxyCallbackAction
         try {
             log.debug("Attempting proxy authentication to {}", proxyCallbackUri);
             proxyValidator.validate(profileRequestContext, proxyCallbackUri);
-            final Instant expiration = DateTime.now().plus(config.getTicketValidityPeriod()).toInstant();
+            final Instant expiration = Instant.now().plusMillis(config.getTicketValidityPeriod());
             if (ticket instanceof ServiceTicket) {
                 ticketServiceEx.createProxyGrantingTicket(proxyIds.getPgtId(), expiration, (ServiceTicket) ticket);
             } else {
diff --git a/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/flow/impl/ValidateTicketAction.java b/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/flow/impl/ValidateTicketAction.java
index 625be7f..6b03b6e 100644
--- a/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/flow/impl/ValidateTicketAction.java
+++ b/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/flow/impl/ValidateTicketAction.java
@@ -17,6 +17,8 @@
 
 package net.shibboleth.idp.cas.flow.impl;
 
+import java.time.Instant;
+
 import javax.annotation.Nonnull;
 
 import net.shibboleth.idp.cas.config.impl.ConfigLookupFunction;
@@ -105,7 +107,7 @@ public class ValidateTicketAction extends AbstractCASProtocolAction<TicketValida
             return ProtocolError.TicketRetrievalError.event(this);
         }
 
-        if (ticket == null || ticket.getExpirationInstant().isBeforeNow()) {
+        if (ticket == null || ticket.getExpirationInstant().isBefore(Instant.now())) {
             return ProtocolError.TicketExpired.event(this);
         }
 
diff --git a/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/service/impl/ServiceEntityDescriptor.java b/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/service/impl/ServiceEntityDescriptor.java
index 6b395ef..ef20e6e 100644
--- a/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/service/impl/ServiceEntityDescriptor.java
+++ b/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/service/impl/ServiceEntityDescriptor.java
@@ -17,6 +17,8 @@
 
 package net.shibboleth.idp.cas.service.impl;
 
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
 import java.util.Collections;
 import java.util.List;
 
@@ -27,10 +29,9 @@ import javax.xml.namespace.QName;
 import net.shibboleth.idp.cas.service.Service;
 import net.shibboleth.utilities.java.support.logic.Constraint;
 import net.shibboleth.utilities.java.support.primitive.StringSupport;
-import org.joda.time.DateTime;
-import org.opensaml.core.xml.AbstractXMLObject;
 import org.opensaml.core.xml.XMLObject;
 import org.opensaml.core.xml.util.AttributeMap;
+import org.opensaml.saml.common.AbstractSAMLObject;
 import org.opensaml.saml.common.xml.SAMLConstants;
 import org.opensaml.saml.metadata.EntityGroupName;
 import org.opensaml.saml.saml2.metadata.AdditionalMetadataLocation;
@@ -52,7 +53,7 @@ import org.opensaml.xmlsec.signature.Signature;
  *
  * @author Marvin S. Addison
  */
-public class ServiceEntityDescriptor extends AbstractXMLObject implements EntityDescriptor {
+public class ServiceEntityDescriptor extends AbstractSAMLObject implements EntityDescriptor {
 
     /** Underlying CAS service. */
     @Nonnull private final Service svc;
@@ -236,17 +237,13 @@ public class ServiceEntityDescriptor extends AbstractXMLObject implements Entity
     }
 
     /** {@inheritDoc} */
-    public DateTime getValidUntil() {
-        return DateTime.now().plusDays(1);
+    public Instant getValidUntil() {
+        return Instant.now().plus(1, ChronoUnit.DAYS);
     }
 
-    /**
-     * Throws {@link UnsupportedOperationException}.
-     * 
-     * {@inheritDoc}
-     */
-    public void setValidUntil(final DateTime validUntil) {
-        throw new UnsupportedOperationException();
+    /** {@inheritDoc} */
+    public void setValidUntil(final Instant validUntil) {
+        throw new UnsupportedOperationException();        
     }
 
     /** {@inheritDoc} */
diff --git a/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/session/impl/CASSPSession.java b/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/session/impl/CASSPSession.java
index b14a890..ba08f73 100644
--- a/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/session/impl/CASSPSession.java
+++ b/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/session/impl/CASSPSession.java
@@ -17,12 +17,12 @@
 
 package net.shibboleth.idp.cas.session.impl;
 
+import java.time.Instant;
+
 import javax.annotation.Nonnull;
 
 import net.shibboleth.idp.session.BasicSPSession;
-import net.shibboleth.utilities.java.support.annotation.Duration;
 import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
-import net.shibboleth.utilities.java.support.annotation.constraint.Positive;
 import net.shibboleth.utilities.java.support.logic.Constraint;
 import net.shibboleth.utilities.java.support.primitive.StringSupport;
 
@@ -34,22 +34,20 @@ import net.shibboleth.utilities.java.support.primitive.StringSupport;
 public class CASSPSession extends BasicSPSession {
 
     /** Validated ticket that started the SP session. */
-    @Nonnull @NotEmpty
-    private final String ticket;
-
+    @Nonnull @NotEmpty private final String ticket;
 
     /**
      * Creates a new CAS SP session.
      *
      * @param id         the identifier of the service associated with this session
-     * @param creation   creation time of session, in milliseconds since the epoch
-     * @param expiration expiration time of session, in milliseconds since the epoch
+     * @param creation   creation time of session
+     * @param expiration expiration time of session
      * @param ticketId   ticket ID used to gain access to the service
      */
     public CASSPSession(
             @Nonnull @NotEmpty final String id,
-            @Duration @Positive final long creation,
-            @Duration @Positive final long expiration,
+            @Nonnull final Instant creation,
+            @Nonnull final Instant expiration,
             @Nonnull @NotEmpty final String ticketId) {
         super(id, creation, expiration);
         ticket = Constraint.isNotNull(StringSupport.trimOrNull(ticketId), "Ticket ID cannot be null or empty");
@@ -60,8 +58,7 @@ public class CASSPSession extends BasicSPSession {
      * 
      * @return ticket ID
      */
-    @Nonnull @NotEmpty
-    public String getTicketId() {
+    @Nonnull @NotEmpty public String getTicketId() {
         return ticket;
     }
 
diff --git a/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/session/impl/CASSPSessionSerializer.java b/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/session/impl/CASSPSessionSerializer.java
index 2c340c2..82e219c 100644
--- a/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/session/impl/CASSPSessionSerializer.java
+++ b/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/session/impl/CASSPSessionSerializer.java
@@ -18,6 +18,8 @@
 package net.shibboleth.idp.cas.session.impl;
 
 import java.io.IOException;
+import java.time.Duration;
+import java.time.Instant;
 
 import javax.annotation.Nonnull;
 import javax.json.JsonObject;
@@ -25,9 +27,7 @@ import javax.json.stream.JsonGenerator;
 
 import net.shibboleth.idp.session.AbstractSPSessionSerializer;
 import net.shibboleth.idp.session.SPSession;
-import net.shibboleth.utilities.java.support.annotation.Duration;
 import net.shibboleth.utilities.java.support.annotation.ParameterName;
-import net.shibboleth.utilities.java.support.annotation.constraint.NonNegative;
 import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
 
 /**
@@ -38,16 +38,14 @@ import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
 public class CASSPSessionSerializer extends AbstractSPSessionSerializer {
 
     /** Field name of CAS ticket. */
-    @Nonnull @NotEmpty
-    private static final String TICKET_FIELD = "st";
-
+    @Nonnull @NotEmpty private static final String TICKET_FIELD = "st";
 
     /**
      * Constructor.
      *
-     * @param offset milliseconds to subtract from record expiration to establish session expiration value
+     * @param offset time to subtract from record expiration to establish session expiration value
      */
-    public CASSPSessionSerializer(@Duration @NonNegative @ParameterName(name="offset") final long offset) {
+    public CASSPSessionSerializer(@Nonnull @ParameterName(name="offset") final Duration offset) {
         super(offset);
     }
 
@@ -59,13 +57,11 @@ public class CASSPSessionSerializer extends AbstractSPSessionSerializer {
         generator.write(TICKET_FIELD, ((CASSPSession) instance).getTicketId());
     }
 
-    @Nonnull
+    /** {@inheritDoc} */
     @Override
-    protected SPSession doDeserialize(
-            @Nonnull final JsonObject obj,
-            @Nonnull @NotEmpty final String id,
-            final long creation,
-            final long expiration) throws IOException {
+    @Nonnull protected SPSession doDeserialize(@Nonnull final JsonObject obj, @Nonnull @NotEmpty final String id,
+            @Nonnull final Instant creation, @Nonnull final Instant expiration) throws IOException {
         return new CASSPSession(id, creation, expiration, obj.getString(TICKET_FIELD));
     }
-}
+    
+}
\ No newline at end of file
diff --git a/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/ticket/impl/AbstractTicketService.java b/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/ticket/impl/AbstractTicketService.java
index 7675c0d..73fcb52 100644
--- a/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/ticket/impl/AbstractTicketService.java
+++ b/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/ticket/impl/AbstractTicketService.java
@@ -18,6 +18,7 @@
 package net.shibboleth.idp.cas.ticket.impl;
 
 import java.io.IOException;
+import java.time.Instant;
 import java.util.HashMap;
 import java.util.Map;
 
@@ -36,7 +37,6 @@ import net.shibboleth.idp.cas.ticket.serialization.impl.ProxyGrantingTicketSeria
 import net.shibboleth.idp.cas.ticket.serialization.impl.ProxyTicketSerializer;
 import net.shibboleth.idp.cas.ticket.serialization.impl.ServiceTicketSerializer;
 import net.shibboleth.utilities.java.support.logic.Constraint;
-import org.joda.time.Instant;
 import org.opensaml.storage.StorageRecord;
 import org.opensaml.storage.StorageSerializer;
 import org.opensaml.storage.StorageService;
@@ -186,7 +186,7 @@ public abstract class AbstractTicketService implements TicketServiceEx {
         final String context = context(ticket.getClass());
         try {
             final String sessionId = ticket.getSessionId();
-            final long expiry = ticket.getExpirationInstant().getMillis();
+            final long expiry = ticket.getExpirationInstant().toEpochMilli();
             log.debug("Storing mapping of {} to {} in context {}", ticket, sessionId, context);
             if (!storageService.create(context, ticket.getId(), sessionId, expiry)) {
                 throw new RuntimeException("Failed to store ticket " + ticket);
diff --git a/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/ticket/impl/EncodingTicketService.java b/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/ticket/impl/EncodingTicketService.java
index 7c0627c..8b2c777 100644
--- a/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/ticket/impl/EncodingTicketService.java
+++ b/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/ticket/impl/EncodingTicketService.java
@@ -17,6 +17,8 @@
 
 package net.shibboleth.idp.cas.ticket.impl;
 
+import java.time.Instant;
+
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
@@ -30,7 +32,6 @@ import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
 import net.shibboleth.utilities.java.support.logic.Constraint;
 import net.shibboleth.utilities.java.support.security.DataSealer;
 
-import org.joda.time.Instant;
 import org.opensaml.storage.StorageService;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -176,7 +177,7 @@ public class EncodingTicketService extends AbstractTicketService {
         final String opaque;
         try {
             opaque = dataSealer.wrap(
-                    serializer(ticketClass).serialize(ticket), ticket.getExpirationInstant().getMillis());
+                    serializer(ticketClass).serialize(ticket), ticket.getExpirationInstant().toEpochMilli());
         } catch (final Exception e) {
             throw new RuntimeException("Ticket encoding failed", e);
         }
diff --git a/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/ticket/impl/SimpleTicketService.java b/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/ticket/impl/SimpleTicketService.java
index 775bd02..324ef8f 100644
--- a/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/ticket/impl/SimpleTicketService.java
+++ b/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/ticket/impl/SimpleTicketService.java
@@ -17,6 +17,8 @@
 
 package net.shibboleth.idp.cas.ticket.impl;
 
+import java.time.Instant;
+
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
@@ -27,7 +29,6 @@ import net.shibboleth.idp.cas.ticket.TicketState;
 import net.shibboleth.utilities.java.support.annotation.ParameterName;
 import net.shibboleth.utilities.java.support.logic.Constraint;
 
-import org.joda.time.Instant;
 import org.opensaml.storage.StorageService;
 
 /**
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 01ee28c..01e606c 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
@@ -20,6 +20,7 @@ package net.shibboleth.idp.cas.ticket.serialization.impl;
 import java.io.IOException;
 import java.io.StringReader;
 import java.io.StringWriter;
+import java.time.Instant;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -35,7 +36,6 @@ import net.shibboleth.idp.cas.ticket.Ticket;
 import net.shibboleth.idp.cas.ticket.TicketState;
 import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
 import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
-import org.joda.time.Instant;
 import org.opensaml.storage.StorageSerializer;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -98,12 +98,12 @@ public abstract class AbstractTicketSerializer<T extends Ticket> implements Stor
         try (final JsonGenerator gen = generatorFactory.createGenerator(buffer)) {
             gen.writeStartObject()
                     .write(SERVICE_FIELD, ticket.getService())
-                    .write(EXPIRATION_FIELD, ticket.getExpirationInstant().getMillis());
+                    .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().getMillis())
+                        .write(AUTHN_INSTANT_FIELD, ticket.getTicketState().getAuthenticationInstant().toEpochMilli())
                         .write(AUTHN_METHOD_FIELD, ticket.getTicketState().getAuthenticationMethod())
                         .writeEnd();
             }
@@ -128,14 +128,14 @@ public abstract class AbstractTicketSerializer<T extends Ticket> implements Stor
         try (final JsonReader reader = readerFactory.createReader(new StringReader(value))) {
             final JsonObject to = reader.readObject();
             final String service = to.getString(SERVICE_FIELD);
-            final Instant expiry = new Instant(to.getJsonNumber(EXPIRATION_FIELD).longValueExact());
+            final Instant expiry = Instant.ofEpochMilli(to.getJsonNumber(EXPIRATION_FIELD).longValueExact());
             final JsonObject so = to.getJsonObject(STATE_FIELD);
             final TicketState state;
             if (so != null) {
                 state = new TicketState(
                         so.getString(SESSION_FIELD),
                         so.getString(PRINCIPAL_FIELD),
-                        new Instant(so.getJsonNumber(AUTHN_INSTANT_FIELD).longValueExact()),
+                        Instant.ofEpochMilli(so.getJsonNumber(AUTHN_INSTANT_FIELD).longValueExact()),
                         so.getString(AUTHN_METHOD_FIELD));
             } else {
                 state = null;
diff --git a/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/ticket/serialization/impl/ProxyGrantingTicketSerializer.java b/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/ticket/serialization/impl/ProxyGrantingTicketSerializer.java
index d397310..74d81a1 100644
--- a/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/ticket/serialization/impl/ProxyGrantingTicketSerializer.java
+++ b/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/ticket/serialization/impl/ProxyGrantingTicketSerializer.java
@@ -17,12 +17,13 @@
 
 package net.shibboleth.idp.cas.ticket.serialization.impl;
 
+import java.time.Instant;
+
 import javax.annotation.Nonnull;
 import javax.json.JsonObject;
 import javax.json.stream.JsonGenerator;
 
 import net.shibboleth.idp.cas.ticket.ProxyGrantingTicket;
-import org.joda.time.Instant;
 
 /**
  * Serializes proxy-granting tickets in simple field-delimited form.
diff --git a/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/ticket/serialization/impl/ProxyTicketSerializer.java b/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/ticket/serialization/impl/ProxyTicketSerializer.java
index 7610c8a..e5469c6 100644
--- a/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/ticket/serialization/impl/ProxyTicketSerializer.java
+++ b/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/ticket/serialization/impl/ProxyTicketSerializer.java
@@ -17,12 +17,13 @@
 
 package net.shibboleth.idp.cas.ticket.serialization.impl;
 
+import java.time.Instant;
+
 import javax.annotation.Nonnull;
 import javax.json.JsonObject;
 import javax.json.stream.JsonGenerator;
 
 import net.shibboleth.idp.cas.ticket.ProxyTicket;
-import org.joda.time.Instant;
 
 /**
  * Proxy ticket storage serializer.
diff --git a/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/ticket/serialization/impl/ServiceTicketSerializer.java b/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/ticket/serialization/impl/ServiceTicketSerializer.java
index 67575e9..d0a9ffa 100644
--- a/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/ticket/serialization/impl/ServiceTicketSerializer.java
+++ b/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/ticket/serialization/impl/ServiceTicketSerializer.java
@@ -17,12 +17,13 @@
 
 package net.shibboleth.idp.cas.ticket.serialization.impl;
 
+import java.time.Instant;
+
 import javax.annotation.Nonnull;
 import javax.json.JsonObject;
 import javax.json.stream.JsonGenerator;
 
 import net.shibboleth.idp.cas.ticket.ServiceTicket;
-import org.joda.time.Instant;
 
 /**
  * Serializes service tickets in simple field-delimited form.
diff --git a/idp-cas-impl/src/test/java/net/shibboleth/idp/cas/flow/impl/AbstractFlowActionTest.java b/idp-cas-impl/src/test/java/net/shibboleth/idp/cas/flow/impl/AbstractFlowActionTest.java
index b3b6210..145c88b 100644
--- a/idp-cas-impl/src/test/java/net/shibboleth/idp/cas/flow/impl/AbstractFlowActionTest.java
+++ b/idp-cas-impl/src/test/java/net/shibboleth/idp/cas/flow/impl/AbstractFlowActionTest.java
@@ -17,6 +17,7 @@
 
 package net.shibboleth.idp.cas.flow.impl;
 
+import java.time.Instant;
 import java.util.Arrays;
 import java.util.HashSet;
 
@@ -30,8 +31,6 @@ import net.shibboleth.idp.cas.ticket.impl.TicketIdentifierGenerationStrategy;
 import net.shibboleth.idp.session.IdPSession;
 import net.shibboleth.idp.session.SessionException;
 import net.shibboleth.idp.spring.IdPPropertiesApplicationContextInitializer;
-import org.joda.time.DateTime;
-import org.joda.time.Instant;
 import org.opensaml.core.config.InitializationException;
 import org.opensaml.core.config.InitializationService;
 import org.opensaml.profile.context.ProfileRequestContext;
@@ -97,7 +96,7 @@ public abstract class AbstractFlowActionTest extends AbstractTestNGSpringContext
     }
 
     protected static Instant expiry() {
-        return DateTime.now().plusSeconds(30).toInstant();
+        return Instant.now().plusSeconds(30);
     }
 
     protected String generateServiceTicketId() {
diff --git a/idp-cas-impl/src/test/java/net/shibboleth/idp/cas/flow/impl/ValidateProxyCallbackActionTest.java b/idp-cas-impl/src/test/java/net/shibboleth/idp/cas/flow/impl/ValidateProxyCallbackActionTest.java
index 772fa64..a669931 100644
--- a/idp-cas-impl/src/test/java/net/shibboleth/idp/cas/flow/impl/ValidateProxyCallbackActionTest.java
+++ b/idp-cas-impl/src/test/java/net/shibboleth/idp/cas/flow/impl/ValidateProxyCallbackActionTest.java
@@ -19,6 +19,7 @@ package net.shibboleth.idp.cas.flow.impl;
 
 import java.net.URI;
 import java.security.cert.CertificateException;
+import java.time.Instant;
 
 import net.shibboleth.idp.cas.config.impl.ValidateConfiguration;
 import net.shibboleth.idp.cas.protocol.ProtocolError;
@@ -27,7 +28,6 @@ import net.shibboleth.idp.cas.protocol.TicketValidationResponse;
 import net.shibboleth.idp.cas.proxy.ProxyValidator;
 import net.shibboleth.idp.cas.ticket.ServiceTicket;
 import net.shibboleth.idp.cas.ticket.TicketState;
-import org.joda.time.Instant;
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.springframework.webflow.execution.RequestContext;
 import org.testng.annotations.Test;
diff --git a/idp-cas-impl/src/test/java/net/shibboleth/idp/cas/session/impl/CASSPSessionSerializerTest.java b/idp-cas-impl/src/test/java/net/shibboleth/idp/cas/session/impl/CASSPSessionSerializerTest.java
index 0891564..6f6b2b0 100644
--- a/idp-cas-impl/src/test/java/net/shibboleth/idp/cas/session/impl/CASSPSessionSerializerTest.java
+++ b/idp-cas-impl/src/test/java/net/shibboleth/idp/cas/session/impl/CASSPSessionSerializerTest.java
@@ -21,20 +21,24 @@ import org.testng.annotations.Test;
 
 import static org.testng.Assert.assertEquals;
 
+import java.time.Duration;
+import java.time.Instant;
+
 public class CASSPSessionSerializerTest {
 
-    private CASSPSessionSerializer serializer = new CASSPSessionSerializer(0);
+    private CASSPSessionSerializer serializer = new CASSPSessionSerializer(Duration.ZERO);
 
     @Test
     public void testSerializeDeserialize() throws Exception{
-        final long exp = 1410539474000000000L;
+        final Instant exp = Instant.ofEpochMilli(1410539474000000000L);
         final CASSPSession original = new CASSPSession(
                 "https://foo.example.com/shibboleth",
-                1410532279838046000L,
+                Instant.ofEpochMilli(1410532279838046000L),
                 exp,
                 "ST-1234126-ABC1346DEADBEEF");
         final String serialized = serializer.serialize(original);
-        final CASSPSession deserialized = (CASSPSession) serializer.deserialize(1, "context", "key", serialized, exp);
+        final CASSPSession deserialized =
+                (CASSPSession) serializer.deserialize(1, "context", "key", serialized, exp.toEpochMilli());
         assertEquals(deserialized.getId(), original.getId());
         assertEquals(deserialized.getCreationInstant(), original.getCreationInstant());
         assertEquals(deserialized.getExpirationInstant(), original.getExpirationInstant());
diff --git a/idp-cas-impl/src/test/java/net/shibboleth/idp/cas/ticket/impl/EncodingTicketServiceTest.java b/idp-cas-impl/src/test/java/net/shibboleth/idp/cas/ticket/impl/EncodingTicketServiceTest.java
index 27316a0..6109e91 100644
--- a/idp-cas-impl/src/test/java/net/shibboleth/idp/cas/ticket/impl/EncodingTicketServiceTest.java
+++ b/idp-cas-impl/src/test/java/net/shibboleth/idp/cas/ticket/impl/EncodingTicketServiceTest.java
@@ -19,6 +19,8 @@ package net.shibboleth.idp.cas.ticket.impl;
 
 import java.security.NoSuchAlgorithmException;
 import java.security.SecureRandom;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
 
 import net.shibboleth.ext.spring.resource.ResourceHelper;
 import net.shibboleth.idp.cas.ticket.ProxyGrantingTicket;
@@ -30,7 +32,6 @@ import net.shibboleth.utilities.java.support.security.DataSealer;
 import net.shibboleth.utilities.java.support.security.IdentifierGenerationStrategy;
 import net.shibboleth.utilities.java.support.security.RandomIdentifierGenerationStrategy;
 import org.apache.commons.codec.binary.Base32;
-import org.joda.time.Instant;
 import org.opensaml.storage.impl.MemoryStorageService;
 import org.springframework.core.io.ClassPathResource;
 import org.testng.annotations.BeforeTest;
@@ -85,7 +86,7 @@ public class EncodingTicketServiceTest {
     public void testCreateRemoveServiceTicketSuccess() throws Exception {
         final TicketState state = newState("aloysius");
         final String service = "https://www.example.com/s1/";
-        final Instant expiry = new Instant().plus(5000);
+        final Instant expiry = Instant.now().plusSeconds(5);
         final String id = String.valueOf(System.currentTimeMillis());
         final ServiceTicket st1 = ticketService.createServiceTicket(id, expiry, service, state, true);
         assertNotNull(st1);
@@ -93,7 +94,7 @@ public class EncodingTicketServiceTest {
         final ServiceTicket st2 = ticketService.removeServiceTicket(st1.getId());
         assertNotNull(st2);
         assertEquals(st1.getId(), st2.getId());
-        assertEquals(expiry, st2.getExpirationInstant());
+        assertEquals(expiry.truncatedTo(ChronoUnit.MILLIS), st2.getExpirationInstant());
         assertEquals(service, st2.getService());
         assertTrue(st2.isRenew());
         assertEquals(state.getSessionId(), st2.getSessionId());
@@ -108,7 +109,7 @@ public class EncodingTicketServiceTest {
     public void testCreateRemoveServiceTicketInvalid() throws Exception {
         final ServiceTicket st1 = ticketService.createServiceTicket(
                 String.valueOf(System.currentTimeMillis()),
-                new Instant().plus(5000),
+                Instant.now().plusSeconds(5),
                 "https://www.example.com/s2/",
                 newState("bartholomew"),
                 true);
@@ -122,7 +123,7 @@ public class EncodingTicketServiceTest {
         final String service = "https://www.google.com/maps/place/Mountain+Lake+Lodge/"
                 + "@37.3554696,-80.539459,17z/data=!3m1!4b1!4m5!3m4!1s0x884dc6a58faa2119:0x17f3cc6a2c82b614!"
                 + "8m2!3d37.3554696!4d-80.537265";
-        final Instant expiry = new Instant().plus(5000);
+        final Instant expiry = Instant.now().plusSeconds(5);
         final String id = String.valueOf(System.currentTimeMillis());
         final ServiceTicket st1 = ticketService.createServiceTicket(id, expiry, service, state, true);
         assertNotNull(st1);
@@ -134,7 +135,7 @@ public class EncodingTicketServiceTest {
     public void testCreateRemoveProxyTicketSuccess() throws Exception {
         final ProxyGrantingTicket pgt = newPGT(newState("donegal"), "https://www.example.com/s1/");
         final String service = "https://www.example.com/s2/";
-        final Instant expiry = new Instant().plus(5000);
+        final Instant expiry = Instant.now().plusSeconds(5);
         final String id = String.valueOf(System.currentTimeMillis());
         final ProxyTicket pt1 = ticketService.createProxyTicket(id, expiry, pgt, service);
         assertNotNull(pt1);
@@ -142,7 +143,7 @@ public class EncodingTicketServiceTest {
         final ProxyTicket pt2 = ticketService.removeProxyTicket(pt1.getId());
         assertNotNull(pt2);
         assertEquals(pt1.getId(), pt2.getId());
-        assertEquals(expiry, pt2.getExpirationInstant());
+        assertEquals(expiry.truncatedTo(ChronoUnit.MILLIS), pt2.getExpirationInstant());
         assertEquals(service, pt2.getService());
         assertEquals(pgt.getId(), pt2.getPgtId());
         assertEquals(pgt.getTicketState(), pt2.getTicketState());
@@ -156,7 +157,7 @@ public class EncodingTicketServiceTest {
     public void testCreateRemoveProxyTicketInvalid() throws Exception {
         final ProxyGrantingTicket pgt = newPGT(newState("esquire"), "https://www.example.com/s1/");
         final String service = "https://www.example.com/s2/";
-        final Instant expiry = new Instant().plus(5000);
+        final Instant expiry = Instant.now().plusSeconds(5);
         final String id = String.valueOf(System.currentTimeMillis());
         final ProxyTicket pt1 = ticketService.createProxyTicket(id, expiry, pgt, service);
         assertNotNull(pt1);
@@ -164,12 +165,13 @@ public class EncodingTicketServiceTest {
     }
 
     private TicketState newState(final String principal) {
-        return new TicketState(sessionIdGenerator.generateIdentifier(), principal, new Instant(), "authn/Password");
+        return new TicketState(sessionIdGenerator.generateIdentifier(), principal,
+                Instant.now().truncatedTo(ChronoUnit.MILLIS), "authn/Password");
     }
 
     private ProxyGrantingTicket newPGT(final TicketState state, final String service) {
         final ProxyGrantingTicket pgt = new ProxyGrantingTicket(
-                pgtIdGenerator.generateIdentifier(), service, new Instant().plus(300000), "PGT-12345");
+                pgtIdGenerator.generateIdentifier(), service, Instant.now().plusSeconds(300), "PGT-12345");
         pgt.setTicketState(state);
         return pgt;
     }
diff --git a/idp-cas-impl/src/test/java/net/shibboleth/idp/cas/ticket/impl/SimpleTicketServiceTest.java b/idp-cas-impl/src/test/java/net/shibboleth/idp/cas/ticket/impl/SimpleTicketServiceTest.java
index fd26187..71357f7 100644
--- a/idp-cas-impl/src/test/java/net/shibboleth/idp/cas/ticket/impl/SimpleTicketServiceTest.java
+++ b/idp-cas-impl/src/test/java/net/shibboleth/idp/cas/ticket/impl/SimpleTicketServiceTest.java
@@ -21,14 +21,15 @@ import net.shibboleth.idp.cas.ticket.ProxyGrantingTicket;
 import net.shibboleth.idp.cas.ticket.ProxyTicket;
 import net.shibboleth.idp.cas.ticket.ServiceTicket;
 import net.shibboleth.idp.cas.ticket.TicketState;
-import org.joda.time.DateTime;
-import org.joda.time.Instant;
 import org.opensaml.storage.impl.MemoryStorageService;
 import org.testng.annotations.BeforeTest;
 import org.testng.annotations.Test;
 
 import static org.testng.Assert.*;
 
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+
 /**
  * Unit test for {@link SimpleTicketService} class.
  *
@@ -103,7 +104,7 @@ public class SimpleTicketServiceTest {
                 new TicketIdentifierGenerationStrategy("ST", 25).generateIdentifier(),
                 expiry(),
                 TEST_SERVICE,
-                new TicketState(TEST_SESSION_ID, "bob", Instant.now(), "Password"),
+                new TicketState(TEST_SESSION_ID, "bob", Instant.now().truncatedTo(ChronoUnit.MILLIS), "Password"),
                 false);
     }
 
@@ -115,6 +116,6 @@ public class SimpleTicketServiceTest {
     }
 
     private static Instant expiry() {
-        return DateTime.now().plusSeconds(10).toInstant();
+        return Instant.now().plusSeconds(10).truncatedTo(ChronoUnit.MILLIS);
     }
 }
diff --git a/idp-cas-impl/src/test/java/net/shibboleth/idp/cas/ticket/serialization/impl/ProxyGrantingTicketSerializerTest.java b/idp-cas-impl/src/test/java/net/shibboleth/idp/cas/ticket/serialization/impl/ProxyGrantingTicketSerializerTest.java
index 8494d54..d41a706 100644
--- a/idp-cas-impl/src/test/java/net/shibboleth/idp/cas/ticket/serialization/impl/ProxyGrantingTicketSerializerTest.java
+++ b/idp-cas-impl/src/test/java/net/shibboleth/idp/cas/ticket/serialization/impl/ProxyGrantingTicketSerializerTest.java
@@ -19,11 +19,13 @@ package net.shibboleth.idp.cas.ticket.serialization.impl;
 
 import net.shibboleth.idp.cas.ticket.ProxyGrantingTicket;
 import net.shibboleth.idp.cas.ticket.TicketState;
-import org.joda.time.Instant;
 import org.testng.annotations.Test;
 
 import static org.testng.Assert.*;
 
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+
 /**
  * Unit test for {@link ProxyGrantingTicketSerializer}.
  */
@@ -36,7 +38,7 @@ public class ProxyGrantingTicketSerializerTest {
         final ProxyGrantingTicket pgt1 = new ProxyGrantingTicket(
                 "ST-0123456789-9d22c36953a31fd12f12d30d76b344d3",
                 "https://nobody.example.org",
-                Instant.now(),
+                Instant.now().truncatedTo(ChronoUnit.MILLIS),
                 null);
         final String serialized = serializer.serialize(pgt1);
         final ProxyGrantingTicket pgt2 = serializer.deserialize(1, "notused", pgt1.getId(), serialized, null);
@@ -52,9 +54,10 @@ public class ProxyGrantingTicketSerializerTest {
         final ProxyGrantingTicket pgt1 = new ProxyGrantingTicket(
                 "ST-0123456789-fbca86ba09d1be7ec3ac17e6f372be87",
                 "https://nobody.example.org",
-                Instant.now(),
+                Instant.now().truncatedTo(ChronoUnit.MILLIS),
                 null);
-        pgt1.setTicketState(new TicketState("idpsess-1e663b80e6c6b2b7ae778cb3ea265", "bob", Instant.now(), "Password"));
+        pgt1.setTicketState(new TicketState("idpsess-1e663b80e6c6b2b7ae778cb3ea265", "bob",
+                Instant.now().truncatedTo(ChronoUnit.MILLIS), "Password"));
         final String serialized = serializer.serialize(pgt1);
         final ProxyGrantingTicket pgt2 = serializer.deserialize(1, "notused", pgt1.getId(), serialized, null);
         assertEquals(pgt2.getId(), pgt1.getId());
@@ -69,9 +72,10 @@ public class ProxyGrantingTicketSerializerTest {
         final ProxyGrantingTicket pgt1 = new ProxyGrantingTicket(
                 "ST-0123456789-fbca86ba09d1be7ec3ac17e6f372be87",
                 "https://nobody.example.org",
-                Instant.now(),
+                Instant.now().truncatedTo(ChronoUnit.MILLIS),
                 "PGT-0987654321-0040c390cf67e571c7e12fcc78fc0cb3");
-        pgt1.setTicketState(new TicketState("idpsess-6d986af1280b7f52f9e7da434cfd0", "bob", Instant.now(), "Password"));
+        pgt1.setTicketState(new TicketState("idpsess-6d986af1280b7f52f9e7da434cfd0", "bob",
+                Instant.now().truncatedTo(ChronoUnit.MILLIS), "Password"));
         final String serialized = serializer.serialize(pgt1);
         final ProxyGrantingTicket pgt2 = serializer.deserialize(1, "notused", pgt1.getId(), serialized, null);
         assertEquals(pgt2.getId(), pgt1.getId());
@@ -80,4 +84,5 @@ public class ProxyGrantingTicketSerializerTest {
         assertEquals(pgt2.getParentId(), pgt1.getParentId());
         assertEquals(pgt2.getTicketState(), pgt1.getTicketState());
     }
+    
 }
\ No newline at end of file
diff --git a/idp-cas-impl/src/test/java/net/shibboleth/idp/cas/ticket/serialization/impl/ProxyTicketSerializerTest.java b/idp-cas-impl/src/test/java/net/shibboleth/idp/cas/ticket/serialization/impl/ProxyTicketSerializerTest.java
index 22c5d95..8cc3700 100644
--- a/idp-cas-impl/src/test/java/net/shibboleth/idp/cas/ticket/serialization/impl/ProxyTicketSerializerTest.java
+++ b/idp-cas-impl/src/test/java/net/shibboleth/idp/cas/ticket/serialization/impl/ProxyTicketSerializerTest.java
@@ -19,11 +19,13 @@ package net.shibboleth.idp.cas.ticket.serialization.impl;
 
 import net.shibboleth.idp.cas.ticket.ProxyTicket;
 import net.shibboleth.idp.cas.ticket.TicketState;
-import org.joda.time.Instant;
 import org.testng.annotations.Test;
 
 import static org.testng.Assert.*;
 
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+
 /**
  * Unit test for {@link ProxyTicketSerializer}.
  */
@@ -36,7 +38,7 @@ public class ProxyTicketSerializerTest {
         final ProxyTicket pt1 = new ProxyTicket(
                 "ST-0123456789-6027f6e93c11b1f587857ee0e7689c27",
                 "https://nobody.example.org",
-                Instant.now(),
+                Instant.now().truncatedTo(ChronoUnit.MILLIS),
                 "PGT-0123456789-87182857dcbc70f8aa2e0ec87ec3e707");
         final String serialized = serializer.serialize(pt1);
         final ProxyTicket pt2 = serializer.deserialize(1, "notused", pt1.getId(), serialized, null);
@@ -52,9 +54,10 @@ public class ProxyTicketSerializerTest {
         final ProxyTicket pt1 = new ProxyTicket(
                 "ST-0123456789-e1e212143527d57053e7a72d75b3ccd6",
                 "https://nobody.example.org",
-                Instant.now(),
+                Instant.now().truncatedTo(ChronoUnit.MILLIS),
                 "PGT-0123456789-c0dddd0f73b9494f7fe0b549e8c28002");
-        pt1.setTicketState(new TicketState("idpsess-6ebae421b142adb35a3a6303116c3f", "bob", Instant.now(), "Password"));
+        pt1.setTicketState(new TicketState("idpsess-6ebae421b142adb35a3a6303116c3f", "bob",
+                Instant.now().truncatedTo(ChronoUnit.MILLIS), "Password"));
         final String serialized = serializer.serialize(pt1);
         final ProxyTicket pt2 = serializer.deserialize(1, "notused", pt1.getId(), serialized, null);
         assertEquals(pt2.getId(), pt1.getId());
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 7ebdc31..3692df1 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
@@ -19,11 +19,13 @@ package net.shibboleth.idp.cas.ticket.serialization.impl;
 
 import net.shibboleth.idp.cas.ticket.ServiceTicket;
 import net.shibboleth.idp.cas.ticket.TicketState;
-import org.joda.time.Instant;
 import org.testng.annotations.Test;
 
 import static org.testng.Assert.*;
 
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+
 /**
  * Unit test for {@link ServiceTicketSerializer}.
  */
@@ -36,7 +38,7 @@ public class ServiceTicketSerializerTest {
         final ServiceTicket st1 = new ServiceTicket(
                 "ST-0123456789-616ea1550eef862761e5931bdccaaba0",
                 "https://nobody.example.org",
-                Instant.now(),
+                Instant.now().truncatedTo(ChronoUnit.MILLIS),
                 true);
         final String serialized = serializer.serialize(st1);
         final ServiceTicket st2 = serializer.deserialize(1, "notused", st1.getId(), serialized, null);
@@ -52,9 +54,10 @@ public class ServiceTicketSerializerTest {
         final ServiceTicket st1 = new ServiceTicket(
                 "ST-0123456789-e6342d467a4414e599aa3c323528e96f",
                 "https://nobody.example.org",
-                Instant.now(),
+                Instant.now().truncatedTo(ChronoUnit.MILLIS),
                 true);
-        st1.setTicketState(new TicketState("idpsess-d2db22058dc178d3b917363859e", "bob", Instant.now(), "Password"));
+        st1.setTicketState(new TicketState("idpsess-d2db22058dc178d3b917363859e", "bob",
+                Instant.now().truncatedTo(ChronoUnit.MILLIS), "Password"));
         final String serialized = serializer.serialize(st1);
         final ServiceTicket st2 = serializer.deserialize(1, "notused", st1.getId(), serialized, null);
         assertEquals(st2.getId(), st1.getId());
diff --git a/idp-cas-impl/src/test/resources/logback-test.xml b/idp-cas-impl/src/test/resources/logback-test.xml
index 491ec54..7c6fa81 100644
--- a/idp-cas-impl/src/test/resources/logback-test.xml
+++ b/idp-cas-impl/src/test/resources/logback-test.xml
@@ -12,7 +12,7 @@
     <logger name="net.shibboleth.idp.cas" level="DEBUG"/>
 
     <root>
-        <level value="warn" />
+        <level value="WARN" />
         <appender-ref ref="STDOUT" />
     </root>
     
diff --git a/idp-cas-impl/src/test/resources/spring/test-flow-beans.xml b/idp-cas-impl/src/test/resources/spring/test-flow-beans.xml
index 47916f6..3777d41 100644
--- a/idp-cas-impl/src/test/resources/spring/test-flow-beans.xml
+++ b/idp-cas-impl/src/test/resources/spring/test-flow-beans.xml
@@ -24,6 +24,7 @@
                 <bean class="net.shibboleth.ext.spring.config.BooleanToPredicateConverter" />
                 <bean class="net.shibboleth.ext.spring.config.StringBooleanToPredicateConverter" />
                 <bean class="net.shibboleth.ext.spring.config.StringToResourceConverter" />
+                <bean class="net.shibboleth.ext.spring.config.StringToDurationConverter" />
             </set>
         </property>
     </bean>
diff --git a/idp-conf/src/main/resources/system/conf/global-system.xml b/idp-conf/src/main/resources/system/conf/global-system.xml
index 3c9ca5e..6a3f0f9 100644
--- a/idp-conf/src/main/resources/system/conf/global-system.xml
+++ b/idp-conf/src/main/resources/system/conf/global-system.xml
@@ -27,6 +27,7 @@
                 <bean class="net.shibboleth.ext.spring.config.StringToResourceConverter" />
                 <bean class="net.shibboleth.ext.spring.config.FunctionToFunctionConverter" />
                 <bean class="net.shibboleth.ext.spring.config.PredicateToPredicateConverter" />
+                <bean class="net.shibboleth.ext.spring.config.StringToDurationConverter" />
             </set>
         </property>
     </bean>
@@ -411,7 +412,7 @@
     <bean id="shibboleth.X509TrustManager" abstract="true"
         class="org.opensaml.security.trust.impl.TrustEngineX509TrustManager" />
     
-    <!-- Default Jackson ObjectMapper with JodaTime support. -->
+    <!-- Default Jackson ObjectMapper. -->
     
     <bean id="shibboleth.JSONObjectMapper" class="com.fasterxml.jackson.databind.ObjectMapper" />
 
@@ -427,7 +428,7 @@
             p:targetObject-ref="shibboleth.JSONObjectMapper"
             p:targetMethod="registerModule">
         <property name="arguments">
-            <bean class="com.fasterxml.jackson.datatype.joda.JodaModule" />
+            <bean class="com.fasterxml.jackson.datatype.jsr310.JavaTimeModule" />
         </property>
     </bean>
 
diff --git a/idp-conf/src/test/java/net/shibboleth/idp/test/flows/cas/ProxyFlowTest.java b/idp-conf/src/test/java/net/shibboleth/idp/test/flows/cas/ProxyFlowTest.java
index 725416e..25c9f6b 100644
--- a/idp-conf/src/test/java/net/shibboleth/idp/test/flows/cas/ProxyFlowTest.java
+++ b/idp-conf/src/test/java/net/shibboleth/idp/test/flows/cas/ProxyFlowTest.java
@@ -25,8 +25,6 @@ import net.shibboleth.idp.cas.ticket.impl.TicketIdentifierGenerationStrategy;
 import net.shibboleth.idp.session.IdPSession;
 import net.shibboleth.idp.session.SessionManager;
 import net.shibboleth.idp.test.flows.AbstractFlowTest;
-import org.joda.time.DateTime;
-import org.joda.time.Instant;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.test.context.ContextConfiguration;
@@ -39,6 +37,9 @@ import javax.annotation.Nonnull;
 
 import static org.testng.Assert.*;
 
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+
 /**
  * Tests the flow behind the <code>/proxy</code> endpoint.
  *
@@ -165,13 +166,13 @@ public class ProxyFlowTest extends AbstractFlowTest {
     private ProxyGrantingTicket createProxyGrantingTicket(final String sessionId, final String principal) {
         final ServiceTicket st = ticketService.createServiceTicket(
                 new TicketIdentifierGenerationStrategy("ST", 25).generateIdentifier(),
-                DateTime.now().plusSeconds(10).toInstant(),
+                Instant.now().plusSeconds(10),
                 "https://service.example.org/",
                 new TicketState(sessionId, principal, Instant.now(), "Password"),
                 false);
         return ticketService.createProxyGrantingTicket(
                 new TicketIdentifierGenerationStrategy("PGT", 50).generateIdentifier(),
-                DateTime.now().plusHours(1).toInstant(),
+                Instant.now().plus(1, ChronoUnit.HOURS),
                 st);
     }
 }
diff --git a/idp-conf/src/test/java/net/shibboleth/idp/test/flows/cas/ProxyValidateFlowTest.java b/idp-conf/src/test/java/net/shibboleth/idp/test/flows/cas/ProxyValidateFlowTest.java
index 355abf5..7c979ef 100644
--- a/idp-conf/src/test/java/net/shibboleth/idp/test/flows/cas/ProxyValidateFlowTest.java
+++ b/idp-conf/src/test/java/net/shibboleth/idp/test/flows/cas/ProxyValidateFlowTest.java
@@ -29,8 +29,6 @@ import net.shibboleth.idp.session.IdPSession;
 import net.shibboleth.idp.session.SessionManager;
 import net.shibboleth.idp.test.flows.AbstractFlowTest;
 
-import org.joda.time.DateTime;
-import org.joda.time.Instant;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.test.context.ContextConfiguration;
@@ -39,6 +37,8 @@ import org.testng.annotations.Test;
 
 import static org.testng.Assert.*;
 
+import java.time.Instant;
+
 /**
  * Tests the flow behind the <code>/proxyValidate</code> endpoint.
  *
@@ -161,17 +161,17 @@ public class ProxyValidateFlowTest extends AbstractFlowTest {
     private ProxyTicket createProxyTicket(final String sessionId, final String principal) {
         final ServiceTicket st = ticketService.createServiceTicket(
                 new TicketIdentifierGenerationStrategy("ST", 25).generateIdentifier(),
-                DateTime.now().plusSeconds(5).toInstant(),
+                Instant.now().plusSeconds(5),
                 "https://service.example.org/",
                 new TicketState(sessionId, principal, Instant.now(), "Password"),
                 false);
         final ProxyGrantingTicket pgt = ticketService.createProxyGrantingTicket(
                 new TicketIdentifierGenerationStrategy("PGT", 50).generateIdentifier(),
-                DateTime.now().plusSeconds(10).toInstant(),
+                Instant.now().plusSeconds(10),
                 st);
         return ticketService.createProxyTicket(
                 new TicketIdentifierGenerationStrategy("PT", 25).generateIdentifier(),
-                DateTime.now().plusSeconds(5).toInstant(),
+                Instant.now().plusSeconds(5),
                 pgt,
                 "https://proxyA.example.org/");
     }
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 ae1296e..0235305 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
@@ -25,8 +25,6 @@ import net.shibboleth.idp.profile.context.RelyingPartyContext;
 import net.shibboleth.idp.session.IdPSession;
 import net.shibboleth.idp.session.SessionManager;
 import net.shibboleth.idp.test.flows.AbstractFlowTest;
-import org.joda.time.DateTime;
-import org.joda.time.Instant;
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Qualifier;
@@ -39,6 +37,8 @@ import javax.annotation.Nonnull;
 
 import static org.testng.Assert.*;
 
+import java.time.Instant;
+
 /**
  * Tests the flow behind the <code>/samlValidate</code> endpoint.
  *
@@ -75,7 +75,7 @@ public class SamlValidateFlowTest extends AbstractFlowTest {
         final IdPSession session = sessionManager.createSession(principal);
         final ServiceTicket ticket = ticketService.createServiceTicket(
                 "ST-1415133132-ompog68ygxKyX9BPwPuw0hESQBjuA",
-                DateTime.now().plusSeconds(5).toInstant(),
+                Instant.now().plusSeconds(5),
                 "https://test.example.org/",
                 new TicketState(session.getId(), principal, Instant.now(), "Password"),
                 false);
@@ -122,7 +122,7 @@ public class SamlValidateFlowTest extends AbstractFlowTest {
         final IdPSession session = sessionManager.createSession(principal);
         final ServiceTicket ticket = ticketService.createServiceTicket(
                 "ST-2718281828-ompog68ygxKyX9BPwPuw0hESQBjuA",
-                DateTime.now().plusSeconds(5).toInstant(),
+                Instant.now().plusSeconds(5),
                 "https://no-attrs.example.org/",
                 new TicketState(session.getId(), principal, Instant.now(), "Password"),
                 false);
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 259b3d9..078bdd1 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.net.URI;
+import java.time.Instant;
 
 import javax.annotation.Nonnull;
 
@@ -34,8 +35,6 @@ import net.shibboleth.idp.session.criterion.SessionIdCriterion;
 import net.shibboleth.idp.test.flows.AbstractFlowTest;
 
 import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
-import org.joda.time.DateTime;
-import org.joda.time.Instant;
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.opensaml.security.trust.TrustEngine;
 import org.opensaml.security.x509.X509Credential;
@@ -112,7 +111,7 @@ public class ServiceValidateFlowTest extends AbstractFlowTest {
         final IdPSession session = sessionManager.createSession(principal);
         final ServiceTicket ticket = ticketService.createServiceTicket(
                 "ST-1415133132-ompog68ygxKyX9BPwPuw0hESQBjuA",
-                DateTime.now().plusSeconds(5).toInstant(),
+                Instant.now().plusSeconds(5),
                 "https://test.example.org/",
                 new TicketState(session.getId(), principal, Instant.now(), "Password"),
                 false);
@@ -148,7 +147,7 @@ public class ServiceValidateFlowTest extends AbstractFlowTest {
         final IdPSession session = sessionManager.createSession(principal);
         final ServiceTicket ticket = ticketService.createServiceTicket(
                 "ST-1415133132-ompog68ygxKyX9BPwPuw0hESQBjuA",
-                DateTime.now().plusSeconds(5).toInstant(),
+                Instant.now().plusSeconds(5),
                 "https://slo.example.org/",
                 new TicketState(session.getId(), principal, Instant.now(), "Password"),
                 false);
@@ -193,7 +192,7 @@ public class ServiceValidateFlowTest extends AbstractFlowTest {
         final IdPSession session = sessionManager.createSession(principal);
         final ServiceTicket ticket = ticketService.createServiceTicket(
                 "ST-1415133132-ompog68ygxKyX9BPwPuw0hESQBjuA",
-                DateTime.now().plusSeconds(5).toInstant(),
+                Instant.now().plusSeconds(5),
                 "https://test.example.org/",
                 new TicketState(session.getId(), principal, Instant.now(), "Password"),
                 false);
@@ -223,7 +222,7 @@ public class ServiceValidateFlowTest extends AbstractFlowTest {
         final IdPSession session = sessionManager.createSession(principal);
         final ServiceTicket ticket = ticketService.createServiceTicket(
                 "ST-1415133132-ompog68ygxKyX9BPwPuw0hESQBjuA",
-                DateTime.now().plusSeconds(5).toInstant(),
+                Instant.now().plusSeconds(5),
                 "https://test.example.org/",
                 new TicketState(session.getId(), principal, Instant.now(), "Password"),
                 false);
@@ -249,7 +248,7 @@ public class ServiceValidateFlowTest extends AbstractFlowTest {
         final IdPSession session = sessionManager.createSession(principal);
         final ServiceTicket ticket = ticketService.createServiceTicket(
                 "ST-1415133132-pnqph79ygxKyX9BPwPuw0hESQBjuA",
-                DateTime.now().plusSeconds(5).toInstant(),
+                Instant.now().plusSeconds(5),
                 "https://alt-username.example.org/",
                 new TicketState(session.getId(), principal, Instant.now(), "Password"),
                 false);
@@ -281,7 +280,7 @@ public class ServiceValidateFlowTest extends AbstractFlowTest {
         final IdPSession session = sessionManager.createSession(principal);
         final ServiceTicket ticket = ticketService.createServiceTicket(
                 "ST-2718281828-ompog68ygxKyX9BPwPuw0hESQBjuA",
-                DateTime.now().plusSeconds(5).toInstant(),
+                Instant.now().plusSeconds(5),
                 "https://no-attrs.example.org/",
                 new TicketState(session.getId(), principal, Instant.now(), "Password"),
                 false);
diff --git a/idp-conf/src/test/java/net/shibboleth/idp/test/flows/cas/ThrowingTicketService.java b/idp-conf/src/test/java/net/shibboleth/idp/test/flows/cas/ThrowingTicketService.java
index ded3c3c..ba3cc35 100644
--- a/idp-conf/src/test/java/net/shibboleth/idp/test/flows/cas/ThrowingTicketService.java
+++ b/idp-conf/src/test/java/net/shibboleth/idp/test/flows/cas/ThrowingTicketService.java
@@ -22,7 +22,8 @@ import net.shibboleth.idp.cas.ticket.ProxyTicket;
 import net.shibboleth.idp.cas.ticket.ServiceTicket;
 import net.shibboleth.idp.cas.ticket.TicketServiceEx;
 import net.shibboleth.idp.cas.ticket.TicketState;
-import org.joda.time.Instant;
+
+import java.time.Instant;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -34,55 +35,46 @@ import javax.annotation.Nullable;
  */
 public class ThrowingTicketService implements TicketServiceEx {
     @Nonnull
-    @Override
     public ServiceTicket createServiceTicket(@Nonnull String id, @Nonnull Instant expiry, @Nonnull String sessionId, @Nonnull String service, boolean renew) {
         throw new RuntimeException("createServiceTicket failed for ticket " + id);
     }
 
     @Nonnull
-    @Override
     public ServiceTicket createServiceTicket(@Nonnull String id, @Nonnull Instant expiry, @Nonnull String service, @Nonnull TicketState state, boolean renew) {
         throw new RuntimeException("createServiceTicket failed for ticket " + id);
     }
 
     @Nullable
-    @Override
     public ServiceTicket removeServiceTicket(@Nonnull String id) {
         throw new RuntimeException("removeServiceTicket failed for ticket " + id);
     }
 
     @Nonnull
-    @Override
     public ProxyGrantingTicket createProxyGrantingTicket(@Nonnull String id, @Nonnull Instant expiry, @Nonnull ServiceTicket serviceTicket) {
         throw new RuntimeException("createProxyGrantingTicket failed for ticket " + id);
     }
 
     @Nonnull
-    @Override
     public ProxyGrantingTicket createProxyGrantingTicket(@Nonnull String id, @Nonnull Instant expiry, @Nonnull ProxyTicket proxyTicket) {
         throw new RuntimeException("createProxyGrantingTicket failed for ticket " + id);
     }
 
     @Nullable
-    @Override
     public ProxyGrantingTicket fetchProxyGrantingTicket(@Nonnull String id) {
         throw new RuntimeException("fetchProxyGrantingTicket failed for ticket " + id);
     }
 
     @Nullable
-    @Override
     public ProxyGrantingTicket removeProxyGrantingTicket(@Nonnull String id) {
         throw new RuntimeException("removeProxyGrantingTicket failed for ticket " + id);
     }
 
     @Nonnull
-    @Override
     public ProxyTicket createProxyTicket(@Nonnull String id, @Nonnull Instant expiry, @Nonnull ProxyGrantingTicket pgt, @Nonnull String service) {
         throw new RuntimeException("createProxyTicket failed for ticket " + id);
     }
 
     @Nullable
-    @Override
     public ProxyTicket removeProxyTicket(@Nonnull String id) {
         throw new RuntimeException("removeProxyTicket failed for ticket " + id);
     }
diff --git a/idp-conf/src/test/java/net/shibboleth/idp/test/flows/cas/ValidateFlowTest.java b/idp-conf/src/test/java/net/shibboleth/idp/test/flows/cas/ValidateFlowTest.java
index 102bf27..95a981b 100644
--- a/idp-conf/src/test/java/net/shibboleth/idp/test/flows/cas/ValidateFlowTest.java
+++ b/idp-conf/src/test/java/net/shibboleth/idp/test/flows/cas/ValidateFlowTest.java
@@ -28,8 +28,6 @@ import net.shibboleth.idp.session.SessionResolver;
 import net.shibboleth.idp.session.criterion.SessionIdCriterion;
 import net.shibboleth.idp.test.flows.AbstractFlowTest;
 import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
-import org.joda.time.DateTime;
-import org.joda.time.Instant;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.test.context.ContextConfiguration;
@@ -39,6 +37,8 @@ import org.testng.annotations.Test;
 
 import static org.testng.Assert.*;
 
+import java.time.Instant;
+
 /**
  * Tests the flow behind the <code>/validate</code> endpoint.
  *
@@ -69,7 +69,7 @@ public class ValidateFlowTest extends AbstractFlowTest {
         final IdPSession session = sessionManager.createSession(principal);
         final ServiceTicket ticket = ticketService.createServiceTicket(
                 "ST-1415133132-ompog68ygxKyX9BPwPuw0hESQBjuA",
-                DateTime.now().plusSeconds(5).toInstant(),
+                Instant.now().plusSeconds(5),
                 "https://test.example.org/",
                 new TicketState(session.getId(), principal, Instant.now(), "Password"),
                 false);
diff --git a/idp-conf/src/test/java/net/shibboleth/idp/test/flows/saml1/SAML1AttributeQueryFlowTest.java b/idp-conf/src/test/java/net/shibboleth/idp/test/flows/saml1/SAML1AttributeQueryFlowTest.java
index f0a7838..a1b83a4 100644
--- a/idp-conf/src/test/java/net/shibboleth/idp/test/flows/saml1/SAML1AttributeQueryFlowTest.java
+++ b/idp-conf/src/test/java/net/shibboleth/idp/test/flows/saml1/SAML1AttributeQueryFlowTest.java
@@ -18,6 +18,7 @@
 package net.shibboleth.idp.test.flows.saml1;
 
 import java.security.cert.X509Certificate;
+import java.time.Instant;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -26,7 +27,6 @@ import net.shibboleth.idp.saml.xml.SAMLConstants;
 import net.shibboleth.utilities.java.support.security.SecureRandomIdentifierGenerationStrategy;
 import net.shibboleth.utilities.java.support.xml.SerializeSupport;
 
-import org.joda.time.DateTime;
 import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
 import org.opensaml.saml.common.SAMLObjectBuilder;
 import org.opensaml.saml.saml1.core.AttributeDesignator;
@@ -143,7 +143,7 @@ public class SAML1AttributeQueryFlowTest extends AbstractSAML1FlowTest {
         final Subject subject = SAML1ActionTestingSupport.buildSubject("jdoe");
 
         final Request attributeQuery = SAML1ActionTestingSupport.buildAttributeQueryRequest(subject);
-        attributeQuery.setIssueInstant(new DateTime());
+        attributeQuery.setIssueInstant(Instant.now());
         attributeQuery.getAttributeQuery().setResource(SP_ENTITY_ID);
         attributeQuery.setID(new SecureRandomIdentifierGenerationStrategy().generateIdentifier());
         
diff --git a/idp-conf/src/test/java/net/shibboleth/idp/test/flows/saml2/AbstractSAML2SSOFlowTest.java b/idp-conf/src/test/java/net/shibboleth/idp/test/flows/saml2/AbstractSAML2SSOFlowTest.java
index 886999e..000f1b2 100644
--- a/idp-conf/src/test/java/net/shibboleth/idp/test/flows/saml2/AbstractSAML2SSOFlowTest.java
+++ b/idp-conf/src/test/java/net/shibboleth/idp/test/flows/saml2/AbstractSAML2SSOFlowTest.java
@@ -18,6 +18,7 @@
 package net.shibboleth.idp.test.flows.saml2;
 
 import java.net.MalformedURLException;
+import java.time.Instant;
 
 import javax.annotation.Nonnull;
 import javax.servlet.http.HttpServletRequest;
@@ -26,7 +27,6 @@ import net.shibboleth.idp.test.flows.AbstractFlowTest;
 import net.shibboleth.utilities.java.support.net.SimpleURLCanonicalizer;
 import net.shibboleth.utilities.java.support.net.URLBuilder;
 
-import org.joda.time.DateTime;
 import org.opensaml.messaging.context.MessageContext;
 import org.opensaml.saml.common.SAMLObject;
 import org.opensaml.saml.common.messaging.context.SAMLEndpointContext;
@@ -69,7 +69,7 @@ public abstract class AbstractSAML2SSOFlowTest extends AbstractSAML2FlowTest {
             URLBuilder urlBuilder = new URLBuilder(baseUrl);
             urlBuilder.setPath(destinationPath);
             return urlBuilder.buildURL();
-        } catch (MalformedURLException e) {
+        } catch (final MalformedURLException e) {
             log.error("Couldn't parse base URL, reverting to internal default destination");
             return "http://localhost:8080" + destinationPath;
         }
@@ -83,7 +83,7 @@ public abstract class AbstractSAML2SSOFlowTest extends AbstractSAML2FlowTest {
             URLBuilder urlBuilder = new URLBuilder(baseUrl);
             urlBuilder.setPath(destinationPath);
             return urlBuilder.buildURL();
-        } catch (MalformedURLException e) {
+        } catch (final MalformedURLException e) {
             log.error("Couldn't parse base URL, reverting to internal default destination: {}", baseUrl);
             return "http://localhost:8080" + destinationPath;
         }
@@ -97,7 +97,7 @@ public abstract class AbstractSAML2SSOFlowTest extends AbstractSAML2FlowTest {
             URLBuilder urlBuilder = new URLBuilder(baseUrl);
             urlBuilder.setPath(destinationPath);
             return urlBuilder.buildURL();
-        } catch (MalformedURLException e) {
+        } catch (final MalformedURLException e) {
             log.error("Couldn't parse base URL, reverting to internal default destination: {}", baseUrl);
             return "http://localhost:8080" + destinationPath;
         }
@@ -109,7 +109,7 @@ public abstract class AbstractSAML2SSOFlowTest extends AbstractSAML2FlowTest {
                         AuthnRequest.DEFAULT_ELEMENT_NAME);
 
         authnRequest.setID(idGenerator.generateIdentifier());
-        authnRequest.setIssueInstant(new DateTime());
+        authnRequest.setIssueInstant(Instant.now());
         authnRequest.setAssertionConsumerServiceURL(getAcsUrl(servletRequest));
         authnRequest.setProtocolBinding(SAMLConstants.SAML2_POST_BINDING_URI);
 
diff --git a/idp-conf/src/test/java/net/shibboleth/idp/test/flows/saml2/SAML2AttributeQueryFlowTest.java b/idp-conf/src/test/java/net/shibboleth/idp/test/flows/saml2/SAML2AttributeQueryFlowTest.java
index 3214932..e16930d 100644
--- a/idp-conf/src/test/java/net/shibboleth/idp/test/flows/saml2/SAML2AttributeQueryFlowTest.java
+++ b/idp-conf/src/test/java/net/shibboleth/idp/test/flows/saml2/SAML2AttributeQueryFlowTest.java
@@ -18,6 +18,7 @@
 package net.shibboleth.idp.test.flows.saml2;
 
 import java.security.cert.X509Certificate;
+import java.time.Instant;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -25,7 +26,6 @@ import javax.annotation.Nullable;
 import net.shibboleth.utilities.java.support.security.SecureRandomIdentifierGenerationStrategy;
 import net.shibboleth.utilities.java.support.xml.SerializeSupport;
 
-import org.joda.time.DateTime;
 import org.opensaml.core.xml.XMLObjectBuilder;
 import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
 import org.opensaml.core.xml.schema.XSAny;
@@ -149,7 +149,7 @@ public class SAML2AttributeQueryFlowTest extends AbstractSAML2FlowTest {
         final Subject subject = SAML2ActionTestingSupport.buildSubject("jdoe");
 
         final AttributeQuery attributeQuery = SAML2ActionTestingSupport.buildAttributeQueryRequest(subject);
-        attributeQuery.setIssueInstant(new DateTime());
+        attributeQuery.setIssueInstant(Instant.now());
         attributeQuery.getIssuer().setValue(SP_ENTITY_ID);
         attributeQuery.setID(new SecureRandomIdentifierGenerationStrategy().generateIdentifier());
 
diff --git a/idp-consent-impl/src/main/java/net/shibboleth/idp/consent/flow/impl/ConsentFlowDescriptor.java b/idp-consent-impl/src/main/java/net/shibboleth/idp/consent/flow/impl/ConsentFlowDescriptor.java
index 33b6859..6a83180 100644
--- a/idp-consent-impl/src/main/java/net/shibboleth/idp/consent/flow/impl/ConsentFlowDescriptor.java
+++ b/idp-consent-impl/src/main/java/net/shibboleth/idp/consent/flow/impl/ConsentFlowDescriptor.java
@@ -17,14 +17,13 @@
 
 package net.shibboleth.idp.consent.flow.impl;
 
+import java.time.Duration;
+
 import javax.annotation.Nonnull;
 
 import net.shibboleth.idp.profile.interceptor.ProfileInterceptorFlowDescriptor;
-import net.shibboleth.utilities.java.support.annotation.Duration;
-import net.shibboleth.utilities.java.support.annotation.constraint.NonNegative;
 import net.shibboleth.utilities.java.support.component.ComponentSupport;
 import net.shibboleth.utilities.java.support.logic.Constraint;
-import net.shibboleth.utilities.java.support.xml.DOMTypeSupport;
 
 /**
  * Descriptor for a consent flow.
@@ -37,8 +36,8 @@ public class ConsentFlowDescriptor extends ProfileInterceptorFlowDescriptor {
     /** Whether consent equality includes comparing consent values. */
     private boolean compareValues;
 
-    /** Time in milliseconds to expire consent storage records. Default value: 1 year. */
-    @Nonnull @Duration @NonNegative private Long lifetime;
+    /** Time to expire consent storage records. Default value: 1 year. */
+    @Nonnull private Duration lifetime;
 
     /** Maximum number of records stored in the storage service. */
     private int maxStoredRecords;
@@ -51,7 +50,7 @@ public class ConsentFlowDescriptor extends ProfileInterceptorFlowDescriptor {
 
     /** Constructor. */
     public ConsentFlowDescriptor() {
-        lifetime = DOMTypeSupport.durationToLong("P1Y");
+        lifetime = Duration.ofDays(365);
         expandedStorageThreshold = 1024 * 1024;
     }
 
@@ -65,11 +64,11 @@ public class ConsentFlowDescriptor extends ProfileInterceptorFlowDescriptor {
     }
 
     /**
-     * Time in milliseconds to expire consent storage records.
+     * Time to expire consent storage records.
      * 
-     * @return time in milliseconds to expire consent storage records
+     * @return time to expire consent storage records
      */
-    @Nonnull @NonNegative @Duration public Long getLifetime() {
+    @Nonnull public Duration getLifetime() {
         return lifetime;
     }
 
@@ -112,15 +111,14 @@ public class ConsentFlowDescriptor extends ProfileInterceptorFlowDescriptor {
     }
 
     /**
-     * Set time in milliseconds to expire consent storage records.
+     * Set time to expire consent storage records.
      * 
-     * @param consentLifetime time in milliseconds to expire consent storage records
+     * @param consentLifetime time to expire consent storage records
      */
-    @Duration public void setLifetime(@Nonnull @Duration @NonNegative final Long consentLifetime) {
+    public void setLifetime(@Nonnull final Duration consentLifetime) {
         ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
-        Constraint.isNotNull(consentLifetime, "Lifetime cannot be null");
 
-        lifetime = Constraint.isGreaterThanOrEqual(0, consentLifetime, "Lifetime must be greater than or equal to 0");
+        lifetime = Constraint.isNotNull(consentLifetime, "Lifetime cannot be null");
     }
 
     /**
diff --git a/idp-consent-impl/src/main/java/net/shibboleth/idp/consent/flow/storage/impl/CreateGlobalConsentResult.java b/idp-consent-impl/src/main/java/net/shibboleth/idp/consent/flow/storage/impl/CreateGlobalConsentResult.java
index 2193767..b794195 100644
--- a/idp-consent-impl/src/main/java/net/shibboleth/idp/consent/flow/storage/impl/CreateGlobalConsentResult.java
+++ b/idp-consent-impl/src/main/java/net/shibboleth/idp/consent/flow/storage/impl/CreateGlobalConsentResult.java
@@ -18,6 +18,7 @@
 package net.shibboleth.idp.consent.flow.storage.impl;
 
 import java.io.IOException;
+import java.time.Instant;
 import java.util.Collections;
 
 import javax.annotation.Nonnull;
@@ -27,7 +28,6 @@ import net.shibboleth.idp.consent.storage.impl.ConsentResult;
 import net.shibboleth.idp.profile.context.ProfileInterceptorContext;
 import net.shibboleth.idp.profile.interceptor.ProfileInterceptorResult;
 
-import org.joda.time.DateTime;
 import org.opensaml.profile.action.ActionSupport;
 import org.opensaml.profile.action.EventIds;
 import org.opensaml.profile.context.ProfileRequestContext;
@@ -58,7 +58,7 @@ public class CreateGlobalConsentResult extends AbstractConsentIndexedStorageActi
             final String value =
                     getStorageSerializer().serialize(Collections.singletonMap(globalConsent.getId(), globalConsent));
 
-            final long expiration = DateTime.now().plus(getConsentFlowDescriptor().getLifetime()).getMillis();
+            final long expiration = Instant.now().plus(getConsentFlowDescriptor().getLifetime()).toEpochMilli();
 
             final ProfileInterceptorResult result =
                     new ConsentResult(getStorageContext(), getStorageKey(), value, expiration);
diff --git a/idp-consent-impl/src/main/java/net/shibboleth/idp/consent/flow/storage/impl/CreateResult.java b/idp-consent-impl/src/main/java/net/shibboleth/idp/consent/flow/storage/impl/CreateResult.java
index c0652a1..de58a29 100644
--- a/idp-consent-impl/src/main/java/net/shibboleth/idp/consent/flow/storage/impl/CreateResult.java
+++ b/idp-consent-impl/src/main/java/net/shibboleth/idp/consent/flow/storage/impl/CreateResult.java
@@ -18,6 +18,7 @@
 package net.shibboleth.idp.consent.flow.storage.impl;
 
 import java.io.IOException;
+import java.time.Instant;
 import java.util.Map;
 
 import javax.annotation.Nonnull;
@@ -27,7 +28,6 @@ import net.shibboleth.idp.consent.storage.impl.ConsentResult;
 import net.shibboleth.idp.profile.context.ProfileInterceptorContext;
 import net.shibboleth.idp.profile.interceptor.ProfileInterceptorResult;
 
-import org.joda.time.DateTime;
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -71,7 +71,7 @@ public class CreateResult extends AbstractConsentIndexedStorageAction {
             final Map<String, Consent> currentConsents = getConsentContext().getCurrentConsents();
             final String value = getStorageSerializer().serialize(currentConsents);
 
-            final long expiration = DateTime.now().plus(getConsentFlowDescriptor().getLifetime()).getMillis();
+            final long expiration = Instant.now().plus(getConsentFlowDescriptor().getLifetime()).toEpochMilli();
 
             final ProfileInterceptorResult result =
                     new ConsentResult(getStorageContext(), getStorageKey(), value, expiration);
diff --git a/idp-consent-impl/src/test/java/net/shibboleth/idp/consent/flow/impl/ConsentFlowDescriptorTest.java b/idp-consent-impl/src/test/java/net/shibboleth/idp/consent/flow/impl/ConsentFlowDescriptorTest.java
index 245c504..deec486 100644
--- a/idp-consent-impl/src/test/java/net/shibboleth/idp/consent/flow/impl/ConsentFlowDescriptorTest.java
+++ b/idp-consent-impl/src/test/java/net/shibboleth/idp/consent/flow/impl/ConsentFlowDescriptorTest.java
@@ -18,7 +18,8 @@
 package net.shibboleth.idp.consent.flow.impl;
 
 import net.shibboleth.utilities.java.support.logic.ConstraintViolationException;
-import net.shibboleth.utilities.java.support.xml.DOMTypeSupport;
+
+import java.time.Duration;
 
 import org.testng.Assert;
 import org.testng.annotations.BeforeMethod;
@@ -37,7 +38,7 @@ public class ConsentFlowDescriptorTest {
     @Test public void testInstantation() {
         Assert.assertEquals(descriptor.getId(), "test");
         Assert.assertFalse(descriptor.compareValues());
-        Assert.assertEquals(Long.valueOf(DOMTypeSupport.durationToLong("P1Y")), descriptor.getLifetime());
+        Assert.assertEquals(Duration.ofDays(365), descriptor.getLifetime());
     }
 
     @Test public void testCompareValues() {
@@ -51,16 +52,12 @@ public class ConsentFlowDescriptorTest {
         descriptor.setLifetime(null);
     }
 
-    @Test(expectedExceptions = ConstraintViolationException.class) public void testNegativeLifetime() {
-        descriptor.setLifetime(-10L);
-    }
-
     @Test public void testLifetime() {
-        Long lifetime = Long.valueOf(1000);
+        Duration lifetime = Duration.ofSeconds(1);
         descriptor.setLifetime(lifetime);
         Assert.assertEquals(descriptor.getLifetime(), lifetime);
 
-        lifetime = 0L;
+        lifetime = Duration.ZERO;
         descriptor.setLifetime(lifetime);
         Assert.assertEquals(descriptor.getLifetime(), lifetime);
     }
diff --git a/idp-consent-impl/src/test/java/net/shibboleth/idp/consent/storage/impl/ConsentSerializerTest.java b/idp-consent-impl/src/test/java/net/shibboleth/idp/consent/storage/impl/ConsentSerializerTest.java
index 0912513..21a530f 100644
--- a/idp-consent-impl/src/test/java/net/shibboleth/idp/consent/storage/impl/ConsentSerializerTest.java
+++ b/idp-consent-impl/src/test/java/net/shibboleth/idp/consent/storage/impl/ConsentSerializerTest.java
@@ -33,7 +33,6 @@ import net.shibboleth.idp.consent.logic.impl.AttributeValuesHashFunction;
 import net.shibboleth.utilities.java.support.component.UnmodifiableComponentException;
 import net.shibboleth.utilities.java.support.logic.ConstraintViolationException;
 
-import org.joda.time.DateTime;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.testng.Assert;
@@ -66,8 +65,6 @@ public class ConsentSerializerTest {
 
     protected Consent consent2;
 
-    protected DateTime expiration;
-
     protected Map<String, Consent> consents;
 
     protected Function<Collection<IdPAttributeValue<?>>, String> attributeValuesHashFunction;
diff --git a/idp-core/pom.xml b/idp-core/pom.xml
index 6175e92..57a8e0c 100644
--- a/idp-core/pom.xml
+++ b/idp-core/pom.xml
@@ -23,10 +23,6 @@
     <dependencies>
         <!-- Compile Dependencies -->
         <dependency>
-            <groupId>joda-time</groupId>
-            <artifactId>joda-time</artifactId>
-        </dependency>
-        <dependency>
             <groupId>com.beust</groupId>
             <artifactId>jcommander</artifactId>
         </dependency>
@@ -62,7 +58,7 @@
         </dependency>
         <dependency>
             <groupId>com.fasterxml.jackson.datatype</groupId>
-            <artifactId>jackson-datatype-joda</artifactId>
+            <artifactId>jackson-datatype-jsr310</artifactId>
         </dependency>    
         <dependency>
             <groupId>io.dropwizard.metrics</groupId>
diff --git a/idp-core/src/main/java/net/shibboleth/idp/log/LogbackLoggingService.java b/idp-core/src/main/java/net/shibboleth/idp/log/LogbackLoggingService.java
index be1372d..4605b64 100644
--- a/idp-core/src/main/java/net/shibboleth/idp/log/LogbackLoggingService.java
+++ b/idp-core/src/main/java/net/shibboleth/idp/log/LogbackLoggingService.java
@@ -19,6 +19,7 @@ package net.shibboleth.idp.log;
 
 import java.io.IOException;
 import java.io.InputStream;
+import java.time.Instant;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -33,7 +34,6 @@ import net.shibboleth.utilities.java.support.service.AbstractReloadableService;
 import net.shibboleth.utilities.java.support.service.ServiceException;
 import net.shibboleth.utilities.java.support.service.ServiceableComponent;
 
-import org.joda.time.DateTime;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.context.ApplicationContext;
@@ -126,11 +126,11 @@ public class LogbackLoggingService extends AbstractReloadableService<Object>
     /** {@inheritDoc} */
     @Override protected synchronized boolean shouldReload() {
         try {
-            final DateTime lastReload = getLastSuccessfulReloadInstant();
+            final Instant lastReload = getLastSuccessfulReloadInstant();
             if (null == lastReload) {
                 return true;
             }
-            return configurationResource.lastModified() > lastReload.getMillis();
+            return configurationResource.lastModified() > lastReload.toEpochMilli();
         } catch (final IOException e) {
             statusManager.add(new ErrorStatus(
                     "Error checking last modified time of logging service configuration resource "
diff --git a/idp-core/src/main/java/net/shibboleth/idp/metrics/ReloadableServiceGaugeSet.java b/idp-core/src/main/java/net/shibboleth/idp/metrics/ReloadableServiceGaugeSet.java
index b4c1fb5..1abc167 100644
--- a/idp-core/src/main/java/net/shibboleth/idp/metrics/ReloadableServiceGaugeSet.java
+++ b/idp-core/src/main/java/net/shibboleth/idp/metrics/ReloadableServiceGaugeSet.java
@@ -33,14 +33,13 @@ import net.shibboleth.utilities.java.support.component.ComponentSupport;
 import net.shibboleth.utilities.java.support.logic.Constraint;
 import net.shibboleth.utilities.java.support.service.ReloadableService;
 
+import java.time.Instant;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 
 import javax.annotation.Nonnull;
 
-import org.joda.time.DateTime;
-
 /**
  * A set of gauges for a reloadable service.
  */
@@ -67,16 +66,16 @@ public class ReloadableServiceGaugeSet extends AbstractInitializableComponent im
 
         gauges.put(
                 MetricRegistry.name(DEFAULT_METRIC_NAME, metricName, "reload", "success"),
-                new Gauge<DateTime>() {
-                    public DateTime getValue() {
+                new Gauge<Instant>() {
+                    public Instant getValue() {
                         return service.getLastSuccessfulReloadInstant();
                     }
                 });
         
         gauges.put(
                 MetricRegistry.name(DEFAULT_METRIC_NAME, metricName, "reload", "attempt"),
-                new Gauge<DateTime>() {
-                    public DateTime getValue() {
+                new Gauge<Instant>() {
+                    public Instant getValue() {
                         return service.getLastReloadAttemptInstant();
                     }
                 });
diff --git a/idp-core/src/main/java/net/shibboleth/idp/metrics/impl/HTTPReporter.java b/idp-core/src/main/java/net/shibboleth/idp/metrics/impl/HTTPReporter.java
index 15e0396..a8dbab1 100644
--- a/idp-core/src/main/java/net/shibboleth/idp/metrics/impl/HTTPReporter.java
+++ b/idp-core/src/main/java/net/shibboleth/idp/metrics/impl/HTTPReporter.java
@@ -35,7 +35,6 @@ import org.apache.http.client.methods.HttpPost;
 import org.apache.http.client.methods.HttpUriRequest;
 import org.apache.http.client.protocol.HttpClientContext;
 import org.apache.http.entity.ContentType;
-import org.joda.time.DateTime;
 import org.opensaml.security.httpclient.HttpClientSecurityParameters;
 import org.opensaml.security.httpclient.HttpClientSecuritySupport;
 import org.slf4j.Logger;
@@ -52,7 +51,8 @@ import com.codahale.metrics.Timer;
 import com.codahale.metrics.json.MetricsModule;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.ObjectWriter;
-import com.fasterxml.jackson.datatype.joda.JodaModule;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
 
 import net.shibboleth.utilities.java.support.annotation.ParameterName;
 import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
@@ -225,8 +225,9 @@ public class HTTPReporter extends ScheduledReporter implements InitializableComp
 
             jsonMapper = new ObjectMapper().registerModule(
                     new MetricsModule(rateUnit, durationUnit, true, metricFilter));
-            jsonMapper.registerModule(new JodaModule());
+            jsonMapper.registerModule(new JavaTimeModule());
             jsonMapper.setDateFormat(new SimpleDateFormat(dateTimeFormat != null ? dateTimeFormat : DEFAULT_DT_FORMAT));
+            jsonMapper.configure(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS, false);
             
             isInitialized = true;
         }
diff --git a/idp-core/src/main/java/net/shibboleth/idp/metrics/impl/IdPGaugeSet.java b/idp-core/src/main/java/net/shibboleth/idp/metrics/impl/IdPGaugeSet.java
index 2bb5661..22d23f7 100644
--- a/idp-core/src/main/java/net/shibboleth/idp/metrics/impl/IdPGaugeSet.java
+++ b/idp-core/src/main/java/net/shibboleth/idp/metrics/impl/IdPGaugeSet.java
@@ -26,13 +26,13 @@ import net.shibboleth.idp.Version;
 import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
 import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
 
+import java.time.Instant;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 
 import javax.annotation.Nonnull;
 
-import org.joda.time.DateTime;
 import org.springframework.context.support.ApplicationObjectSupport;
 
 /**
@@ -68,9 +68,9 @@ public class IdPGaugeSet extends ApplicationObjectSupport implements MetricSet,
 
         gauges.put(
                 MetricRegistry.name(DEFAULT_METRIC_NAME, "starttime"),
-                new Gauge<DateTime>() {
-                    public DateTime getValue() {
-                        return new DateTime(getApplicationContext().getStartupDate());
+                new Gauge<Instant>() {
+                    public Instant getValue() {
+                        return Instant.ofEpochMilli(getApplicationContext().getStartupDate());
                     }
                 });
         
diff --git a/idp-installer/src/test/java/net/shibboleth/idp/installer/TestMetadataGenerator.java b/idp-installer/src/test/java/net/shibboleth/idp/installer/TestMetadataGenerator.java
index 7a84a60..077e71d 100644
--- a/idp-installer/src/test/java/net/shibboleth/idp/installer/TestMetadataGenerator.java
+++ b/idp-installer/src/test/java/net/shibboleth/idp/installer/TestMetadataGenerator.java
@@ -23,12 +23,12 @@ import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
+import java.time.Instant;
 import java.util.List;
 
 import net.shibboleth.idp.installer.ant.impl.MetadataGeneratorTask;
 import net.shibboleth.utilities.java.support.xml.XMLParserException;
 
-import org.joda.time.DateTime;
 import org.opensaml.core.xml.XMLObject;
 import org.opensaml.core.xml.XMLObjectBaseTestCase;
 import org.opensaml.saml.common.xml.SAMLConstants;
@@ -64,9 +64,9 @@ public class TestMetadataGenerator extends XMLObjectBaseTestCase {
 
             final EntityDescriptor entity = (EntityDescriptor) unmarshallElement(out.getAbsolutePath());
             
-            final DateTime validUntil = entity.getValidUntil();
-            Assert.assertTrue(validUntil.isBefore(1000 + System.currentTimeMillis()));
-            Assert.assertTrue(validUntil.isAfter(System.currentTimeMillis()-1000));
+            final Instant validUntil = entity.getValidUntil();
+            Assert.assertTrue(validUntil.isBefore(Instant.now().plusSeconds(1)));
+            Assert.assertTrue(validUntil.isAfter(Instant.now().minusSeconds(1)));
             
             
             final IDPSSODescriptor idpsso = entity.getIDPSSODescriptor(SAMLConstants.SAML20P_NS);
diff --git a/idp-profile-api/pom.xml b/idp-profile-api/pom.xml
index 01893af..14be53b 100644
--- a/idp-profile-api/pom.xml
+++ b/idp-profile-api/pom.xml
@@ -56,6 +56,12 @@
             <artifactId>spring-webflow</artifactId>
         </dependency>
 
+        <!-- DEPRECATED - remove in V5 -->
+        <dependency>
+            <groupId>joda-time</groupId>
+            <artifactId>joda-time</artifactId>
+        </dependency>
+
         <!-- Provided Dependencies -->
 
         <!-- Runtime Dependencies -->
diff --git a/idp-profile-api/src/main/java/net/shibboleth/idp/profile/logic/DateAttributePredicate.java b/idp-profile-api/src/main/java/net/shibboleth/idp/profile/logic/DateAttributePredicate.java
index 12de7f9..0d43a80 100644
--- a/idp-profile-api/src/main/java/net/shibboleth/idp/profile/logic/DateAttributePredicate.java
+++ b/idp-profile-api/src/main/java/net/shibboleth/idp/profile/logic/DateAttributePredicate.java
@@ -25,8 +25,9 @@ import net.shibboleth.utilities.java.support.annotation.ParameterName;
 import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
 import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
 import net.shibboleth.utilities.java.support.logic.Constraint;
-import org.joda.time.format.DateTimeFormat;
-import org.joda.time.format.DateTimeFormatter;
+import net.shibboleth.utilities.java.support.primitive.DeprecationSupport;
+import net.shibboleth.utilities.java.support.primitive.DeprecationSupport.ObjectType;
+
 import org.joda.time.format.ISODateTimeFormat;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -34,6 +35,8 @@ import org.slf4j.LoggerFactory;
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
+import java.time.Instant;
+import java.time.format.DateTimeFormatter;
 import java.util.Map;
 
 /**
@@ -53,10 +56,13 @@ public class DateAttributePredicate extends AbstractAttributePredicate {
     @Nonnull @NotEmpty private final String attributeName;
 
     /** Formatter used to parse string-based date attribute values. */
-    @Nonnull private final DateTimeFormatter dateTimeFormatter;
+    @Nullable private DateTimeFormatter dateTimeFormatter;
+    
+    /** Legacy formatter used to parse string-based date attribute values. */
+    @Deprecated @Nullable private org.joda.time.format.DateTimeFormatter legacyFormatter;
 
     /** Offset from system time used for date comparisons. */
-    @Nullable private org.joda.time.Duration systemTimeOffset;
+    @Nonnull private java.time.Duration systemTimeOffset;
     
     /** Result of predicate if attribute is missing or has no values. */
     private boolean resultIfMissing;
@@ -64,26 +70,52 @@ public class DateAttributePredicate extends AbstractAttributePredicate {
     /**
      * Create a new instance that performs date comparisons against the given attribute
      * using ISO date/time format parser by default.
+     * 
+     * <p>This is deprecated in favor of the Java 8 API version.</p>
      *
      * @param attribute Attribute name that provides candidate date values to test.
      */
+    @Deprecated
     public DateAttributePredicate(@Nonnull @NotEmpty @ParameterName(name="attribute") final String attribute) {
+        // This isn't easily reproducible with Java 8's API, so I'm just going to
+        // deprecate the "no formatter supplied" scenario.
         this(attribute, ISODateTimeFormat.dateOptionalTimeParser());
     }
 
     /**
      * Create a new instance that performs date comparisons against the given attribute
      * using the given date parser.
+     * 
+     * <p>This is deprecated in favor of the Java 8 API version.</p>
      *
      * @param attribute Attribute name that provides candidate date values to test.
      * @param formatter Date/time parser.
      */
     @Deprecated
     public DateAttributePredicate(@Nonnull @NotEmpty @ParameterName(name="attribute") final String attribute,
+            @Nonnull @ParameterName(name="formatter") final org.joda.time.format.DateTimeFormatter formatter) {
+        // This is a V4 deprecation, don't remove until V5.
+        DeprecationSupport.warnOnce(ObjectType.METHOD, "Joda-Time-based constructor",
+                DateAttributePredicate.class.getName(), "(see Javadoc)");
+        
+        attributeName = Constraint.isNotNull(attribute, "Attribute cannot be null");
+        legacyFormatter = Constraint.isNotNull(formatter, "Formatter cannot be null");
+        systemTimeOffset = java.time.Duration.ZERO;
+    }
+
+    /**
+     * Create a new instance that performs date comparisons against the given attribute
+     * using the given date parser.
+     *
+     * @param attribute Attribute name that provides candidate date values to test.
+     * @param formatter Date/time parser.
+     */
+    public DateAttributePredicate(@Nonnull @NotEmpty @ParameterName(name="attribute") final String attribute,
             @Nonnull @ParameterName(name="formatter") final DateTimeFormatter formatter) {
+        
         attributeName = Constraint.isNotNull(attribute, "Attribute cannot be null");
         dateTimeFormatter = Constraint.isNotNull(formatter, "Formatter cannot be null");
-        resultIfMissing = false;
+        systemTimeOffset = java.time.Duration.ZERO;
     }
 
     /**
@@ -96,9 +128,9 @@ public class DateAttributePredicate extends AbstractAttributePredicate {
     public DateAttributePredicate(@Nonnull @NotEmpty @ParameterName(name="attribute") final String attribute,
             @Nonnull @NotEmpty @ParameterName(name="formatString") final String formatString) {
         attributeName = Constraint.isNotNull(attribute, "Attribute cannot be null");
-        dateTimeFormatter = DateTimeFormat.forPattern(
+        dateTimeFormatter = DateTimeFormatter.ofPattern(
                 Constraint.isNotNull(formatString, "Format string cannot be null"));
-        resultIfMissing = false;
+        systemTimeOffset = java.time.Duration.ZERO;
     }
     
     /**
@@ -110,18 +142,35 @@ public class DateAttributePredicate extends AbstractAttributePredicate {
      */
     @Deprecated
     public void setSystemTimeOffset(@Nonnull final org.joda.time.Duration offset) {
-        systemTimeOffset = Constraint.isNotNull(offset, "Offset cannot be null");
+        // This is a V4 deprecation, don't remove until V5.
+        DeprecationSupport.warnOnce(ObjectType.METHOD, "Joda-Time-based setSystemTimeOffset",
+                DateAttributePredicate.class.getName(), "setOffset");
+        systemTimeOffset = java.time.Duration.ofMillis(
+                Constraint.isNotNull(offset, "Offset cannot be null").getMillis());
     }
 
     /**
-     * Set the system time offset, which affects the reference date for comparisons.
-     * By default all comparisons are against system time, i.e. zero offset.
+     * Set the system time offset in milliseconds, which affects the reference date for comparisons.
+     * 
+     * <p>By default all comparisons are against system time, i.e. zero offset.</p>
      *
      * @param offset System time offset. A negative value decreases the target date (sooner);
      *                         a positive value increases the target date (later).
      */
     @Duration public void setOffset(@Duration final long offset) {
-        systemTimeOffset = org.joda.time.Duration.millis(offset);
+        systemTimeOffset = java.time.Duration.ofMillis(offset);
+    }
+
+    /**
+     * Set the system time offset, which affects the reference date for comparisons.
+     * 
+     * <p>By default all comparisons are against system time, i.e. zero offset.</p>
+     *
+     * @param offset System time offset. A negative value decreases the target date (sooner);
+     *                         a positive value increases the target date (later).
+     */
+    public void setOffset(@Nonnull final java.time.Duration offset) {
+        systemTimeOffset = Constraint.isNotNull(offset, "Offset cannot be null");
     }
     
     /**
@@ -141,7 +190,7 @@ public class DateAttributePredicate extends AbstractAttributePredicate {
 
     /** {@inheritDoc} */
     @Override
-    protected boolean hasMatch(@Nonnull @NonnullElements final Map<String, IdPAttribute> attributeMap) {
+    protected boolean hasMatch(@Nonnull @NonnullElements final Map<String,IdPAttribute> attributeMap) {
         
         final IdPAttribute attribute = attributeMap.get(attributeName);
         if (attribute == null) {
@@ -152,13 +201,21 @@ public class DateAttributePredicate extends AbstractAttributePredicate {
             return resultIfMissing;
         }
         
+        final Instant now = Instant.now();
+        
         String dateString;
         for (final IdPAttributeValue<?> value : attribute.getValues()) {
             if (value instanceof StringAttributeValue) {
                 dateString = ((StringAttributeValue) value).getValue();
                 try {
-                    if (dateTimeFormatter.parseDateTime(dateString).plus(systemTimeOffset).isAfterNow()) {
-                        return true;
+                    if (dateTimeFormatter != null) {
+                        if (Instant.from(dateTimeFormatter.parse(dateString)).plus(systemTimeOffset).isAfter(now)) {
+                            return true;
+                        }
+                    } else {
+                        if (legacyFormatter.parseDateTime(dateString).plus(systemTimeOffset.toMillis()).isAfterNow()) {
+                            return true;
+                        }
                     }
                 } catch (final RuntimeException e) {
                     log.warn("{} is not a valid date for the configured date parser", dateString, e);
diff --git a/idp-profile-api/src/test/java/net/shibboleth/idp/profile/logic/DateAttributePredicateTest.java b/idp-profile-api/src/test/java/net/shibboleth/idp/profile/logic/DateAttributePredicateTest.java
index 8d56fbe..a67f364 100644
--- a/idp-profile-api/src/test/java/net/shibboleth/idp/profile/logic/DateAttributePredicateTest.java
+++ b/idp-profile-api/src/test/java/net/shibboleth/idp/profile/logic/DateAttributePredicateTest.java
@@ -30,6 +30,7 @@ import org.opensaml.profile.context.ProfileRequestContext;
 import org.testng.annotations.DataProvider;
 import org.testng.annotations.Test;
 
+import java.time.ZonedDateTime;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
@@ -38,58 +39,111 @@ import static org.testng.Assert.*;
 
 /**
  * Unit test for {@link DateAttributePredicate}.
- *
- * @author Marvin S. Addison
  */
 public class DateAttributePredicateTest {
 
     private final DateTimeFormatter formatter = ISODateTimeFormat.dateTime();
+    
+    private final java.time.format.DateTimeFormatter javaformatter =
+            java.time.format.DateTimeFormatter.ISO_ZONED_DATE_TIME;
 
-    @DataProvider(name = "test-data")
-    public Object[][] provideTestData() {
+    @SuppressWarnings("deprecation")
+    @DataProvider(name = "test-data-joda")
+    public Object[][] provideTestDataJoda() {
         return new Object[][] {
                 // Future date matches
                 new Object[] {
                         new DateAttributePredicate("expirationDate"),
                         "expirationDate",
-                        dateStrings(Duration.standardDays(1)),
+                        jodaDateStrings(Duration.standardDays(1)),
                         true,
                 },
                 // Current date does not match
                 new Object[] {
                         new DateAttributePredicate("expirationDate"),
                         "expirationDate",
-                        dateStrings(Duration.ZERO),
+                        jodaDateStrings(Duration.ZERO),
                         false,
                 },
                 // Past date does not match
                 new Object[] {
                         new DateAttributePredicate("expirationDate"),
                         "expirationDate",
-                        dateStrings(Duration.standardDays(-1)),
+                        jodaDateStrings(Duration.standardDays(-1)),
                         false,
                 },
                 // Increase target date by 90 days
                 new Object[] {
-                        newPredicate("expirationDate", Duration.standardDays(90)),
+                        newJodaPredicate("expirationDate", Duration.standardDays(90)),
                         "expirationDate",
-                        dateStrings(Duration.standardDays(91)),
+                        jodaDateStrings(Duration.standardDays(91)),
                         true,
                 },
                 // Decrease target date by 30 days
                 // e.g. expiration warning case
                 new Object[] {
-                        newPredicate("expirationDate", Duration.standardDays(-30)),
+                        newJodaPredicate("expirationDate", Duration.standardDays(-30)),
                         "expirationDate",
-                        dateStrings(Duration.standardDays(29)),
+                        jodaDateStrings(Duration.standardDays(29)),
+                        false,
+                },
+        };
+    }
+
+    @DataProvider(name = "test-data-java")
+    public Object[][] provideTestDataJava() {
+        return new Object[][] {
+                // Future date matches
+                new Object[] {
+                        new DateAttributePredicate("expirationDate", javaformatter),
+                        "expirationDate",
+                        javaDateStrings(java.time.Duration.ofDays(1)),
+                        true,
+                },
+                // Current date does not match
+                new Object[] {
+                        new DateAttributePredicate("expirationDate", javaformatter),
+                        "expirationDate",
+                        javaDateStrings(java.time.Duration.ZERO),
+                        false,
+                },
+                // Past date does not match
+                new Object[] {
+                        new DateAttributePredicate("expirationDate", javaformatter),
+                        "expirationDate",
+                        javaDateStrings(java.time.Duration.ofDays(-1)),
+                        false,
+                },
+                // Increase target date by 90 days
+                new Object[] {
+                        newJavaPredicate("expirationDate", java.time.Duration.ofDays(90)),
+                        "expirationDate",
+                        javaDateStrings(java.time.Duration.ofDays(91)),
+                        true,
+                },
+                // Decrease target date by 30 days
+                // e.g. expiration warning case
+                new Object[] {
+                        newJavaPredicate("expirationDate", java.time.Duration.ofDays(-30)),
+                        "expirationDate",
+                        javaDateStrings(java.time.Duration.ofDays(29)),
                         false,
                 },
         };
     }
 
 
-    @Test(dataProvider = "test-data")
-    public void testApply(
+    @Test(dataProvider = "test-data-joda")
+    public void testJodaTime(
+            final DateAttributePredicate predicate,
+            final String attribute,
+            final String[] values,
+            final boolean expected) throws Exception {
+        assertEquals(predicate.test(createProfileRequestContext(attribute, values)), expected);
+    }
+
+    @Test(dataProvider = "test-data-java")
+    public void testJavaTime(
             final DateAttributePredicate predicate,
             final String attribute,
             final String[] values,
@@ -121,7 +175,7 @@ public class DateAttributePredicateTest {
      *
      * @return Array of date strings, one for each provided offset.
      */
-    private String[] dateStrings(final Duration ... offsets) {
+    private String[] jodaDateStrings(final Duration ... offsets) {
         final String[] dates = new String[offsets.length];
         for (int i = 0; i < offsets.length; i++) {
             dates[i] = formatter.print(DateTime.now().plus(offsets[i]));
@@ -129,9 +183,32 @@ public class DateAttributePredicateTest {
         return dates;
     }
 
-    private DateAttributePredicate newPredicate(final String attribute, final Duration offset) {
+    /**
+     * Produces an array of date strings that are offsets from current system time.
+     *
+     * @param offsets One or more durations that are added to the current system time.
+     *
+     * @return Array of date strings, one for each provided offset.
+     */
+    private String[] javaDateStrings(final java.time.Duration ... offsets) {
+        final String[] dates = new String[offsets.length];
+        for (int i = 0; i < offsets.length; i++) {
+            dates[i] = javaformatter.format(ZonedDateTime.now().plus(offsets[i]));
+        }
+        return dates;
+    }
+    
+    @SuppressWarnings("deprecation")
+    private DateAttributePredicate newJodaPredicate(final String attribute, final Duration offset) {
         final DateAttributePredicate p = new DateAttributePredicate(attribute);
         p.setSystemTimeOffset(offset);
         return p;
     }
+
+    private DateAttributePredicate newJavaPredicate(final String attribute, final java.time.Duration offset) {
+        final DateAttributePredicate p = new DateAttributePredicate(attribute, javaformatter);
+        p.setOffset(offset);
+        return p;
+    }
+    
 }
\ No newline at end of file
diff --git a/idp-profile-impl/src/main/java/net/shibboleth/idp/profile/audit/impl/PopulateAuditContext.java b/idp-profile-impl/src/main/java/net/shibboleth/idp/profile/audit/impl/PopulateAuditContext.java
index e672433..272fa3a 100644
--- a/idp-profile-impl/src/main/java/net/shibboleth/idp/profile/audit/impl/PopulateAuditContext.java
+++ b/idp-profile-impl/src/main/java/net/shibboleth/idp/profile/audit/impl/PopulateAuditContext.java
@@ -17,6 +17,9 @@
 
 package net.shibboleth.idp.profile.audit.impl;
 
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.TemporalAccessor;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
@@ -32,12 +35,11 @@ import net.shibboleth.idp.profile.AbstractProfileAction;
 import net.shibboleth.idp.profile.context.AuditContext;
 import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
 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.primitive.StringSupport;
 
-import org.joda.time.DateTime;
-import org.joda.time.DateTimeZone;
 import org.opensaml.messaging.context.navigate.ChildContextLookup;
 import org.opensaml.profile.action.ActionSupport;
 import org.opensaml.profile.action.EventIds;
@@ -76,10 +78,10 @@ public class PopulateAuditContext extends AbstractProfileAction {
     /** Map allowing substitutions of values during field extraction. */
     @Nonnull private Map<String,String> fieldReplacements;
     
-    /** Formatting string for {@link DateTime} fields. */
-    @Nullable private String dateTimeFormat;
+    /** Formatter for date/time fields. */
+    @Nonnull private DateTimeFormatter dateTimeFormatter;
     
-    /** Convert {@link DateTime} fields to default time zone. */
+    /** Convert date/time fields to default time zone. */
     private boolean useDefaultTimeZone;
     
     /** {@link AuditContext} to populate. */
@@ -91,6 +93,8 @@ public class PopulateAuditContext extends AbstractProfileAction {
         fieldExtractors = Collections.emptyMap();
         fieldsToExtract = Collections.emptySet();
         fieldReplacements = Collections.emptyMap();
+        
+        dateTimeFormatter = DateTimeFormatter.ISO_INSTANT;
     }
 
     /**
@@ -181,18 +185,20 @@ public class PopulateAuditContext extends AbstractProfileAction {
     }
     
     /**
-     * Set the {@link DateTime} formatting string to apply when extracting {@link DateTime}-valued fields.
+     * Set the formatting string to apply when extracting date/time fields.
      * 
      * @param format formatting string
      */
     public void setDateTimeFormat(@Nullable @NotEmpty final String format) {
         ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
         
-        dateTimeFormat = StringSupport.trimOrNull(format);
+        if (format != null) {
+            dateTimeFormatter = DateTimeFormatter.ofPattern(StringSupport.trimOrNull(format));
+        }
     }
     
     /**
-     * Convert {@link DateTime}-valued fields to default time zone.
+     * Convert date/time fields to default time zone.
      * 
      * @param flag flag to set
      */
@@ -204,6 +210,16 @@ public class PopulateAuditContext extends AbstractProfileAction {
     
     /** {@inheritDoc} */
     @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        
+        if (useDefaultTimeZone) {
+            dateTimeFormatter = dateTimeFormatter.withZone(ZoneId.systemDefault());
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
     protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
         
         if (!super.doPreExecute(profileRequestContext) || fieldExtractors.isEmpty()) {
@@ -258,10 +274,8 @@ public class PopulateAuditContext extends AbstractProfileAction {
     private void addField(@Nonnull @NotEmpty final String key, @Nullable final Object value) {
         
         if (value != null) {
-            if (value instanceof DateTime) {
-                final DateTime dt = useDefaultTimeZone
-                        ? ((DateTime) value).withZone(DateTimeZone.getDefault()) : (DateTime) value;
-                auditCtx.getFieldValues(key).add(dt.toString(dateTimeFormat));
+            if (value instanceof TemporalAccessor) {
+                auditCtx.getFieldValues(key).add(dateTimeFormatter.format((TemporalAccessor) value));
             } else {
                 String s = value.toString();
                 if (fieldReplacements.containsKey(s)) {
diff --git a/idp-profile-impl/src/main/java/net/shibboleth/idp/profile/audit/impl/WriteAuditLog.java b/idp-profile-impl/src/main/java/net/shibboleth/idp/profile/audit/impl/WriteAuditLog.java
index 61c07c0..6a96fff 100644
--- a/idp-profile-impl/src/main/java/net/shibboleth/idp/profile/audit/impl/WriteAuditLog.java
+++ b/idp-profile-impl/src/main/java/net/shibboleth/idp/profile/audit/impl/WriteAuditLog.java
@@ -17,6 +17,9 @@
 
 package net.shibboleth.idp.profile.audit.impl;
 
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
@@ -36,14 +39,11 @@ import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElemen
 import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
 import net.shibboleth.utilities.java.support.annotation.constraint.NotLive;
 import net.shibboleth.utilities.java.support.annotation.constraint.Unmodifiable;
+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.primitive.StringSupport;
 
-import org.joda.time.DateTime;
-import org.joda.time.DateTimeZone;
-import org.joda.time.format.DateTimeFormatter;
-import org.joda.time.format.ISODateTimeFormat;
 import org.opensaml.messaging.context.navigate.ChildContextLookup;
 import org.opensaml.profile.action.EventIds;
 import org.opensaml.profile.context.ProfileRequestContext;
@@ -61,9 +61,6 @@ import com.google.common.collect.ImmutableMap;
  */
 public class WriteAuditLog extends AbstractProfileAction {
 
-    /** Formatter used to convert timestamps to strings. */
-    @Nonnull private static DateTimeFormatter v2Formatter = ISODateTimeFormat.basicDateTimeNoMillis();
-
     /** Class logger. */
     @Nonnull private final Logger log = LoggerFactory.getLogger(WriteAuditLog.class);
     
@@ -73,10 +70,10 @@ public class WriteAuditLog extends AbstractProfileAction {
     /** Map of log category to formatting tokens and literals to output. */
     @Nonnull @NotEmpty private Map<String,List<String>> formattingMap;
 
-    /** Formatting string for {@link DateTime} fields. */
-    @Nullable private String dateTimeFormat;
+    /** Formatter for date/time fields. */
+    @Nonnull private DateTimeFormatter dateTimeFormatter;
     
-    /** Convert {@link DateTime} fields to default time zone. */
+    /** Convert date/time fields to default time zone. */
     private boolean useDefaultTimeZone;
 
     /** The Spring RequestContext to operate on. */
@@ -92,6 +89,7 @@ public class WriteAuditLog extends AbstractProfileAction {
     public WriteAuditLog() {
         auditContextLookupStrategy = new ChildContextLookup<>(AuditContext.class);
         formattingMap = Collections.emptyMap();
+        dateTimeFormatter = DateTimeFormatter.ISO_INSTANT;
     }
 
     /**
@@ -171,18 +169,20 @@ public class WriteAuditLog extends AbstractProfileAction {
 // Checkstyle: CyclomaticComplexity ON
 
     /**
-     * Set the {@link DateTime} formatting string to apply when extracting {@link DateTime}-valued fields.
+     * Set the formatting string to apply when extracting date/time fields.
      * 
      * @param format formatting string
      */
     public void setDateTimeFormat(@Nullable @NotEmpty final String format) {
         ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
         
-        dateTimeFormat = StringSupport.trimOrNull(format);
+        if (format != null) {
+            dateTimeFormatter = DateTimeFormatter.ofPattern(StringSupport.trimOrNull(format));
+        }
     }
     
     /**
-     * Convert {@link DateTime}-valued fields to default time zone.
+     * Convert date/time fields to default time zone.
      * 
      * @param flag flag to set
      */
@@ -191,6 +191,16 @@ public class WriteAuditLog extends AbstractProfileAction {
         
         useDefaultTimeZone = flag;
     }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        
+        if (useDefaultTimeZone) {
+            dateTimeFormatter = dateTimeFormatter.withZone(ZoneId.systemDefault());
+        }
+    }
 
     /** {@inheritDoc} */
     @Override
@@ -232,14 +242,7 @@ public class WriteAuditLog extends AbstractProfileAction {
                         final String field = token.substring(1);
                         
                         if (IdPAuditFields.EVENT_TIME.equals(field)) {
-                            if (dateTimeFormat != null) {
-                                record.append(new DateTime(
-                                        useDefaultTimeZone ? DateTimeZone.getDefault() : DateTimeZone.UTC).toString(
-                                                dateTimeFormat));
-                            } else {
-                                record.append(new DateTime().toString(v2Formatter.withZone(
-                                        useDefaultTimeZone ? DateTimeZone.getDefault() : DateTimeZone.UTC)));
-                            }
+                            record.append(dateTimeFormatter.format(Instant.now()));
                         } else if (IdPAuditFields.EVENT_TYPE.equals(field)) {
                             final Event event = requestContext.getCurrentEvent();
                             if (event != null && !event.getId().equals(EventIds.PROCEED_EVENT_ID)) {
diff --git a/idp-profile-impl/src/test/java/net/shibboleth/idp/profile/interceptor/impl/WriteProfileInterceptorResultToStorageTest.java b/idp-profile-impl/src/test/java/net/shibboleth/idp/profile/interceptor/impl/WriteProfileInterceptorResultToStorageTest.java
index fcd4aa6..b5281ef 100644
--- a/idp-profile-impl/src/test/java/net/shibboleth/idp/profile/interceptor/impl/WriteProfileInterceptorResultToStorageTest.java
+++ b/idp-profile-impl/src/test/java/net/shibboleth/idp/profile/interceptor/impl/WriteProfileInterceptorResultToStorageTest.java
@@ -17,6 +17,8 @@
 
 package net.shibboleth.idp.profile.interceptor.impl;
 
+import java.time.Instant;
+
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
@@ -30,7 +32,6 @@ import net.shibboleth.utilities.java.support.annotation.Duration;
 import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
 import net.shibboleth.utilities.java.support.annotation.constraint.Positive;
 
-import org.joda.time.DateTime;
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.opensaml.storage.StorageRecord;
 import org.opensaml.storage.impl.MemoryStorageService;
@@ -109,7 +110,7 @@ public class WriteProfileInterceptorResultToStorageTest {
     }
 
     @Test public void testCreateStorageRecordWithExpiration() throws Exception {
-        final Long expiration = DateTime.now().getMillis() + 1000 * 60;
+        final Long expiration = Instant.now().plusSeconds(60).toEpochMilli();
         final MockProfileInterceptorResult result =
                 new MockProfileInterceptorResult("context", "key", "value", expiration);
         prc.getSubcontext(ProfileInterceptorContext.class).getResults().add(result);
@@ -125,7 +126,7 @@ public class WriteProfileInterceptorResultToStorageTest {
     }
 
     @Test public void testUpdateStorageRecord() throws Exception {
-        final Long expiration = DateTime.now().getMillis() + 1000 * 60;
+        final Long expiration = Instant.now().plusSeconds(60).toEpochMilli();
         MockProfileInterceptorResult result = new MockProfileInterceptorResult("context", "key", "value", null);
         prc.getSubcontext(ProfileInterceptorContext.class).getResults().add(result);
 
diff --git a/idp-profile-spring/src/test/java/net/shibboleth/idp/profile/spring/relyingparty/metadata/ReloadServiceConfigurationTest.java b/idp-profile-spring/src/test/java/net/shibboleth/idp/profile/spring/relyingparty/metadata/ReloadServiceConfigurationTest.java
index 3dcedfa..55e8089 100644
--- a/idp-profile-spring/src/test/java/net/shibboleth/idp/profile/spring/relyingparty/metadata/ReloadServiceConfigurationTest.java
+++ b/idp-profile-spring/src/test/java/net/shibboleth/idp/profile/spring/relyingparty/metadata/ReloadServiceConfigurationTest.java
@@ -18,6 +18,7 @@
 package net.shibboleth.idp.profile.spring.relyingparty.metadata;
 
 import java.io.IOException;
+import java.time.Instant;
 
 import javax.servlet.http.HttpServletResponse;
 
@@ -29,7 +30,6 @@ import net.shibboleth.utilities.java.support.component.ComponentSupport;
 import net.shibboleth.utilities.java.support.logic.FunctionSupport;
 import net.shibboleth.utilities.java.support.service.ReloadableService;
 
-import org.joda.time.DateTime;
 import org.opensaml.profile.action.EventIds;
 import org.opensaml.saml.metadata.resolver.RefreshableMetadataResolver;
 import org.springframework.mock.web.MockHttpServletResponse;
@@ -57,7 +57,7 @@ public class ReloadServiceConfigurationTest extends AbstractMetadataParserTest {
     }
 
     @Test public void service() {
-        final DateTime time = service.getLastReloadAttemptInstant();
+        final Instant time = service.getLastReloadAttemptInstant();
 
         service.reload();
         Assert.assertNotEquals(time, service.getLastReloadAttemptInstant());
@@ -87,7 +87,7 @@ public class ReloadServiceConfigurationTest extends AbstractMetadataParserTest {
     }
     
     @Test public void serviceAction() throws ComponentInitializationException {
-        final DateTime time = service.getLastReloadAttemptInstant();
+        final Instant time = service.getLastReloadAttemptInstant();
 
         final MockHttpServletResponse response = new MockHttpServletResponse();
         
diff --git a/idp-profile-spring/src/test/java/net/shibboleth/idp/profile/spring/relyingparty/security/credential/X509FilesystemParserTest.java b/idp-profile-spring/src/test/java/net/shibboleth/idp/profile/spring/relyingparty/security/credential/X509FilesystemParserTest.java
index b40f277..7f5931d 100644
--- a/idp-profile-spring/src/test/java/net/shibboleth/idp/profile/spring/relyingparty/security/credential/X509FilesystemParserTest.java
+++ b/idp-profile-spring/src/test/java/net/shibboleth/idp/profile/spring/relyingparty/security/credential/X509FilesystemParserTest.java
@@ -18,10 +18,10 @@
 package net.shibboleth.idp.profile.spring.relyingparty.security.credential;
 
 import java.io.IOException;
+import java.time.Instant;
 
 import net.shibboleth.idp.profile.spring.relyingparty.security.AbstractSecurityParserTest;
 
-import org.joda.time.DateTime;
 import org.opensaml.security.credential.Credential;
 import org.opensaml.security.credential.UsageType;
 import org.opensaml.security.x509.BasicX509Credential;
@@ -45,7 +45,7 @@ public class X509FilesystemParserTest extends AbstractSecurityParserTest {
         Assert.assertTrue(credential.getEntityCertificateChain().contains(credential.getEntityCertificate()));
 
         Assert.assertEquals(credential.getEntityCertificate().getNotAfter().getTime(),
-                DateTime.parse("2024-04-08T13:39:18Z").getMillis());
+                Instant.parse("2024-04-08T13:39:18Z").toEpochMilli());
     }
 
     @Test(expectedExceptions = {BeanCreationException.class,}) public void twoCert() throws IOException {
@@ -60,12 +60,12 @@ public class X509FilesystemParserTest extends AbstractSecurityParserTest {
         final BasicX509Credential credential = lookup("fileKeyCertCrl.xml");
 
         Assert.assertEquals(credential.getEntityCertificate().getNotAfter().getTime(),
-                DateTime.parse("2024-04-08T13:39:18Z").getMillis());
+                Instant.parse("2024-04-08T13:39:18Z").toEpochMilli());
         Assert.assertEquals(credential.getEntityCertificateChain().size(), 3);
         Assert.assertTrue(credential.getEntityCertificateChain().contains(credential.getEntityCertificate()));
         Assert.assertEquals(credential.getCRLs().size(), 1);
         Assert.assertEquals(credential.getCRLs().iterator().next().getNextUpdate().getTime(),
-                DateTime.parse("2007-09-02T14:14:48Z").getMillis());
+                Instant.parse("2007-09-02T14:14:48Z").toEpochMilli());
     }
 
     @Test public void certElementsKeyNames() throws IOException {
@@ -75,7 +75,7 @@ public class X509FilesystemParserTest extends AbstractSecurityParserTest {
         Assert.assertTrue(credential.getEntityCertificateChain().contains(credential.getEntityCertificate()));
 
         Assert.assertEquals(credential.getEntityCertificate().getNotAfter().getTime(),
-                DateTime.parse("2024-04-08T13:39:18Z").getMillis());
+                Instant.parse("2024-04-08T13:39:18Z").toEpochMilli());
 
         Assert.assertEquals(credential.getUsageType(), UsageType.SIGNING);
         Assert.assertEquals(credential.getKeyNames().size(), 2);
diff --git a/idp-profile-spring/src/test/java/net/shibboleth/idp/profile/spring/relyingparty/security/credential/X509InlineParserTest.java b/idp-profile-spring/src/test/java/net/shibboleth/idp/profile/spring/relyingparty/security/credential/X509InlineParserTest.java
index c29f553..02ea26f 100644
--- a/idp-profile-spring/src/test/java/net/shibboleth/idp/profile/spring/relyingparty/security/credential/X509InlineParserTest.java
+++ b/idp-profile-spring/src/test/java/net/shibboleth/idp/profile/spring/relyingparty/security/credential/X509InlineParserTest.java
@@ -18,10 +18,10 @@
 package net.shibboleth.idp.profile.spring.relyingparty.security.credential;
 
 import java.io.IOException;
+import java.time.Instant;
 
 import net.shibboleth.idp.profile.spring.relyingparty.security.AbstractSecurityParserTest;
 
-import org.joda.time.DateTime;
 import org.opensaml.security.credential.Credential;
 import org.opensaml.security.x509.BasicX509Credential;
 import org.springframework.beans.factory.BeanCreationException;
@@ -40,7 +40,7 @@ public class X509InlineParserTest extends AbstractSecurityParserTest {
         Assert.assertEquals(credential.getEntityCertificateChain().size(), 1);
         Assert.assertTrue(credential.getEntityCertificateChain().contains(credential.getEntityCertificate()));
 
-        Assert.assertEquals(credential.getEntityCertificate().getNotAfter().getTime(), DateTime.parse("2024-04-08T13:39:18Z").getMillis());
+        Assert.assertEquals(credential.getEntityCertificate().getNotAfter().getTime(), Instant.parse("2024-04-08T13:39:18Z").toEpochMilli());
     }
     
     @Test(expectedExceptions={BeanCreationException.class,}) public void wrongCert() throws IOException {
@@ -51,11 +51,11 @@ public class X509InlineParserTest extends AbstractSecurityParserTest {
         final BasicX509Credential credential =
                 (BasicX509Credential) getBean(Credential.class, true, "credential/inlineKeyCertCrl.xml");
 
-        Assert.assertEquals(credential.getEntityCertificate().getNotAfter().getTime(), DateTime.parse("2024-04-08T13:39:18Z").getMillis());
+        Assert.assertEquals(credential.getEntityCertificate().getNotAfter().getTime(), Instant.parse("2024-04-08T13:39:18Z").toEpochMilli());
         Assert.assertEquals(credential.getEntityCertificateChain().size(), 3);
         Assert.assertTrue(credential.getEntityCertificateChain().contains(credential.getEntityCertificate()));
         Assert.assertEquals(credential.getCRLs().size(), 1);
-        Assert.assertEquals(credential.getCRLs().iterator().next().getNextUpdate().getTime(), DateTime.parse("2007-09-02T14:14:48Z").getMillis());
+        Assert.assertEquals(credential.getCRLs().iterator().next().getNextUpdate().getTime(), Instant.parse("2007-09-02T14:14:48Z").toEpochMilli());
     }
     
 }
\ No newline at end of file
diff --git a/idp-profile-spring/src/test/java/net/shibboleth/idp/profile/spring/relyingparty/security/trustengine/SignatureChainingParserTest.java b/idp-profile-spring/src/test/java/net/shibboleth/idp/profile/spring/relyingparty/security/trustengine/SignatureChainingParserTest.java
index cf6f880..ebeb48e 100644
--- a/idp-profile-spring/src/test/java/net/shibboleth/idp/profile/spring/relyingparty/security/trustengine/SignatureChainingParserTest.java
+++ b/idp-profile-spring/src/test/java/net/shibboleth/idp/profile/spring/relyingparty/security/trustengine/SignatureChainingParserTest.java
@@ -18,11 +18,11 @@
 package net.shibboleth.idp.profile.spring.relyingparty.security.trustengine;
 
 import java.io.IOException;
+import java.time.Instant;
 
 import net.shibboleth.idp.profile.spring.relyingparty.security.AbstractSecurityParserTest;
 import net.shibboleth.utilities.java.support.resolver.ResolverException;
 
-import org.joda.time.DateTime;
 import org.opensaml.security.credential.impl.StaticCredentialResolver;
 import org.opensaml.security.trust.TrustedCredentialTrustEngine;
 import org.opensaml.security.x509.BasicX509Credential;
@@ -49,7 +49,7 @@ public class SignatureChainingParserTest extends AbstractSecurityParserTest {
         Assert.assertEquals(credential.getEntityCertificateChain().size(), 1);
         Assert.assertTrue(credential.getEntityCertificateChain().contains(credential.getEntityCertificate()));
 
-        Assert.assertEquals(credential.getEntityCertificate().getNotAfter().getTime(), DateTime.parse("2024-04-08T13:39:18Z").getMillis());
+        Assert.assertEquals(credential.getEntityCertificate().getNotAfter().getTime(), Instant.parse("2024-04-08T13:39:18Z").toEpochMilli());
         
     }
 }
diff --git a/idp-profile-spring/src/test/java/net/shibboleth/idp/profile/spring/relyingparty/security/trustengine/StaticExplicitKeyFactoryBeanTest.java b/idp-profile-spring/src/test/java/net/shibboleth/idp/profile/spring/relyingparty/security/trustengine/StaticExplicitKeyFactoryBeanTest.java
index 508920e..6002177 100644
--- a/idp-profile-spring/src/test/java/net/shibboleth/idp/profile/spring/relyingparty/security/trustengine/StaticExplicitKeyFactoryBeanTest.java
+++ b/idp-profile-spring/src/test/java/net/shibboleth/idp/profile/spring/relyingparty/security/trustengine/StaticExplicitKeyFactoryBeanTest.java
@@ -18,12 +18,12 @@
 package net.shibboleth.idp.profile.spring.relyingparty.security.trustengine;
 
 import java.io.IOException;
+import java.time.Instant;
 import java.util.Iterator;
 
 import net.shibboleth.idp.profile.spring.relyingparty.security.AbstractSecurityParserTest;
 import net.shibboleth.utilities.java.support.resolver.ResolverException;
 
-import org.joda.time.DateTime;
 import org.opensaml.security.credential.Credential;
 import org.opensaml.security.credential.impl.StaticCredentialResolver;
 import org.opensaml.security.trust.TrustEngine;
@@ -57,7 +57,7 @@ public class StaticExplicitKeyFactoryBeanTest extends AbstractSecurityParserTest
         Assert.assertEquals(credential.getEntityCertificateChain().size(), 1);
         Assert.assertTrue(credential.getEntityCertificateChain().contains(credential.getEntityCertificate()));
 
-        Assert.assertEquals(credential.getEntityCertificate().getNotAfter().getTime(), DateTime.parse("2024-04-08T13:39:18Z").getMillis());
+        Assert.assertEquals(credential.getEntityCertificate().getNotAfter().getTime(), Instant.parse("2024-04-08T13:39:18Z").toEpochMilli());
     }
     
     @Test public void multipleCert() throws IOException, ResolverException {
@@ -91,6 +91,6 @@ public class StaticExplicitKeyFactoryBeanTest extends AbstractSecurityParserTest
         Assert.assertTrue(credentials.hasNext());
         final X509Credential second = (X509Credential) credentials.next();
         Assert.assertEquals(second.getEntityCertificateChain().size(), 1);
-        Assert.assertEquals(second.getEntityCertificate().getNotAfter().getTime(), DateTime.parse("2024-04-08T13:39:18Z").getMillis());
+        Assert.assertEquals(second.getEntityCertificate().getNotAfter().getTime(), Instant.parse("2024-04-08T13:39:18Z").toEpochMilli());
     }
 }
\ No newline at end of file
diff --git a/idp-profile-spring/src/test/java/net/shibboleth/idp/profile/spring/relyingparty/security/trustengine/StaticExplicitKeyParserTest.java b/idp-profile-spring/src/test/java/net/shibboleth/idp/profile/spring/relyingparty/security/trustengine/StaticExplicitKeyParserTest.java
index 97a2a91..b891424 100644
--- a/idp-profile-spring/src/test/java/net/shibboleth/idp/profile/spring/relyingparty/security/trustengine/StaticExplicitKeyParserTest.java
+++ b/idp-profile-spring/src/test/java/net/shibboleth/idp/profile/spring/relyingparty/security/trustengine/StaticExplicitKeyParserTest.java
@@ -18,11 +18,11 @@
 package net.shibboleth.idp.profile.spring.relyingparty.security.trustengine;
 
 import java.io.IOException;
+import java.time.Instant;
 
 import net.shibboleth.idp.profile.spring.relyingparty.security.AbstractSecurityParserTest;
 import net.shibboleth.utilities.java.support.resolver.ResolverException;
 
-import org.joda.time.DateTime;
 import org.opensaml.security.credential.impl.StaticCredentialResolver;
 import org.opensaml.security.trust.TrustEngine;
 import org.opensaml.security.trust.impl.ExplicitKeyTrustEngine;
@@ -45,7 +45,7 @@ public class StaticExplicitKeyParserTest extends AbstractSecurityParserTest {
         Assert.assertEquals(credential.getEntityCertificateChain().size(), 1);
         Assert.assertTrue(credential.getEntityCertificateChain().contains(credential.getEntityCertificate()));
 
-        Assert.assertEquals(credential.getEntityCertificate().getNotAfter().getTime(), DateTime.parse("2024-04-08T13:39:18Z").getMillis());
+        Assert.assertEquals(credential.getEntityCertificate().getNotAfter().getTime(), Instant.parse("2024-04-08T13:39:18Z").toEpochMilli());
         
     }
 }
diff --git a/idp-profile-spring/src/test/java/net/shibboleth/idp/profile/spring/relyingparty/security/trustengine/StaticExplicitKeySignatureParserTest.java b/idp-profile-spring/src/test/java/net/shibboleth/idp/profile/spring/relyingparty/security/trustengine/StaticExplicitKeySignatureParserTest.java
index 4b1b6ba..b9500e2 100644
--- a/idp-profile-spring/src/test/java/net/shibboleth/idp/profile/spring/relyingparty/security/trustengine/StaticExplicitKeySignatureParserTest.java
+++ b/idp-profile-spring/src/test/java/net/shibboleth/idp/profile/spring/relyingparty/security/trustengine/StaticExplicitKeySignatureParserTest.java
@@ -18,11 +18,11 @@
 package net.shibboleth.idp.profile.spring.relyingparty.security.trustengine;
 
 import java.io.IOException;
+import java.time.Instant;
 
 import net.shibboleth.idp.profile.spring.relyingparty.security.AbstractSecurityParserTest;
 import net.shibboleth.utilities.java.support.resolver.ResolverException;
 
-import org.joda.time.DateTime;
 import org.opensaml.security.credential.impl.StaticCredentialResolver;
 import org.opensaml.security.trust.TrustEngine;
 import org.opensaml.security.trust.TrustedCredentialTrustEngine;
@@ -45,7 +45,7 @@ public class StaticExplicitKeySignatureParserTest extends AbstractSecurityParser
         Assert.assertEquals(credential.getEntityCertificateChain().size(), 1);
         Assert.assertTrue(credential.getEntityCertificateChain().contains(credential.getEntityCertificate()));
 
-        Assert.assertEquals(credential.getEntityCertificate().getNotAfter().getTime(), DateTime.parse("2024-04-08T13:39:18Z").getMillis());
+        Assert.assertEquals(credential.getEntityCertificate().getNotAfter().getTime(), Instant.parse("2024-04-08T13:39:18Z").toEpochMilli());
         
     }
 }
diff --git a/idp-saml-api/pom.xml b/idp-saml-api/pom.xml
index 45c5a6d..98150c5 100644
--- a/idp-saml-api/pom.xml
+++ b/idp-saml-api/pom.xml
@@ -77,10 +77,6 @@
             <groupId>${spring-webflow.groupId}</groupId>
             <artifactId>spring-webflow</artifactId>
         </dependency>
-        <dependency>
-            <groupId>joda-time</groupId>
-            <artifactId>joda-time</artifactId>
-        </dependency>
 
         
         <!-- Provided Dependencies -->
diff --git a/idp-saml-api/src/main/java/net/shibboleth/idp/saml/attribute/mapping/AbstractSAMLAttributeValueMapper.java b/idp-saml-api/src/main/java/net/shibboleth/idp/saml/attribute/mapping/AbstractSAMLAttributeValueMapper.java
index a4f41c4..1bd2dd0 100644
--- a/idp-saml-api/src/main/java/net/shibboleth/idp/saml/attribute/mapping/AbstractSAMLAttributeValueMapper.java
+++ b/idp-saml-api/src/main/java/net/shibboleth/idp/saml/attribute/mapping/AbstractSAMLAttributeValueMapper.java
@@ -17,6 +17,7 @@
 
 package net.shibboleth.idp.saml.attribute.mapping;
 
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
@@ -33,7 +34,6 @@ import net.shibboleth.utilities.java.support.component.ComponentInitializationEx
 import net.shibboleth.utilities.java.support.component.ComponentSupport;
 import net.shibboleth.utilities.java.support.logic.Constraint;
 
-import org.joda.time.DateTime;
 import org.opensaml.core.xml.XMLObject;
 import org.opensaml.core.xml.schema.XSAny;
 import org.opensaml.core.xml.schema.XSBase64Binary;
@@ -106,9 +106,9 @@ public abstract class AbstractSAMLAttributeValueMapper extends AbstractInitializ
 
         } else if (object instanceof XSDateTime) {
 
-            final DateTime dt = ((XSDateTime) object).getValue();
+            final Instant dt = ((XSDateTime) object).getValue();
             if (dt != null) {
-                retVal = ((XSDateTime) object).getDateTimeFormatter().print(dt);
+                retVal = ((XSDateTime) object).getDateTimeFormatter().format(dt);
             } else {
                 retVal = null;
             }
diff --git a/idp-saml-api/src/main/java/net/shibboleth/idp/saml/metadata/RelyingPartyMetadataProvider.java b/idp-saml-api/src/main/java/net/shibboleth/idp/saml/metadata/RelyingPartyMetadataProvider.java
index 8b8d9c7..bf2e69b 100644
--- a/idp-saml-api/src/main/java/net/shibboleth/idp/saml/metadata/RelyingPartyMetadataProvider.java
+++ b/idp-saml-api/src/main/java/net/shibboleth/idp/saml/metadata/RelyingPartyMetadataProvider.java
@@ -17,6 +17,8 @@
 
 package net.shibboleth.idp.saml.metadata;
 
+import java.time.Instant;
+
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
@@ -31,7 +33,6 @@ import net.shibboleth.utilities.java.support.primitive.DeprecationSupport.Object
 import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
 import net.shibboleth.utilities.java.support.resolver.ResolverException;
 
-import org.joda.time.DateTime;
 import org.opensaml.saml.metadata.resolver.ClearableMetadataResolver;
 import org.opensaml.saml.metadata.resolver.MetadataResolver;
 import org.opensaml.saml.metadata.resolver.RefreshableMetadataResolver;
@@ -191,7 +192,7 @@ public class RelyingPartyMetadataProvider extends AbstractServiceableComponent<M
 
     /** {@inheritDoc} */
     @Override
-    public DateTime getLastRefresh() {
+    public Instant getLastRefresh() {
         if (resolver instanceof RefreshableMetadataResolver) {
             return ((RefreshableMetadataResolver) resolver).getLastRefresh();
         } else {
@@ -201,7 +202,7 @@ public class RelyingPartyMetadataProvider extends AbstractServiceableComponent<M
 
     /** {@inheritDoc} */
     @Override
-    public DateTime getLastUpdate() {
+    public Instant getLastUpdate() {
         if (resolver instanceof RefreshableMetadataResolver) {
             return ((RefreshableMetadataResolver) resolver).getLastUpdate();
         } else {
diff --git a/idp-saml-api/src/main/java/net/shibboleth/idp/saml/profile/config/AbstractCollectionConfigurationLookupStrategy.java b/idp-saml-api/src/main/java/net/shibboleth/idp/saml/profile/config/AbstractCollectionConfigurationLookupStrategy.java
index 8771a40..664f0eb 100644
--- a/idp-saml-api/src/main/java/net/shibboleth/idp/saml/profile/config/AbstractCollectionConfigurationLookupStrategy.java
+++ b/idp-saml-api/src/main/java/net/shibboleth/idp/saml/profile/config/AbstractCollectionConfigurationLookupStrategy.java
@@ -17,10 +17,11 @@
 
 package net.shibboleth.idp.saml.profile.config;
 
+import java.time.Instant;
+
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
-import org.joda.time.DateTime;
 import org.opensaml.core.xml.XMLObject;
 import org.opensaml.core.xml.schema.XSAny;
 import org.opensaml.core.xml.schema.XSBase64Binary;
@@ -124,8 +125,8 @@ public abstract class AbstractCollectionConfigurationLookupStrategy<T1,T2>
             final Integer value = ((XSInteger) object).getValue();
             return value != null ? value.toString() : null;
         } else if (object instanceof XSDateTime) {
-            final DateTime dt = ((XSDateTime) object).getValue();
-            return dt != null ? Long.toString(dt.getMillis()) : null;
+            final Instant dt = ((XSDateTime) object).getValue();
+            return dt != null ? Long.toString(dt.toEpochMilli()) : null;
         } else if (object instanceof XSBase64Binary) {
             return ((XSBase64Binary) object).getValue();
         } else if (object instanceof XSAny) {
diff --git a/idp-saml-api/src/main/java/net/shibboleth/idp/saml/profile/config/LongConfigurationLookupStrategy.java b/idp-saml-api/src/main/java/net/shibboleth/idp/saml/profile/config/LongConfigurationLookupStrategy.java
index dec5c67..59fad52 100644
--- a/idp-saml-api/src/main/java/net/shibboleth/idp/saml/profile/config/LongConfigurationLookupStrategy.java
+++ b/idp-saml-api/src/main/java/net/shibboleth/idp/saml/profile/config/LongConfigurationLookupStrategy.java
@@ -17,12 +17,12 @@
 
 package net.shibboleth.idp.saml.profile.config;
 
+import java.time.Instant;
 import java.util.List;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
-import org.joda.time.DateTime;
 import org.opensaml.core.xml.XMLObject;
 import org.opensaml.core.xml.schema.XSAny;
 import org.opensaml.core.xml.schema.XSBoolean;
@@ -78,8 +78,8 @@ public class LongConfigurationLookupStrategy extends AbstractMetadataDrivenConfi
             final Integer value = ((XSInteger) object).getValue(); 
             return value != null ? value.longValue() : null;
         } else if (object instanceof XSDateTime) {
-            final DateTime dt = ((XSDateTime) object).getValue();
-            return dt != null ? dt.getMillis() : null;
+            final Instant dt = ((XSDateTime) object).getValue();
+            return dt != null ? dt.toEpochMilli() : null;
         } else if (object instanceof XSAny) {
             final XSAny wc = (XSAny) object;
             if (wc.getUnknownAttributes().isEmpty() && wc.getUnknownXMLObjects().isEmpty()) {
diff --git a/idp-saml-api/src/main/java/net/shibboleth/idp/saml/profile/config/StringConfigurationLookupStrategy.java b/idp-saml-api/src/main/java/net/shibboleth/idp/saml/profile/config/StringConfigurationLookupStrategy.java
index 459d2d9..8b3bda5 100644
--- a/idp-saml-api/src/main/java/net/shibboleth/idp/saml/profile/config/StringConfigurationLookupStrategy.java
+++ b/idp-saml-api/src/main/java/net/shibboleth/idp/saml/profile/config/StringConfigurationLookupStrategy.java
@@ -17,12 +17,12 @@
 
 package net.shibboleth.idp.saml.profile.config;
 
+import java.time.Instant;
 import java.util.List;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
-import org.joda.time.DateTime;
 import org.opensaml.core.xml.XMLObject;
 import org.opensaml.core.xml.schema.XSAny;
 import org.opensaml.core.xml.schema.XSBase64Binary;
@@ -81,8 +81,8 @@ public class StringConfigurationLookupStrategy extends AbstractMetadataDrivenCon
             final Integer value = ((XSInteger) object).getValue();
             return value != null ? value.toString() : null;
         } else if (object instanceof XSDateTime) {
-            final DateTime dt = ((XSDateTime) object).getValue();
-            return dt != null ? Long.toString(dt.getMillis()) : null;
+            final Instant dt = ((XSDateTime) object).getValue();
+            return dt != null ? Long.toString(dt.toEpochMilli()) : null;
         } else if (object instanceof XSBase64Binary) {
             return ((XSBase64Binary) object).getValue();
         } else if (object instanceof XSAny) {
diff --git a/idp-saml-api/src/main/java/net/shibboleth/idp/saml/session/SAML1SPSession.java b/idp-saml-api/src/main/java/net/shibboleth/idp/saml/session/SAML1SPSession.java
index 69c2450..b1c3c89 100644
--- a/idp-saml-api/src/main/java/net/shibboleth/idp/saml/session/SAML1SPSession.java
+++ b/idp-saml-api/src/main/java/net/shibboleth/idp/saml/session/SAML1SPSession.java
@@ -17,12 +17,12 @@
 
 package net.shibboleth.idp.saml.session;
 
+import java.time.Instant;
+
 import javax.annotation.Nonnull;
 
 import net.shibboleth.idp.session.BasicSPSession;
-import net.shibboleth.utilities.java.support.annotation.Duration;
 import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
-import net.shibboleth.utilities.java.support.annotation.constraint.Positive;
 
 /**
  * Marker subtype for a SAML 1 session, adds no actual information other than its identity as
@@ -37,8 +37,8 @@ public class SAML1SPSession extends BasicSPSession {
      * @param creation creation time of session, in milliseconds since the epoch
      * @param expiration expiration time of session, in milliseconds since the epoch
      */
-    public SAML1SPSession(@Nonnull @NotEmpty final String id, @Duration @Positive final long creation,
-            @Duration @Positive final long expiration) {
+    public SAML1SPSession(@Nonnull @NotEmpty final String id, @Nonnull final Instant creation,
+            @Nonnull final Instant expiration) {
         super(id, creation, expiration);
     }
 
diff --git a/idp-saml-api/src/main/java/net/shibboleth/idp/saml/session/SAML2SPSession.java b/idp-saml-api/src/main/java/net/shibboleth/idp/saml/session/SAML2SPSession.java
index 69365f8..35145b9 100644
--- a/idp-saml-api/src/main/java/net/shibboleth/idp/saml/session/SAML2SPSession.java
+++ b/idp-saml-api/src/main/java/net/shibboleth/idp/saml/session/SAML2SPSession.java
@@ -17,6 +17,7 @@
 
 package net.shibboleth.idp.saml.session;
 
+import java.time.Instant;
 import java.util.Objects;
 
 import javax.annotation.Nonnull;
@@ -30,9 +31,7 @@ import org.opensaml.saml.saml2.profile.SAML2ObjectSupport;
 import com.google.common.base.MoreObjects;
 
 import net.shibboleth.idp.session.BasicSPSession;
-import net.shibboleth.utilities.java.support.annotation.Duration;
 import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
-import net.shibboleth.utilities.java.support.annotation.constraint.Positive;
 import net.shibboleth.utilities.java.support.logic.Constraint;
 import net.shibboleth.utilities.java.support.primitive.StringSupport;
 import net.shibboleth.utilities.java.support.xml.SerializeSupport;
@@ -59,8 +58,8 @@ public class SAML2SPSession extends BasicSPSession {
      * @param assertedNameID the NameID asserted to the SP
      * @param assertedIndex the SessionIndex asserted to the SP
      */
-    public SAML2SPSession(@Nonnull @NotEmpty final String id, @Duration @Positive final long creation,
-            @Duration @Positive final long expiration, @Nonnull final NameID assertedNameID,
+    public SAML2SPSession(@Nonnull @NotEmpty final String id, @Nonnull final Instant creation,
+            @Nonnull final Instant expiration, @Nonnull final NameID assertedNameID,
             @Nonnull @NotEmpty final String assertedIndex) {
         super(id, creation, expiration);
         
diff --git a/idp-saml-api/src/test/java/net/shibboleth/idp/saml/session/SAML2SPSessionTest.java b/idp-saml-api/src/test/java/net/shibboleth/idp/saml/session/SAML2SPSessionTest.java
index ba558e9..5b2772d 100644
--- a/idp-saml-api/src/test/java/net/shibboleth/idp/saml/session/SAML2SPSessionTest.java
+++ b/idp-saml-api/src/test/java/net/shibboleth/idp/saml/session/SAML2SPSessionTest.java
@@ -19,6 +19,8 @@ package net.shibboleth.idp.saml.session;
 
 import net.shibboleth.utilities.java.support.logic.ConstraintViolationException;
 
+import java.time.Instant;
+
 import org.opensaml.core.OpenSAMLInitBaseTestCase;
 import org.opensaml.core.xml.util.XMLObjectSupport;
 import org.opensaml.saml.saml2.core.NameID;
@@ -35,49 +37,49 @@ public class SAML2SPSessionTest extends OpenSAMLInitBaseTestCase {
         nameID.setValue("joe at example.org");
         nameID.setFormat(NameID.EMAIL);
         
-        long start = System.currentTimeMillis();
+        Instant start = Instant.now();
         // this is here to allow the event's creation time to deviate from the 'start' time
         Thread.sleep(50);
 
-        SAML2SPSession session = new SAML2SPSession("test", System.currentTimeMillis(),
-                System.currentTimeMillis() + 60000L, nameID, "1234567890");
+        SAML2SPSession session = new SAML2SPSession("test", Instant.now(),
+                Instant.now().plusSeconds(60), nameID, "1234567890");
         Assert.assertEquals(session.getId(), "test");
-        Assert.assertTrue(session.getCreationInstant() > start);
-        Assert.assertTrue(session.getExpirationInstant() > session.getCreationInstant());
+        Assert.assertTrue(session.getCreationInstant().isAfter(start));
+        Assert.assertTrue(session.getExpirationInstant().isAfter(session.getCreationInstant()));
         Assert.assertSame(session.getNameID(), nameID);
         Assert.assertEquals(session.getSessionIndex(), "1234567890");
         Assert.assertEquals(session.getSPSessionKey(), "joe at example.org");
 
         try {
-            new SAML2SPSession(null, 0, 0, null, null);
+            new SAML2SPSession(null, Instant.ofEpochMilli(0), Instant.ofEpochMilli(0), null, null);
             Assert.fail();
         } catch (ConstraintViolationException e) {
 
         }
 
         try {
-            new SAML2SPSession("", 0, 0, null, null);
+            new SAML2SPSession("", Instant.ofEpochMilli(0), Instant.ofEpochMilli(0), null, null);
             Assert.fail();
         } catch (ConstraintViolationException e) {
 
         }
 
         try {
-            new SAML2SPSession("  ", 0, 0, null, null);
+            new SAML2SPSession("  ", Instant.ofEpochMilli(0), Instant.ofEpochMilli(0), null, null);
             Assert.fail();
         } catch (ConstraintViolationException e) {
 
         }
 
         try {
-            new SAML2SPSession("foo", 0, 0, null, null);
+            new SAML2SPSession("foo", Instant.ofEpochMilli(0), Instant.ofEpochMilli(0), null, null);
             Assert.fail();
         } catch (ConstraintViolationException e) {
 
         }
 
         try {
-            new SAML2SPSession("foo", start, 0, null, null);
+            new SAML2SPSession("foo", start, Instant.ofEpochMilli(0), null, null);
             Assert.fail();
         } catch (ConstraintViolationException e) {
 
diff --git a/idp-saml-impl/pom.xml b/idp-saml-impl/pom.xml
index 51acbd8..b5fc4a0 100644
--- a/idp-saml-impl/pom.xml
+++ b/idp-saml-impl/pom.xml
@@ -89,10 +89,6 @@
             <groupId>${spring-webflow.groupId}</groupId>
             <artifactId>spring-webflow</artifactId>
         </dependency>
-        <dependency>
-            <groupId>joda-time</groupId>
-            <artifactId>joda-time</artifactId>
-        </dependency>
 
         <!-- Provided Dependencies -->
 
diff --git a/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/attribute/mapping/impl/SAML1AttributeDesignatorsMapperService.java b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/attribute/mapping/impl/SAML1AttributeDesignatorsMapperService.java
index 97587b1..cc44d99 100644
--- a/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/attribute/mapping/impl/SAML1AttributeDesignatorsMapperService.java
+++ b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/attribute/mapping/impl/SAML1AttributeDesignatorsMapperService.java
@@ -17,13 +17,13 @@
 
 package net.shibboleth.idp.saml.attribute.mapping.impl;
 
+import java.time.Instant;
 import java.util.List;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.ThreadSafe;
 
-import org.joda.time.DateTime;
 import org.opensaml.saml.saml1.core.AttributeDesignator;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -62,7 +62,7 @@ public class SAML1AttributeDesignatorsMapperService implements AttributesMapper<
     @Nullable private SAML1AttributeDesignatorsMapper attributesMapper;
 
     /** Date when the cache was last refreshed. */
-    @Nullable private DateTime lastReload;
+    @Nullable private Instant lastReload;
 
     /**
      * Constructor.
@@ -106,7 +106,7 @@ public class SAML1AttributeDesignatorsMapperService implements AttributesMapper<
         SAML1AttributeDesignatorsMapper am = null;
         try {
             // Get date before we get the component. That way we'll not leak changes.
-            final DateTime when = attributeResolverService.getLastSuccessfulReloadInstant();
+            final Instant when = attributeResolverService.getLastSuccessfulReloadInstant();
             component = attributeResolverService.getServiceableComponent();
             if (null == component) {
                 if (!captiveServiceReloadFailed) {
diff --git a/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/attribute/mapping/impl/SAML2AttributesMapperService.java b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/attribute/mapping/impl/SAML2AttributesMapperService.java
index 145647c..bc30707 100644
--- a/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/attribute/mapping/impl/SAML2AttributesMapperService.java
+++ b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/attribute/mapping/impl/SAML2AttributesMapperService.java
@@ -17,13 +17,13 @@
 
 package net.shibboleth.idp.saml.attribute.mapping.impl;
 
+import java.time.Instant;
 import java.util.List;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.ThreadSafe;
 
-import org.joda.time.DateTime;
 import org.opensaml.saml.saml2.core.Attribute;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -62,7 +62,7 @@ public class SAML2AttributesMapperService implements AttributesMapper<Attribute,
     @Nullable private SAML2AttributesMapper attributesMapper;
 
     /** Date when the cache was last refreshed. */
-    @Nullable private DateTime lastReload;
+    @Nullable private Instant lastReload;
 
     /**
      * Constructor.
@@ -104,7 +104,7 @@ public class SAML2AttributesMapperService implements AttributesMapper<Attribute,
         SAML2AttributesMapper am = null;
         try {
             // Get date before we get the component. That way we'll not leak changes.
-            final DateTime when = attributeResolverService.getLastSuccessfulReloadInstant();
+            final Instant when = attributeResolverService.getLastSuccessfulReloadInstant();
             component = attributeResolverService.getServiceableComponent();
             if (null == component) {
                 if (!captiveServiceReloadFailed) {
diff --git a/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/audit/impl/AssertionInstantAuditExtractor.java b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/audit/impl/AssertionInstantAuditExtractor.java
index 73105f1..9f4bf5f 100644
--- a/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/audit/impl/AssertionInstantAuditExtractor.java
+++ b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/audit/impl/AssertionInstantAuditExtractor.java
@@ -17,6 +17,7 @@
 
 package net.shibboleth.idp.saml.audit.impl;
 
+import java.time.Instant;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
@@ -25,7 +26,6 @@ import java.util.function.Function;
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
-import org.joda.time.DateTime;
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.opensaml.saml.common.SAMLObject;
 import org.opensaml.saml.saml2.core.ArtifactResponse;
@@ -35,7 +35,7 @@ import com.google.common.collect.Collections2;
 import net.shibboleth.utilities.java.support.logic.Constraint;
 
 /** {@link Function} that returns the IssueInstant attribute from the assertions in a response. */
-public class AssertionInstantAuditExtractor implements Function<ProfileRequestContext,Collection<DateTime>> {
+public class AssertionInstantAuditExtractor implements Function<ProfileRequestContext,Collection<Instant>> {
 
     /** Lookup strategy for message to read from. */
     @Nonnull private final Function<ProfileRequestContext,SAMLObject> responseLookupStrategy;
@@ -51,7 +51,7 @@ public class AssertionInstantAuditExtractor implements Function<ProfileRequestCo
 
     /** {@inheritDoc} */
     @Override
-    @Nullable public Collection<DateTime> apply(@Nullable final ProfileRequestContext input) {
+    @Nullable public Collection<Instant> apply(@Nullable final ProfileRequestContext input) {
         SAMLObject message = responseLookupStrategy.apply(input);
         if (message != null) {
             
diff --git a/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/audit/impl/AuthnInstantAuditExtractor.java b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/audit/impl/AuthnInstantAuditExtractor.java
index c188755..528fa19 100644
--- a/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/audit/impl/AuthnInstantAuditExtractor.java
+++ b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/audit/impl/AuthnInstantAuditExtractor.java
@@ -17,12 +17,12 @@
 
 package net.shibboleth.idp.saml.audit.impl;
 
+import java.time.Instant;
 import java.util.function.Function;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
-import org.joda.time.DateTime;
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.opensaml.saml.common.SAMLObject;
 import org.opensaml.saml.saml1.core.AuthenticationStatement;
@@ -32,7 +32,7 @@ import org.opensaml.saml.saml2.core.AuthnStatement;
 import net.shibboleth.utilities.java.support.logic.Constraint;
 
 /** {@link Function} that returns the first authentication timestamp from an assertions in a response. */
-public class AuthnInstantAuditExtractor implements Function<ProfileRequestContext,DateTime> {
+public class AuthnInstantAuditExtractor implements Function<ProfileRequestContext,Instant> {
 
     /** Lookup strategy for message to read from. */
     @Nonnull private final Function<ProfileRequestContext,SAMLObject> responseLookupStrategy;
@@ -48,7 +48,7 @@ public class AuthnInstantAuditExtractor implements Function<ProfileRequestContex
 
 // Checkstyle: CyclomaticComplexity OFF
     /** {@inheritDoc} */
-    @Nullable public DateTime apply(@Nullable final ProfileRequestContext input) {
+    @Nullable public Instant apply(@Nullable final ProfileRequestContext input) {
         SAMLObject response = responseLookupStrategy.apply(input);
         if (response != null) {
             
diff --git a/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/metadata/impl/AttributeMappingNodeProcessor.java b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/metadata/impl/AttributeMappingNodeProcessor.java
index b699763..1fe014d 100644
--- a/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/metadata/impl/AttributeMappingNodeProcessor.java
+++ b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/metadata/impl/AttributeMappingNodeProcessor.java
@@ -17,6 +17,7 @@
 
 package net.shibboleth.idp.saml.metadata.impl;
 
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -35,7 +36,6 @@ import net.shibboleth.utilities.java.support.logic.Constraint;
 import net.shibboleth.utilities.java.support.service.ReloadableService;
 import net.shibboleth.utilities.java.support.service.ServiceableComponent;
 
-import org.joda.time.DateTime;
 import org.opensaml.core.xml.XMLObject;
 import org.opensaml.saml.ext.saml2mdattr.EntityAttributes;
 import org.opensaml.saml.metadata.resolver.filter.FilterException;
@@ -73,7 +73,7 @@ public class AttributeMappingNodeProcessor implements MetadataNodeProcessor {
     @Nullable private SAML2AttributesMapper attributesMapper;
 
     /** Date when the cache was last refreshed. */
-    @Nullable private DateTime lastReload;
+    @Nullable private Instant lastReload;
 
     /**
      * Constructor.
@@ -137,7 +137,7 @@ public class AttributeMappingNodeProcessor implements MetadataNodeProcessor {
         SAML2AttributesMapper am = null;
         try {
             // get date before we get the component. That way we'll not leak changes.
-            final DateTime when = attributeResolverService.getLastSuccessfulReloadInstant();
+            final Instant when = attributeResolverService.getLastSuccessfulReloadInstant();
             component = attributeResolverService.getServiceableComponent();
             if (null == component) {
                 if (!refreshFailed) {
diff --git a/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/metadata/impl/MetadataResolverServiceGaugeSet.java b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/metadata/impl/MetadataResolverServiceGaugeSet.java
index db07d69..fd772c5 100644
--- a/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/metadata/impl/MetadataResolverServiceGaugeSet.java
+++ b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/metadata/impl/MetadataResolverServiceGaugeSet.java
@@ -16,12 +16,12 @@
  */
 package net.shibboleth.idp.saml.metadata.impl;
 
+import java.time.Instant;
 import java.util.Collections;
 import java.util.Map;
 
 import javax.annotation.Nonnull;
 
-import org.joda.time.DateTime;
 import org.opensaml.saml.metadata.resolver.ChainingMetadataResolver;
 import org.opensaml.saml.metadata.resolver.ExtendedBatchMetadataResolver;
 import org.opensaml.saml.metadata.resolver.ExtendedRefreshableMetadataResolver;
@@ -60,9 +60,9 @@ public class MetadataResolverServiceGaugeSet extends ReloadableServiceGaugeSet i
         
         getMetricMap().put(
                 MetricRegistry.name(DEFAULT_METRIC_NAME, metricName, "update"),
-                new Gauge<Map<String,DateTime>>() {
-                    public Map<String,DateTime> getValue() {
-                        final Builder mapBuilder = ImmutableMap.<String,DateTime>builder();
+                new Gauge<Map<String,Instant>>() {
+                    public Map<String,Instant> getValue() {
+                        final Builder mapBuilder = ImmutableMap.<String,Instant>builder();
                         final ServiceableComponent<MetadataResolver> component = getService().getServiceableComponent();
                         if (component != null) {
                             try {                                
@@ -83,9 +83,9 @@ public class MetadataResolverServiceGaugeSet extends ReloadableServiceGaugeSet i
         
         getMetricMap().put(
                 MetricRegistry.name(DEFAULT_METRIC_NAME, metricName, "refresh"),
-                new Gauge<Map<String,DateTime>>() {
-                    public Map<String,DateTime> getValue() {
-                        final Builder mapBuilder = ImmutableMap.<String,DateTime>builder();
+                new Gauge<Map<String,Instant>>() {
+                    public Map<String,Instant> getValue() {
+                        final Builder mapBuilder = ImmutableMap.<String,Instant>builder();
                         final ServiceableComponent<MetadataResolver> component = getService().getServiceableComponent();
                         if (component != null) {
                             try {                                
@@ -108,9 +108,9 @@ public class MetadataResolverServiceGaugeSet extends ReloadableServiceGaugeSet i
         // Checkstyle: AnonInnerLength OFF
         getMetricMap().put(
                 MetricRegistry.name(DEFAULT_METRIC_NAME, metricName, "successfulRefresh"),
-                new Gauge<Map<String,DateTime>>() {
-                    public Map<String,DateTime> getValue() {
-                        final Builder mapBuilder = ImmutableMap.<String,DateTime>builder();
+                new Gauge<Map<String,Instant>>() {
+                    public Map<String,Instant> getValue() {
+                        final Builder mapBuilder = ImmutableMap.<String,Instant>builder();
                         final ServiceableComponent<MetadataResolver> component = getService().getServiceableComponent();
                         if (component != null) {
                             try {                                
@@ -135,9 +135,9 @@ public class MetadataResolverServiceGaugeSet extends ReloadableServiceGaugeSet i
         //TODO v4.0.0 - Switch to use BatchMetadataResolver when new methods promoted up
         getMetricMap().put(
                 MetricRegistry.name(DEFAULT_METRIC_NAME, metricName, "rootValidUntil"),
-                new Gauge<Map<String,DateTime>>() {
-                    public Map<String,DateTime> getValue() {
-                        final Builder mapBuilder = ImmutableMap.<String,DateTime>builder();
+                new Gauge<Map<String,Instant>>() {
+                    public Map<String,Instant> getValue() {
+                        final Builder mapBuilder = ImmutableMap.<String,Instant>builder();
                         final ServiceableComponent<MetadataResolver> component = getService().getServiceableComponent();
                         if (component != null) {
                             try {                                
diff --git a/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/profile/impl/BaseIdPInitiatedSSORequestMessageDecoder.java b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/profile/impl/BaseIdPInitiatedSSORequestMessageDecoder.java
index 61a53cb..02645cf 100644
--- a/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/profile/impl/BaseIdPInitiatedSSORequestMessageDecoder.java
+++ b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/profile/impl/BaseIdPInitiatedSSORequestMessageDecoder.java
@@ -17,6 +17,8 @@
 
 package net.shibboleth.idp.saml.profile.impl;
 
+import java.time.Instant;
+
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.NotThreadSafe;
@@ -162,7 +164,7 @@ public abstract class BaseIdPInitiatedSSORequestMessageDecoder<RequestType> exte
      * @throws MessageDecodingException thrown if the time parameter given by the service provider is non-numeric or a
      *             negative time
      */
-    @Nullable protected Long getTime(@Nonnull final HttpServletRequest request) throws MessageDecodingException {
+    @Nullable protected Instant getTime(@Nonnull final HttpServletRequest request) throws MessageDecodingException {
         final String timeString = StringSupport.trimOrNull(request.getParameter(TIME_PARAM));
         if (timeString == null) {
             return null;
@@ -173,7 +175,7 @@ public abstract class BaseIdPInitiatedSSORequestMessageDecoder<RequestType> exte
             if (time < 0) {
                 throw new MessageDecodingException("Shibboleth Authentication Request contained a negative time value");
             }
-            return time * 1000;
+            return Instant.ofEpochSecond(time);
         } catch (final NumberFormatException e) {
             throw new MessageDecodingException("Shibboleth Authentication Request contained a non-numeric time value");
         }
diff --git a/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/profile/impl/IdPInitiatedSSORequest.java b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/profile/impl/IdPInitiatedSSORequest.java
index 14851d0..b609a25 100644
--- a/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/profile/impl/IdPInitiatedSSORequest.java
+++ b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/profile/impl/IdPInitiatedSSORequest.java
@@ -17,17 +17,14 @@
 
 package net.shibboleth.idp.saml.profile.impl;
 
+import java.time.Instant;
 import java.util.Objects;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.ThreadSafe;
 
-import org.joda.time.DateTime;
-import org.joda.time.chrono.ISOChronology;
-
 import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
-import net.shibboleth.utilities.java.support.annotation.constraint.Positive;
 import net.shibboleth.utilities.java.support.logic.Constraint;
 import net.shibboleth.utilities.java.support.primitive.StringSupport;
 
@@ -58,7 +55,7 @@ public class IdPInitiatedSSORequest {
     @Nullable private final String relayState;
 
     /** The current time, at the service provider, in milliseconds since the epoch. */
-    @Positive private final long time;
+    @Nonnull private final Instant time;
 
     /**
      * Constructor.
@@ -71,7 +68,7 @@ public class IdPInitiatedSSORequest {
      * @param newTime current time at the SP, in milliseconds since the epoch, must be greater than zero
      */
     public IdPInitiatedSSORequest(@Nonnull @NotEmpty final String newEntityId, @Nullable final String url,
-            @Nullable final String target, @Nullable @Positive final Long newTime) {
+            @Nullable final String target, @Nullable final Instant newTime) {
         
         entityId = Constraint.isNotNull(StringSupport.trimOrNull(newEntityId),
                 "Service provider ID cannot be null or empty");
@@ -80,9 +77,9 @@ public class IdPInitiatedSSORequest {
         relayState = StringSupport.trimOrNull(target);
         
         if (newTime != null) {
-            time = Constraint.isGreaterThan(0, newTime, "Time must be greater than 0");
+            time = newTime;
         } else {
-            time = System.currentTimeMillis();
+            time = Instant.now();
         }
     }
 
@@ -118,7 +115,7 @@ public class IdPInitiatedSSORequest {
      * 
      * @return current time at the SP, in milliseconds since the epoch 
      */
-    public long getTime() {
+    @Nonnull public Instant getTime() {
         return time;
     }
 
@@ -129,7 +126,7 @@ public class IdPInitiatedSSORequest {
             .add("entityId", entityId)
             .add("acsURL", acsURL)
             .add("relayState", relayState)
-            .add("time", new DateTime(time, ISOChronology.getInstanceUTC()))
+            .add("time", time)
             .toString();
     }
 
@@ -153,7 +150,7 @@ public class IdPInitiatedSSORequest {
             result = prime * result + 0;
         }
 
-        result = prime * result + (int) (time ^ (time >>> 32));
+        result = prime * result + (int) (time.toEpochMilli() ^ (time.toEpochMilli() >>> 32));
 
         return result;
     }
@@ -175,7 +172,7 @@ public class IdPInitiatedSSORequest {
 
         final IdPInitiatedSSORequest other = (IdPInitiatedSSORequest) obj;
         return Objects.equals(entityId, other.entityId) && Objects.equals(acsURL, other.acsURL)
-                && Objects.equals(relayState, other.relayState) && time == other.time;
+                && Objects.equals(relayState, other.relayState) && time.equals(other.time);
     }
     
 }
\ No newline at end of file
diff --git a/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/saml1/profile/impl/AddAuthenticationStatementToAssertion.java b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/saml1/profile/impl/AddAuthenticationStatementToAssertion.java
index 6b442bb..f1d7e4c 100644
--- a/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/saml1/profile/impl/AddAuthenticationStatementToAssertion.java
+++ b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/saml1/profile/impl/AddAuthenticationStatementToAssertion.java
@@ -38,7 +38,6 @@ import net.shibboleth.utilities.java.support.component.ComponentSupport;
 import net.shibboleth.utilities.java.support.logic.Constraint;
 import net.shibboleth.utilities.java.support.security.IdentifierGenerationStrategy;
 
-import org.joda.time.DateTime;
 import org.opensaml.core.xml.XMLObjectBuilderFactory;
 import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
 import org.opensaml.saml.common.SAMLObjectBuilder;
@@ -161,7 +160,7 @@ public class AddAuthenticationStatementToAssertion extends BaseAddAuthentication
                 bf.<SubjectLocality>getBuilderOrThrow(SubjectLocality.TYPE_NAME);
 
         final AuthenticationStatement statement = statementBuilder.buildObject();
-        statement.setAuthenticationInstant(new DateTime(getAuthenticationResult().getAuthenticationInstant()));
+        statement.setAuthenticationInstant(getAuthenticationResult().getAuthenticationInstant());
         
         if (requestedPrincipalContext != null && requestedPrincipalContext.getMatchingPrincipal() != null
                 && requestedPrincipalContext.getMatchingPrincipal() instanceof AuthenticationMethodPrincipal) {
diff --git a/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/saml1/profile/impl/IdPInitiatedSSORequestMessageDecoder.java b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/saml1/profile/impl/IdPInitiatedSSORequestMessageDecoder.java
index 7aa86d4..c212c42 100644
--- a/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/saml1/profile/impl/IdPInitiatedSSORequestMessageDecoder.java
+++ b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/saml1/profile/impl/IdPInitiatedSSORequestMessageDecoder.java
@@ -24,8 +24,6 @@ import net.shibboleth.idp.saml.profile.impl.BaseIdPInitiatedSSORequestMessageDec
 import net.shibboleth.idp.saml.profile.impl.IdPInitiatedSSORequest;
 import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
 
-import org.joda.time.DateTime;
-import org.joda.time.chrono.ISOChronology;
 import org.opensaml.messaging.context.MessageContext;
 import org.opensaml.messaging.decoder.MessageDecodingException;
 import org.opensaml.saml.common.messaging.context.SAMLBindingContext;
@@ -64,7 +62,7 @@ public class IdPInitiatedSSORequestMessageDecoder extends
         messageContext.getSubcontext(SAMLPeerEntityContext.class, true).setEntityId(ssoRequest.getEntityId());
         
         final SAMLMessageInfoContext msgInfoContext = messageContext.getSubcontext(SAMLMessageInfoContext.class, true);
-        msgInfoContext.setMessageIssueInstant(new DateTime(ssoRequest.getTime(), ISOChronology.getInstanceUTC()));
+        msgInfoContext.setMessageIssueInstant(ssoRequest.getTime());
         msgInfoContext.setMessageId(getMessageID());
         
         populateBindingContext(messageContext);
diff --git a/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/saml2/profile/delegation/impl/AddDelegationRestrictionToAssertions.java b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/saml2/profile/delegation/impl/AddDelegationRestrictionToAssertions.java
index f33e945..74666b6 100644
--- a/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/saml2/profile/delegation/impl/AddDelegationRestrictionToAssertions.java
+++ b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/saml2/profile/delegation/impl/AddDelegationRestrictionToAssertions.java
@@ -17,6 +17,7 @@
 
 package net.shibboleth.idp.saml.saml2.profile.delegation.impl;
 
+import java.time.Instant;
 import java.util.List;
 import java.util.function.Function;
 
@@ -28,7 +29,6 @@ import net.shibboleth.idp.saml.saml2.profile.delegation.LibertySSOSContext;
 import net.shibboleth.utilities.java.support.component.ComponentSupport;
 import net.shibboleth.utilities.java.support.logic.Constraint;
 
-import org.joda.time.DateTime;
 import org.opensaml.core.xml.io.MarshallingException;
 import org.opensaml.core.xml.io.UnmarshallingException;
 import org.opensaml.core.xml.util.XMLObjectSupport;
@@ -97,7 +97,7 @@ public class AddDelegationRestrictionToAssertions extends AbstractProfileAction
     @Nullable private String presenterEntityID;
     
     /** The instant of delegation. */
-    @Nullable private DateTime delegationInstant;
+    @Nullable private Instant delegationInstant;
     
     /**
      * Constructor.
@@ -200,7 +200,7 @@ public class AddDelegationRestrictionToAssertions extends AbstractProfileAction
         }
         
         //TODO do we have a single harmonized "issue instant" for the outbound request data?
-        delegationInstant = new DateTime();
+        delegationInstant = Instant.now();
         
         return super.doPreExecute(profileRequestContext);
     }
diff --git a/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/saml2/profile/delegation/impl/PopulateOutboundMessageContext.java b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/saml2/profile/delegation/impl/PopulateOutboundMessageContext.java
index 41c9a38..0df21e8 100644
--- a/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/saml2/profile/delegation/impl/PopulateOutboundMessageContext.java
+++ b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/saml2/profile/delegation/impl/PopulateOutboundMessageContext.java
@@ -19,9 +19,10 @@ package net.shibboleth.idp.saml.saml2.profile.delegation.impl;
 
 import net.shibboleth.idp.profile.AbstractProfileAction;
 
+import java.time.Instant;
+
 import javax.annotation.Nonnull;
 
-import org.joda.time.DateTime;
 import org.opensaml.messaging.context.MessageContext;
 import org.opensaml.profile.action.ActionSupport;
 import org.opensaml.profile.action.EventIds;
@@ -76,7 +77,7 @@ public class PopulateOutboundMessageContext extends AbstractProfileAction {
             outboundMessageContext.getSubcontext(WSSecurityContext.class, true).setTimestampCreated(
                     samlMsgInfoCtx.getMessageIssueInstant());
         } else {
-            outboundMessageContext.getSubcontext(WSSecurityContext.class, true).setTimestampCreated(new DateTime());
+            outboundMessageContext.getSubcontext(WSSecurityContext.class, true).setTimestampCreated(Instant.now());
         }
         log.debug("Set outbound WS-Security Timestamp Created: {}", 
                 outboundMessageContext.getSubcontext(WSSecurityContext.class).getTimestampCreated());
diff --git a/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/saml2/profile/impl/AddAuthnStatementToAssertion.java b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/saml2/profile/impl/AddAuthnStatementToAssertion.java
index eff89e6..d24eb80 100644
--- a/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/saml2/profile/impl/AddAuthnStatementToAssertion.java
+++ b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/saml2/profile/impl/AddAuthnStatementToAssertion.java
@@ -18,6 +18,7 @@
 package net.shibboleth.idp.saml.saml2.profile.impl;
 
 import java.security.Principal;
+import java.time.Instant;
 import java.util.Set;
 import java.util.function.Function;
 
@@ -42,7 +43,6 @@ import net.shibboleth.utilities.java.support.component.ComponentInitializationEx
 import net.shibboleth.utilities.java.support.component.ComponentSupport;
 import net.shibboleth.utilities.java.support.logic.Constraint;
 
-import org.joda.time.DateTime;
 import org.opensaml.core.xml.XMLObjectBuilderFactory;
 import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
 import org.opensaml.saml.common.SAMLObjectBuilder;
@@ -189,7 +189,7 @@ public class AddAuthnStatementToAssertion extends BaseAddAuthenticationStatement
                 bf.<SubjectLocality>getBuilderOrThrow(SubjectLocality.TYPE_NAME);
 
         final AuthnStatement statement = statementBuilder.buildObject();
-        statement.setAuthnInstant(new DateTime(getAuthenticationResult().getAuthenticationInstant()));
+        statement.setAuthnInstant(getAuthenticationResult().getAuthenticationInstant());
         
         final AuthnContext authnContext = authnContextBuilder.buildObject();
         statement.setAuthnContext(authnContext);
@@ -232,7 +232,7 @@ public class AddAuthnStatementToAssertion extends BaseAddAuthenticationStatement
         if (sessionLifetimeLookupStrategy != null) {
             final Long lifetime = sessionLifetimeLookupStrategy.apply(profileRequestContext);
             if (lifetime != null && lifetime > 0) {
-                statement.setSessionNotOnOrAfter(new DateTime().plus(lifetime));
+                statement.setSessionNotOnOrAfter(Instant.now().plusMillis(lifetime));
             }
         }
         
diff --git a/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/saml2/profile/impl/IdPInitiatedSSORequestMessageDecoder.java b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/saml2/profile/impl/IdPInitiatedSSORequestMessageDecoder.java
index 88aebcb..762be68 100644
--- a/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/saml2/profile/impl/IdPInitiatedSSORequestMessageDecoder.java
+++ b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/saml2/profile/impl/IdPInitiatedSSORequestMessageDecoder.java
@@ -25,8 +25,6 @@ import net.shibboleth.idp.saml.profile.impl.IdPInitiatedSSORequest;
 import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
 import net.shibboleth.utilities.java.support.xml.SerializeSupport;
 
-import org.joda.time.DateTime;
-import org.joda.time.chrono.ISOChronology;
 import org.opensaml.core.xml.XMLObjectBuilderFactory;
 import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
 import org.opensaml.core.xml.io.MarshallingException;
@@ -125,7 +123,7 @@ public class IdPInitiatedSSORequestMessageDecoder extends BaseIdPInitiatedSSOReq
         nip.setAllowCreate(true);
         authnRequest.setNameIDPolicy(nip);
         authnRequest.setAssertionConsumerServiceURL(ssoRequest.getAssertionConsumerServiceURL());
-        authnRequest.setIssueInstant(new DateTime(ssoRequest.getTime(), ISOChronology.getInstanceUTC()));
+        authnRequest.setIssueInstant(ssoRequest.getTime());
         authnRequest.setVersion(SAMLVersion.VERSION_20);
         authnRequest.setID(getMessageID());
         
diff --git a/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/session/impl/AddLogoutRequest.java b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/session/impl/AddLogoutRequest.java
index 3b3cae3..e9adc6e 100644
--- a/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/session/impl/AddLogoutRequest.java
+++ b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/session/impl/AddLogoutRequest.java
@@ -17,6 +17,7 @@
 
 package net.shibboleth.idp.saml.session.impl;
 
+import java.time.Instant;
 import java.util.function.Function;
 
 import javax.annotation.Nonnull;
@@ -29,8 +30,6 @@ import net.shibboleth.utilities.java.support.logic.Constraint;
 import net.shibboleth.utilities.java.support.security.IdentifierGenerationStrategy;
 import net.shibboleth.utilities.java.support.security.SecureRandomIdentifierGenerationStrategy;
 
-import org.joda.time.DateTime;
-import org.joda.time.chrono.ISOChronology;
 import org.opensaml.core.xml.XMLObjectBuilderFactory;
 import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
 import org.opensaml.core.xml.io.MarshallingException;
@@ -232,7 +231,7 @@ public class AddLogoutRequest extends AbstractProfileAction {
         final LogoutRequest object = requestBuilder.buildObject();
         
         object.setID(idGenerator.generateIdentifier());
-        object.setIssueInstant(new DateTime(ISOChronology.getInstanceUTC()));
+        object.setIssueInstant(Instant.now());
         object.setVersion(SAMLVersion.VERSION_20);
 
         try {
diff --git a/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/session/impl/SAML1SPSessionCreationStrategy.java b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/session/impl/SAML1SPSessionCreationStrategy.java
index db25e3c..1d5db2e 100644
--- a/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/session/impl/SAML1SPSessionCreationStrategy.java
+++ b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/session/impl/SAML1SPSessionCreationStrategy.java
@@ -17,6 +17,8 @@
 
 package net.shibboleth.idp.saml.session.impl;
 
+import java.time.Duration;
+import java.time.Instant;
 import java.util.function.Function;
 
 import javax.annotation.Nonnull;
@@ -30,8 +32,6 @@ import org.slf4j.LoggerFactory;
 import net.shibboleth.idp.profile.context.RelyingPartyContext;
 import net.shibboleth.idp.saml.session.SAML1SPSession;
 import net.shibboleth.idp.session.SPSession;
-import net.shibboleth.utilities.java.support.annotation.Duration;
-import net.shibboleth.utilities.java.support.annotation.constraint.Positive;
 import net.shibboleth.utilities.java.support.logic.Constraint;
 
 /**
@@ -44,13 +44,13 @@ import net.shibboleth.utilities.java.support.logic.Constraint;
  * The session has a creation time based on the time of execution, and the expiration is based on
  * a configurable lifetime.</p> 
  */
-public class SAML1SPSessionCreationStrategy implements Function<ProfileRequestContext, SPSession> {
+public class SAML1SPSessionCreationStrategy implements Function<ProfileRequestContext,SPSession> {
 
     /** Class logger. */
     @Nonnull private final Logger log = LoggerFactory.getLogger(SAML1SPSessionCreationStrategy.class);
     
     /** Lifetime of sessions to create. */
-    @Positive @Duration private final long sessionLifetime;
+    @Nonnull private final Duration sessionLifetime;
 
     /** RelyingPartyContext lookup strategy. */
     @Nonnull private Function<ProfileRequestContext,RelyingPartyContext> relyingPartyContextLookupStrategy;
@@ -60,8 +60,8 @@ public class SAML1SPSessionCreationStrategy implements Function<ProfileRequestCo
      * 
      * @param lifetime lifetime in milliseconds, determines expiration of {@link SPSession} to be created
      */
-    public SAML1SPSessionCreationStrategy(@Positive @Duration final long lifetime) {
-        sessionLifetime = Constraint.isGreaterThan(0, lifetime, "Lifetime must be greater than 0");
+    public SAML1SPSessionCreationStrategy(@Nonnull final Duration lifetime) {
+        sessionLifetime = Constraint.isNotNull(lifetime, "Lifetime cannot be null");
         relyingPartyContextLookupStrategy = new ChildContextLookup<>(RelyingPartyContext.class);
     }
 
@@ -91,8 +91,8 @@ public class SAML1SPSessionCreationStrategy implements Function<ProfileRequestCo
             return null;
         }
         
-        final long now = System.currentTimeMillis();
-        return new SAML1SPSession(issuer, now, now + sessionLifetime);
+        final Instant now = Instant.now();
+        return new SAML1SPSession(issuer, now, now.plus(sessionLifetime));
     }
 
 }
\ No newline at end of file
diff --git a/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/session/impl/SAML1SPSessionSerializer.java b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/session/impl/SAML1SPSessionSerializer.java
index 33ab423..8da3bb7 100644
--- a/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/session/impl/SAML1SPSessionSerializer.java
+++ b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/session/impl/SAML1SPSessionSerializer.java
@@ -18,6 +18,8 @@
 package net.shibboleth.idp.saml.session.impl;
 
 import java.io.IOException;
+import java.time.Duration;
+import java.time.Instant;
 
 import javax.annotation.Nonnull;
 import javax.annotation.concurrent.ThreadSafe;
@@ -26,9 +28,7 @@ import javax.json.JsonObject;
 import net.shibboleth.idp.saml.session.SAML1SPSession;
 import net.shibboleth.idp.session.AbstractSPSessionSerializer;
 import net.shibboleth.idp.session.SPSession;
-import net.shibboleth.utilities.java.support.annotation.Duration;
 import net.shibboleth.utilities.java.support.annotation.ParameterName;
-import net.shibboleth.utilities.java.support.annotation.constraint.NonNegative;
 
 /**
  * A serializer for {@link SAML1SPSession} objects.
@@ -41,14 +41,14 @@ public class SAML1SPSessionSerializer extends AbstractSPSessionSerializer {
      * 
      * @param offset milliseconds to subtract from record expiration to establish session expiration value
      */
-    public SAML1SPSessionSerializer(@Duration @NonNegative @Nonnull @ParameterName(name="offset") final long offset) {
+    public SAML1SPSessionSerializer(@Nonnull @ParameterName(name="offset") final Duration offset) {
         super(offset);
     }
 
     /** {@inheritDoc} */
     @Override
     @Nonnull protected SPSession doDeserialize(final JsonObject obj, final String id,
-            final long creation, final long expiration) throws IOException {
+            @Nonnull final Instant creation, @Nonnull final Instant expiration) throws IOException {
         
         return new SAML1SPSession(id, creation, expiration);
     }
diff --git a/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/session/impl/SAML2SPSessionCreationStrategy.java b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/session/impl/SAML2SPSessionCreationStrategy.java
index c6797c6..160729f 100644
--- a/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/session/impl/SAML2SPSessionCreationStrategy.java
+++ b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/session/impl/SAML2SPSessionCreationStrategy.java
@@ -17,12 +17,13 @@
 
 package net.shibboleth.idp.saml.session.impl;
 
+import java.time.Duration;
+import java.time.Instant;
 import java.util.function.Function;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
-import org.joda.time.DateTime;
 import org.opensaml.messaging.context.navigate.ChildContextLookup;
 import org.opensaml.messaging.context.navigate.MessageLookup;
 import org.opensaml.profile.context.ProfileRequestContext;
@@ -37,8 +38,6 @@ import net.shibboleth.idp.profile.context.RelyingPartyContext;
 import net.shibboleth.idp.saml.session.SAML2SPSession;
 import net.shibboleth.idp.session.BasicSPSession;
 import net.shibboleth.idp.session.SPSession;
-import net.shibboleth.utilities.java.support.annotation.Duration;
-import net.shibboleth.utilities.java.support.annotation.constraint.Positive;
 import net.shibboleth.utilities.java.support.collection.Pair;
 import net.shibboleth.utilities.java.support.logic.Constraint;
 
@@ -62,7 +61,7 @@ public class SAML2SPSessionCreationStrategy implements Function<ProfileRequestCo
     @Nonnull private final Logger log = LoggerFactory.getLogger(SAML2SPSessionCreationStrategy.class);
     
     /** Lifetime of sessions to create. */
-    @Positive @Duration private final long sessionLifetime;
+    @Nonnull private final Duration sessionLifetime;
     
     /** RelyingPartyContext lookup strategy. */
     @Nonnull private Function<ProfileRequestContext,RelyingPartyContext> relyingPartyContextLookupStrategy;
@@ -76,8 +75,8 @@ public class SAML2SPSessionCreationStrategy implements Function<ProfileRequestCo
      * @param lifetime lifetime in milliseconds, determines upper bound for expiration of
      * {@link SAML2SPSession} to be created
      */
-    public SAML2SPSessionCreationStrategy(@Positive @Duration final long lifetime) {
-        sessionLifetime = Constraint.isGreaterThan(0, lifetime, "Lifetime must be greater than 0");
+    public SAML2SPSessionCreationStrategy(@Nonnull final Duration lifetime) {
+        sessionLifetime = Constraint.isNotNull(lifetime, "Lifetime cannot be null");
         relyingPartyContextLookupStrategy = new ChildContextLookup<>(RelyingPartyContext.class);
         responseLookupStrategy = new MessageLookup<>(Response.class).compose(new OutboundMessageContextLookup());
     }
@@ -120,17 +119,17 @@ public class SAML2SPSessionCreationStrategy implements Function<ProfileRequestCo
         final Pair<Assertion, AuthnStatement> result = getAssertionAndStatement(input);
         if (result == null) {
             log.info("Creating BasicSPSession in the absence of necessary information");
-            final long now = System.currentTimeMillis();
-            return new BasicSPSession(issuer, now, now + sessionLifetime);
+            final Instant now = Instant.now();
+            return new BasicSPSession(issuer, now, now.plus(sessionLifetime));
         }
         
-        final long now = System.currentTimeMillis();
-        final DateTime sessionBound = result.getSecond().getSessionNotOnOrAfter();
-        final long expiration;
+        final Instant now = Instant.now();
+        final Instant sessionBound = result.getSecond().getSessionNotOnOrAfter();
+        final Instant expiration;
         if (sessionBound != null) {
-            expiration = sessionBound.getMillis(); 
+            expiration = sessionBound;
         } else {
-            expiration = now + sessionLifetime;
+            expiration = now.plus(sessionLifetime);
         }
         
         return new SAML2SPSession(issuer, now, expiration, result.getFirst().getSubject().getNameID(),
diff --git a/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/session/impl/SAML2SPSessionSerializer.java b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/session/impl/SAML2SPSessionSerializer.java
index 3745d44..8dc75b9 100644
--- a/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/session/impl/SAML2SPSessionSerializer.java
+++ b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/session/impl/SAML2SPSessionSerializer.java
@@ -19,6 +19,8 @@ package net.shibboleth.idp.saml.session.impl;
 
 import java.io.IOException;
 import java.io.StringReader;
+import java.time.Duration;
+import java.time.Instant;
 import java.util.Collections;
 import java.util.Map;
 
@@ -37,11 +39,8 @@ import org.opensaml.saml.saml2.core.NameID;
 import net.shibboleth.idp.saml.session.SAML2SPSession;
 import net.shibboleth.idp.session.AbstractSPSessionSerializer;
 import net.shibboleth.idp.session.SPSession;
-import net.shibboleth.utilities.java.support.annotation.Duration;
 import net.shibboleth.utilities.java.support.annotation.ParameterName;
-import net.shibboleth.utilities.java.support.annotation.constraint.NonNegative;
 import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
-import net.shibboleth.utilities.java.support.annotation.constraint.Positive;
 import net.shibboleth.utilities.java.support.annotation.constraint.ThreadSafeAfterInit;
 import net.shibboleth.utilities.java.support.component.ComponentSupport;
 import net.shibboleth.utilities.java.support.logic.Constraint;
@@ -72,7 +71,7 @@ public class SAML2SPSessionSerializer extends AbstractSPSessionSerializer {
      * 
      * @param offset milliseconds to subtract from record expiration to establish session expiration value
      */
-    public SAML2SPSessionSerializer(@Duration @NonNegative @ParameterName(name="offset") final long offset) {
+    public SAML2SPSessionSerializer(@Nonnull @ParameterName(name="offset") final Duration offset) {
         super(offset);
         
         parserPool = Constraint.isNotNull(XMLObjectProviderRegistrySupport.getParserPool(),
@@ -107,7 +106,7 @@ public class SAML2SPSessionSerializer extends AbstractSPSessionSerializer {
     /** {@inheritDoc} */
     @Override
     @Nonnull protected SPSession doDeserialize(@Nonnull final JsonObject obj, @Nonnull @NotEmpty final String id, 
-            @Duration @Positive final long creation, @Duration @Positive final long expiration) throws IOException {
+            @Nonnull final Instant creation, @Nonnull final Instant expiration) throws IOException {
         
         final String rawNameID = obj.getString(NAMEID_FIELD);
         final String sessionIndex = obj.getString(SESSION_INDEX_FIELD);
diff --git a/idp-saml-impl/src/test/java/net/shibboleth/idp/saml/saml1/profile/impl/AddAuthenticationStatementToAssertionTest.java b/idp-saml-impl/src/test/java/net/shibboleth/idp/saml/saml1/profile/impl/AddAuthenticationStatementToAssertionTest.java
index a40b2a1..abb24c9 100644
--- a/idp-saml-impl/src/test/java/net/shibboleth/idp/saml/saml1/profile/impl/AddAuthenticationStatementToAssertionTest.java
+++ b/idp-saml-impl/src/test/java/net/shibboleth/idp/saml/saml1/profile/impl/AddAuthenticationStatementToAssertionTest.java
@@ -17,6 +17,8 @@
 
 package net.shibboleth.idp.saml.saml1.profile.impl;
 
+import java.time.Instant;
+
 import javax.security.auth.Subject;
 
 import net.shibboleth.idp.authn.AuthenticationFlowDescriptor;
@@ -103,7 +105,7 @@ public class AddAuthenticationStatementToAssertionTest extends OpenSAMLInitBaseT
 
     /** Test that the authentication statement is properly added. */
     @Test public void testAddAuthenticationStatement() throws Exception {
-        final long now = System.currentTimeMillis();
+        final Instant now = Instant.now();
         // this is here to allow the event's creation time to deviate from the 'start' time
         Thread.sleep(50);
         
@@ -136,7 +138,7 @@ public class AddAuthenticationStatementToAssertionTest extends OpenSAMLInitBaseT
         Assert.assertNotNull(assertion.getAuthenticationStatements().get(0));
 
         final AuthenticationStatement authenticationStatement = assertion.getAuthenticationStatements().get(0);
-        Assert.assertTrue(authenticationStatement.getAuthenticationInstant().getMillis() > now);
+        Assert.assertTrue(authenticationStatement.getAuthenticationInstant().isAfter(now));
         Assert.assertEquals(authenticationStatement.getAuthenticationMethod(), "Test");
         
         Assert.assertNotNull(authenticationStatement.getSubjectLocality());
diff --git a/idp-saml-impl/src/test/java/net/shibboleth/idp/saml/saml1/profile/impl/IdPInitiatedSSORequestMessageDecoderTest.java b/idp-saml-impl/src/test/java/net/shibboleth/idp/saml/saml1/profile/impl/IdPInitiatedSSORequestMessageDecoderTest.java
index d0bfb49..8799479 100644
--- a/idp-saml-impl/src/test/java/net/shibboleth/idp/saml/saml1/profile/impl/IdPInitiatedSSORequestMessageDecoderTest.java
+++ b/idp-saml-impl/src/test/java/net/shibboleth/idp/saml/saml1/profile/impl/IdPInitiatedSSORequestMessageDecoderTest.java
@@ -21,8 +21,9 @@ import net.shibboleth.idp.saml.profile.impl.BaseIdPInitiatedSSORequestMessageDec
 import net.shibboleth.idp.saml.profile.impl.IdPInitiatedSSORequest;
 import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
 
-import org.joda.time.DateTime;
-import org.joda.time.chrono.ISOChronology;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+
 import org.opensaml.messaging.context.MessageContext;
 import org.opensaml.messaging.decoder.MessageDecodingException;
 import org.opensaml.saml.common.messaging.context.SAMLBindingContext;
@@ -52,15 +53,13 @@ public class IdPInitiatedSSORequestMessageDecoderTest {
     
     private String messageID;
     
-    private Long time;
+    private Instant time;
     
     @BeforeMethod
     public void setUp() throws ComponentInitializationException {
-        // Note: protocol takes time in seconds, so divide by 1000.
-        // Components usually produce milliseconds, so later multiply or divide by 1000 in assertions as appropriate.
-        time = System.currentTimeMillis()/1000;
+        time = Instant.now();
         
-        messageID = "_" + sessionID + "!" + time.toString();
+        messageID = "_" + sessionID + "!" + Long.toUnsignedString(time.getEpochSecond());
         
         request = new MockHttpServletRequest();
         request.setRequestedSessionId(sessionID);
@@ -75,7 +74,7 @@ public class IdPInitiatedSSORequestMessageDecoderTest {
         request.addParameter(BaseIdPInitiatedSSORequestMessageDecoder.PROVIDER_ID_PARAM,  entityId);
         request.addParameter(BaseIdPInitiatedSSORequestMessageDecoder.SHIRE_PARAM,  acsUrl);
         request.addParameter(BaseIdPInitiatedSSORequestMessageDecoder.TARGET_PARAM,  relayState);
-        request.addParameter(BaseIdPInitiatedSSORequestMessageDecoder.TIME_PARAM,  time.toString());
+        request.addParameter(BaseIdPInitiatedSSORequestMessageDecoder.TIME_PARAM,  Long.toUnsignedString(time.getEpochSecond()));
         
         decoder.decode();
         
@@ -87,7 +86,7 @@ public class IdPInitiatedSSORequestMessageDecoderTest {
         Assert.assertEquals(ssoRequest.getEntityId(), entityId, "Incorrect decoded entityId value");
         Assert.assertEquals(ssoRequest.getAssertionConsumerServiceURL(), acsUrl, "Incorrect decoded ACS URL value");
         Assert.assertEquals(ssoRequest.getRelayState(), relayState, "Incorrect decoded relay state value");
-        Assert.assertEquals(Long.valueOf(ssoRequest.getTime()/1000), time, "Incorrect decoded time value");
+        Assert.assertEquals(ssoRequest.getTime(), time.truncatedTo(ChronoUnit.SECONDS), "Incorrect decoded time value");
         
         Assert.assertEquals(messageContext.getSubcontext(SAMLPeerEntityContext.class, true).getEntityId(), entityId,
                 "Incorrect decoded entityId value in peer context");
@@ -98,16 +97,17 @@ public class IdPInitiatedSSORequestMessageDecoderTest {
                 "Incorrect binding URI in binding context");
         
         SAMLMessageInfoContext msgInfoContext = messageContext.getSubcontext(SAMLMessageInfoContext.class, true);
-        Assert.assertEquals(msgInfoContext.getMessageIssueInstant(), new DateTime(time*1000, ISOChronology.getInstanceUTC()),
+        Assert.assertEquals(msgInfoContext.getMessageIssueInstant(), time.truncatedTo(ChronoUnit.SECONDS),
                 "Incorrect decoded issue instant value in message info context");
-        Assert.assertEquals(msgInfoContext.getMessageId(), messageID, "Incorrect decoded message ID value in message info context");
+        Assert.assertEquals(msgInfoContext.getMessageId(), messageID,
+                "Incorrect decoded message ID value in message info context");
     }
 
     @Test(expectedExceptions=MessageDecodingException.class)
     public void testMissingTarget() throws MessageDecodingException {
         request.addParameter(BaseIdPInitiatedSSORequestMessageDecoder.PROVIDER_ID_PARAM,  entityId);
         request.addParameter(BaseIdPInitiatedSSORequestMessageDecoder.SHIRE_PARAM,  acsUrl);
-        request.addParameter(BaseIdPInitiatedSSORequestMessageDecoder.TIME_PARAM,  time.toString());
+        request.addParameter(BaseIdPInitiatedSSORequestMessageDecoder.TIME_PARAM,  Long.toUnsignedString(time.getEpochSecond()));
         
         decoder.decode();
     }
diff --git a/idp-saml-impl/src/test/java/net/shibboleth/idp/saml/saml2/profile/delegation/impl/PopulateOutboundMessageContextTest.java b/idp-saml-impl/src/test/java/net/shibboleth/idp/saml/saml2/profile/delegation/impl/PopulateOutboundMessageContextTest.java
index ab0bbca..d1962c7 100644
--- a/idp-saml-impl/src/test/java/net/shibboleth/idp/saml/saml2/profile/delegation/impl/PopulateOutboundMessageContextTest.java
+++ b/idp-saml-impl/src/test/java/net/shibboleth/idp/saml/saml2/profile/delegation/impl/PopulateOutboundMessageContextTest.java
@@ -19,7 +19,8 @@ package net.shibboleth.idp.saml.saml2.profile.delegation.impl;
 
 import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
 
-import org.joda.time.DateTime;
+import java.time.Instant;
+
 import org.opensaml.messaging.context.MessageContext;
 import org.opensaml.profile.action.EventIds;
 import org.opensaml.profile.context.EventContext;
@@ -51,7 +52,7 @@ public class PopulateOutboundMessageContextTest {
     
     @Test
     public void testBasic() throws ComponentInitializationException {
-        DateTime now = new DateTime();
+        Instant now = Instant.now();
         prc.getInboundMessageContext().getSubcontext(WSAddressingContext.class, true).setMessageIDURI("urn:test:abc123");
         prc.getOutboundMessageContext().getSubcontext(SAMLMessageInfoContext.class, true).setMessageIssueInstant(now);
         
@@ -67,7 +68,7 @@ public class PopulateOutboundMessageContextTest {
     
     @Test
     public void testNoInboundMessageID() throws ComponentInitializationException {
-        DateTime now = new DateTime();
+        Instant now = Instant.now();
         prc.getOutboundMessageContext().getSubcontext(SAMLMessageInfoContext.class, true).setMessageIssueInstant(now);
         
         action.initialize();
diff --git a/idp-saml-impl/src/test/java/net/shibboleth/idp/saml/saml2/profile/delegation/messaging/impl/LibertyHTTPSOAP11EncoderTest.java b/idp-saml-impl/src/test/java/net/shibboleth/idp/saml/saml2/profile/delegation/messaging/impl/LibertyHTTPSOAP11EncoderTest.java
index 507be80..1592496 100644
--- a/idp-saml-impl/src/test/java/net/shibboleth/idp/saml/saml2/profile/delegation/messaging/impl/LibertyHTTPSOAP11EncoderTest.java
+++ b/idp-saml-impl/src/test/java/net/shibboleth/idp/saml/saml2/profile/delegation/messaging/impl/LibertyHTTPSOAP11EncoderTest.java
@@ -20,7 +20,8 @@ package net.shibboleth.idp.saml.saml2.profile.delegation.messaging.impl;
 import net.shibboleth.idp.saml.saml2.profile.delegation.impl.LibertyConstants;
 import net.shibboleth.utilities.java.support.testing.TestSupport;
 
-import org.joda.time.DateTime;
+import java.time.Instant;
+
 import org.opensaml.core.xml.XMLObjectBaseTestCase;
 import org.opensaml.messaging.context.MessageContext;
 import org.opensaml.saml.common.SAMLObject;
@@ -68,7 +69,7 @@ public class LibertyHTTPSOAP11EncoderTest extends XMLObjectBaseTestCase {
         Response samlMessage = responseBuilder.buildObject();
         samlMessage.setID("foo");
         samlMessage.setVersion(SAMLVersion.VERSION_20);
-        samlMessage.setIssueInstant(new DateTime(0));
+        samlMessage.setIssueInstant(Instant.ofEpochMilli(0));
         samlMessage.setStatus(responseStatus);
 
         SAMLObjectBuilder<Endpoint> endpointBuilder = (SAMLObjectBuilder<Endpoint>) builderFactory
diff --git a/idp-saml-impl/src/test/java/net/shibboleth/idp/saml/saml2/profile/impl/AddAuthnStatementToAssertionTest.java b/idp-saml-impl/src/test/java/net/shibboleth/idp/saml/saml2/profile/impl/AddAuthnStatementToAssertionTest.java
index d0bd2da..a692c01 100644
--- a/idp-saml-impl/src/test/java/net/shibboleth/idp/saml/saml2/profile/impl/AddAuthnStatementToAssertionTest.java
+++ b/idp-saml-impl/src/test/java/net/shibboleth/idp/saml/saml2/profile/impl/AddAuthnStatementToAssertionTest.java
@@ -17,6 +17,7 @@
 
 package net.shibboleth.idp.saml.saml2.profile.impl;
 
+import java.time.Instant;
 import java.util.Arrays;
 
 import javax.security.auth.Subject;
@@ -111,7 +112,7 @@ public class AddAuthnStatementToAssertionTest extends OpenSAMLInitBaseTestCase {
      * @throws InterruptedException 
      * @throws ComponentInitializationException */
     @Test public void testAddAuthenticationStatement() throws InterruptedException, ComponentInitializationException {
-        final long now = System.currentTimeMillis();
+        final Instant now = Instant.now();
         // this is here to allow the event's creation time to deviate from the 'start' time
         Thread.sleep(50);
         
@@ -144,7 +145,7 @@ public class AddAuthnStatementToAssertionTest extends OpenSAMLInitBaseTestCase {
         Assert.assertNotNull(assertion.getAuthnStatements().get(0));
 
         final AuthnStatement authenticationStatement = assertion.getAuthnStatements().get(0);
-        Assert.assertTrue(authenticationStatement.getAuthnInstant().getMillis() > now);
+        Assert.assertTrue(authenticationStatement.getAuthnInstant().isAfter(now));
         Assert.assertNotNull(authenticationStatement.getSessionIndex());
         Assert.assertNull(authenticationStatement.getSessionNotOnOrAfter());
 
diff --git a/idp-saml-impl/src/test/java/net/shibboleth/idp/saml/saml2/profile/impl/IdPInitiatedSSORequestMessageDecoderTest.java b/idp-saml-impl/src/test/java/net/shibboleth/idp/saml/saml2/profile/impl/IdPInitiatedSSORequestMessageDecoderTest.java
index 706f26d..39ffea8 100644
--- a/idp-saml-impl/src/test/java/net/shibboleth/idp/saml/saml2/profile/impl/IdPInitiatedSSORequestMessageDecoderTest.java
+++ b/idp-saml-impl/src/test/java/net/shibboleth/idp/saml/saml2/profile/impl/IdPInitiatedSSORequestMessageDecoderTest.java
@@ -20,8 +20,9 @@ package net.shibboleth.idp.saml.saml2.profile.impl;
 import net.shibboleth.idp.saml.profile.impl.BaseIdPInitiatedSSORequestMessageDecoder;
 import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
 
-import org.joda.time.DateTime;
-import org.joda.time.chrono.ISOChronology;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+
 import org.opensaml.core.xml.XMLObjectBaseTestCase;
 import org.opensaml.messaging.context.MessageContext;
 import org.opensaml.messaging.decoder.MessageDecodingException;
@@ -54,15 +55,13 @@ public class IdPInitiatedSSORequestMessageDecoderTest extends XMLObjectBaseTestC
     
     private String messageID;
     
-    private Long time;
+    private Instant time;
     
     @BeforeMethod
     public void setUp() throws ComponentInitializationException {
-        // Note: protocol takes time in seconds, so divide by 1000.
-        // Components usually produce milliseconds, so later multiply or divide by 1000 in assertions as appropriate.
-        time = System.currentTimeMillis()/1000;
+        time = Instant.now();
         
-        messageID = "_" + sessionID + "!" + time.toString();
+        messageID = "_" + sessionID + "!" + Long.toUnsignedString(time.getEpochSecond());
         
         request = new MockHttpServletRequest();
         request.setRequestedSessionId(sessionID);
@@ -77,7 +76,7 @@ public class IdPInitiatedSSORequestMessageDecoderTest extends XMLObjectBaseTestC
         request.addParameter(BaseIdPInitiatedSSORequestMessageDecoder.PROVIDER_ID_PARAM,  entityId);
         request.addParameter(BaseIdPInitiatedSSORequestMessageDecoder.SHIRE_PARAM,  acsUrl);
         request.addParameter(BaseIdPInitiatedSSORequestMessageDecoder.TARGET_PARAM,  relayState);
-        request.addParameter(BaseIdPInitiatedSSORequestMessageDecoder.TIME_PARAM,  time.toString());
+        request.addParameter(BaseIdPInitiatedSSORequestMessageDecoder.TIME_PARAM,  Long.toString(time.getEpochSecond()));
         
         decoder.decode();
         
@@ -88,7 +87,7 @@ public class IdPInitiatedSSORequestMessageDecoderTest extends XMLObjectBaseTestC
         
         Assert.assertEquals(authnRequest.getIssuer().getValue(), entityId, "Incorrect decoded message entityId value");
         Assert.assertEquals(authnRequest.getAssertionConsumerServiceURL(), acsUrl, "Incorrect decoded message ACS URL value");
-        Assert.assertEquals(authnRequest.getIssueInstant(), new DateTime(time*1000, ISOChronology.getInstanceUTC()),
+        Assert.assertEquals(authnRequest.getIssueInstant(), time.truncatedTo(ChronoUnit.SECONDS),
                 "Incorrect decoded message issue instant value");
         Assert.assertEquals(authnRequest.getID(), messageID, "Incorrect decoded message ID value");
         
@@ -96,14 +95,16 @@ public class IdPInitiatedSSORequestMessageDecoderTest extends XMLObjectBaseTestC
                 "Incorrect decoded entityId value in peer context");
         
         SAMLBindingContext bindingContext = messageContext.getSubcontext(SAMLBindingContext.class, true);
-        Assert.assertEquals(bindingContext.getRelayState(), relayState, "Incorrect decoded relay state value in binding context");
+        Assert.assertEquals(bindingContext.getRelayState(), relayState,
+                "Incorrect decoded relay state value in binding context");
         Assert.assertEquals(bindingContext.getBindingUri(), "urn:mace:shibboleth:2.0:profiles:AuthnRequest",
                 "Incorrect binding URI in binding context");
         
         SAMLMessageInfoContext msgInfoContext = messageContext.getSubcontext(SAMLMessageInfoContext.class, true);
-        Assert.assertEquals(msgInfoContext.getMessageIssueInstant(), new DateTime(time*1000, ISOChronology.getInstanceUTC()),
+        Assert.assertEquals(msgInfoContext.getMessageIssueInstant(), time.truncatedTo(ChronoUnit.SECONDS),
                 "Incorrect decoded issue instant value in message info context");
-        Assert.assertEquals(msgInfoContext.getMessageId(), messageID, "Incorrect decoded message ID value in message info context");
+        Assert.assertEquals(msgInfoContext.getMessageId(), messageID,
+                "Incorrect decoded message ID value in message info context");
     }
 
 }
diff --git a/idp-saml-impl/src/test/java/net/shibboleth/idp/saml/saml2/profile/impl/ProcessLogoutRequestTest.java b/idp-saml-impl/src/test/java/net/shibboleth/idp/saml/saml2/profile/impl/ProcessLogoutRequestTest.java
index 8c1ad30..8748816 100644
--- a/idp-saml-impl/src/test/java/net/shibboleth/idp/saml/saml2/profile/impl/ProcessLogoutRequestTest.java
+++ b/idp-saml-impl/src/test/java/net/shibboleth/idp/saml/saml2/profile/impl/ProcessLogoutRequestTest.java
@@ -17,6 +17,9 @@
 
 package net.shibboleth.idp.saml.saml2.profile.impl;
 
+import java.time.Duration;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
@@ -98,11 +101,11 @@ public class ProcessLogoutRequestTest extends SessionManagerBaseTestCase {
     protected void adjustProperties() throws ComponentInitializationException {
         sessionManager.setTrackSPSessions(true);
         sessionManager.setSecondaryServiceIndex(true);
-        sessionManager.setSessionSlop(900 * 60 * 1000);
+        sessionManager.setSessionSlop(Duration.ofSeconds(900 * 60));
         final SPSessionSerializerRegistry registry = new SPSessionSerializerRegistry();
         final Map<Class<? extends SPSession>,StorageSerializer<? extends SPSession>> mappings = new HashMap<>();
-        mappings.put(SAML1SPSession.class, new SAML1SPSessionSerializer(900 * 60 * 1000));
-        mappings.put(SAML2SPSession.class, new SAML2SPSessionSerializer(900 * 60 * 1000));
+        mappings.put(SAML1SPSession.class, new SAML1SPSessionSerializer(Duration.ofSeconds(900 * 60)));
+        mappings.put(SAML2SPSession.class, new SAML2SPSessionSerializer(Duration.ofSeconds(900 * 60)));
         registry.setMappings(mappings);
         registry.initialize();
         sessionManager.setSPSessionSerializerRegistry(registry);
@@ -162,8 +165,8 @@ public class ProcessLogoutRequestTest extends SessionManagerBaseTestCase {
         HttpServletRequestResponseContext.loadCurrent(new MockHttpServletRequest(), new MockHttpServletResponse());
         ((MockHttpServletRequest) HttpServletRequestResponseContext.getRequest()).setCookies(cookie);
         
-        final long creation = System.currentTimeMillis();
-        final long expiration = creation + 3600 * 60 * 1000;
+        final Instant creation = Instant.now();
+        final Instant expiration = creation.plusSeconds(3600);
         
         final IdPSession session = sessionManager.resolveSingle(new CriteriaSet(new HttpServletRequestCriterion()));
         Assert.assertNotNull(session);
@@ -190,8 +193,8 @@ public class ProcessLogoutRequestTest extends SessionManagerBaseTestCase {
         HttpServletRequestResponseContext.loadCurrent(new MockHttpServletRequest(), new MockHttpServletResponse());
         ((MockHttpServletRequest) HttpServletRequestResponseContext.getRequest()).setCookies(cookie);
         
-        final long creation = System.currentTimeMillis();
-        final long expiration = creation + 3600 * 60 * 1000;
+        final Instant creation = Instant.now();
+        final Instant expiration = creation.plusSeconds(3600);
         
         final IdPSession session = sessionManager.resolveSingle(new CriteriaSet(new HttpServletRequestCriterion()));
         Assert.assertNotNull(session);
@@ -227,8 +230,8 @@ public class ProcessLogoutRequestTest extends SessionManagerBaseTestCase {
         HttpServletRequestResponseContext.loadCurrent(new MockHttpServletRequest(), new MockHttpServletResponse());
         ((MockHttpServletRequest) HttpServletRequestResponseContext.getRequest()).setCookies(cookie);
         
-        final long creation = System.currentTimeMillis();
-        final long expiration = creation + 3600 * 60 * 1000;
+        final Instant creation = Instant.now();
+        final Instant expiration = creation.plusSeconds(3600);
         
         final IdPSession session = sessionManager.resolveSingle(new CriteriaSet(new HttpServletRequestCriterion()));
         Assert.assertNotNull(session);
@@ -266,8 +269,8 @@ public class ProcessLogoutRequestTest extends SessionManagerBaseTestCase {
         HttpServletRequestResponseContext.loadCurrent(new MockHttpServletRequest(), new MockHttpServletResponse());
         ((MockHttpServletRequest) HttpServletRequestResponseContext.getRequest()).setCookies(cookie);
         
-        final long creation = System.currentTimeMillis();
-        final long expiration = creation + 3600 * 60 * 1000;
+        final Instant creation = Instant.now();
+        final Instant expiration = creation.plusSeconds(3600);
         
         final IdPSession session = sessionManager.resolveSingle(new CriteriaSet(new HttpServletRequestCriterion()));
         Assert.assertNotNull(session);
@@ -303,8 +306,8 @@ public class ProcessLogoutRequestTest extends SessionManagerBaseTestCase {
         HttpServletRequestResponseContext.loadCurrent(new MockHttpServletRequest(), new MockHttpServletResponse());
         ((MockHttpServletRequest) HttpServletRequestResponseContext.getRequest()).setCookies(cookie);
         
-        final long creation = System.currentTimeMillis();
-        final long expiration = creation + 3600 * 60 * 1000;
+        final Instant creation = Instant.now();
+        final Instant expiration = creation.plusSeconds(3600);
         
         final IdPSession session = sessionManager.resolveSingle(new CriteriaSet(new HttpServletRequestCriterion()));
         Assert.assertNotNull(session);
@@ -331,8 +334,8 @@ public class ProcessLogoutRequestTest extends SessionManagerBaseTestCase {
         
         final SAML2SPSession sp = (SAML2SPSession) logoutCtx.getSessions(ActionTestingSupport.INBOUND_MSG_ISSUER + "/2").iterator().next();
         Assert.assertNotNull(sp);
-        Assert.assertEquals(sp.getCreationInstant(), creation);
-        Assert.assertEquals(sp.getExpirationInstant(), expiration);
+        Assert.assertEquals(sp.getCreationInstant(), creation.truncatedTo(ChronoUnit.MILLIS));
+        Assert.assertEquals(sp.getExpirationInstant(), expiration.truncatedTo(ChronoUnit.MILLIS));
         Assert.assertTrue(SAML2ObjectSupport.areNameIDsEquivalent(nameId2, sp.getNameID()));
         Assert.assertEquals(sp.getSessionIndex(), "index2");
         
@@ -349,8 +352,8 @@ public class ProcessLogoutRequestTest extends SessionManagerBaseTestCase {
         HttpServletRequestResponseContext.loadCurrent(new MockHttpServletRequest(), new MockHttpServletResponse());
         ((MockHttpServletRequest) HttpServletRequestResponseContext.getRequest()).setCookies(cookie);
         
-        final long creation = System.currentTimeMillis();
-        final long expiration = creation + 3600 * 60 * 1000;
+        final Instant creation = Instant.now();
+        final Instant expiration = creation.plusSeconds(3600);
         
         final IdPSession session = sessionManager.resolveSingle(new CriteriaSet(new HttpServletRequestCriterion()));
         Assert.assertNotNull(session);
@@ -378,8 +381,8 @@ public class ProcessLogoutRequestTest extends SessionManagerBaseTestCase {
         prc.getInboundMessageContext().setMessage(SAML2ActionTestingSupport.buildLogoutRequest(nameId));
         ((LogoutRequest) prc.getInboundMessageContext().getMessage()).getSessionIndexes().add(sessionIndex);
 
-        final long creation = System.currentTimeMillis();
-        final long expiration = creation + 3600 * 60 * 1000;
+        final Instant creation = Instant.now();
+        final Instant expiration = creation.plusSeconds(3600);
 
         HttpServletRequestResponseContext.loadCurrent(new MockHttpServletRequest(), new MockHttpServletResponse());
         ((MockHttpServletRequest) HttpServletRequestResponseContext.getRequest()).setCookies(cookie);
@@ -424,8 +427,8 @@ public class ProcessLogoutRequestTest extends SessionManagerBaseTestCase {
         final NameID nameId = SAML2ActionTestingSupport.buildNameID("joe");
         prc.getInboundMessageContext().setMessage(SAML2ActionTestingSupport.buildLogoutRequest(nameId));
 
-        final long creation = System.currentTimeMillis();
-        final long expiration = creation + 3600 * 60 * 1000;
+        final Instant creation = Instant.now();
+        final Instant expiration = creation.plusSeconds(3600);
 
         HttpServletRequestResponseContext.loadCurrent(new MockHttpServletRequest(), new MockHttpServletResponse());
         ((MockHttpServletRequest) HttpServletRequestResponseContext.getRequest()).setCookies(cookie);
diff --git a/idp-saml-impl/src/test/java/net/shibboleth/idp/saml/session/impl/PrepareInboundMessageContextTest.java b/idp-saml-impl/src/test/java/net/shibboleth/idp/saml/session/impl/PrepareInboundMessageContextTest.java
index 78c3524..0a9b222 100644
--- a/idp-saml-impl/src/test/java/net/shibboleth/idp/saml/session/impl/PrepareInboundMessageContextTest.java
+++ b/idp-saml-impl/src/test/java/net/shibboleth/idp/saml/session/impl/PrepareInboundMessageContextTest.java
@@ -24,6 +24,8 @@ import net.shibboleth.idp.saml.session.SAML2SPSession;
 import net.shibboleth.idp.session.context.LogoutPropagationContext;
 import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
 
+import java.time.Instant;
+
 import org.opensaml.core.OpenSAMLInitBaseTestCase;
 import org.opensaml.profile.action.EventIds;
 import org.opensaml.profile.context.ProfileRequestContext;
@@ -49,8 +51,8 @@ public class PrepareInboundMessageContextTest extends OpenSAMLInitBaseTestCase {
         prc = new WebflowRequestContextProfileRequestContextLookup().apply(src);
         prc.setInboundMessageContext(null);
                
-        final SAML2SPSession session = new SAML2SPSession("https://sp.example.org", System.currentTimeMillis(),
-                System.currentTimeMillis() + 1800000, SAML2ActionTestingSupport.buildNameID("jdoe"), "foo");
+        final SAML2SPSession session = new SAML2SPSession("https://sp.example.org", Instant.now(),
+                Instant.now().plusSeconds(1800), SAML2ActionTestingSupport.buildNameID("jdoe"), "foo");
         prc.getSubcontext(LogoutPropagationContext.class, true).setSession(session);
         
         action = new PrepareInboundMessageContext();
diff --git a/idp-saml-impl/src/test/java/net/shibboleth/idp/saml/session/impl/SAML2SPSessionSerializerTest.java b/idp-saml-impl/src/test/java/net/shibboleth/idp/saml/session/impl/SAML2SPSessionSerializerTest.java
index 083d9cc..eeccd9b 100644
--- a/idp-saml-impl/src/test/java/net/shibboleth/idp/saml/session/impl/SAML2SPSessionSerializerTest.java
+++ b/idp-saml-impl/src/test/java/net/shibboleth/idp/saml/session/impl/SAML2SPSessionSerializerTest.java
@@ -21,6 +21,8 @@ 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 net.shibboleth.idp.saml.session.SAML2SPSession;
 
@@ -36,7 +38,7 @@ public class SAML2SPSessionSerializerTest extends OpenSAMLInitBaseTestCase {
 
     private static final String DATAPATH = "/net/shibboleth/idp/saml/impl/session/";
     
-    private static final long INSTANT = 1378827849463L;
+    private static final Instant INSTANT = Instant.ofEpochMilli(1378827849463L);
     
     private static final String SESSION_INDEX = "1234567890";
     
@@ -47,33 +49,33 @@ public class SAML2SPSessionSerializerTest extends OpenSAMLInitBaseTestCase {
     private SAML2SPSessionSerializer serializer;
     
     @BeforeMethod public void setUp() {
-        serializer = new SAML2SPSessionSerializer(0);
+        serializer = new SAML2SPSessionSerializer(Duration.ofMillis(0));
     }
 
     @Test public void testInvalid() throws Exception {
         try {
-            serializer.deserialize(1, CONTEXT, KEY, fileToString(DATAPATH + "noNameID.json"), INSTANT);
+            serializer.deserialize(1, CONTEXT, KEY, fileToString(DATAPATH + "noNameID.json"), INSTANT.toEpochMilli());
             Assert.fail();
         } catch (IOException e) {
             
         }
         
         try {
-            serializer.deserialize(1, CONTEXT, KEY, fileToString(DATAPATH + "noSessionIndex.json"), INSTANT);
+            serializer.deserialize(1, CONTEXT, KEY, fileToString(DATAPATH + "noSessionIndex.json"), INSTANT.toEpochMilli());
             Assert.fail();
         } catch (IOException e) {
             
         }
 
         try {
-            serializer.deserialize(1, CONTEXT, KEY, fileToString(DATAPATH + "invalidXML.json"), INSTANT);
+            serializer.deserialize(1, CONTEXT, KEY, fileToString(DATAPATH + "invalidXML.json"), INSTANT.toEpochMilli());
             Assert.fail();
         } catch (IOException e) {
             
         }
 
         try {
-            serializer.deserialize(1, CONTEXT, KEY, fileToString(DATAPATH + "invalidNameID.json"), INSTANT);
+            serializer.deserialize(1, CONTEXT, KEY, fileToString(DATAPATH + "invalidNameID.json"), INSTANT.toEpochMilli());
             Assert.fail();
         } catch (IOException e) {
             
@@ -81,7 +83,7 @@ public class SAML2SPSessionSerializerTest extends OpenSAMLInitBaseTestCase {
     }
     
     @Test public void testValid() throws Exception {
-        long exp = INSTANT + 60000L;
+        final Instant exp = INSTANT.plusSeconds(60);
         
         NameID nameID = (NameID) XMLObjectSupport.buildXMLObject(NameID.DEFAULT_ELEMENT_NAME);
         nameID.setValue("joe at example.org");
@@ -92,7 +94,7 @@ public class SAML2SPSessionSerializerTest extends OpenSAMLInitBaseTestCase {
         String s2 = fileToString(DATAPATH + "saml2SPSession.json");
         Assert.assertEquals(s, s2);
         
-        SAML2SPSession session2 = (SAML2SPSession) serializer.deserialize(1, CONTEXT, KEY, s2, exp);
+        SAML2SPSession session2 = (SAML2SPSession) serializer.deserialize(1, CONTEXT, KEY, s2, exp.toEpochMilli());
 
         Assert.assertEquals(session.getId(), session2.getId());
         Assert.assertEquals(session.getCreationInstant(), session2.getCreationInstant());
diff --git a/idp-session-api/src/main/java/net/shibboleth/idp/session/AbstractIdPSession.java b/idp-session-api/src/main/java/net/shibboleth/idp/session/AbstractIdPSession.java
index ce04c74..a32d41e 100644
--- a/idp-session-api/src/main/java/net/shibboleth/idp/session/AbstractIdPSession.java
+++ b/idp-session-api/src/main/java/net/shibboleth/idp/session/AbstractIdPSession.java
@@ -17,6 +17,7 @@
 
 package net.shibboleth.idp.session;
 
+import java.time.Instant;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
@@ -28,17 +29,14 @@ import javax.annotation.Nullable;
 import javax.annotation.concurrent.ThreadSafe;
 
 import net.shibboleth.idp.authn.AuthenticationResult;
-import net.shibboleth.utilities.java.support.annotation.Duration;
 import net.shibboleth.utilities.java.support.annotation.constraint.Live;
 import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
 import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
 import net.shibboleth.utilities.java.support.annotation.constraint.NotLive;
-import net.shibboleth.utilities.java.support.annotation.constraint.Positive;
 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;
 
-import org.joda.time.DateTime;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -83,10 +81,10 @@ public abstract class AbstractIdPSession implements IdPSession {
     @Nonnull @NotEmpty private final String principalName;
 
     /** Time, in milliseconds since the epoch, when this session was created. */
-    @Duration private final long creationInstant;
+    @Nonnull private final Instant creationInstant;
 
     /** Last activity instant, in milliseconds since the epoch, for this session. */
-    @Duration private long lastActivityInstant;
+    @Nonnull private Instant lastActivityInstant;
 
     /** Addresses to which the session is bound. */
     @Nullable private String ipV4Address;
@@ -108,12 +106,12 @@ public abstract class AbstractIdPSession implements IdPSession {
      * @param creationTime creation time of session in milliseconds
      */
     public AbstractIdPSession(@Nonnull @NotEmpty final String sessionId, @Nonnull @NotEmpty final String canonicalName,
-            @Positive final long creationTime) {
+            @Nonnull final Instant creationTime) {
         id = Constraint.isNotNull(StringSupport.trimOrNull(sessionId), "Session ID cannot be null or empty");
         principalName = Constraint.isNotNull(StringSupport.trimOrNull(canonicalName),
                 "Principal name cannot be null or empty.");
         
-        creationInstant = Constraint.isGreaterThan(0, creationTime, "Creation time must be greater than 0");
+        creationInstant = Constraint.isNotNull(creationTime, "Creation time cannot be null");
         lastActivityInstant = creationTime;
 
         authenticationResults = new ConcurrentHashMap(5);
@@ -121,25 +119,22 @@ public abstract class AbstractIdPSession implements IdPSession {
     }
 
     /** {@inheritDoc} */
-    @Override
     @Nonnull @NotEmpty public String getId() {
         return id;
     }
     
     /** {@inheritDoc} */
-    @Override
     @Nonnull @NotEmpty public String getPrincipalName() {
         return principalName;
     }
 
     /** {@inheritDoc} */
-    @Override
-    public long getCreationInstant() {
+    @Nonnull public Instant getCreationInstant() {
         return creationInstant;
     }
 
     /** {@inheritDoc} */
-    @Override @Duration public long getLastActivityInstant() {
+    @Nonnull public Instant getLastActivityInstant() {
         return lastActivityInstant;
     }
 
@@ -149,7 +144,7 @@ public abstract class AbstractIdPSession implements IdPSession {
      * @param instant last activity instant, in milliseconds since the epoch, for the session, must be greater than 0
      * @throws SessionException if an error occurs updating the session
      */
-    @Duration public void setLastActivityInstant(@Duration @Positive final long instant) throws SessionException {
+    public void setLastActivityInstant(@Nonnull final Instant instant) throws SessionException {
         doSetLastActivityInstant(instant);
     }
     
@@ -161,8 +156,8 @@ public abstract class AbstractIdPSession implements IdPSession {
      * 
      * @param instant last activity instant, in milliseconds since the epoch, for the session, must be greater than 0
      */
-    @Duration public void doSetLastActivityInstant(@Duration @Positive final long instant) {
-        lastActivityInstant = Constraint.isGreaterThan(0, instant, "Last activity instant must be greater than 0");
+    public void doSetLastActivityInstant(@Nonnull final Instant instant) {
+        lastActivityInstant = Constraint.isNotNull(instant, "Last activity instant cannot be null");
     }
 
     /**
@@ -219,27 +214,23 @@ public abstract class AbstractIdPSession implements IdPSession {
     }
 
     /** {@inheritDoc} */
-    @Override
     @Nonnull @NonnullElements @NotLive @Unmodifiable public Set<AuthenticationResult> getAuthenticationResults() {
         return ImmutableSet.copyOf(Optional.presentInstances(authenticationResults.values()));
     }
 
     /** {@inheritDoc} */
-    @Override
     @Nullable public AuthenticationResult getAuthenticationResult(@Nonnull @NotEmpty final String flowId) {
         final Optional<AuthenticationResult> mapped = authenticationResults.get(StringSupport.trimOrNull(flowId));
         return (mapped != null) ? mapped.orNull() : null;
     }
 
     /** {@inheritDoc} */
-    @Override
     @Nullable public AuthenticationResult addAuthenticationResult(@Nonnull final AuthenticationResult result)
             throws SessionException {
         return doAddAuthenticationResult(result);
     }
 
     /** {@inheritDoc} */
-    @Override
     public boolean removeAuthenticationResult(@Nonnull final AuthenticationResult result) throws SessionException {
         return doRemoveAuthenticationResult(result);
     }
@@ -388,14 +379,12 @@ public abstract class AbstractIdPSession implements IdPSession {
     }
     
     /** {@inheritDoc} */
-    @Override
     public boolean checkTimeout() throws SessionException {
-        setLastActivityInstant(System.currentTimeMillis());
+        setLastActivityInstant(Instant.now());
         return true;
     }
 
     /** {@inheritDoc} */
-    @Override
     public boolean equals(final Object obj) {
         if (obj == null) {
             return false;
@@ -413,18 +402,16 @@ public abstract class AbstractIdPSession implements IdPSession {
     }
 
     /** {@inheritDoc} */
-    @Override
     public int hashCode() {
         return id.hashCode();
     }
 
     /** {@inheritDoc} */
-    @Override
     public String toString() {
         return MoreObjects.toStringHelper(this).add("sessionId", id).add("principalName", principalName)
                 .add("IPv4", ipV4Address).add("IPv6", ipV6Address)
-                .add("creationInstant", new DateTime(creationInstant))
-                .add("lastActivityInstant", new DateTime(lastActivityInstant))
+                .add("creationInstant", creationInstant)
+                .add("lastActivityInstant", lastActivityInstant)
                 .add("authenticationResults", getAuthenticationResults()).add("spSessions", getSPSessions())
                 .toString();
     }
diff --git a/idp-session-api/src/main/java/net/shibboleth/idp/session/AbstractSPSessionSerializer.java b/idp-session-api/src/main/java/net/shibboleth/idp/session/AbstractSPSessionSerializer.java
index cfa7c51..9c6175c 100644
--- a/idp-session-api/src/main/java/net/shibboleth/idp/session/AbstractSPSessionSerializer.java
+++ b/idp-session-api/src/main/java/net/shibboleth/idp/session/AbstractSPSessionSerializer.java
@@ -20,6 +20,8 @@ package net.shibboleth.idp.session;
 import java.io.IOException;
 import java.io.StringReader;
 import java.io.StringWriter;
+import java.time.Duration;
+import java.time.Instant;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -31,10 +33,7 @@ import javax.json.JsonReader;
 import javax.json.JsonStructure;
 import javax.json.stream.JsonGenerator;
 
-import net.shibboleth.utilities.java.support.annotation.Duration;
-import net.shibboleth.utilities.java.support.annotation.constraint.NonNegative;
 import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
-import net.shibboleth.utilities.java.support.annotation.constraint.Positive;
 import net.shibboleth.utilities.java.support.component.AbstractInitializableComponent;
 import net.shibboleth.utilities.java.support.logic.Constraint;
 
@@ -59,26 +58,25 @@ public abstract class AbstractSPSessionSerializer extends AbstractInitializableC
     @Nonnull private final Logger log = LoggerFactory.getLogger(AbstractSPSessionSerializer.class);
 
     /** Milliseconds to subtract from record expiration to establish session expiration value. */
-    @Duration @NonNegative private final long expirationOffset;
+    @Nonnull private final Duration expirationOffset;
     
     /**
      * Constructor.
      * 
-     * @param offset milliseconds to subtract from record expiration to establish session expiration value
+     * @param offset time to subtract from record expiration to establish session expiration value
      */
-    protected AbstractSPSessionSerializer(@Duration @NonNegative final long offset) {
-        expirationOffset = Constraint.isGreaterThanOrEqual(0, offset, "Offset must be greater than or equal to zero");
+    protected AbstractSPSessionSerializer(@Nonnull final Duration offset) {
+        expirationOffset = Constraint.isNotNull(offset, "Offset cannot be null");
     }
 
     /** {@inheritDoc} */
-    @Override
     @Nonnull @NotEmpty public String serialize(@Nonnull final SPSession instance) throws IOException {
         try {
             final StringWriter sink = new StringWriter(128);
             final JsonGenerator gen = Json.createGenerator(sink);
             gen.writeStartObject()
                 .write(SERVICE_ID_FIELD, instance.getId())
-                .write(CREATION_INSTANT_FIELD, instance.getCreationInstant());
+                .write(CREATION_INSTANT_FIELD, instance.getCreationInstant().toEpochMilli());
             
             doSerializeAdditional(instance, gen);
             
@@ -92,7 +90,6 @@ public abstract class AbstractSPSessionSerializer extends AbstractInitializableC
     }
 
     /** {@inheritDoc} */
-    @Override
     @Nonnull public SPSession deserialize(final long version, @Nonnull @NotEmpty final String context,
             @Nonnull @NotEmpty final String key, @Nonnull @NotEmpty final String value, @Nullable final Long expiration)
                     throws IOException {
@@ -110,9 +107,9 @@ public abstract class AbstractSPSessionSerializer extends AbstractInitializableC
             final JsonObject obj = (JsonObject) st;
             
             final String serviceId = obj.getString(SERVICE_ID_FIELD);
-            final long creation = obj.getJsonNumber(CREATION_INSTANT_FIELD).longValueExact();
+            final Instant creation = Instant.ofEpochMilli(obj.getJsonNumber(CREATION_INSTANT_FIELD).longValueExact());
 
-            return doDeserialize(obj, serviceId, creation, expiration - expirationOffset);
+            return doDeserialize(obj, serviceId, creation, Instant.ofEpochMilli(expiration).minus(expirationOffset));
             
         } catch (final NullPointerException | ClassCastException | ArithmeticException | JsonException e) {
             log.error("Exception while parsing SPSession", e);
@@ -148,8 +145,8 @@ public abstract class AbstractSPSessionSerializer extends AbstractInitializableC
      * @return the newly constructed object
      * @throws IOException if an error occurs during deserialization
      */
-    @Nonnull @Duration protected abstract SPSession doDeserialize(@Nonnull final JsonObject obj,
-            @Nonnull @NotEmpty final String id, @Duration @Positive final long creation,
-            @Duration @Positive final long expiration) throws IOException;
+    @Nonnull protected abstract SPSession doDeserialize(@Nonnull final JsonObject obj,
+            @Nonnull @NotEmpty final String id, @Nonnull final Instant creation,
+            @Nonnull final Instant expiration) throws IOException;
     
 }
\ No newline at end of file
diff --git a/idp-session-api/src/main/java/net/shibboleth/idp/session/BasicSPSession.java b/idp-session-api/src/main/java/net/shibboleth/idp/session/BasicSPSession.java
index 7624f17..1cece70 100644
--- a/idp-session-api/src/main/java/net/shibboleth/idp/session/BasicSPSession.java
+++ b/idp-session-api/src/main/java/net/shibboleth/idp/session/BasicSPSession.java
@@ -17,20 +17,17 @@
 
 package net.shibboleth.idp.session;
 
+import java.time.Instant;
 import java.util.Objects;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.ThreadSafe;
 
-import net.shibboleth.utilities.java.support.annotation.Duration;
 import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
-import net.shibboleth.utilities.java.support.annotation.constraint.Positive;
 import net.shibboleth.utilities.java.support.logic.Constraint;
 import net.shibboleth.utilities.java.support.primitive.StringSupport;
 
-import org.joda.time.DateTime;
-
 import com.google.common.base.MoreObjects;
 
 /**
@@ -43,10 +40,10 @@ public class BasicSPSession implements SPSession {
     @Nonnull @NotEmpty private final String serviceId;
     
     /** The time, in milliseconds since the epoch, when this session was created. */
-    @Duration @Positive private final long creationInstant;
+    @Nonnull private final Instant creationInstant;
 
     /** The time, in milliseconds since the epoch, when this session expires. */
-    @Duration @Positive private final long expirationInstant;
+    @Nonnull private final Instant expirationInstant;
     
     /**
      * Constructor.
@@ -55,44 +52,40 @@ public class BasicSPSession implements SPSession {
      * @param creation creation time of session, in milliseconds since the epoch
      * @param expiration expiration time of session, in milliseconds since the epoch
      */
-    public BasicSPSession(@Nonnull @NotEmpty final String id, @Duration @Positive final long creation,
-            @Duration @Positive final long expiration) {
+    public BasicSPSession(@Nonnull @NotEmpty final String id, @Nonnull final Instant creation,
+            @Nonnull final Instant expiration) {
         serviceId = Constraint.isNotNull(StringSupport.trimOrNull(id), "Service ID cannot be null nor empty");
-        creationInstant = Constraint.isGreaterThan(0, creation, "Creation instant must be greater than 0");
-        expirationInstant = Constraint.isGreaterThan(0, expiration, "Expiration instant must be greater than 0");
+        creationInstant = Constraint.isNotNull(creation, "Creation instant cannot be null");
+        expirationInstant = Constraint.isNotNull(expiration, "Expiration instant cannot be null");
     }
     
     /** {@inheritDoc} */
-    @Override
     @Nonnull @NotEmpty public String getId() {
         return serviceId;
     }
     
     /** {@inheritDoc} */
-    @Override @Duration @Positive public long getCreationInstant() {
+    @Nonnull public Instant getCreationInstant() {
         return creationInstant;
     }
 
     /** {@inheritDoc} */
-    @Override @Duration @Positive public long getExpirationInstant() {
+    @Nonnull public Instant getExpirationInstant() {
         return expirationInstant;
     }
 
     /** {@inheritDoc} */
-    @Override
     public String getSPSessionKey() {
         // A basic session doesn't have a secondary lookup key.
         return null;
     }
     
     /** {@inheritDoc} */
-    @Override
     public int hashCode() {
         return serviceId.hashCode();
     }
 
     /** {@inheritDoc} */
-    @Override
     public boolean equals(@Nullable final Object obj) {
         if (obj == null) {
             return false;
@@ -110,11 +103,10 @@ public class BasicSPSession implements SPSession {
     }
 
     /** {@inheritDoc} */
-    @Override
     public String toString() {
         return MoreObjects.toStringHelper(this).add("id", serviceId)
-                .add("creationInstant", new DateTime(creationInstant))
-                .add("expirationInstant", new DateTime(expirationInstant)).toString();
+                .add("creationInstant", creationInstant)
+                .add("expirationInstant", expirationInstant).toString();
     }
     
 }
\ No newline at end of file
diff --git a/idp-session-api/src/main/java/net/shibboleth/idp/session/IdPSession.java b/idp-session-api/src/main/java/net/shibboleth/idp/session/IdPSession.java
index 53f6c73..56092e5 100644
--- a/idp-session-api/src/main/java/net/shibboleth/idp/session/IdPSession.java
+++ b/idp-session-api/src/main/java/net/shibboleth/idp/session/IdPSession.java
@@ -17,6 +17,7 @@
 
 package net.shibboleth.idp.session;
 
+import java.time.Instant;
 import java.util.Set;
 
 import javax.annotation.Nonnull;
@@ -27,7 +28,6 @@ import net.shibboleth.idp.authn.AuthenticationResult;
 import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
 import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
 import net.shibboleth.utilities.java.support.annotation.constraint.NotLive;
-import net.shibboleth.utilities.java.support.annotation.constraint.Positive;
 import net.shibboleth.utilities.java.support.annotation.constraint.Unmodifiable;
 import net.shibboleth.utilities.java.support.component.IdentifiedComponent;
 
@@ -53,14 +53,14 @@ public interface IdPSession extends IdentifiedComponent {
      * 
      * @return time this session was created, never less than 0
      */
-    @Positive public long getCreationInstant();
+    @Nonnull public Instant getCreationInstant();
     
     /**
      * Get the last activity instant, in milliseconds since the epoch, for the session.
      * 
      * @return last activity instant, in milliseconds since the epoch, for the session, never less than 0
      */
-    @Positive public long getLastActivityInstant();
+    @Nonnull public Instant getLastActivityInstant();
     
     /**
      * Test the session's validity based on the supplied client address, possibly binding it
diff --git a/idp-session-api/src/main/java/net/shibboleth/idp/session/SPSession.java b/idp-session-api/src/main/java/net/shibboleth/idp/session/SPSession.java
index 9d988c1..686235c 100644
--- a/idp-session-api/src/main/java/net/shibboleth/idp/session/SPSession.java
+++ b/idp-session-api/src/main/java/net/shibboleth/idp/session/SPSession.java
@@ -17,10 +17,12 @@
 
 package net.shibboleth.idp.session;
 
+import java.time.Instant;
+
+import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.ThreadSafe;
 
-import net.shibboleth.utilities.java.support.annotation.constraint.Positive;
 import net.shibboleth.utilities.java.support.component.IdentifiedComponent;
 
 /**
@@ -37,14 +39,14 @@ public interface SPSession extends IdentifiedComponent {
      * 
      * @return time, in milliseconds since the epoch, when this session was created, never less than 0
      */
-    @Positive long getCreationInstant();
+    @Nonnull Instant getCreationInstant();
 
     /**
      * Get the time, in milliseconds since the epoch, when this session will expire.
      * 
      * @return time, in milliseconds since the epoch, when this session will expire, never less than 0
      */
-    @Positive long getExpirationInstant();
+    @Nonnull Instant getExpirationInstant();
     
     /**
      * Get a unique key identifying this subject's session with the service.
diff --git a/idp-session-api/src/test/java/net/shibboleth/idp/session/BasicSPSessionTest.java b/idp-session-api/src/test/java/net/shibboleth/idp/session/BasicSPSessionTest.java
index b40f35f..6d19b76 100644
--- a/idp-session-api/src/test/java/net/shibboleth/idp/session/BasicSPSessionTest.java
+++ b/idp-session-api/src/test/java/net/shibboleth/idp/session/BasicSPSessionTest.java
@@ -19,6 +19,8 @@ package net.shibboleth.idp.session;
 
 import net.shibboleth.utilities.java.support.logic.ConstraintViolationException;
 
+import java.time.Instant;
+
 import org.testng.Assert;
 import org.testng.annotations.Test;
 
@@ -27,45 +29,32 @@ public class BasicSPSessionTest {
 
     /** Tests that everything is properly initialized during object construction. */
     @Test public void testInstantiation() throws Exception {
-        long start = System.currentTimeMillis();
+        final Instant start = Instant.now();
         // this is here to allow the event's creation time to deviate from the 'start' time
         Thread.sleep(50);
 
-        BasicSPSession session = new BasicSPSession("test", System.currentTimeMillis(),
-                System.currentTimeMillis() + 60000L);
+        BasicSPSession session = new BasicSPSession("test", Instant.now(),
+                Instant.now().plusSeconds(60));
         Assert.assertEquals(session.getId(), "test");
-        Assert.assertTrue(session.getCreationInstant() > start);
-        Assert.assertTrue(session.getExpirationInstant() > session.getCreationInstant());
-
-        try {
-            new BasicSPSession(null, 0, 0);
-            Assert.fail();
-        } catch (ConstraintViolationException e) {
-
-        }
+        Assert.assertTrue(session.getCreationInstant().isAfter(start));
+        Assert.assertTrue(session.getExpirationInstant().isAfter(session.getCreationInstant()));
 
         try {
-            new BasicSPSession("", 0, 0);
+            new BasicSPSession(null, Instant.ofEpochMilli(0), Instant.ofEpochMilli(0));
             Assert.fail();
         } catch (ConstraintViolationException e) {
 
         }
 
         try {
-            new BasicSPSession("  ", 0, 0);
-            Assert.fail();
-        } catch (ConstraintViolationException e) {
-
-        }
-        try {
-            new BasicSPSession("foo", 0, 0);
+            new BasicSPSession("", Instant.ofEpochMilli(0), Instant.ofEpochMilli(0));
             Assert.fail();
         } catch (ConstraintViolationException e) {
 
         }
 
         try {
-            new BasicSPSession("foo", start, 0);
+            new BasicSPSession("  ", Instant.ofEpochMilli(0), Instant.ofEpochMilli(0));
             Assert.fail();
         } catch (ConstraintViolationException e) {
 
diff --git a/idp-session-api/src/test/java/net/shibboleth/idp/session/IdPSessionTest.java b/idp-session-api/src/test/java/net/shibboleth/idp/session/IdPSessionTest.java
index 92d0d7a..7162d2d 100644
--- a/idp-session-api/src/test/java/net/shibboleth/idp/session/IdPSessionTest.java
+++ b/idp-session-api/src/test/java/net/shibboleth/idp/session/IdPSessionTest.java
@@ -21,6 +21,8 @@ import net.shibboleth.idp.authn.AuthenticationResult;
 import net.shibboleth.idp.authn.principal.UsernamePrincipal;
 import net.shibboleth.utilities.java.support.logic.ConstraintViolationException;
 
+import java.time.Instant;
+
 import org.testng.Assert;
 import org.testng.annotations.Test;
 
@@ -29,13 +31,13 @@ public class IdPSessionTest {
 
     /** Tests that everything is properly initialized during object construction. */
     @Test public void testInstantiation() throws Exception {
-        long start = System.currentTimeMillis();
+        final Instant start = Instant.now();
         Thread.sleep(50);
 
         AbstractIdPSession session = new DummyIdPSession("test", "foo");
         Assert.assertNotNull(session.getAuthenticationResults());
         Assert.assertFalse(session.getAuthenticationResults().iterator().hasNext());
-        Assert.assertTrue(session.getCreationInstant() > start);
+        Assert.assertTrue(session.getCreationInstant().isAfter(start));
         Assert.assertEquals(session.getId(), "test");
         Assert.assertEquals(session.getPrincipalName(), "foo");
         Assert.assertEquals(session.getLastActivityInstant(), session.getCreationInstant());
@@ -75,12 +77,12 @@ public class IdPSessionTest {
     @Test public void testLastActivityInstant() throws Exception {
         AbstractIdPSession session = new DummyIdPSession("test", "foo");
 
-        long now = System.currentTimeMillis();
+        final Instant now = Instant.now();
         // this is here to allow the event's last activity time to deviate from the time 'now'
         Thread.sleep(50);
 
-        session.setLastActivityInstant(System.currentTimeMillis());
-        Assert.assertTrue(session.getLastActivityInstant() > now);
+        session.setLastActivityInstant(Instant.now());
+        Assert.assertTrue(session.getLastActivityInstant().isAfter(now));
 
         session.setLastActivityInstant(now);
         Assert.assertEquals(session.getLastActivityInstant(), now);
@@ -101,8 +103,8 @@ public class IdPSessionTest {
     /** Tests adding service sessions. 
      * @throws SessionException */
     @Test public void testAddSPSessions() throws SessionException {
-        long now = System.currentTimeMillis();
-        long exp = now + 60000L;
+        final Instant now = Instant.now();
+        final Instant exp = now.plusSeconds(60);
         
         BasicSPSession svcSession1 = new BasicSPSession("svc1", now, exp);
         BasicSPSession svcSession2 = new BasicSPSession("svc2", now, exp);
@@ -152,8 +154,8 @@ public class IdPSessionTest {
     /** Tests removing service sessions. 
      * @throws SessionException */
     @Test public void testRemoveSPSession() throws SessionException {
-        long now = System.currentTimeMillis();
-        long exp = now + 60000L;
+        final Instant now = Instant.now();
+        final Instant exp = now.plusSeconds(60);
 
         BasicSPSession svcSession1 = new BasicSPSession("svc1", now, exp);
         BasicSPSession svcSession2 = new BasicSPSession("svc2", now, exp);
@@ -231,7 +233,7 @@ public class IdPSessionTest {
          * @param canonicalName
          */
         public DummyIdPSession(String sessionId, String canonicalName) {
-            super(sessionId, canonicalName, System.currentTimeMillis());
+            super(sessionId, canonicalName, Instant.now());
         }
 
         /** {@inheritDoc} */
diff --git a/idp-session-impl/pom.xml b/idp-session-impl/pom.xml
index 1122d89..6b5b8d4 100644
--- a/idp-session-impl/pom.xml
+++ b/idp-session-impl/pom.xml
@@ -44,10 +44,6 @@
         </dependency>
 
         <dependency>
-            <groupId>joda-time</groupId>
-            <artifactId>joda-time</artifactId>
-        </dependency>
-        <dependency>
             <groupId>javax.json</groupId>
             <artifactId>javax.json-api</artifactId>
         </dependency>
diff --git a/idp-session-impl/src/main/java/net/shibboleth/idp/session/impl/BasicSPSessionCreationStrategy.java b/idp-session-impl/src/main/java/net/shibboleth/idp/session/impl/BasicSPSessionCreationStrategy.java
index a6eac10..400c89d 100644
--- a/idp-session-impl/src/main/java/net/shibboleth/idp/session/impl/BasicSPSessionCreationStrategy.java
+++ b/idp-session-impl/src/main/java/net/shibboleth/idp/session/impl/BasicSPSessionCreationStrategy.java
@@ -17,6 +17,8 @@
 
 package net.shibboleth.idp.session.impl;
 
+import java.time.Duration;
+import java.time.Instant;
 import java.util.function.Function;
 
 import javax.annotation.Nonnull;
@@ -30,8 +32,6 @@ import org.slf4j.LoggerFactory;
 import net.shibboleth.idp.profile.context.RelyingPartyContext;
 import net.shibboleth.idp.session.BasicSPSession;
 import net.shibboleth.idp.session.SPSession;
-import net.shibboleth.utilities.java.support.annotation.Duration;
-import net.shibboleth.utilities.java.support.annotation.constraint.Positive;
 import net.shibboleth.utilities.java.support.logic.Constraint;
 
 /**
@@ -49,7 +49,7 @@ public class BasicSPSessionCreationStrategy implements Function<ProfileRequestCo
     @Nonnull private final Logger log = LoggerFactory.getLogger(BasicSPSessionCreationStrategy.class);
     
     /** Lifetime of sessions to create. */
-    @Positive @Duration private final long sessionLifetime;
+    @Nonnull private final Duration sessionLifetime;
 
     /** RelyingPartyContext lookup strategy. */
     @Nonnull private Function<ProfileRequestContext,RelyingPartyContext> relyingPartyContextLookupStrategy;
@@ -59,8 +59,8 @@ public class BasicSPSessionCreationStrategy implements Function<ProfileRequestCo
      * 
      * @param lifetime lifetime in milliseconds, determines expiration of {@link SPSession} to be created
      */
-    public BasicSPSessionCreationStrategy(@Positive @Duration final long lifetime) {
-        sessionLifetime = Constraint.isGreaterThan(0, lifetime, "Lifetime must be greater than 0");
+    public BasicSPSessionCreationStrategy(@Nonnull final Duration lifetime) {
+        sessionLifetime = Constraint.isNotNull(lifetime, "Lifetime cannot be null");
         relyingPartyContextLookupStrategy = new ChildContextLookup<>(RelyingPartyContext.class);
     }
 
@@ -90,8 +90,8 @@ public class BasicSPSessionCreationStrategy implements Function<ProfileRequestCo
             return null;
         }
         
-        final long now = System.currentTimeMillis();
-        return new BasicSPSession(issuer, now, now + sessionLifetime);
+        final Instant now = Instant.now();
+        return new BasicSPSession(issuer, now, now.plus(sessionLifetime));
     }
 
 }
\ No newline at end of file
diff --git a/idp-session-impl/src/main/java/net/shibboleth/idp/session/impl/BasicSPSessionSerializer.java b/idp-session-impl/src/main/java/net/shibboleth/idp/session/impl/BasicSPSessionSerializer.java
index caab2f0..30295e7 100644
--- a/idp-session-impl/src/main/java/net/shibboleth/idp/session/impl/BasicSPSessionSerializer.java
+++ b/idp-session-impl/src/main/java/net/shibboleth/idp/session/impl/BasicSPSessionSerializer.java
@@ -18,6 +18,8 @@
 package net.shibboleth.idp.session.impl;
 
 import java.io.IOException;
+import java.time.Duration;
+import java.time.Instant;
 
 import javax.annotation.Nonnull;
 import javax.annotation.concurrent.ThreadSafe;
@@ -26,11 +28,8 @@ import javax.json.JsonObject;
 import net.shibboleth.idp.session.AbstractSPSessionSerializer;
 import net.shibboleth.idp.session.BasicSPSession;
 import net.shibboleth.idp.session.SPSession;
-import net.shibboleth.utilities.java.support.annotation.Duration;
 import net.shibboleth.utilities.java.support.annotation.ParameterName;
-import net.shibboleth.utilities.java.support.annotation.constraint.NonNegative;
 import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
-import net.shibboleth.utilities.java.support.annotation.constraint.Positive;
 
 /**
  * A serializer for {@link BasicSPSession} objects.
@@ -43,15 +42,15 @@ public class BasicSPSessionSerializer extends AbstractSPSessionSerializer {
      * 
      * @param offset milliseconds to subtract from record expiration to establish session expiration value
      */
-    public BasicSPSessionSerializer(@Duration @NonNegative @ParameterName(name="offset") final long offset) {
+    public BasicSPSessionSerializer(@Nonnull @ParameterName(name="offset") final Duration offset) {
         super(offset);
     }
 
     /** {@inheritDoc} */
     @Override 
     @Nonnull protected SPSession doDeserialize(@Nonnull final JsonObject obj,
-            @Nonnull @NotEmpty final String id, @Duration @Positive final long creation,
-            @Duration @Positive final long expiration) throws IOException {
+            @Nonnull @NotEmpty final String id, @Nonnull final Instant creation,
+            @Nonnull final Instant expiration) throws IOException {
         
         return new BasicSPSession(id, creation, expiration);
     }
diff --git a/idp-session-impl/src/main/java/net/shibboleth/idp/session/impl/ExtractActiveAuthenticationResults.java b/idp-session-impl/src/main/java/net/shibboleth/idp/session/impl/ExtractActiveAuthenticationResults.java
index 88fc81a..8e1d800 100644
--- a/idp-session-impl/src/main/java/net/shibboleth/idp/session/impl/ExtractActiveAuthenticationResults.java
+++ b/idp-session-impl/src/main/java/net/shibboleth/idp/session/impl/ExtractActiveAuthenticationResults.java
@@ -17,6 +17,7 @@
 
 package net.shibboleth.idp.session.impl;
 
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.function.Function;
@@ -110,7 +111,7 @@ public class ExtractActiveAuthenticationResults extends AbstractAuthenticationAc
         }
         
         final long maxAge = authenticationContext.getMaxAge();
-        final long now = System.currentTimeMillis();
+        final Instant now = Instant.now();
         
         final List<AuthenticationResult> actives = new ArrayList<>();
         for (final AuthenticationResult result : session.getAuthenticationResults()) {
@@ -123,8 +124,8 @@ public class ExtractActiveAuthenticationResults extends AbstractAuthenticationAc
             }
             
             if (descriptor.isResultActive(result)) {
-                if (maxAge > 0 && result.getAuthenticationInstant() + maxAge < now) {
-                    log.debug("{} Authentication result {} exceeds maxAge setting, skiping it", getLogPrefix(),
+                if (maxAge > 0 && result.getAuthenticationInstant().plusMillis(maxAge).isBefore(now)) {
+                    log.debug("{} Authentication result {} exceeds maxAge setting, skipping it", getLogPrefix(),
                             result.getAuthenticationFlowId());
                     continue;
                 }
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 9338eb6..7e1dc7b 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
@@ -18,6 +18,7 @@
 package net.shibboleth.idp.session.impl;
 
 import java.io.IOException;
+import java.time.Instant;
 import java.util.Iterator;
 import java.util.Map;
 import java.util.Set;
@@ -30,18 +31,15 @@ import net.shibboleth.idp.authn.AuthenticationResult;
 import net.shibboleth.idp.session.AbstractIdPSession;
 import net.shibboleth.idp.session.SPSession;
 import net.shibboleth.idp.session.SessionException;
-import net.shibboleth.utilities.java.support.annotation.Duration;
 import net.shibboleth.utilities.java.support.annotation.constraint.Live;
 import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
 import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
 import net.shibboleth.utilities.java.support.annotation.constraint.NotLive;
-import net.shibboleth.utilities.java.support.annotation.constraint.Positive;
 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;
 
 import org.apache.commons.codec.digest.DigestUtils;
-import org.joda.time.DateTime;
 import org.opensaml.storage.StorageRecord;
 import org.opensaml.storage.StorageSerializer;
 import org.opensaml.storage.VersionMismatchException;
@@ -74,7 +72,7 @@ public class StorageBackedIdPSession extends AbstractIdPSession {
      */
     public StorageBackedIdPSession(@Nonnull final StorageBackedSessionManager manager,
             @Nonnull @NotEmpty final String sessionId, @Nonnull @NotEmpty final String canonicalName,
-            final long creationTime) {
+            @Nonnull final Instant creationTime) {
         super(sessionId, canonicalName, creationTime);
         
         sessionManager = Constraint.isNotNull(manager, "SessionManager cannot be null");
@@ -83,20 +81,22 @@ public class StorageBackedIdPSession extends AbstractIdPSession {
     }
     
     /** {@inheritDoc} */
-    @Override @Duration public void setLastActivityInstant(@Duration @Positive final long instant) 
+    @Override public void setLastActivityInstant(@Nonnull final Instant instant) 
             throws SessionException {
         
-        final long exp = instant + sessionManager.getSessionTimeout() + sessionManager.getSessionSlop();
-        log.debug("Updating expiration of master record for session {} to {}", getId(), new DateTime(exp));
+        final Instant exp =
+                instant.plus(sessionManager.getSessionTimeout()).plus(sessionManager.getSessionSlop());
+        log.debug("Updating expiration of master record for session {} to {}", getId(), exp);
         
         try {
             sessionManager.getStorageService().updateExpiration(
-                    getId(), StorageBackedSessionManager.SESSION_MASTER_KEY, exp);
+                    getId(), StorageBackedSessionManager.SESSION_MASTER_KEY, exp.toEpochMilli());
             super.setLastActivityInstant(instant);
         } catch (final IOException e) {
-            log.error("Exception updating expiration of master record for session {}", getId(), e);
             if (!sessionManager.isMaskStorageFailure()) {
                 throw new SessionException("Exception updating expiration of session record", e);
+            } else {
+                log.error("Exception updating expiration of master record for session {}", getId(), e);
             }
         }
     }
@@ -107,7 +107,6 @@ public class StorageBackedIdPSession extends AbstractIdPSession {
         return sessionManager.isConsistentAddress() ? super.checkAddress(address) : true;
     }
 
- // Checkstyle: ReturnCount OFF
     /** {@inheritDoc} */
     @Override
     public void bindToAddress(@Nonnull @NotEmpty final String address) throws SessionException {
@@ -141,13 +140,13 @@ public class StorageBackedIdPSession extends AbstractIdPSession {
                 log.error("Exhausted retry attempts updating record for session {}", getId());
             }
         } catch (final IOException e) {
-            log.error("Exception updating address binding of master record for session {}", getId(), e);
             if (!sessionManager.isMaskStorageFailure()) {
                 throw new SessionException("Exception updating address binding of session record", e);
+            } else {
+                log.error("Exception updating address binding of master record for session {}", getId(), e);
             }
         }
     }
- // Checkstyle: ReturnCount ON
 
     /** {@inheritDoc} */
     @Override
@@ -237,10 +236,11 @@ public class StorageBackedIdPSession extends AbstractIdPSession {
             }
             return prev;
         } catch (final IOException e) {
-            log.error("Exception saving AuthenticationResult record for session {} and flow {}", getId(),
-                    result.getAuthenticationFlowId(), e);
             if (!sessionManager.isMaskStorageFailure()) {
                 throw new SessionException("Exception saving AuthenticationResult record to storage", e);
+            } else {
+                log.error("Exception saving AuthenticationResult record for session {} and flow {}", getId(),
+                        result.getAuthenticationFlowId(), e);
             }
             return null;
         }
@@ -254,16 +254,17 @@ public class StorageBackedIdPSession extends AbstractIdPSession {
         if (flow != null) {
             try {
                 if (!sessionManager.getStorageService().updateExpiration(getId(), result.getAuthenticationFlowId(),
-                        result.getLastActivityInstant() + flow.getInactivityTimeout()
+                        result.getLastActivityInstant().toEpochMilli() + flow.getInactivityTimeout()
                             + AuthenticationFlowDescriptor.STORAGE_EXPIRATION_OFFSET)) {
                     log.warn("Skipping update, AuthenticationResult for flow {} in session {} not found in storage",
                             flowId, getId());
                 }
             } catch (final IOException e) {
-                log.error("Exception updating AuthenticationResult expiration for session {} and flow {}", getId(),
-                        flowId, e);
                 if (!sessionManager.isMaskStorageFailure()) {
                     throw new SessionException("Exception updating AuthenticationResult expiration in storage", e);
+                } else {
+                    log.error("Exception updating AuthenticationResult expiration for session {} and flow {}", getId(),
+                            flowId, e);
                 }
             }
         } else {
@@ -279,10 +280,11 @@ public class StorageBackedIdPSession extends AbstractIdPSession {
                 // Remove the separate record.
                 sessionManager.getStorageService().delete(getId(), result.getAuthenticationFlowId());
             } catch (final IOException e) {
-                log.error("Exception removing AuthenticationResult record for session {} and flow {}", getId(),
-                        result.getAuthenticationFlowId(), e);
                 if (!sessionManager.isMaskStorageFailure()) {
                     throw new SessionException("Exception removing AuthenticationResult record from storage", e);
+                } else {
+                    log.error("Exception removing AuthenticationResult record for session {} and flow {}", getId(),
+                            result.getAuthenticationFlowId(), e);
                 }
             }
             
@@ -304,10 +306,11 @@ public class StorageBackedIdPSession extends AbstractIdPSession {
                     log.error("Exhausted retry attempts updating record for session {}", getId());
                 }
             } catch (final IOException e) {
-                log.error("Exception updating record for session {}", getId(), e);
                 if (!sessionManager.isMaskStorageFailure()) {
                     throw new SessionException(
                             "Exception updating session record after AuthenticationResult removal", e);
+                } else {
+                    log.error("Exception updating record for session {}", getId(), e);
                 }
             }
             // If we reach here and a problem occurred, we must be masking storage problems.
@@ -422,10 +425,11 @@ public class StorageBackedIdPSession extends AbstractIdPSession {
                 sessionManager.indexBySPSession(this, spSession, 10);
                 return prev;
             } catch (final IOException e) {
-                log.error("Exception saving SPSession record for IdP session {} and service {}", getId(),
-                        spSession.getId(), e);
                 if (!sessionManager.isMaskStorageFailure()) {
                     throw new SessionException("Exception saving SPSession record to storage", e);
+                } else {
+                    log.error("Exception saving SPSession record for IdP session {} and service {}", getId(),
+                            spSession.getId(), e);
                 }
                 return null;
             }
@@ -469,10 +473,11 @@ public class StorageBackedIdPSession extends AbstractIdPSession {
                     log.error("Exhausted retry attempts updating record for session {}", getId());
                 }
             } catch (final IOException e) {
-                log.error("Exception updating record for session {}", getId(), e);
                 if (!sessionManager.isMaskStorageFailure()) {
                     throw new SessionException(
                             "Exception updating session record after SPSession removal", e);
+                } else {
+                    log.error("Exception updating record for session {}", getId(), e);
                 }
             }
             // If we reach here and a problem occurred, we must be masking storage problems.
@@ -485,7 +490,7 @@ public class StorageBackedIdPSession extends AbstractIdPSession {
     /** {@inheritDoc} */
     @Override
     public boolean checkTimeout() throws SessionException {
-        if (getLastActivityInstant() + sessionManager.getSessionTimeout() > System.currentTimeMillis()) {
+        if (getLastActivityInstant().plus(sessionManager.getSessionTimeout()).isAfter(Instant.now())) {
             return super.checkTimeout();
         } else {
             return false;
@@ -580,12 +585,12 @@ public class StorageBackedIdPSession extends AbstractIdPSession {
             boolean success = false;
             do {
                 success = sessionManager.getStorageService().create(getId(), flowId, result, flow,
-                        result.getLastActivityInstant() + flow.getInactivityTimeout()
+                        result.getLastActivityInstant().toEpochMilli() + flow.getInactivityTimeout()
                             + AuthenticationFlowDescriptor.STORAGE_EXPIRATION_OFFSET);
                 if (!success) {
                     // The record already exists, so we need to overwrite via an update.
                     success = sessionManager.getStorageService().update(getId(), flowId, result, flow,
-                            result.getLastActivityInstant() + flow.getInactivityTimeout()
+                            result.getLastActivityInstant().toEpochMilli() + flow.getInactivityTimeout()
                                 + AuthenticationFlowDescriptor.STORAGE_EXPIRATION_OFFSET);
                 }
             } while (!success && attempts-- > 0);
@@ -681,12 +686,13 @@ public class StorageBackedIdPSession extends AbstractIdPSession {
             int attempts = 10;
             boolean success = false;
             do {
+                final Instant exp = session.getExpirationInstant().plus(sessionManager.getSessionSlop());
                 success = sessionManager.getStorageService().create(getId(), key, builder.toString(),
-                        session.getExpirationInstant() + sessionManager.getSessionSlop());
+                        exp.toEpochMilli());
                 if (!success) {
                     // The record already exists, so we need to overwrite via an update.
                     success = sessionManager.getStorageService().update(getId(), key, builder.toString(),
-                            session.getExpirationInstant() + sessionManager.getSessionSlop());
+                            exp.toEpochMilli());
                 }
             } while (!success && attempts-- > 0);
             
@@ -725,9 +731,11 @@ public class StorageBackedIdPSession extends AbstractIdPSession {
      */
     private boolean writeToStorage() throws IOException {
         try {
+            final Instant exp = getLastActivityInstant().plus(sessionManager.getSessionTimeout()).plus(
+                    sessionManager.getSessionSlop());
             final Long ver = sessionManager.getStorageService().updateWithVersion(version, getId(),
                     StorageBackedSessionManager.SESSION_MASTER_KEY, this, sessionManager.getStorageSerializer(),
-                    getLastActivityInstant() + sessionManager.getSessionTimeout() + sessionManager.getSessionSlop());
+                    exp.toEpochMilli());
             if (ver == null) {
                 log.error("Record for session {} has disappeared from backing store", getId());
                 throw new IOException("Unable to update session, record disappeared");
diff --git a/idp-session-impl/src/main/java/net/shibboleth/idp/session/impl/StorageBackedIdPSessionSerializer.java b/idp-session-impl/src/main/java/net/shibboleth/idp/session/impl/StorageBackedIdPSessionSerializer.java
index 6b2aac3..42e9c09 100644
--- a/idp-session-impl/src/main/java/net/shibboleth/idp/session/impl/StorageBackedIdPSessionSerializer.java
+++ b/idp-session-impl/src/main/java/net/shibboleth/idp/session/impl/StorageBackedIdPSessionSerializer.java
@@ -20,6 +20,7 @@ package net.shibboleth.idp.session.impl;
 import java.io.IOException;
 import java.io.StringReader;
 import java.io.StringWriter;
+import java.time.Instant;
 import java.util.Set;
 
 import javax.annotation.Nonnull;
@@ -104,7 +105,7 @@ public class StorageBackedIdPSessionSerializer extends AbstractInitializableComp
         try {
             final StringWriter sink = new StringWriter(128);
             final JsonGenerator gen = jsonProvider.createGenerator(sink);
-            gen.writeStartObject().write(CREATION_INSTANT_FIELD, instance.getCreationInstant())
+            gen.writeStartObject().write(CREATION_INSTANT_FIELD, instance.getCreationInstant().toEpochMilli())
                     .write(PRINCIPAL_NAME_FIELD, instance.getPrincipalName());
 
             if (instance.getAddress(AbstractIdPSession.AddressFamily.IPV4) != null) {
@@ -169,15 +170,17 @@ public class StorageBackedIdPSessionSerializer extends AbstractInitializableComp
             // Create new object if necessary.
             StorageBackedIdPSession objectToPopulate = targetObject;
             if (objectToPopulate == null) {
-                final long creation = obj.getJsonNumber(CREATION_INSTANT_FIELD).longValueExact();
+                final Instant creation =
+                        Instant.ofEpochMilli(obj.getJsonNumber(CREATION_INSTANT_FIELD).longValueExact());
                 final String principalName = obj.getString(PRINCIPAL_NAME_FIELD);
                 objectToPopulate = new StorageBackedIdPSession(sessionManager, context, principalName, creation);
             }
 
             // Populate fields in-place, bypassing any storage interactions.
             objectToPopulate.setVersion(version);
-            objectToPopulate.doSetLastActivityInstant(expiration - sessionManager.getSessionTimeout()
-                    - sessionManager.getSessionSlop());
+            objectToPopulate.doSetLastActivityInstant(
+                    Instant.ofEpochMilli(expiration).minus(sessionManager.getSessionTimeout()).minus(
+                            sessionManager.getSessionSlop()));
             if (obj.containsKey(IPV4_ADDRESS_FIELD)) {
                 objectToPopulate.doBindToAddress(obj.getString(IPV4_ADDRESS_FIELD));
             }
diff --git a/idp-session-impl/src/main/java/net/shibboleth/idp/session/impl/StorageBackedSessionManager.java b/idp-session-impl/src/main/java/net/shibboleth/idp/session/impl/StorageBackedSessionManager.java
index 14e3400..f7788dd 100644
--- a/idp-session-impl/src/main/java/net/shibboleth/idp/session/impl/StorageBackedSessionManager.java
+++ b/idp-session-impl/src/main/java/net/shibboleth/idp/session/impl/StorageBackedSessionManager.java
@@ -18,6 +18,8 @@
 package net.shibboleth.idp.session.impl;
 
 import java.io.IOException;
+import java.time.Duration;
+import java.time.Instant;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.Map;
@@ -38,12 +40,9 @@ import net.shibboleth.idp.session.SessionResolver;
 import net.shibboleth.idp.session.criterion.HttpServletRequestCriterion;
 import net.shibboleth.idp.session.criterion.SPSessionCriterion;
 import net.shibboleth.idp.session.criterion.SessionIdCriterion;
-import net.shibboleth.utilities.java.support.annotation.Duration;
-import net.shibboleth.utilities.java.support.annotation.constraint.NonNegative;
 import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
 import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
 import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
-import net.shibboleth.utilities.java.support.annotation.constraint.Positive;
 import net.shibboleth.utilities.java.support.component.AbstractIdentifiableInitializableComponent;
 import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
 import net.shibboleth.utilities.java.support.component.ComponentSupport;
@@ -125,11 +124,11 @@ public class StorageBackedSessionManager extends AbstractIdentifiableInitializab
     /** Servlet response to write to. */
     @Nullable private HttpServletResponse httpResponse;
 
-    /** Inactivity timeout for sessions in milliseconds. */
-    @Duration @Positive private long sessionTimeout;
+    /** Inactivity timeout for sessions. */
+    @Nonnull private Duration sessionTimeout;
 
-    /** Amount of time in milliseconds to defer expiration of records for better handling of logout. */
-    @Duration @NonNegative private long sessionSlop;
+    /** Amount of time to defer expiration of records for better handling of logout. */
+    @Nonnull private Duration sessionSlop;
 
     /** Indicates that storage service failures should be masked as much as possible. */
     private boolean maskStorageFailure;
@@ -172,7 +171,8 @@ public class StorageBackedSessionManager extends AbstractIdentifiableInitializab
      * 
      */
     public StorageBackedSessionManager() {
-        sessionTimeout = 60 * 60 * 1000;
+        sessionTimeout = Duration.ofHours(1);
+        sessionSlop = Duration.ZERO;
         serializer = new StorageBackedIdPSessionSerializer(this, null);
         flowDescriptorMap = new HashMap();
         consistentAddress = true;
@@ -203,43 +203,43 @@ public class StorageBackedSessionManager extends AbstractIdentifiableInitializab
     }
 
     /**
-     * Get the session inactivity timeout policy in milliseconds.
+     * Get the session inactivity timeout policy.
      * 
      * @return inactivity timeout
      */
-    @Duration @Positive public long getSessionTimeout() {
+    @Nonnull public Duration getSessionTimeout() {
         return sessionTimeout;
     }
 
     /**
-     * Set the session inactivity timeout policy in milliseconds, must be greater than zero.
+     * Set the session inactivity timeout policy.
      * 
      * @param timeout the policy to set
      */
-    @Duration public void setSessionTimeout(@Duration @Positive final long timeout) {
+    public void setSessionTimeout(@Nonnull final Duration timeout) {
         ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
 
-        sessionTimeout = Constraint.isGreaterThan(0, timeout, "Timeout must be greater than zero");
+        sessionTimeout = Constraint.isNotNull(timeout, "Timeout cannot be null");
     }
 
     /**
-     * Get the amount of time in milliseconds to defer expiration of records.
+     * Get the amount of time to defer expiration of records.
      * 
-     * @return expiration deferrence time
+     * @return expiration amount of time to defer expiration of records
      */
-    @Duration @Positive public long getSessionSlop() {
+    @Nonnull public Duration getSessionSlop() {
         return sessionSlop;
     }
 
     /**
-     * Set the amount of time in milliseconds to defer expiration of records.
+     * Set the amount of time to defer expiration of records.
      * 
-     * @param slop the policy to set
+     * @param slop amount of time to defer expiration of records
      */
-    @Duration public void setSessionSlop(@Duration @NonNegative final long slop) {
+    public void setSessionSlop(@Nonnull final Duration slop) {
         ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
 
-        sessionSlop = Constraint.isGreaterThanOrEqual(0, slop, "Slop must be greater than or equal to zero");
+        sessionSlop = Constraint.isNotNull(slop, "Slop cannot be null");
     }
 
     /**
@@ -514,14 +514,14 @@ public class StorageBackedSessionManager extends AbstractIdentifiableInitializab
         }
 
         final StorageBackedIdPSession newSession =
-                new StorageBackedIdPSession(this, sessionId, principalName, System.currentTimeMillis());
+                new StorageBackedIdPSession(this, sessionId, principalName, Instant.now());
         if (remoteAddr != null) {
             newSession.doBindToAddress(remoteAddr);
         }
 
         try {
             if (!storageService.create(sessionId, SESSION_MASTER_KEY, newSession, serializer,
-                    newSession.getCreationInstant() + sessionTimeout + sessionSlop)) {
+                    newSession.getCreationInstant().plus(sessionTimeout).plus(sessionSlop).toEpochMilli())) {
                 throw new SessionException("A duplicate session ID was generated, unable to create session");
             }
         } catch (final IOException e) {
@@ -675,7 +675,7 @@ public class StorageBackedSessionManager extends AbstractIdentifiableInitializab
                         final String updated = sessionList.getValue() + idpSession.getId() + ',';
                         if (storageService.updateWithVersion(sessionList.getVersion(), serviceId, serviceKey, updated,
                                 Math.max(sessionList.getExpiration(), 
-                                         spSession.getExpirationInstant() + sessionSlop)) == null) {
+                                         spSession.getExpirationInstant().plus(sessionSlop).toEpochMilli())) == null) {
                             log.debug("Secondary index record disappeared, retrying as insert");
                             indexBySPSession(idpSession, spSession, attempts - 1);
                         }
@@ -684,7 +684,7 @@ public class StorageBackedSessionManager extends AbstractIdentifiableInitializab
                                 serviceId, serviceKey);
                     }
                 } else if (!storageService.create(serviceId, serviceKey, idpSession.getId() + ',',
-                        spSession.getExpirationInstant() + sessionSlop)) {
+                        spSession.getExpirationInstant().plus(sessionSlop).toEpochMilli())) {
                     log.debug("Secondary index record appeared, retrying as update");
                     indexBySPSession(idpSession, spSession, attempts - 1);
                 }
diff --git a/idp-session-impl/src/test/java/net/shibboleth/idp/session/impl/BasicSPSessionSerializerTest.java b/idp-session-impl/src/test/java/net/shibboleth/idp/session/impl/BasicSPSessionSerializerTest.java
index bd7b5e7..92ee776 100644
--- a/idp-session-impl/src/test/java/net/shibboleth/idp/session/impl/BasicSPSessionSerializerTest.java
+++ b/idp-session-impl/src/test/java/net/shibboleth/idp/session/impl/BasicSPSessionSerializerTest.java
@@ -21,6 +21,8 @@ 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 net.shibboleth.idp.session.BasicSPSession;
 import net.shibboleth.idp.session.SPSession;
@@ -44,7 +46,7 @@ public class BasicSPSessionSerializerTest {
     private BasicSPSessionSerializer serializer;
     
     @BeforeMethod public void setUp() throws ComponentInitializationException {
-        serializer = new BasicSPSessionSerializer(0);
+        serializer = new BasicSPSessionSerializer(Duration.ZERO);
         serializer.initialize();
     }
 
@@ -80,15 +82,15 @@ public class BasicSPSessionSerializerTest {
     }
     
     @Test public void testBasic() throws Exception {
-        long exp = INSTANT + 60000L;
+        final Instant exp = Instant.ofEpochMilli(INSTANT).plusSeconds(60);
         
-        BasicSPSession session = new BasicSPSession("test", INSTANT, exp);
+        BasicSPSession session = new BasicSPSession("test", Instant.ofEpochMilli(INSTANT), exp);
         
         String s = serializer.serialize(session);
         String s2 = fileToString(DATAPATH + "basicSPSession.json");
         Assert.assertEquals(s, s2);
         
-        SPSession session2 = serializer.deserialize(1, CONTEXT, KEY, s2, exp);
+        SPSession session2 = serializer.deserialize(1, CONTEXT, KEY, s2, exp.toEpochMilli());
 
         Assert.assertEquals(session.getId(), session2.getId());
         Assert.assertEquals(session.getCreationInstant(), session2.getCreationInstant());
diff --git a/idp-session-impl/src/test/java/net/shibboleth/idp/session/impl/DestroySessionsTest.java b/idp-session-impl/src/test/java/net/shibboleth/idp/session/impl/DestroySessionsTest.java
index fcaabc4..a7ea2e6 100644
--- a/idp-session-impl/src/test/java/net/shibboleth/idp/session/impl/DestroySessionsTest.java
+++ b/idp-session-impl/src/test/java/net/shibboleth/idp/session/impl/DestroySessionsTest.java
@@ -17,6 +17,7 @@
 
 package net.shibboleth.idp.session.impl;
 
+import java.time.Duration;
 import java.util.Collections;
 
 import javax.servlet.http.Cookie;
@@ -71,11 +72,11 @@ public class DestroySessionsTest extends SessionManagerBaseTestCase {
     protected void adjustProperties() throws ComponentInitializationException {
         sessionManager.setTrackSPSessions(true);
         sessionManager.setSecondaryServiceIndex(true);
-        sessionManager.setSessionSlop(900 * 60 * 1000);
+        sessionManager.setSessionSlop(Duration.ofSeconds(900));
         final SPSessionSerializerRegistry registry = new SPSessionSerializerRegistry();
         registry.setMappings(
                 Collections.<Class<? extends SPSession>,StorageSerializer<? extends SPSession>>singletonMap(
-                        BasicSPSession.class, new BasicSPSessionSerializer(900 * 60 * 1000)));
+                        BasicSPSession.class, new BasicSPSessionSerializer(Duration.ofSeconds(900))));
         registry.initialize();
         sessionManager.setSPSessionSerializerRegistry(registry);
     }
diff --git a/idp-session-impl/src/test/java/net/shibboleth/idp/session/impl/ProcessLogoutTest.java b/idp-session-impl/src/test/java/net/shibboleth/idp/session/impl/ProcessLogoutTest.java
index ceddf98..c08fb69 100644
--- a/idp-session-impl/src/test/java/net/shibboleth/idp/session/impl/ProcessLogoutTest.java
+++ b/idp-session-impl/src/test/java/net/shibboleth/idp/session/impl/ProcessLogoutTest.java
@@ -17,6 +17,8 @@
 
 package net.shibboleth.idp.session.impl;
 
+import java.time.Duration;
+import java.time.Instant;
 import java.util.Collections;
 
 import javax.servlet.http.Cookie;
@@ -72,11 +74,11 @@ public class ProcessLogoutTest extends SessionManagerBaseTestCase {
     protected void adjustProperties() throws ComponentInitializationException {
         sessionManager.setTrackSPSessions(true);
         sessionManager.setSecondaryServiceIndex(true);
-        sessionManager.setSessionSlop(900 * 60 * 1000);
+        sessionManager.setSessionSlop(Duration.ofSeconds(900));
         final SPSessionSerializerRegistry registry = new SPSessionSerializerRegistry();
         registry.setMappings(
                 Collections.<Class<? extends SPSession>,StorageSerializer<? extends SPSession>>singletonMap(
-                        BasicSPSession.class, new BasicSPSessionSerializer(900 * 60 * 1000)));
+                        BasicSPSession.class, new BasicSPSessionSerializer(Duration.ofSeconds(900))));
         registry.initialize();
         sessionManager.setSPSessionSerializerRegistry(registry);
     }
@@ -118,8 +120,9 @@ public class ProcessLogoutTest extends SessionManagerBaseTestCase {
         HttpServletRequestResponseContext.loadCurrent(new MockHttpServletRequest(), new MockHttpServletResponse());
         ((MockHttpServletRequest) HttpServletRequestResponseContext.getRequest()).setCookies(cookie);
 
-        final long creation = System.currentTimeMillis();
-        final long expiration = creation + 3600 * 60 * 1000;
+        // Limit granularity to milliseconds for storage roundtrip.
+        final Instant creation = Instant.ofEpochMilli(System.currentTimeMillis());
+        final Instant expiration = creation.plusSeconds(3600);
 
         final IdPSession session = sessionManager.resolveSingle(new CriteriaSet(new HttpServletRequestCriterion()));
         Assert.assertNotNull(session);
diff --git a/idp-session-impl/src/test/java/net/shibboleth/idp/session/impl/SessionManagerBaseTestCase.java b/idp-session-impl/src/test/java/net/shibboleth/idp/session/impl/SessionManagerBaseTestCase.java
index 440ed9d..135e915 100644
--- a/idp-session-impl/src/test/java/net/shibboleth/idp/session/impl/SessionManagerBaseTestCase.java
+++ b/idp-session-impl/src/test/java/net/shibboleth/idp/session/impl/SessionManagerBaseTestCase.java
@@ -17,6 +17,8 @@
 
 package net.shibboleth.idp.session.impl;
 
+import java.time.Duration;
+
 import javax.annotation.Nonnull;
 import javax.servlet.http.Cookie;
 
@@ -60,7 +62,7 @@ public class SessionManagerBaseTestCase extends OpenSAMLInitBaseTestCase {
         cookieManager.initialize();
         
         sessionManager = new StorageBackedSessionManager();
-        sessionManager.setSessionTimeout(5000);
+        sessionManager.setSessionTimeout(Duration.ofSeconds(5));
         sessionManager.setStorageService(storageService);
         sessionManager.setIDGenerator(new SecureRandomIdentifierGenerationStrategy());
         sessionManager.setHttpServletRequest(requestProxy);
diff --git a/idp-session-impl/src/test/java/net/shibboleth/idp/session/impl/StorageBackedIdPSessionSerializerTest.java b/idp-session-impl/src/test/java/net/shibboleth/idp/session/impl/StorageBackedIdPSessionSerializerTest.java
index 7191479..b612d02 100644
--- a/idp-session-impl/src/test/java/net/shibboleth/idp/session/impl/StorageBackedIdPSessionSerializerTest.java
+++ b/idp-session-impl/src/test/java/net/shibboleth/idp/session/impl/StorageBackedIdPSessionSerializerTest.java
@@ -21,6 +21,7 @@ import java.io.File;
 import java.io.FileInputStream;
 import java.io.IOException;
 import java.net.URISyntaxException;
+import java.time.Instant;
 
 import net.shibboleth.idp.authn.AuthenticationResult;
 import net.shibboleth.idp.authn.principal.UsernamePrincipal;
@@ -113,7 +114,7 @@ public class StorageBackedIdPSessionSerializerTest {
     @Test public void testBasic() throws Exception {
         long exp = INSTANT + (60 * 60 * 1000);
         
-        StorageBackedIdPSession session = new StorageBackedIdPSession(manager, "test", "foo", INSTANT);
+        StorageBackedIdPSession session = new StorageBackedIdPSession(manager, "test", "foo", Instant.ofEpochMilli(INSTANT));
         session.doBindToAddress("127.0.0.1");
         
         String s = serializer.serialize(session);
@@ -129,21 +130,21 @@ public class StorageBackedIdPSessionSerializerTest {
     }
 
     @Test public void testComplex() throws Exception {
-        long exp = INSTANT + (60 * 60 * 1000);
+        final Instant exp = Instant.ofEpochMilli(INSTANT).plusSeconds(3600);
         
-        StorageBackedIdPSession session = new StorageBackedIdPSession(manager, "test", "foo", INSTANT);
+        StorageBackedIdPSession session = new StorageBackedIdPSession(manager, "test", "foo", Instant.ofEpochMilli(INSTANT));
         session.doBindToAddress("127.0.0.1");
         session.doAddAuthenticationResult(new AuthenticationResult("a", new UsernamePrincipal("jdoe")));
         session.doAddAuthenticationResult(new AuthenticationResult("b", new UsernamePrincipal("jdoe")));
         session.doAddAuthenticationResult(new AuthenticationResult("c", new UsernamePrincipal("jdoe")));
-        session.doAddSPSession(new BasicSPSession("bar", INSTANT, exp));
-        session.doAddSPSession(new BasicSPSession("baz", INSTANT, exp));
+        session.doAddSPSession(new BasicSPSession("bar", Instant.ofEpochMilli(INSTANT), exp));
+        session.doAddSPSession(new BasicSPSession("baz", Instant.ofEpochMilli(INSTANT), exp));
         
         String s = serializer.serialize(session);
         String s2 = fileToString(DATAPATH + "complexIdPSession." + (TestSupport.isJavaV8OrLater() ? "jdk8" : "json"));
         Assert.assertEquals(s, s2);
         
-        StorageBackedIdPSession session2 = serializer.deserialize(1, "test", KEY, s2, exp);
+        StorageBackedIdPSession session2 = serializer.deserialize(1, "test", KEY, s2, exp.toEpochMilli());
 
         Assert.assertEquals(session.getId(), session2.getId());
         Assert.assertEquals(session.getPrincipalName(), session2.getPrincipalName());
diff --git a/idp-session-impl/src/test/java/net/shibboleth/idp/session/impl/StorageBackedSessionManagerTest.java b/idp-session-impl/src/test/java/net/shibboleth/idp/session/impl/StorageBackedSessionManagerTest.java
index d653974..9d14028 100644
--- a/idp-session-impl/src/test/java/net/shibboleth/idp/session/impl/StorageBackedSessionManagerTest.java
+++ b/idp-session-impl/src/test/java/net/shibboleth/idp/session/impl/StorageBackedSessionManagerTest.java
@@ -18,6 +18,9 @@
 package net.shibboleth.idp.session.impl;
 
 import java.io.IOException;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -58,7 +61,7 @@ import org.testng.annotations.Test;
 /** {@link StorageBackedSessionManager} unit test. */
 public class StorageBackedSessionManagerTest extends SessionManagerBaseTestCase {
 
-    private static final long sessionSlop = 60 * 5 * 1000;
+    private static final Duration sessionSlop = Duration.ofMinutes(5);
     
     private Collection<AuthenticationFlowDescriptor> flowDescriptors;
     
@@ -136,7 +139,7 @@ public class StorageBackedSessionManagerTest extends SessionManagerBaseTestCase
         
         // Test basic session content.
         IdPSession session = sessionManager.createSession("joe");
-        Assert.assertTrue(session.getCreationInstant() <= System.currentTimeMillis());
+        Assert.assertTrue(session.getCreationInstant().compareTo(Instant.now()) <= 0);
         Assert.assertEquals(session.getCreationInstant(), session.getLastActivityInstant());
         Assert.assertEquals(session.getPrincipalName(), "joe");
         Assert.assertTrue(session.getAuthenticationResults().isEmpty());
@@ -151,14 +154,14 @@ public class StorageBackedSessionManagerTest extends SessionManagerBaseTestCase
         Assert.assertNotEquals(session.getCreationInstant(), session.getLastActivityInstant());
 
         // Do a lookup and compare the results.
-        long creation = session.getCreationInstant();
-        long lastActivity = session.getLastActivityInstant();
+        final Instant creation = session.getCreationInstant();
+        final Instant lastActivity = session.getLastActivityInstant();
         String sessionId = session.getId();
         session = sessionManager.resolveSingle(new CriteriaSet(new SessionIdCriterion(sessionId)));
         Assert.assertNotNull(session);
         Assert.assertEquals(session.getPrincipalName(), "joe");
-        Assert.assertEquals(session.getCreationInstant(), creation);
-        Assert.assertEquals(session.getLastActivityInstant(), lastActivity);
+        Assert.assertEquals(session.getCreationInstant(), creation.truncatedTo(ChronoUnit.MILLIS));
+        Assert.assertEquals(session.getLastActivityInstant(), lastActivity.truncatedTo(ChronoUnit.MILLIS));
         
         // Test a destroy and a failed lookup.
         sessionManager.destroySession(sessionId, true);
@@ -234,8 +237,8 @@ public class StorageBackedSessionManagerTest extends SessionManagerBaseTestCase
         Assert.assertSame(foo, foo2);
         
         // Update timestamp.
-        long now = System.currentTimeMillis();
-        foo.setLastActivityInstant(now);
+        // Limit granularity to milliseconds for storage roundtrip.
+        foo.setLastActivityInstant(Instant.ofEpochMilli(System.currentTimeMillis()));
         session.updateAuthenticationResultActivity(foo);
         
         // Load from storage and re-test.
@@ -243,7 +246,7 @@ public class StorageBackedSessionManagerTest extends SessionManagerBaseTestCase
         Assert.assertNull(session2.getAuthenticationResult("AuthenticationFlow/Bar"));
         foo2 = session2.getAuthenticationResult("AuthenticationFlow/Foo");
         Assert.assertNotNull(foo2);
-        Assert.assertEquals(foo.getAuthenticationInstant(), foo2.getAuthenticationInstant());
+        Assert.assertEquals(foo.getAuthenticationInstant().truncatedTo(ChronoUnit.MILLIS), foo2.getAuthenticationInstant());
         Assert.assertEquals(foo.getLastActivityInstant(), foo2.getLastActivityInstant());
         Assert.assertEquals(foo.getSubject(), foo2.getSubject());
         
@@ -263,11 +266,11 @@ public class StorageBackedSessionManagerTest extends SessionManagerBaseTestCase
         IdPSession session = sessionManager.createSession("joe");
         Assert.assertTrue(session.getSPSessions().isEmpty());
 
+        final Instant now = Instant.now();
+        
         // Add some sessions.
-        SPSession foo = new BasicSPSession("https://sp.example.org/shibboleth",
-                System.currentTimeMillis(), System.currentTimeMillis() + 60 * 60 * 1000);
-        SPSession bar = new BasicSPSession("https://sp2.example.org/shibboleth",
-                System.currentTimeMillis(), System.currentTimeMillis() + 60 * 60 * 1000);
+        SPSession foo = new BasicSPSession("https://sp.example.org/shibboleth", now, now.plusSeconds(3600));
+        SPSession bar = new BasicSPSession("https://sp2.example.org/shibboleth", now, now.plusSeconds(3600));
 
         Assert.assertNull(session.addSPSession(foo));
         Assert.assertNull(session.addSPSession(bar));
@@ -292,8 +295,8 @@ public class StorageBackedSessionManagerTest extends SessionManagerBaseTestCase
         Assert.assertNull(session.getSPSession("https://sp2.example.org/shibboleth"));
         foo2 = session2.getSPSession("https://sp.example.org/shibboleth");
         Assert.assertNotNull(foo2);
-        Assert.assertEquals(foo.getCreationInstant(), foo2.getCreationInstant());
-        Assert.assertEquals(foo.getExpirationInstant(), foo2.getExpirationInstant());
+        Assert.assertEquals(foo.getCreationInstant().truncatedTo(ChronoUnit.MILLIS), foo2.getCreationInstant());
+        Assert.assertEquals(foo.getExpirationInstant().truncatedTo(ChronoUnit.MILLIS), foo2.getExpirationInstant());
 
         // Test removal while multiple objects are active.
         session2 = sessionManager.resolveSingle(new CriteriaSet(new SessionIdCriterion(session.getId())));
@@ -311,11 +314,11 @@ public class StorageBackedSessionManagerTest extends SessionManagerBaseTestCase
         IdPSession session = sessionManager.createSession("joe");
         IdPSession session2 = sessionManager.createSession("joe2");
 
+        final Instant now = Instant.now();
+        
         // Add some sessions.
-        SPSession foo = new ExtendedSPSession("https://sp.example.org/shibboleth",
-                System.currentTimeMillis(), System.currentTimeMillis() + 60 * 60 * 1000);
-        SPSession bar = new ExtendedSPSession("https://sp2.example.org/shibboleth",
-                System.currentTimeMillis(), System.currentTimeMillis() + 60 * 60 * 1000);
+        SPSession foo = new ExtendedSPSession("https://sp.example.org/shibboleth", now, now.plusSeconds(3600));
+        SPSession bar = new ExtendedSPSession("https://sp2.example.org/shibboleth", now, now.plusSeconds(3600));
 
         Assert.assertNull(session.addSPSession(foo));
         Assert.assertNull(session.addSPSession(bar));
@@ -360,7 +363,7 @@ public class StorageBackedSessionManagerTest extends SessionManagerBaseTestCase
 
         public static final String SESSION_KEY = "PerSessionNameWouldGoHere";
         
-        public ExtendedSPSession(String id, long creation, long expiration) {
+        public ExtendedSPSession(String id, Instant creation, Instant expiration) {
             super(id, creation, expiration);
         }
 
@@ -372,13 +375,13 @@ public class StorageBackedSessionManagerTest extends SessionManagerBaseTestCase
 
     private static class ExtendedSPSessionSerializer extends BasicSPSessionSerializer {
 
-        public ExtendedSPSessionSerializer(long offset) {
+        public ExtendedSPSessionSerializer(Duration offset) {
             super(offset);
         }
         
         /** {@inheritDoc} */
         @Override
-        protected SPSession doDeserialize(JsonObject obj, String id, long creation, long expiration)
+        protected SPSession doDeserialize(JsonObject obj, String id, Instant creation, Instant expiration)
                 throws IOException {
             // Check if field got serialized.
             obj.getString("sk");
diff --git a/idp-session-impl/src/test/java/net/shibboleth/idp/session/impl/UpdateSessionWithAuthenticationResultTest.java b/idp-session-impl/src/test/java/net/shibboleth/idp/session/impl/UpdateSessionWithAuthenticationResultTest.java
index 9cba5a9..e900993 100644
--- a/idp-session-impl/src/test/java/net/shibboleth/idp/session/impl/UpdateSessionWithAuthenticationResultTest.java
+++ b/idp-session-impl/src/test/java/net/shibboleth/idp/session/impl/UpdateSessionWithAuthenticationResultTest.java
@@ -17,6 +17,7 @@
 
 package net.shibboleth.idp.session.impl;
 
+import java.time.Instant;
 import java.util.Arrays;
 
 import net.shibboleth.idp.authn.AuthenticationFlowDescriptor;
@@ -171,7 +172,8 @@ public class UpdateSessionWithAuthenticationResultTest extends SessionManagerBas
         sessionCtx.setIdPSession(sessionManager.createSession("joe"));
         sessionCtx.getIdPSession().addAuthenticationResult(ac.getAuthenticationResult());
         
-        long ts = System.currentTimeMillis() + 5 * 60 * 1000;
+        // Limit granularity to milliseconds for storage roundtrip.
+        final Instant ts = Instant.ofEpochMilli(System.currentTimeMillis()).plusSeconds(300);
         ac.getAuthenticationResult().setLastActivityInstant(ts);
         
         final Event event = action.execute(src);
diff --git a/idp-session-impl/src/test/java/net/shibboleth/idp/session/impl/UpdateSessionWithSPSessionTest.java b/idp-session-impl/src/test/java/net/shibboleth/idp/session/impl/UpdateSessionWithSPSessionTest.java
index 7f845e8..f2b4c94 100644
--- a/idp-session-impl/src/test/java/net/shibboleth/idp/session/impl/UpdateSessionWithSPSessionTest.java
+++ b/idp-session-impl/src/test/java/net/shibboleth/idp/session/impl/UpdateSessionWithSPSessionTest.java
@@ -17,6 +17,8 @@
 
 package net.shibboleth.idp.session.impl;
 
+import java.time.Duration;
+import java.time.Instant;
 import java.util.Collections;
 import java.util.function.Function;
 
@@ -68,7 +70,7 @@ public class UpdateSessionWithSPSessionTest extends SessionManagerBaseTestCase {
         final SPSessionSerializerRegistry registry = new SPSessionSerializerRegistry();
         registry.setMappings(
                 Collections.<Class<? extends SPSession>,StorageSerializer<? extends SPSession>>singletonMap(
-                        BasicSPSession.class, new BasicSPSessionSerializer(900 * 60 * 1000)));
+                        BasicSPSession.class, new BasicSPSessionSerializer(Duration.ofSeconds(900))));
         registry.initialize();
         sessionManager.setSPSessionSerializerRegistry(registry);
     }
@@ -102,8 +104,8 @@ public class UpdateSessionWithSPSessionTest extends SessionManagerBaseTestCase {
         final IdPSession session = sessionManager.createSession("joe");
         prc.getSubcontext(SessionContext.class, true).setIdPSession(session);
         
-        final long creation = System.currentTimeMillis();
-        final long expiration = creation + 3600 * 60 * 1000;
+        final Instant creation = Instant.now();
+        final Instant expiration = creation.plusSeconds(3600);
         
         action.setSPSessionCreationStrategy(new DummyStrategy(creation, expiration));
         action.initialize();
@@ -119,12 +121,12 @@ public class UpdateSessionWithSPSessionTest extends SessionManagerBaseTestCase {
     /** Returns a simple example session. */
     private class DummyStrategy implements Function<ProfileRequestContext, SPSession> {
 
-        private long creationTime;
+        private Instant creationTime;
         
-        private long expirationTime;
+        private Instant expirationTime;
         
         /** Constructor. */
-        DummyStrategy(final long creation, final long expiration) {
+        DummyStrategy(final Instant creation, final Instant expiration) {
             creationTime = creation;
             expirationTime = expiration;
         }

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


More information about the commits mailing list