[java-idp-plugin-duo] 01/16: JDUO-15 - Switch IdP Token Handling to Nimbus JWT

Phil Smart philip.smart at jisc.ac.uk
Fri Oct 2 10:40:52 UTC 2020


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

philsmart pushed a commit to branch main
in repository java-idp-plugin-duo.

View the commit online:
http://git.shibboleth.net/view/?p=java-idp-plugin-duo.git;a=commit;h=6cb19ec30b2831f34fe0ee08e7aa055ce2820716

commit 6cb19ec30b2831f34fe0ee08e7aa055ce2820716
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Fri Sep 18 17:26:15 2020 +0100

    JDUO-15 - Switch IdP Token Handling to Nimbus JWT
    
     - First draft
    
    https://issues.shibboleth.net/jira/browse/JDUO-15
---
 .../idp/plugin/authn/duo/DuoOIDCAuthAPI.java       |   9 +
 .../idp/plugin/authn/duo/DuoOIDCClient.java        |  11 +-
 .../duo/context/DuoOIDCAuthenticationContext.java  |  10 +-
 .../authn/duo/impl/DuoJWTClaimsVerifier.java       | 176 +++++++++++
 .../authn/duo/impl/ExchangeCodeForDuoToken.java    |  17 +-
 .../authn/duo/impl/IdPJWTSecurityContext.java      |  37 +++
 .../authn/duo/impl/ValidateDuoResponseState.java   |   2 +-
 .../authn/duo/impl/ValidateDuoTokenAudience.java   | 102 ------
 .../impl/ValidateDuoTokenAuthenticationResult.java |  99 ++++--
 .../impl/ValidateDuoTokenAuthenticationTime.java   | 166 ----------
 .../duo/impl/ValidateDuoTokenExpirationTime.java   | 131 --------
 .../authn/duo/impl/ValidateDuoTokenIssuedAt.java   | 143 ---------
 .../authn/duo/impl/ValidateDuoTokenIssuer.java     | 149 ---------
 .../authn/duo/impl/ValidateDuoTokenSubject.java    | 106 -------
 .../plugin/authn/duo/impl/ValidateTokenClaims.java | 242 ++++++++++++++
 .../authn/duo/impl/ValidateTokenSignature.java     | 131 ++++++++
 .../flows/authn/DuoOIDC/duo-oidc-authn-beans.xml   |  20 +-
 .../flows/authn/DuoOIDC/duo-oidc-authn-flow.xml    |   7 +-
 .../authn/duo/impl/AbstractDuoActionTest.java      | 262 +++++++++++++--
 .../duo/impl/ValidateDuoTokenAudienceTest.java     |  95 ------
 .../ValidateDuoTokenAuthenticationResultTest.java  |  38 ++-
 .../ValidateDuoTokenAuthenticationTimeTest.java    | 165 ----------
 .../impl/ValidateDuoTokenExpirationTimeTest.java   | 130 --------
 .../duo/impl/ValidateDuoTokenIssuedAtTest.java     | 110 -------
 .../authn/duo/impl/ValidateDuoTokenIssuerTest.java |  88 ------
 .../duo/impl/ValidateDuoTokenSubjectTest.java      |  83 -----
 .../authn/duo/impl/ValidateTokenClaimsTest.java    | 351 +++++++++++++++++++++
 .../authn/duo/impl/ValidateTokenSignatureTest.java | 182 +++++++++++
 .../plugin/authn/mock/MockDuoOIDCClient_FAIL.java  | 108 +++++--
 .../plugin/authn/mock/MockDuoOIDCClient_OK.java    | 115 +++++--
 .../mock/MockDuoOIDCClient_OK_OLD_AUTH_TIME.java   | 115 +++++--
 .../authn/mock/MockDuoOIDCClient_UNKNOWN.java      | 107 +++++--
 .../authn/duo/sdk/impl/DuoSDKClientAdaptor.java    | 119 ++++---
 pom.xml                                            |  16 +
 34 files changed, 1896 insertions(+), 1746 deletions(-)

diff --git a/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/DuoOIDCAuthAPI.java b/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/DuoOIDCAuthAPI.java
index d17d7a7..cefb62e 100644
--- a/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/DuoOIDCAuthAPI.java
+++ b/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/DuoOIDCAuthAPI.java
@@ -45,6 +45,15 @@ public final class DuoOIDCAuthAPI {
     
     /** Duo response success status. */
     @Nonnull @NotEmpty public static final String DUO_RESPONSE_STATUS_OK = "OK";
+    
+    /** The name of the JSON authentication result object.*/
+    @Nonnull @NotEmpty public static final String DUO_AUTH_RESULT_JSON_OBJECT = "auth_result";
+    
+    /** The name of the JSON result status property.*/
+    @Nonnull @NotEmpty public static final String DUO_AUTH_RESULT_STATUS_JSON_OBJECT = "status";
+    
+    /** The name of the JSON result status message property.*/
+    @Nonnull @NotEmpty public static final String DUO_AUTH_RESULT_STATUS_MSG_JSON_OBJECT = "status_msg";
 
     /** Constructor. */
     private DuoOIDCAuthAPI() {
diff --git a/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/DuoOIDCClient.java b/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/DuoOIDCClient.java
index fb9a839..69fcb19 100644
--- a/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/DuoOIDCClient.java
+++ b/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/DuoOIDCClient.java
@@ -19,7 +19,8 @@ package net.shibboleth.idp.plugin.authn.duo;
 
 import javax.annotation.Nonnull;
 
-import net.shibboleth.idp.plugin.authn.duo.model.DuoAuthToken;
+import com.nimbusds.jwt.JWT;
+
 import net.shibboleth.idp.plugin.authn.duo.model.DuoHealthCheck;
 
 /**
@@ -49,19 +50,19 @@ public interface DuoOIDCClient {
     @Nonnull String createAuthUrl(@Nonnull final String username, @Nonnull final String state) throws DuoClientException;
     
     /**
-     * Verifies the code returned by Duo and exchanges it for a token which contains information pertaining to
-     * the authentication.
+     * Verifies the code returned by Duo and exchanges it for a Json Web Token which contains information pertaining to
+     * the authentication. The JWT **must** be signed.
      *
      * @param code An authentication identifier which is exchanged (per OAuth2.0 spec) with Duo for a token.
      *              the token can be used to determine if authentication was successful as well as obtain meta-data 
      *              about the authentication.
      * @param username The user to be authenticated by Duo.
      *
-     * @return the token, never {@code null}.
+     * @return the **signed** JWT, never {@code null}.
      * 
      * @throws DuoClientException if there is an error exchanging the auth_code for a token result.
      */
-    @Nonnull DuoAuthToken exchangeAuthorizationCodeFor2FAResult(@Nonnull final String code, 
+    @Nonnull JWT exchangeAuthorizationCodeFor2FAResult(@Nonnull final String code, 
             @Nonnull final String username) throws DuoClientException;
 
 }
diff --git a/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/context/DuoOIDCAuthenticationContext.java b/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/context/DuoOIDCAuthenticationContext.java
index 30888af..0a0c0cd 100644
--- a/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/context/DuoOIDCAuthenticationContext.java
+++ b/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/context/DuoOIDCAuthenticationContext.java
@@ -22,6 +22,8 @@ import javax.annotation.Nullable;
 
 import org.opensaml.messaging.context.BaseContext;
 
+import com.nimbusds.jwt.JWT;
+
 import net.shibboleth.idp.authn.context.AuthenticationContext;
 import net.shibboleth.idp.plugin.authn.duo.DuoOIDCClient;
 import net.shibboleth.idp.plugin.authn.duo.DuoOIDCIntegration;
@@ -51,8 +53,8 @@ public final class DuoOIDCAuthenticationContext extends BaseContext {
     /** The authorization code return from the Duo authorization request.*/
     @Nullable private String authCode;
     
-    /** The token received from Duo as a result of 2FA.*/
-    @Nullable private DuoAuthToken authToken;
+    /** The JWT token received from Duo as a result of 2FA.*/
+    @Nullable private JWT authToken;
     
     /** The Duo OIDC client to use for the lifetime of this request.*/
     @Nullable private DuoOIDCClient client;   
@@ -111,7 +113,7 @@ public final class DuoOIDCAuthenticationContext extends BaseContext {
      * 
      * @return this context
      */
-    @Nonnull public DuoOIDCAuthenticationContext setAuthToken(@Nullable final DuoAuthToken token) {
+    @Nonnull public DuoOIDCAuthenticationContext setAuthToken(@Nullable final JWT token) {
         authToken = token;
         return this;
     }
@@ -121,7 +123,7 @@ public final class DuoOIDCAuthenticationContext extends BaseContext {
      * 
      * @return the token
      */
-    @Nullable public DuoAuthToken getAuthToken() {
+    @Nullable public JWT getAuthToken() {
         return authToken;
     }
     
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoJWTClaimsVerifier.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoJWTClaimsVerifier.java
new file mode 100644
index 0000000..8b926c6
--- /dev/null
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoJWTClaimsVerifier.java
@@ -0,0 +1,176 @@
+package net.shibboleth.idp.plugin.authn.duo.impl;
+
+import java.text.ParseException;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Date;
+import java.util.Set;
+
+import javax.annotation.Nonnull;
+import javax.annotation.concurrent.ThreadSafe;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.proc.BadJWTException;
+import com.nimbusds.jwt.proc.DefaultJWTClaimsVerifier;
+
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
+/**
+ * 
+ * Extension of the {@link DefaultJWTClaimsVerifier} that also checks:
+ *  <ol>
+ *      <li>The IssuedAt claim exists, and is within a specified window from the current time.</li>
+ *      <li>If the auth_time (when the End-User authentication took place) claim
+ *          is within a valid expiration window. Only when forced authentication is requested.</li>
+ *  </ol>
+ */
+ at ThreadSafe
+public class DuoJWTClaimsVerifier extends DefaultJWTClaimsVerifier<IdPJWTSecurityContext>{
+    
+    /** The name of the authentication time claim.*/
+    @Nonnull public static final String AUTH_TIME_CLAIM_NAME = "auth_time";
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(DuoJWTClaimsVerifier.class);
+    
+    /**
+     *  Maximum amount (in either direction from now) of duration in seconds for which a token is valid after 
+     *  it is issued (Default value: 180 seconds). 
+     */
+    @Nonnull private int iatWindow;
+    
+    /** 
+     * If forced authentication, amount of time in seconds for which a token is valid 
+     * after if it was issued. (Default value: 60 seconds) 
+     */
+    @Nonnull private int authnLifetime;
+    
+    /**
+     * Creates new Duo Specific default JWT claims verifier.
+     *
+     * @param requiredAudience The required JWT audience, {@code null} if
+     *                         not specified.
+     * @param exactMatchClaims The JWT claims that must match exactly,
+     *                         {@code null} if none.
+     * @param requiredClaims   The names of the JWT claims that must be
+     *                         present, empty set or {@code null} if none.
+     */
+    public DuoJWTClaimsVerifier(final String requiredAudience,
+                    final JWTClaimsSet exactMatchClaims,
+                    final Set<String> requiredClaims) {
+        
+        super(requiredAudience,exactMatchClaims,requiredClaims);
+        iatWindow = 180;
+        authnLifetime = 60;
+    }
+    
+    /**
+     * Sets the amount of time in seconds for which a token is valid.
+     * 
+     * @param window window of time in seconds for which a token is valid
+     */
+    public void setIatWindow(@Nonnull final int window) {
+        iatWindow = Constraint.isNotNull(window, "Token issued at window cannot be null");       
+    }
+    
+    /**
+     * Sets the amount of time for which a token is valid.
+     * 
+     * @param lifetime amount of time for which a token is valid
+     */
+    public void setAuthnLifetime(@Nonnull final int lifetime) {
+        authnLifetime = Constraint.isNotNull(lifetime, "Token authentication lifetime cannot be null");
+    }
+    
+    @Override
+    public void verify(@Nonnull final JWTClaimsSet claimsSet, @Nonnull final IdPJWTSecurityContext context)
+        throws BadJWTException {
+        if (context == null) {
+            throw new BadJWTException("Duo claims verifier requires the IdP security context");
+        }
+        super.verify(claimsSet,context);        
+        verifyIat(claimsSet);
+        verifyAuthenticationTime(claimsSet,context);      
+    }
+    
+    private void verifyAuthenticationTime(@Nonnull final JWTClaimsSet claimsSet, 
+            @Nonnull final IdPJWTSecurityContext context) throws BadJWTException {
+        
+        if (context.getPrc() == null) {
+            throw new BadJWTException("No profile request context found, can not validate authentication time");
+        }
+        final AuthenticationContext ac = context.getPrc().getSubcontext(AuthenticationContext.class);
+        if (ac == null) {
+            throw new BadJWTException("No authentication request context found, can not validate authentication time");
+        }
+        
+        if (!ac.isForceAuthn()) {
+            //no forced authn, so do not validate authentication time
+            return;
+        } else {
+            //forced authn, so check authentication time
+            try {
+                final Date authTimeDate = claimsSet.getDateClaim(AUTH_TIME_CLAIM_NAME);
+                if (authTimeDate == null) {
+                    throw new BadJWTException("No authentication time found in token");
+                }
+                final Instant authTime = authTimeDate.toInstant();
+                final Instant now = Instant.now();
+                final Instant expiration = authTime.plus(Duration.ofSeconds(authnLifetime));
+                
+                // Check time of authentication wasn't in the future
+                if (authTime.isAfter(now)) {
+                    log.warn("Authentication forced but is not yet valid: auth_time was {}, latest valid is: {}",
+                            authTime, now);
+                    throw new BadJWTException("JWT token authentication time is not yet valid");
+                }
+
+                // Check time of authentication has not expired
+                if (expiration.isBefore(now)) {
+                    log.warn(
+                            "Authentication required (forced) but has expired: auth_time was '{}', "
+                            + "expired at: '{}', current time: '{}'",
+                            authTime, expiration, now);
+                    throw new BadJWTException("JWT token authentication time has expired");
+                }
+                //is OK.
+                
+            } catch (final ParseException e) {
+                throw new BadJWTException("Autentication forced, but no authentication time found in token",e);
+            }
+        }
+    }
+
+    /**
+     * Verifies the IssuedAt claim exists, and is within a specified window from the current time.
+     * 
+     * @param claimsSet the claimset.
+     * 
+     * @throws BadJWTException if the IssuedAt claim is invalid.
+     */
+    private void verifyIat(@Nonnull final JWTClaimsSet claimsSet) throws BadJWTException {
+        
+        final Date iatDate = claimsSet.getIssueTime();
+        if (iatDate == null) {
+            throw new BadJWTException("JWT issued-at time missing");
+        }
+        final Instant iat = iatDate.toInstant();        
+        final Instant now = Instant.now();        
+        final Duration iatDifference = Duration.between(now, iat).abs();
+        final Duration window = Duration.ofSeconds(iatWindow);
+        
+        if (window.compareTo(iatDifference)  < 0) {
+            log.error("Token issued at '{}' was too far away from the current time '{}' with acceptable "
+                    + " deviation of '{}', difference is '{}'",
+                     iat, now, window, iatDifference);
+            throw new BadJWTException("JWT issued-at time is too far away from the current time");
+        }
+        
+    }
+
+}
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ExchangeCodeForDuoToken.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ExchangeCodeForDuoToken.java
index 9a005db..fd23054 100644
--- a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ExchangeCodeForDuoToken.java
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ExchangeCodeForDuoToken.java
@@ -17,6 +17,8 @@
 
 package net.shibboleth.idp.plugin.authn.duo.impl;
 
+import java.text.ParseException;
+
 import javax.annotation.Nonnull;
 
 import org.opensaml.profile.action.ActionSupport;
@@ -24,13 +26,14 @@ import org.opensaml.profile.context.ProfileRequestContext;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.nimbusds.jwt.JWT;
+
 import net.shibboleth.idp.authn.AuthnEventIds;
 import net.shibboleth.idp.authn.context.AuthenticationContext;
 import net.shibboleth.idp.plugin.authn.duo.AbstractDuoAuthenticationAction;
 import net.shibboleth.idp.plugin.authn.duo.DuoClientException;
 import net.shibboleth.idp.plugin.authn.duo.DuoOIDCClient;
 import net.shibboleth.idp.plugin.authn.duo.context.DuoOIDCAuthenticationContext;
-import net.shibboleth.idp.plugin.authn.duo.model.DuoAuthToken;
 
 
 /**
@@ -77,11 +80,17 @@ public class ExchangeCodeForDuoToken extends AbstractDuoAuthenticationAction{
         }
         
         try {
-            final DuoAuthToken token = client.exchangeAuthorizationCodeFor2FAResult(code,username);
-            log.info("{} Duo 2FA token received for subject '{}'",getLogPrefix(),token.getSub());
+            final JWT token = client.exchangeAuthorizationCodeFor2FAResult(code,username);
+            if (log.isDebugEnabled()) {
+                //avoid parsing claims if debug not enabled, 
+                //if debug is enabled and parsing fails here, you will get different behaviour than if
+                //debug is not enabled!
+                log.debug("{} Duo 2FA token received for subject '{}'",getLogPrefix(),
+                        token.getJWTClaimsSet().getSubject());
+            }
             duoContext.setAuthToken(token);            
             //success
-        } catch (final DuoClientException e) {
+        } catch (final DuoClientException | ParseException e) {
             log.error("{} Unable to exchange authorisation code for 2FA result",getLogPrefix(),e);
             ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.AUTHN_EXCEPTION);
             return;
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/IdPJWTSecurityContext.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/IdPJWTSecurityContext.java
new file mode 100644
index 0000000..97a6c59
--- /dev/null
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/IdPJWTSecurityContext.java
@@ -0,0 +1,37 @@
+package net.shibboleth.idp.plugin.authn.duo.impl;
+
+import javax.annotation.Nonnull;
+import javax.annotation.concurrent.Immutable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import com.nimbusds.jose.proc.SecurityContext;
+
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
+/** Simple IdP Nimbus JWT Verification security context that holds the {@link ProfileRequestContext}.*/
+ at Immutable
+public class IdPJWTSecurityContext implements SecurityContext{
+    
+    /** The profile request context.*/
+    @Nonnull private final ProfileRequestContext prc;
+    
+    /** 
+     * Constructor.
+     * 
+     * @param requestContext the profile request context
+     */
+    public IdPJWTSecurityContext(@Nonnull final ProfileRequestContext requestContext) {
+        prc = Constraint.isNotNull(requestContext, "ProfileRequestContext can not be null");
+    }
+    
+    /**
+     * Get the profile request context.
+     * 
+     * @return the profile request context.
+     */
+    @Nonnull public ProfileRequestContext getPrc() {
+        return prc;
+    }
+
+}
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoResponseState.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoResponseState.java
index e3b7491..39a39ba 100644
--- a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoResponseState.java
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoResponseState.java
@@ -79,7 +79,7 @@ public class ValidateDuoResponseState extends AbstractDuoAuthenticationAction {
     }
     
     /**
-     * Set the request and response states to null so they can't be reused. It is less
+     * Set the request and response states to null so they can't be reused - it is less
      * relevant if they are removed (GC'd) from memory.
      * 
      * @param context the duo context.
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAudience.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAudience.java
deleted file mode 100644
index 87b4316..0000000
--- a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAudience.java
+++ /dev/null
@@ -1,102 +0,0 @@
-/*
- * Licensed to the University Corporation for Advanced Internet Development,
- * Inc. (UCAID) under one or more contributor license agreements.  See the
- * NOTICE file distributed with this work for additional information regarding
- * copyright ownership. The UCAID licenses this file to You under the Apache
- * License, Version 2.0 (the "License"); you may not use this file except in
- * compliance with the License.  You may obtain a copy of the License at
- *
- *    http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package net.shibboleth.idp.plugin.authn.duo.impl;
-
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-
-import org.opensaml.profile.action.ActionSupport;
-import org.opensaml.profile.context.ProfileRequestContext;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import net.shibboleth.idp.authn.AuthnEventIds;
-import net.shibboleth.idp.authn.context.AuthenticationContext;
-import net.shibboleth.idp.plugin.authn.duo.AbstractDuoAuthenticationAction;
-import net.shibboleth.idp.plugin.authn.duo.context.DuoOIDCAuthenticationContext;
-import net.shibboleth.idp.plugin.authn.duo.model.DuoAuthToken;
-
-/**
- * An action that verifies the Audience (aud) claim in the Duo token contains the client_id of this client (as
- * registered at the issuer). See section 3.1.3.7 of OpenID Connect core 1.0.
- * 
- * @pre <pre>
- *      ProfileRequestContext.getSubcontext(AuthenticationContext.class, false) != null
- *      </pre>
- * 
- * @pre <pre>
- *      AuthenticationContext.getSubcontext(DuoOIDCAuthenticationContext.class, false) != null
- *      </pre>
- * 
- * @pre n<pre>
- *      DuoOIDCAuthenticationContext.getAuthToken() != null
- *      </pre>
- * 
- * @event {@link org.opensaml.profile.action.EventIds#PROCEED_EVENT_ID}
- * @event {@link net.shibboleth.idp.authn.AuthnEventIds#NO_CREDENTIALS}
- */
-public class ValidateDuoTokenAudience extends AbstractDuoAuthenticationAction {
-
-    /** Class logger. */
-    @Nonnull private final Logger log = LoggerFactory.getLogger(ValidateDuoTokenAudience.class);
-
-    /** The Duo authentication token. */
-    @Nullable private DuoAuthToken token;
-
-    @Override
-    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext,
-            @Nonnull final AuthenticationContext authenticationContext,
-            @Nonnull final DuoOIDCAuthenticationContext duoContext) {
-
-        token = duoContext.getAuthToken();
-        if (token == null) {
-            log.error("{} Duo 2FA token is not available", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
-            return false;
-        }
-        return true;
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
-            @Nonnull final AuthenticationContext authenticationContext,
-            @Nonnull final DuoOIDCAuthenticationContext duoContext) {
-
-        final String audience = token.getAud();
-        if (audience == null) {
-            log.error("{} No audience found in the token response", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
-            return;
-        }
-        if (duoContext.getIntegration() == null) {
-            log.error("{} No Duo integration found in the Duo context", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
-            return;
-        }
-        if (!audience.equals(duoContext.getIntegration().getClientId())) {
-            log.error("{} Client is not the intended audience", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
-            return;
-        }
-        log.trace("{} Token has the correct audience '{}' for this client",getLogPrefix(),audience);
-        //audience is fine
-
-    }
-
-}
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAuthenticationResult.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAuthenticationResult.java
index 86eb33d..17cf1d8 100644
--- a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAuthenticationResult.java
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAuthenticationResult.java
@@ -17,7 +17,9 @@
 package net.shibboleth.idp.plugin.authn.duo.impl;
 
 import java.security.Principal;
+import java.text.ParseException;
 import java.util.Collection;
+import java.util.Map;
 import java.util.function.Function;
 import java.util.stream.Collectors;
 
@@ -27,15 +29,20 @@ import javax.security.auth.Subject;
 
 import org.opensaml.profile.action.ActionSupport;
 import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.saml.saml2.core.AuthnStatement;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.nimbusds.jwt.JWT;
+import com.nimbusds.jwt.JWTClaimsSet;
+
 import net.shibboleth.idp.authn.AbstractValidationAction;
 import net.shibboleth.idp.authn.AuthenticationResult;
 import net.shibboleth.idp.authn.AuthnEventIds;
 import net.shibboleth.idp.authn.context.AuthenticationContext;
 import net.shibboleth.idp.authn.context.SubjectCanonicalizationContext;
 import net.shibboleth.idp.authn.duo.DuoPrincipal;
+import net.shibboleth.idp.plugin.authn.duo.DuoException;
 import net.shibboleth.idp.plugin.authn.duo.DuoOIDCAuthAPI;
 import net.shibboleth.idp.plugin.authn.duo.context.DuoOIDCAuthenticationContext;
 import net.shibboleth.idp.plugin.authn.duo.model.DuoAuthToken;
@@ -68,6 +75,9 @@ public class ValidateDuoTokenAuthenticationResult extends AbstractValidationActi
     /** The profile request context.*/
     @Nullable private ProfileRequestContext prc;
     
+    /** The parsed claimset. */
+    @Nullable private JWTClaimsSet claimsSet;
+    
     /** Attempted username. */
     @Nullable @NotEmpty private String username;
     
@@ -110,14 +120,33 @@ public class ValidateDuoTokenAuthenticationResult extends AbstractValidationActi
 
         duoContext = authenticationContext.getSubcontext(DuoOIDCAuthenticationContext.class);
         if (duoContext == null) {
-            log.info("{} No DuoAuthenticationContext available", getLogPrefix());
+            log.error("{} No DuoAuthenticationContext available", getLogPrefix());
             handleError(profileRequestContext, authenticationContext, "No DuoAuthenticationContext context available",
                     AuthnEventIds.INVALID_AUTHN_CTX);
             recordFailure(profileRequestContext);
             return false;
-        }         
+        }        
         //we get username from the original context, not the duo response.
-        username = duoContext.getUsername();        
+        username = duoContext.getUsername();      
+        
+        final JWT token = duoContext.getAuthToken();
+        if (token == null) {
+            log.error("{} Duo 2FA token is not available", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
+            return false;
+        }
+        try {
+            //parse the claimset here, so parsing only has to happen once, and we fail fast on error (e.g. bad JSON)
+            claimsSet = token.getJWTClaimsSet();
+            if (claimsSet == null) {
+                throw new DuoException("Duo JWT ClaimsSet is null");
+            }
+        } catch (final ParseException | DuoException e) {
+            log.error("{} Claimset of Duo 2FA token is not available", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
+            return false;
+        }
+        
         return true;
     }
 
@@ -126,32 +155,58 @@ public class ValidateDuoTokenAuthenticationResult extends AbstractValidationActi
     @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
             @Nonnull final AuthenticationContext authenticationContext) {    
         
-        final DuoAuthToken token = duoContext.getAuthToken();
-        if (token == null) {
-            log.error("{} Duo 2FA token is not available",getLogPrefix());
-            handleError(profileRequestContext, authenticationContext,"Duo 2FA result not available",
-                    AuthnEventIds.INVALID_CREDENTIALS);
-            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_CREDENTIALS);
+                 
+        Map<String, Object> authStatusObject = null;
+        try {
+            authStatusObject = claimsSet.getJSONObjectClaim(DuoOIDCAuthAPI.DUO_AUTH_RESULT_JSON_OBJECT);
+            if (authStatusObject == null) {
+                throw new DuoException("Authentication result object is null");
+            }
+        } catch (final ParseException | DuoException e) {
+            log.error("{} Duo 2FA access failed for '{}', auth_result missing",getLogPrefix(), username);
+            handleError(profileRequestContext, authenticationContext,"Unexepected Authentication Response", 
+                    AuthnEventIds.AUTHN_EXCEPTION);
             recordFailure(profileRequestContext);
-            return;
         }
         
-        if (DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW.equalsIgnoreCase(token.getAuthResultStatus())){
-            log.debug("{} Duo 2FA authentication succeeded for '{}'",getLogPrefix(),duoContext.getUsername());
-            recordSuccess(profileRequestContext);
-            buildAuthenticationResult(profileRequestContext, authenticationContext);
-        } else if (DuoOIDCAuthAPI.DUO_AUTH_RESULT_DENY.equalsIgnoreCase(token.getAuthResultStatus())) {
-            log.error("{} Duo 2FA failed for '{}', 2FA status '{}'",getLogPrefix(), username,
-                    token.getAuthResultStatusMessage());
-            handleError(profileRequestContext, authenticationContext, token.getAuthResultStatus(),
-                    AuthnEventIds.INVALID_CREDENTIALS);
-            recordFailure(profileRequestContext);
-        } else {        
-            log.error("{} Duo 2FA access failed for '{}', unknown response", getLogPrefix(), username);
+        final Object statusObj = authStatusObject.get(DuoOIDCAuthAPI.DUO_AUTH_RESULT_STATUS_JSON_OBJECT);
+        final Object statusMsgObj = authStatusObject.get(DuoOIDCAuthAPI.DUO_AUTH_RESULT_STATUS_MSG_JSON_OBJECT);
+        
+        //instanceof includes null check
+        if (statusObj instanceof String && statusMsgObj instanceof String) {
+            final String authResultStatus = (String)statusObj;
+            final String authResultStatusMsg = (String)statusMsgObj;
+            
+            if (DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW.equalsIgnoreCase(authResultStatus)){
+                log.debug("{} Duo 2FA authentication succeeded for '{}'",getLogPrefix(),duoContext.getUsername());
+                recordSuccess(profileRequestContext);
+                buildAuthenticationResult(profileRequestContext, authenticationContext);
+                return;
+            } else if (DuoOIDCAuthAPI.DUO_AUTH_RESULT_DENY.equalsIgnoreCase(authResultStatus)) {
+                log.error("{} Duo 2FA failed for '{}', 2FA status '{}'",getLogPrefix(), username,
+                        authResultStatusMsg);
+                handleError(profileRequestContext, authenticationContext, authResultStatus,
+                        AuthnEventIds.INVALID_CREDENTIALS);
+                recordFailure(profileRequestContext);
+                return;
+            } else {        
+                log.error("{} Duo 2FA access failed for '{}', unknown response", getLogPrefix(), username);
+                handleError(profileRequestContext, authenticationContext,"Unexepected Authentication Response", 
+                        AuthnEventIds.AUTHN_EXCEPTION);
+                recordFailure(profileRequestContext);
+                return;
+            }
+        } else {
+            log.error("{} Duo 2FA access failed for '{}', auth_results missing",getLogPrefix(), username);
             handleError(profileRequestContext, authenticationContext,"Unexepected Authentication Response", 
                     AuthnEventIds.AUTHN_EXCEPTION);
             recordFailure(profileRequestContext);
+            return;
         }
+       
+        
+        
+        
     }
     
     /** {@inheritDoc} */
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAuthenticationTime.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAuthenticationTime.java
deleted file mode 100644
index 036c632..0000000
--- a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAuthenticationTime.java
+++ /dev/null
@@ -1,166 +0,0 @@
-/*
- * Licensed to the University Corporation for Advanced Internet Development,
- * Inc. (UCAID) under one or more contributor license agreements.  See the
- * NOTICE file distributed with this work for additional information regarding
- * copyright ownership. The UCAID licenses this file to You under the Apache
- * License, Version 2.0 (the "License"); you may not use this file except in
- * compliance with the License.  You may obtain a copy of the License at
- *
- *    http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package net.shibboleth.idp.plugin.authn.duo.impl;
-
-import java.time.Duration;
-import java.time.Instant;
-
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-
-import org.opensaml.profile.action.ActionSupport;
-import org.opensaml.profile.context.ProfileRequestContext;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import net.shibboleth.idp.authn.AuthnEventIds;
-import net.shibboleth.idp.authn.context.AuthenticationContext;
-import net.shibboleth.idp.plugin.authn.duo.AbstractDuoAuthenticationAction;
-import net.shibboleth.idp.plugin.authn.duo.context.DuoOIDCAuthenticationContext;
-import net.shibboleth.idp.plugin.authn.duo.model.DuoAuthToken;
-import net.shibboleth.utilities.java.support.component.ComponentSupport;
-import net.shibboleth.utilities.java.support.logic.Constraint;
-
-/**
- * 
- * <p> An action that checks if auth_time (when the End-User authentication took place) stored within the id_token
- * is within a valid expiration window.</p>
- * 
- * <p> This only applies to forced authentication requests to ensure active 2FA was performed.</p> 
- * 
- * @pre <pre>
- *      ProfileRequestContext.getSubcontext(AuthenticationContext.class, false) != null
- *      </pre>
- * 
- * @pre <pre>
- *      AuthenticationContext.getSubcontext(DuoOIDCAuthenticationContext.class, false) != null
- *      </pre>
- * 
- * @pre <pre>
- *      DuoOIDCAuthenticationContext.getAuthToken() != null
- *      </pre>
- * 
- * @event {@link org.opensaml.profile.action.EventIds#PROCEED_EVENT_ID}
- * @event {@link net.shibboleth.idp.authn.AuthnEventIds#INVALID_AUTHN_CTX}
- * @event {@link net.shibboleth.idp.authn.AuthnEventIds#NO_CREDENTIALS}
- */
-public class ValidateDuoTokenAuthenticationTime extends AbstractDuoAuthenticationAction {
-
-    /** Class logger. */
-    @Nonnull private final Logger log = LoggerFactory.getLogger(ValidateDuoTokenAuthenticationTime.class);
-    
-    /** Positive clock skew adjustment to consider still acceptable (Default value: 1 minute). */
-    @Nonnull private Duration clockSkew;
-       
-    /** Amount of time for which a token is valid after if it was issued. (Default value: 1 minute) */
-    @Nonnull private Duration authnLifetime;
-
-    /** The Duo authentication token. */
-    @Nullable private DuoAuthToken token;
-    
-    /** Constructor. */
-    public ValidateDuoTokenAuthenticationTime() {        
-        authnLifetime = Duration.ofMinutes(1);
-        clockSkew = Duration.ofMinutes(1);
-    }
-    
-    /**
-     * Sets the amount of time for which a token is valid.
-     * 
-     * @param lifetime amount of time for which a token is valid
-     */
-    public synchronized void setAuthnLifetime(@Nonnull final Duration lifetime) {
-        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
-        Constraint.isNotNull(lifetime, "Token authentication lifetime cannot be null");
-        Constraint.isFalse(lifetime.isNegative(), "Token authentication lifetime cannot be negative");
-        
-        authnLifetime = lifetime;
-    }
-    
-    /**
-     * Set the clock skew.
-     * 
-     * @param skew clock skew to set
-     */
-    public void setClockSkew(@Nonnull final Duration skew) {
-        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
-                
-        clockSkew = Constraint.isNotNull(skew, "Clock skew cannot be null");
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext,
-            @Nonnull final AuthenticationContext authenticationContext,
-            @Nonnull final DuoOIDCAuthenticationContext duoContext) {
-
-        token = duoContext.getAuthToken();
-        if (token == null) {
-            log.error("{} Duo 2FA token is not available", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
-            return false;
-        }
-        return true;
-
-    }
- 
-    /** {@inheritDoc} */
-    @Override
-    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
-            @Nonnull final AuthenticationContext authenticationContext,
-            @Nonnull final DuoOIDCAuthenticationContext duoContext) {
-        
-        if (!authenticationContext.isForceAuthn()) {
-            return;
-        }
-        
-        //auth_time in seconds (as integer) since Unix EPOCH.
-        final Integer authTimeEpochSeconds = token.getAuthTime();
-        if (authTimeEpochSeconds == null) {
-            log.error("{} No auth_time found in the token response", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
-            return; 
-        }
-        
-        final Instant authTime = Instant.ofEpochSecond(authTimeEpochSeconds);
-        final Instant now = Instant.now();
-        final Instant latestValid = now.plus(clockSkew.abs());
-        final Instant expiration = authTime.plus(clockSkew.abs()).plus(authnLifetime);
-
-        // Check time of authentication wasn't in the future
-        if (authTime.isAfter(latestValid)) {
-            log.warn("{} Authentication is not yet valid: auth_time was {}, latest valid is: {}", getLogPrefix(),
-                    authTime, latestValid);
-            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
-            return;
-        }
-
-        // Check time of authentication has not expired
-        if (expiration.isBefore(now)) {
-            log.warn(
-                    "{} Authentication required (forced) but has expired: auth_time was '{}', "
-                    + "expired at: '{}', current time: '{}'",
-                    getLogPrefix(), authTime, expiration, now);
-            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
-            return;
-        }
-        //is OK.
-        
-    }
-
-}
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenExpirationTime.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenExpirationTime.java
deleted file mode 100644
index 3d6a9a2..0000000
--- a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenExpirationTime.java
+++ /dev/null
@@ -1,131 +0,0 @@
-/*
- * Licensed to the University Corporation for Advanced Internet Development,
- * Inc. (UCAID) under one or more contributor license agreements.  See the
- * NOTICE file distributed with this work for additional information regarding
- * copyright ownership. The UCAID licenses this file to You under the Apache
- * License, Version 2.0 (the "License"); you may not use this file except in
- * compliance with the License.  You may obtain a copy of the License at
- *
- *    http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package net.shibboleth.idp.plugin.authn.duo.impl;
-
-import java.time.Duration;
-import java.time.Instant;
-
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-
-import org.opensaml.profile.action.ActionSupport;
-import org.opensaml.profile.context.ProfileRequestContext;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import net.shibboleth.idp.authn.AuthnEventIds;
-import net.shibboleth.idp.authn.context.AuthenticationContext;
-import net.shibboleth.idp.plugin.authn.duo.AbstractDuoAuthenticationAction;
-import net.shibboleth.idp.plugin.authn.duo.context.DuoOIDCAuthenticationContext;
-import net.shibboleth.idp.plugin.authn.duo.model.DuoAuthToken;
-import net.shibboleth.utilities.java.support.component.ComponentSupport;
-import net.shibboleth.utilities.java.support.logic.Constraint;
-
-/**
- * An action that verifies the expiration time (exp) of an id_token. If the expiration time has past,
- * the id_token must not be accepted. A few minutes of leeway is allowed - clock skew.
- * See section 3.1.3.7 of OpenID Connect core 1.0.
- * 
- * @pre <pre>
- *      ProfileRequestContext.getSubcontext(AuthenticationContext.class, false) != null
- *      </pre>
- * 
- * @pre <pre>
- *      AuthenticationContext.getSubcontext(DuoOIDCAuthenticationContext.class, false) != null
- *      </pre>
- * 
- * @pre <pre>
- *      DuoOIDCAuthenticationContext.getAuthToken() != null
- *      </pre>
- * 
- * @event {@link org.opensaml.profile.action.EventIds#PROCEED_EVENT_ID}
- * @event {@link net.shibboleth.idp.authn.AuthnEventIds#AUTHN_EXCEPTION}
- */
-public class ValidateDuoTokenExpirationTime extends AbstractDuoAuthenticationAction {
-
-    /** Class logger. */
-    @Nonnull private final Logger log = LoggerFactory.getLogger(ValidateDuoTokenExpirationTime.class);
-    
-    /** Positive clock skew adjustment to consider still acceptable (Default value: 1 minute). */
-    @Nonnull private Duration clockSkew;
-
-    /** The Duo authentication token. */
-    @Nullable private DuoAuthToken token;
-    
-    /** Constructor. */
-    public ValidateDuoTokenExpirationTime() {
-        clockSkew = Duration.ofMinutes(1);
-    }
-    
-    /**
-     * Set the clock skew.
-     * 
-     * @param skew clock skew to set
-     */
-    public void setClockSkew(@Nonnull final Duration skew) {
-        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
-                
-        clockSkew = Constraint.isNotNull(skew, "Clock skew cannot be null");
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext,
-            @Nonnull final AuthenticationContext authenticationContext,
-            @Nonnull final DuoOIDCAuthenticationContext duoContext) {
-
-        token = duoContext.getAuthToken();
-        if (token == null) {
-            log.error("{} Duo 2FA token is not available", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
-            return false;
-        }
-        return true;
-
-    }
- 
-    /** {@inheritDoc} */
-    @Override
-    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
-            @Nonnull final AuthenticationContext authenticationContext,
-            @Nonnull final DuoOIDCAuthenticationContext duoContext) {
-
-        //exp is time in seconds since EPOCH.
-        final Integer expEpochSeconds = token.getExp();
-        if (expEpochSeconds == null) {
-            log.error("{} No expiry found in the token response", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
-            return; 
-        }
-        
-        final Instant now = Instant.now();
-        final Instant latestExpired = Instant.ofEpochSecond(expEpochSeconds).plus(clockSkew.abs());
-        
-        log.trace("{} Token expires at '{}', current time is '{}'",getLogPrefix(),latestExpired,now);
-        if (now.isAfter(latestExpired)) {
-            log.warn("{} Token is past expiry date: message expired at: '{}', current time: '{}'", getLogPrefix(),
-                    latestExpired, now);
-            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
-            return;
-        }
-        //has not expired.
-        
-
-    }
-
-}
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenIssuedAt.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenIssuedAt.java
deleted file mode 100644
index 0adee90..0000000
--- a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenIssuedAt.java
+++ /dev/null
@@ -1,143 +0,0 @@
-/*
- * Licensed to the University Corporation for Advanced Internet Development,
- * Inc. (UCAID) under one or more contributor license agreements.  See the
- * NOTICE file distributed with this work for additional information regarding
- * copyright ownership. The UCAID licenses this file to You under the Apache
- * License, Version 2.0 (the "License"); you may not use this file except in
- * compliance with the License.  You may obtain a copy of the License at
- *
- *    http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package net.shibboleth.idp.plugin.authn.duo.impl;
-
-import java.time.Duration;
-import java.time.Instant;
-
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-
-import org.opensaml.profile.action.ActionSupport;
-import org.opensaml.profile.context.ProfileRequestContext;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import net.shibboleth.idp.authn.AuthnEventIds;
-import net.shibboleth.idp.authn.context.AuthenticationContext;
-import net.shibboleth.idp.plugin.authn.duo.AbstractDuoAuthenticationAction;
-import net.shibboleth.idp.plugin.authn.duo.context.DuoOIDCAuthenticationContext;
-import net.shibboleth.idp.plugin.authn.duo.model.DuoAuthToken;
-import net.shibboleth.utilities.java.support.component.ComponentSupport;
-import net.shibboleth.utilities.java.support.logic.Constraint;
-
-/**
- * 
- * An action that rejects tokens that were issued (iat) to far away from the current time. 
- * See section 3.1.3.7 of OpenID Connect core 1.0.
- * 
- * @pre <pre>
- *      ProfileRequestContext.getSubcontext(AuthenticationContext.class, false) != null
- *      </pre>
- * 
- * @pre <pre>
- *      AuthenticationContext.getSubcontext(DuoOIDCAuthenticationContext.class, false) != null
- *      </pre>
- * 
- * @pre <pre>
- *      DuoOIDCAuthenticationContext.getAuthToken() != null
- *      </pre>
- * 
- * @event {@link org.opensaml.profile.action.EventIds#PROCEED_EVENT_ID}
- * @event {@link net.shibboleth.idp.authn.AuthnEventIds#AUTHN_EXCEPTION}
- */
-public class ValidateDuoTokenIssuedAt extends AbstractDuoAuthenticationAction {
-    
-    /** Class logger. */
-    @Nonnull private final Logger log = LoggerFactory.getLogger(ValidateDuoTokenIssuedAt.class);
-       
-    /**
-     *  Maximum amount (in either direction from now) of clock skew for which a token is valid after 
-     *  it is issued (Default value: 3 minutes). 
-     */
-    @Nonnull private Duration iatMaxClockSkew;
-
-    /** The Duo authentication token. */
-    @Nullable private DuoAuthToken token;
-    
-    
-    /** Constructor. */
-    public ValidateDuoTokenIssuedAt() {        
-        iatMaxClockSkew = Duration.ofMinutes(3);
-    }
-    
-    /**
-     * Sets the amount of time for which a token is valid.
-     * 
-     * @param clockSkew amount of time for which a token is valid
-     */
-    public synchronized void setIatMaxClockSkew(@Nonnull final Duration clockSkew) {
-        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
-        Constraint.isNotNull(clockSkew, "Token issued at clock skew cannot be null");
-        Constraint.isFalse(clockSkew.isNegative(), "Token issued at clock skew cannot be negative");
-        
-        iatMaxClockSkew = clockSkew;
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext,
-            @Nonnull final AuthenticationContext authenticationContext,
-            @Nonnull final DuoOIDCAuthenticationContext duoContext) {
-
-        token = duoContext.getAuthToken();
-        if (token == null) {
-            log.error("{} Duo 2FA token is not available", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
-            return false;
-        }
-        return true;
-
-    }
- 
-    /** {@inheritDoc} */
-    @Override
-    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
-            @Nonnull final AuthenticationContext authenticationContext,
-            @Nonnull final DuoOIDCAuthenticationContext duoContext) {
-
-        //iat is time in seconds (as double) since EPOCH.
-        final Double iatEpochSeconds = token.getIat();
-        if (iatEpochSeconds == null) {
-            log.error("{} No expiry found in the token response", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
-            return; 
-        }
-        
-        final Instant now = Instant.now();
-        //double to long conversation with rounding! it is unlikely this field will contain a decimal value
-        final Instant iat = Instant.ofEpochSecond(Math.round(iatEpochSeconds));
-        
-        final Duration iatDifference = Duration.between(now, iat).abs();
-        
-        log.trace("{} Difference between when the token was issued '{}' and now '{}' is '{}'",
-                getLogPrefix(),iat, now, iatDifference);
-        
-        if (iatMaxClockSkew.compareTo(iatDifference)  < 0) {
-            log.error("{} Token issued at '{}' was too far away from the current time '{}' with acceptable "
-                    + " deviation of '{}', difference is '{}'",
-                    getLogPrefix(), iat, now, iatMaxClockSkew, iatDifference);
-            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
-            return;
-        }
-        //is within token lifetime window.
-        
-
-    }
-
-}
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenIssuer.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenIssuer.java
deleted file mode 100644
index 10c7d96..0000000
--- a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenIssuer.java
+++ /dev/null
@@ -1,149 +0,0 @@
-/*
- * Licensed to the University Corporation for Advanced Internet Development,
- * Inc. (UCAID) under one or more contributor license agreements.  See the
- * NOTICE file distributed with this work for additional information regarding
- * copyright ownership. The UCAID licenses this file to You under the Apache
- * License, Version 2.0 (the "License"); you may not use this file except in
- * compliance with the License.  You may obtain a copy of the License at
- *
- *    http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package net.shibboleth.idp.plugin.authn.duo.impl;
-
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-
-import org.opensaml.profile.action.ActionSupport;
-import org.opensaml.profile.context.ProfileRequestContext;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import net.shibboleth.idp.authn.AuthnEventIds;
-import net.shibboleth.idp.authn.context.AuthenticationContext;
-import net.shibboleth.idp.plugin.authn.duo.AbstractDuoAuthenticationAction;
-import net.shibboleth.idp.plugin.authn.duo.context.DuoOIDCAuthenticationContext;
-import net.shibboleth.idp.plugin.authn.duo.model.DuoAuthToken;
-import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
-import net.shibboleth.utilities.java.support.component.ComponentSupport;
-import net.shibboleth.utilities.java.support.logic.Constraint;
-
-/**
- * <p>An action that verifies the issuer (iss) of the id_token exactly matches that of the configured 
- * Duo token provider. See section 3.1.3.7 of OpenID Connect core 1.0.</p>
- * 
- * <p>The logic here is specific to the Duo implementation, and mimics the native Duo client issuer validation.</p>
- * 
- * @pre
- * 
- * <pre>
- *      ProfileRequestContext.getSubcontext(AuthenticationContext.class, false) != null
- * </pre>
- * 
- * @pre
- * 
- *      <pre>
- *      AuthenticationContext.getSubcontext(DuoOIDCAuthenticationContext.class, false) != null
- *      </pre>
- * 
- * @pre
- * 
- *      <pre>
- *      DuoOIDCAuthenticationContext.getAuthToken() != null
- *      </pre>
- * 
- * @event {@link org.opensaml.profile.action.EventIds#PROCEED_EVENT_ID}
- * @event {@link net.shibboleth.idp.authn.AuthnEventIds#NO_CREDENTIALS}
- * 
- */
-public class ValidateDuoTokenIssuer extends AbstractDuoAuthenticationAction {
-    
-    /** HTTPS scheme protocol.*/
-    @Nonnull @NotEmpty public static final String HTTPS = "https://";
-    
-    /** The default issuer path, specific to the v1 Duo flow.*/
-    @Nonnull @NotEmpty public static final String DEFAULT_ISSUER_PATH = "/oauth/v1/token"; 
-
-    /** Class logger. */
-    @Nonnull private final Logger log = LoggerFactory.getLogger(ValidateDuoTokenIssuer.class);
-
-    /** The Duo authentication token. */
-    @Nullable private DuoAuthToken token;
-    
-    /** The URL path component of the issuer.*/
-    @Nonnull @NotEmpty private String issuerPath;
-    
-    /** Constructor.*/
-    public ValidateDuoTokenIssuer() {
-        issuerPath = DEFAULT_ISSUER_PATH;
-    }
-    
-    /**
-     * Sets the issuer URL path component.
-     * 
-     * @param path the issuer path
-     */
-    public synchronized void setIssuerPath(@Nonnull final String path) {
-        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
-        issuerPath = Constraint.isNotNull(path, "Issuer URL path cannot be null");       
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext,
-            @Nonnull final AuthenticationContext authenticationContext,
-            @Nonnull final DuoOIDCAuthenticationContext duoContext) {
-
-        token = duoContext.getAuthToken();
-        if (token == null) {
-            log.error("{} Duo 2FA token is not available", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
-            return false;
-        }
-        return true;
-
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
-            @Nonnull final AuthenticationContext authenticationContext,
-            @Nonnull final DuoOIDCAuthenticationContext duoContext) {
-
-        final String issuer = token.getIss();
-        if (issuer == null) {
-            log.error("{} No issuer found in the token response", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
-            return;
-        }
-        if (duoContext.getIntegration() == null) {
-            log.error("{} No Duo integration found in the Duo context", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
-            return;
-        }
-        final String apiHost = duoContext.getIntegration().getAPIHost();
-        if (apiHost == null) {
-            log.error("{} No Duo integration API host found", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
-            return;
-        }
-        
-        final String issuerFromIntegration = HTTPS+apiHost+issuerPath;
-        log.trace("{} Token issuer is '{}', expected '{}'",getLogPrefix(),issuer, issuerFromIntegration);
-        if (!issuer.equals(issuerFromIntegration)) {
-            log.error("{} Token issuer differs from that expected, issuer is '{}', expected '{}'",getLogPrefix(),
-                    issuer,issuerFromIntegration);
-            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
-            return;
-        }
-        //issuer is fine.
-
-    }
-
-}
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenSubject.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenSubject.java
deleted file mode 100644
index ee95ce6..0000000
--- a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenSubject.java
+++ /dev/null
@@ -1,106 +0,0 @@
-/*
- * Licensed to the University Corporation for Advanced Internet Development,
- * Inc. (UCAID) under one or more contributor license agreements.  See the
- * NOTICE file distributed with this work for additional information regarding
- * copyright ownership. The UCAID licenses this file to You under the Apache
- * License, Version 2.0 (the "License"); you may not use this file except in
- * compliance with the License.  You may obtain a copy of the License at
- *
- *    http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package net.shibboleth.idp.plugin.authn.duo.impl;
-
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-
-import org.opensaml.profile.action.ActionSupport;
-import org.opensaml.profile.context.ProfileRequestContext;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import net.shibboleth.idp.authn.AuthnEventIds;
-import net.shibboleth.idp.authn.context.AuthenticationContext;
-import net.shibboleth.idp.plugin.authn.duo.AbstractDuoAuthenticationAction;
-import net.shibboleth.idp.plugin.authn.duo.context.DuoOIDCAuthenticationContext;
-import net.shibboleth.idp.plugin.authn.duo.model.DuoAuthToken;
-
-/**
- * An action that verifies the Subject (sub) claim in the Duo token matches that user which
- * is currently authenticating.
- * 
- * @pre <pre>
- *      ProfileRequestContext.getSubcontext(AuthenticationContext.class, false) != null
- *      </pre>
- * 
- * @pre <pre>
- *      AuthenticationContext.getSubcontext(DuoOIDCAuthenticationContext.class, false) != null
- *      </pre>
- * 
- * @pre n<pre>
- *      DuoOIDCAuthenticationContext.getAuthToken() != null
- *      </pre>
- * 
- * @event {@link org.opensaml.profile.action.EventIds#PROCEED_EVENT_ID}
- * @event {@link net.shibboleth.idp.authn.AuthnEventIds#INVALID_CREDENTIALS}
- * @event {@link net.shibboleth.idp.authn.AuthnEventIds#NO_CREDENTIALS}
- */
-public class ValidateDuoTokenSubject extends AbstractDuoAuthenticationAction {
-
-    /** Class logger. */
-    @Nonnull private final Logger log = LoggerFactory.getLogger(ValidateDuoTokenSubject.class);
-
-    /** The Duo authentication token. */
-    @Nullable private DuoAuthToken token;
-
-    /** {@inheritDoc} */
-    @Override
-    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext,
-            @Nonnull final AuthenticationContext authenticationContext,
-            @Nonnull final DuoOIDCAuthenticationContext duoContext) {
-
-        token = duoContext.getAuthToken();
-        if (token == null) {
-            log.error("{} Duo 2FA token is not available", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
-            return false;
-        }
-        return true;
-
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
-            @Nonnull final AuthenticationContext authenticationContext,
-            @Nonnull final DuoOIDCAuthenticationContext duoContext) {
-
-        final String subject = token.getSub();
-        if (subject == null) {
-            log.error("{} No subject found in the token response", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
-            return;
-        }
-        final String username = duoContext.getUsername();
-        if (username == null) {
-            log.error("{} No username found in the Duo context", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
-            return;
-        }
-        if (!username.equals(subject)) {
-            log.error("{} Username in the Duo context does not match with the subject of the"
-                    + " Duo token", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_CREDENTIALS);
-            return;
-        }
-        
-
-    }
-
-}
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateTokenClaims.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateTokenClaims.java
new file mode 100644
index 0000000..b2b1c5b
--- /dev/null
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateTokenClaims.java
@@ -0,0 +1,242 @@
+package net.shibboleth.idp.plugin.authn.duo.impl;
+
+import java.text.ParseException;
+import java.time.Duration;
+import java.util.HashSet;
+import java.util.List;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.nimbusds.jwt.JWT;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.proc.BadJWTException;
+
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.plugin.authn.duo.AbstractDuoAuthenticationAction;
+import net.shibboleth.idp.plugin.authn.duo.DuoException;
+import net.shibboleth.idp.plugin.authn.duo.DuoOIDCIntegration;
+import net.shibboleth.idp.plugin.authn.duo.context.DuoOIDCAuthenticationContext;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+import net.shibboleth.utilities.java.support.logic.ConstraintViolationException;
+
+/**
+ * Action that validates the claims of the Duo id_token. More specifically:
+ * <ul>
+ *      <li>Verifies the Audience (aud) claim contains the client_id of this client (as
+ *      registered at the issuer). See section 3.1.3.7 of OpenID Connect core 1.0.</li>
+ *      <li>Verifies if the auth_time (when the End-User authentication took place)
+ *      is within a valid expiration window. Only for forced authentications.</li>
+ *      <li>Verifies the expiration time (exp). If the expiration time has past,
+ *      the token must not be accepted. A few minutes of {@code clockSkew} is allowed.
+ *      See section 3.1.3.7 of OpenID Connect core 1.0.</li>
+ *      <li>Rejects tokens that were issued (iat) to far away from the current time. 
+ *       See section 3.1.3.7 of OpenID Connect core 1.0.</li>
+ *       <li>Verifies the issuer (iss) of the token exactly matches that of the configured 
+ *       Duo token provider. See section 3.1.3.7 of OpenID Connect core 1.0.</li>
+ *       <li>Verifies the Subject (sub) claim in the token matches the user who
+ *       is currently authenticating</li>
+ * </ul>
+ */
+public class ValidateTokenClaims extends AbstractDuoAuthenticationAction {
+    
+    /** HTTPS scheme protocol.*/
+    @Nonnull @NotEmpty public static final String HTTPS = "https://";
+    
+    /** The default issuer path, specific to the v1 Duo flow.*/
+    @Nonnull @NotEmpty public static final String DEFAULT_ISSUER_PATH = "/oauth/v1/token"; 
+    
+    /** Name of the username claim.*/
+    @Nonnull @NotEmpty public static final String USERNAME_CLAIM = "preferred_username";
+   
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(ValidateTokenClaims.class);
+    
+    /** The parsed claimset. */
+    @Nullable private JWTClaimsSet claimsSet;
+    
+    /** The Duo integration appropriate for this request.*/
+    @Nullable private DuoOIDCIntegration integration;
+    
+    /** The URL path component of the issuer.*/
+    @Nonnull @NotEmpty private String issuerPath;
+    
+    /** 
+     * Positive clock skew adjustment to consider the JWT still acceptable from its expiration
+     * in seconds (Default value: 60 seconds). 
+     */
+    @Nonnull private int clockSkew;
+    
+    /**
+     *  Maximum amount (in either direction from now) of duration in seconds for which a token is valid after 
+     *  it is issued (Default value: 180 seconds). 
+     */
+    @Nonnull private int iatWindow;
+    
+    /** 
+     * Amount of time in seconds (for forced authentication) for which a token is valid after if it was issued. 
+     * (Default value: 60 seconds) 
+     */
+    @Nonnull private int authnLifetime;
+    
+    /** Constructor. */
+    public ValidateTokenClaims() {
+        clockSkew = 60;
+        iatWindow = 180;
+        authnLifetime = 60;
+        issuerPath = DEFAULT_ISSUER_PATH;
+    }
+    
+    /**
+     * Sets the amount of time for which a token is valid from when it was issued.
+     * 
+     * @param window amount of time for which a token is valid
+     */
+    public synchronized void setIatWindow(@Nonnull final Duration window) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        Constraint.isNotNull(window, "Token issued at window cannot be null");
+        Constraint.isFalse(window.isNegative(), "Token issued at window cannot be negative");
+        
+        final long windowLong = Constraint.isNotNull(window, "IssuedAt Clock skew cannot be null").getSeconds();
+        try {
+            iatWindow =  Math.toIntExact(windowLong);
+        } catch (final ArithmeticException e) {
+            log.error("{} IssuedAt window in seconds ({}'s) is larger than max {}'s allowed",
+                    getLogPrefix(),windowLong,Integer.MAX_VALUE);
+            throw new ConstraintViolationException("IssuedAt window in seconds value is too high");
+        }
+    }
+    
+    /**
+     * Sets the amount of time for which a token is valid from when the original authentication took place.
+     * Only applies to forced authentications.
+     * 
+     * @param lifetime amount of time for which a token is valid
+     */
+    public synchronized void setAuthnLifetime(@Nonnull final Duration lifetime) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        Constraint.isNotNull(lifetime, "Token authentication lifetime cannot be null");
+        Constraint.isFalse(lifetime.isNegative(), "Token authentication lifetime cannot be negative");
+        
+        final long lifetimeLong = Constraint.isNotNull(lifetime, 
+                "Token authentication lifetime cannot be null").getSeconds();
+        try {
+            authnLifetime =  Math.toIntExact(lifetimeLong);
+        } catch (final ArithmeticException e) {
+            log.error("{} Token authentication lifetime in seconds ({}'s) is larger than max {}'s allowed",
+                    getLogPrefix(),lifetimeLong, Integer.MAX_VALUE);
+            throw new ConstraintViolationException("Token authentication lifetime value in seconds is too high");
+        }
+    }
+    
+    /**
+     * Set the clock skew.
+     * 
+     * @param skew clock skew to set
+     */
+    public synchronized void setClockSkew(@Nonnull final Duration skew) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        
+        final long durationLong = Constraint.isNotNull(skew, "Clock skew cannot be null").getSeconds();
+        try {
+            clockSkew =  Math.toIntExact(durationLong);
+        } catch (final ArithmeticException e) {
+            log.error("{} Clock skew in seconds ({}'s) is larger than max {}'s allowed",getLogPrefix(),
+                    durationLong,Integer.MAX_VALUE);
+            throw new ConstraintViolationException("Clock skew in seconds is too high");
+        }
+    }
+    
+    /**
+     * Sets the issuer URL path component.
+     * 
+     * @param path the issuer path
+     */
+    public synchronized void setIssuerPath(@Nonnull final String path) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        issuerPath = Constraint.isNotNull(path, "Issuer URL path cannot be null");       
+    }
+    
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final AuthenticationContext authenticationContext,
+            @Nonnull final DuoOIDCAuthenticationContext duoContext) {
+
+        final JWT token = duoContext.getAuthToken();
+        if (token == null) {
+            log.error("{} Duo 2FA token is not available", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
+            return false;
+        }
+        try {
+            //parse the claimset here, so parsing only has to happen once, and we fail fast on error (e.g. bad JSON)
+            claimsSet = token.getJWTClaimsSet();
+            if (claimsSet == null) {
+                throw new DuoException("Duo JWT ClaimsSet is null");
+            }
+        } catch (final ParseException | DuoException e) {
+            log.error("{} Claimset of Duo 2FA token is not available", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
+            return false;
+        }
+        integration = duoContext.getIntegration();
+        if (integration == null) {
+            log.error("{} Duo integration is not available", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
+            return false;
+        }
+        
+        return true;
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final AuthenticationContext authenticationContext,
+            @Nonnull final DuoOIDCAuthenticationContext duoContext) {
+
+        log.debug("{} Validating token claims for subject '{}'",getLogPrefix(),claimsSet.getSubject());
+        
+        final String apiHost = duoContext.getIntegration().getAPIHost();
+        if (apiHost == null) {
+            log.error("{} No Duo integration API host found", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
+            return;
+        }
+        
+        final DuoJWTClaimsVerifier claimsVerifier = new DuoJWTClaimsVerifier(
+                //audience
+                duoContext.getIntegration().getClientId(),
+                
+                //exact match claims
+                new JWTClaimsSet.Builder().issuer(HTTPS+apiHost+issuerPath)
+                .claim(USERNAME_CLAIM, duoContext.getUsername()).build(),
+                
+                //required claims, automatically including the exact match above
+                new HashSet<>(List.of("exp","sub"))
+                
+                );
+        
+        claimsVerifier.setMaxClockSkew(clockSkew);
+        claimsVerifier.setIatWindow(iatWindow);
+        claimsVerifier.setAuthnLifetime(authnLifetime);
+        
+        try {
+            claimsVerifier.verify(claimsSet, new IdPJWTSecurityContext(profileRequestContext));
+        } catch (final BadJWTException e) {
+            log.error("{} Token verification failed for subject '{}'", getLogPrefix(),claimsSet.getSubject(),e);
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
+            return;
+        }
+        //fine.
+        log.debug("{} Token claims are valid for subject '{}'",getLogPrefix(),claimsSet.getSubject());
+    }
+}
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateTokenSignature.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateTokenSignature.java
new file mode 100644
index 0000000..6569b33
--- /dev/null
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateTokenSignature.java
@@ -0,0 +1,131 @@
+package net.shibboleth.idp.plugin.authn.duo.impl;
+
+import java.text.ParseException;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jwt.JWT;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.PlainJWT;
+import com.nimbusds.jwt.SignedJWT;
+import com.nimbusds.jose.JWSVerifier;
+import com.nimbusds.jose.crypto.MACVerifier;
+
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.plugin.authn.duo.AbstractDuoAuthenticationAction;
+import net.shibboleth.idp.plugin.authn.duo.DuoOIDCIntegration;
+import net.shibboleth.idp.plugin.authn.duo.context.DuoOIDCAuthenticationContext;
+
+
+/**
+ * Action to validate the JWT signature. The JWT **must** be signed using the HMAC_SHA family.
+ * Any other type, or 'none', emits and error back to the flow.
+ */
+public class ValidateTokenSignature extends AbstractDuoAuthenticationAction {
+    
+    /** 
+     * The HMAC 'family' of signature algorithms is the only supported, based on the
+     * shared secret in the client integration.
+     */
+    @Nonnull private static final JWSAlgorithm.Family SUPPORTED_SIGNATURE_FAMILY = 
+            JWSAlgorithm.Family.HMAC_SHA;
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(ValidateTokenSignature.class);
+
+    /** The Duo authentication token. */
+    @Nullable private JWT token;
+    
+    /** The parsed claimset. */
+    @Nullable private JWTClaimsSet claimSet;
+    
+    /** The Duo integration appropriate for this request.*/
+    @Nullable private DuoOIDCIntegration integration;
+
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final AuthenticationContext authenticationContext,
+            @Nonnull final DuoOIDCAuthenticationContext duoContext) {
+
+        token = duoContext.getAuthToken();
+        if (token == null) {
+            log.error("{} Duo 2FA token is not available", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
+            return false;
+        }
+        try {
+            //parse the claimset here, so parsing only has to happen once, and we fail fast on error (e.g. bad JSON)
+            claimSet = token.getJWTClaimsSet();
+        } catch (final ParseException e) {
+            log.error("{} Claimset of Duo 2FA token is not available", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
+            return false;
+        }
+        integration = duoContext.getIntegration();
+        if (integration == null) {
+            log.error("{} Duo integration is not available", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
+            return false;
+        }
+        
+        return true;
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final AuthenticationContext authenticationContext,
+            @Nonnull final DuoOIDCAuthenticationContext duoContext) {
+
+        log.info("{} Validating token signature for subject '{}'",getLogPrefix(),claimSet.getSubject());
+        
+        //only supports HMAC signatures. Asymmetric or 'none' are not allowed.
+       
+        if (token instanceof PlainJWT || JWSAlgorithm.NONE == token.getHeader().getAlgorithm()) {
+            
+            log.error("{} Invalid token. Token must be signed using one of the supported algorithms '{}'",
+                    getLogPrefix(),SUPPORTED_SIGNATURE_FAMILY); 
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
+            return;
+            
+        } else if (token instanceof SignedJWT) {
+            try {
+                if (SUPPORTED_SIGNATURE_FAMILY.contains(((SignedJWT)token).getHeader().getAlgorithm())) {
+                    final JWSVerifier verifier = new MACVerifier(duoContext.getIntegration().getSecretKey());
+                    if (!((SignedJWT)token).verify(verifier)) {
+                        log.error("{} Token signature is invalid for subject '{}' and client '{}'",getLogPrefix(),
+                                claimSet.getSubject(),integration.getClientId());
+                        ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
+                        return;
+                    }
+                    log.debug("{} Token signature is valid; using algorithm '{}' for client '{}'",
+                            getLogPrefix(),SUPPORTED_SIGNATURE_FAMILY, integration.getClientId());
+                } else {
+                    log.error("{} Invalid token. Token signature algorithm not supported, token algorithm '{}',"
+                            + " supported algorithms '{}', for client '{}'",getLogPrefix(),
+                            ((SignedJWT)token).getHeader().getAlgorithm().getName(),
+                            SUPPORTED_SIGNATURE_FAMILY,integration.getClientId());
+                    ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
+                    return;
+                }
+            } catch (final IllegalStateException | JOSEException e) {
+                log.error("{} Unable to validate token using algorithms '{}' for client '{}'",getLogPrefix(),
+                        SUPPORTED_SIGNATURE_FAMILY,integration.getClientId(),e);
+                ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
+                return;
+            }
+            
+        } 
+        //all good.   
+    }
+
+}
diff --git a/idp-duo-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/DuoOIDC/duo-oidc-authn-beans.xml b/idp-duo-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/DuoOIDC/duo-oidc-authn-beans.xml
index 9df31aa..cc5d62d 100644
--- a/idp-duo-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/DuoOIDC/duo-oidc-authn-beans.xml
+++ b/idp-duo-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/DuoOIDC/duo-oidc-authn-beans.xml
@@ -62,21 +62,13 @@
 
     <bean id="ValidateDuoResponseState" scope="prototype"
         class="net.shibboleth.idp.plugin.authn.duo.impl.ValidateDuoResponseState" />
+        
+    <bean id="ValidateTokenSignature" scope="prototype"
+        class="net.shibboleth.idp.plugin.authn.duo.impl.ValidateTokenSignature" />
+        
+    <bean id="ValidateTokenClaims" scope="prototype"
+        class="net.shibboleth.idp.plugin.authn.duo.impl.ValidateTokenClaims" />
 
-    <bean id="ValidateDuoTokenAudience" scope="prototype"
-        class="net.shibboleth.idp.plugin.authn.duo.impl.ValidateDuoTokenAudience" />
-
-    <bean id="ValidateDuoTokenIssuer" scope="prototype"
-        class="net.shibboleth.idp.plugin.authn.duo.impl.ValidateDuoTokenIssuer" />
-
-    <bean id="ValidateDuoTokenExpirationTime" scope="prototype"
-        class="net.shibboleth.idp.plugin.authn.duo.impl.ValidateDuoTokenExpirationTime" />
-
-    <bean id="ValidateDuoTokenAuthenticationTime" scope="prototype"
-        class="net.shibboleth.idp.plugin.authn.duo.impl.ValidateDuoTokenAuthenticationTime" />
-
-    <bean id="ValidateDuoTokenIssuedAt" scope="prototype"
-        class="net.shibboleth.idp.plugin.authn.duo.impl.ValidateDuoTokenIssuedAt" />
 
     <bean id="ExchangeCodeForDuoToken" scope="prototype"
         class="net.shibboleth.idp.plugin.authn.duo.impl.ExchangeCodeForDuoToken" />
diff --git a/idp-duo-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/DuoOIDC/duo-oidc-authn-flow.xml b/idp-duo-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/DuoOIDC/duo-oidc-authn-flow.xml
index 3015391..c4cc3f3 100644
--- a/idp-duo-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/DuoOIDC/duo-oidc-authn-flow.xml
+++ b/idp-duo-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/DuoOIDC/duo-oidc-authn-flow.xml
@@ -34,11 +34,8 @@
     <action-state id="ExchangeCodeForDuoToken">
         <evaluate expression="ExchangeCodeForDuoToken"/>
         <!-- validate the token and set principal -->
-        <evaluate expression="ValidateDuoTokenAudience"/>
-        <evaluate expression="ValidateDuoTokenIssuer"/>
-        <evaluate expression="ValidateDuoTokenExpirationTime"/>
-        <evaluate expression="ValidateDuoTokenAuthenticationTime"/>
-        <evaluate expression="ValidateDuoTokenIssuedAt"/>
+        <evaluate expression="ValidateTokenSignature"/>
+        <evaluate expression="ValidateTokenClaims"/>
         <!-- final validation of the response status to build an authn result -->
         <evaluate expression="ValidateDuoTokenAuthenticationResult"/>       
         <evaluate expression="'proceed'" />
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/AbstractDuoActionTest.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/AbstractDuoActionTest.java
index 57138c1..abe125a 100644
--- a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/AbstractDuoActionTest.java
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/AbstractDuoActionTest.java
@@ -20,8 +20,11 @@ package net.shibboleth.idp.plugin.authn.duo.impl;
 import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertNotNull;
 import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
 
+import java.text.ParseException;
 import java.time.Instant;
+import java.time.temporal.ChronoUnit;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -34,21 +37,36 @@ import org.springframework.webflow.execution.Event;
 import org.springframework.webflow.execution.RequestContext;
 
 import com.codahale.metrics.MetricRegistry;
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JWSHeader;
+import com.nimbusds.jose.JWSSigner;
+import com.nimbusds.jose.KeyLengthException;
+import com.nimbusds.jose.PlainHeader;
+import com.nimbusds.jose.crypto.MACSigner;
+import com.nimbusds.jose.util.Base64URL;
+import com.nimbusds.jwt.JWT;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.PlainJWT;
+import com.nimbusds.jwt.SignedJWT;
 
 import net.shibboleth.idp.authn.AuthenticationFlowDescriptor;
 import net.shibboleth.idp.authn.context.AuthenticationContext;
 import net.shibboleth.idp.plugin.authn.duo.DefaultDuoOIDCIntegration;
+import net.shibboleth.idp.plugin.authn.duo.DuoClientException;
 import net.shibboleth.idp.plugin.authn.duo.context.DuoOIDCAuthenticationContext;
 import net.shibboleth.idp.plugin.authn.duo.model.DuoAccessDevice;
 import net.shibboleth.idp.plugin.authn.duo.model.DuoApplication;
 import net.shibboleth.idp.plugin.authn.duo.model.DuoAuthContext;
 import net.shibboleth.idp.plugin.authn.duo.model.DuoAuthDevice;
 import net.shibboleth.idp.plugin.authn.duo.model.DuoAuthToken;
+import net.shibboleth.idp.profile.RequestContextBuilder;
 
 import net.shibboleth.idp.profile.context.navigate.WebflowRequestContextProfileRequestContextLookup;
-import net.shibboleth.idp.profile.testing.RequestContextBuilder;
+import net.shibboleth.utilities.java.support.codec.Base64Support;
+import net.shibboleth.utilities.java.support.codec.EncodingException;
 import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
 
+
 /**
  * Abstract class for Duo 2FA OIDC Action tests.
  */
@@ -94,8 +112,46 @@ public abstract class AbstractDuoActionTest {
     }
     
     /**
-     * Create a dummy {@link DuoAuthToken}.
+     * Create a dummy Duo plain (no sig or enc) JWT token.
+     * 
+     * @param authResult the authentication result e.g. allow.
+     * @param authResultMessage a user friendly result message.
+     * @param aud audience
+     * @param exp expiration time.
+     * @param iat issued at.
+     * @param authTime the auth time.
+     * @param apiHost TODO
+     * @param factor TODO
+     * @return the duo auth token.
+     */
+    protected JWT createPlainDummyToken(@Nonnull final String authResult, 
+            @Nonnull final String authResultMessage, @Nonnull final String aud,
+            @Nonnull final Instant exp, @Nonnull final Instant iat, 
+            @Nonnull final Instant authTime, @Nonnull final String apiHost, 
+            @Nonnull final String factor) {
+        
+        final String jwtJson = createJWTJson(authResult,authResultMessage,aud,
+            exp, iat, authTime, apiHost, factor);
+        
+        try {
+           
+            final JWT jwt = new PlainJWT(new PlainHeader().toBase64URL(),
+                    new Base64URL(Base64Support.encodeURLSafe(jwtJson.getBytes())));
+            //test the claims exist by calling it
+            jwt.getJWTClaimsSet();
+            return jwt;
+        } catch (final EncodingException | ParseException e) {            
+            fail("Error creating the Mock JWT",e);
+        }
+        fail();
+        return null;
+    }
+    
+    /**
+     * Create a signed Duo JWT token.
      * 
+     * @param headerJson the header that defines the crypto params.
+     * @param secret the secret used to sign the JWT.
      * @param authResult the authentication result e.g. allow.
      * @param authResultMessage a user friendly result message.
      * @param aud audience
@@ -106,43 +162,178 @@ public abstract class AbstractDuoActionTest {
      * @param factor TODO
      * @return the duo auth token.
      */
-    protected DuoAuthToken createDummyToken(@Nonnull final String authResult, 
+    protected JWT createSignedDummyToken(@Nonnull final String headerJson, 
+            @Nonnull final String secret,
+            @Nonnull final String authResult, 
+            @Nonnull final String authResultMessage, @Nonnull final String aud,
+            @Nonnull final Instant exp, @Nonnull final Instant iat, 
+            @Nonnull final Instant authTime, @Nonnull final String apiHost, 
+            @Nonnull final String factor) {
+        
+        final String jwtJson = createJWTJson(authResult,authResultMessage,aud,
+            exp, iat, authTime, apiHost, factor);
+        
+        try {     
+            final JWSSigner signer = new MACSigner(secret);
+            final JWSHeader header = JWSHeader.parse(headerJson);
+            final JWTClaimsSet claims = JWTClaimsSet.parse(jwtJson);
+            final SignedJWT jwt = new SignedJWT(header,claims);
+            jwt.sign(signer);
+            //test the claims exist by calling it
+            jwt.getJWTClaimsSet();
+            return jwt;
+        } catch (final ParseException | JOSEException e) {            
+            fail("Error creating the Mock JWT",e);
+        }
+        fail();
+        return null;
+    }
+    
+    /**
+     * Create a signed Duo JWT token using the supplied (not computed) signature.
+     * Can be used to generate a token with an invalid signature.
+     * 
+     * @param headerJson the header that defines the crypto params.
+     * @param signatureBase64 the base64 encoded signature.
+     * @param authResult the authentication result e.g. allow.
+     * @param authResultMessage a user friendly result message.
+     * @param aud audience
+     * @param exp expiration time.
+     * @param iat issued at.
+     * @param authTime the auth time.
+     * @param apiHost TODO
+     * @param factor TODO
+     * @return the duo auth token.
+     */
+    protected JWT createSignedDummyTokenFromGivenSignature(@Nonnull final String headerJson, 
+            @Nonnull final String signatureBase64,
+            @Nonnull final String authResult, 
+            @Nonnull final String authResultMessage, @Nonnull final String aud,
+            @Nonnull final Instant exp, @Nonnull final Instant iat, 
+            @Nonnull final Instant authTime, @Nonnull final String apiHost, 
+            @Nonnull final String factor) {
+        
+        final String jwtJson = createJWTJson(authResult,authResultMessage,aud,
+            exp, iat, authTime, apiHost, factor);
+        
+        try {     
+            final JWT jwt = new SignedJWT(new Base64URL(Base64Support.encodeURLSafe(headerJson.getBytes())),
+                    new Base64URL(Base64Support.encodeURLSafe(jwtJson.getBytes())), new Base64URL(signatureBase64));
+            //test the claims exist by calling it
+            jwt.getJWTClaimsSet();
+            return jwt;
+        } catch (final ParseException | EncodingException e) {            
+            fail("Error creating the Mock JWT",e);
+        }
+        fail();
+        return null;
+    }
+    
+    /**
+     * Create a signed Duo JWT token which is NOT signed. Allows testing of unsupported algorithms.
+     * 
+     * @param headerJson the header that defines the crypto params.
+     * @param authResult the authentication result e.g. allow.
+     * @param authResultMessage a user friendly result message.
+     * @param aud audience
+     * @param exp expiration time.
+     * @param iat issued at.
+     * @param authTime the auth time.
+     * @param apiHost TODO
+     * @param factor TODO
+     * @return the duo auth token.
+     */
+    protected JWT createUnsignedSignedDummyToken(@Nonnull final String headerJson, 
+            @Nonnull final String authResult, 
+            @Nonnull final String authResultMessage, @Nonnull final String aud,
+            @Nonnull final Instant exp, @Nonnull final Instant iat, 
+            @Nonnull final Instant authTime, @Nonnull final String apiHost, 
+            @Nonnull final String factor) {
+        
+        final String jwtJson = createJWTJson(authResult,authResultMessage,aud,
+            exp, iat, authTime, apiHost, factor);
+        
+        try {     
+            final JWSHeader header = JWSHeader.parse(headerJson);
+            final JWTClaimsSet claims = JWTClaimsSet.parse(jwtJson);
+            final SignedJWT jwt = new SignedJWT(header,claims);
+            //test the claims exist by calling it
+            jwt.getJWTClaimsSet();
+            return jwt;
+        } catch (final ParseException e) {            
+            fail("Error creating the Mock JWT",e);
+        }
+        fail();
+        return null;
+    }
+    
+    /**
+     * Create a JWT token in JSON using the claims arguments.
+     * 
+     * @param authResult the authentication result e.g. allow.
+     * @param authResultMessage a user friendly result message.
+     * @param aud audience
+     * @param exp expiration time.
+     * @param iat issued at.
+     * @param authTime the auth time.
+     * @param apiHost the api host.
+     * @param factor the factor.
+     * @return the duo auth token.
+     */
+    private String createJWTJson(@Nonnull final String authResult, 
             @Nonnull final String authResultMessage, @Nonnull final String aud,
             @Nonnull final Instant exp, @Nonnull final Instant iat, 
             @Nonnull final Instant authTime, @Nonnull final String apiHost, 
             @Nonnull final String factor) {
-        return DuoAuthToken.builder()
-                .withIss("https://"+apiHost+"/oauth/v1/token")
-                .withSub("jdoe")
-                .withAud(aud)
-                .withExp(Math.toIntExact(exp.getEpochSecond()))
-                .withIat((double)iat.getEpochSecond())
-                .withAuthResultStatusMessage(authResultMessage)
-                .withAuthResultStatus(authResult)
-                .withAuthResult(authResult)
-                .withAuthContext(DuoAuthContext.builder()
-                        .withResult("success")
-                        .withTimestamp(1590070939)
-                        .withAuthDevice(DuoAuthDevice.builder()
-                                .withIp("192.168.0.1")
-                                .withName("99999999")
-                                .build())
-                        .withTxid("b1287968-1dd1-4488-bb3c-0c72fc398b8b")
-                        .withEventType("authenticaiton")
-                        .withReason("user_approved")
-                        .withAccessDevice(DuoAccessDevice.builder()
-                                .withIp("192.168.0.1")
-                                .withHostname("99999999")
-                                .build())
-                        .withApplication(DuoApplication.builder()
-                                .withKey("DIU6GEFXXXXXXX")
-                                .withName("Test")
-                                .build())
-                        .withFactor(factor)
-                        .withUsername("jdoe")
-                        .withUserKey("XXXXXXX")
-                        .build()).withAuthTime(Math.toIntExact(authTime.getEpochSecond()))
-                .build();
+        final String jwtJson = "{\n" + 
+                "    \"iss\": \"https://"+apiHost+"/oauth/v1/token\",\n" + 
+                "    \"sub\": \"jdoe\",\n" + 
+                "    \"preferred_username\": \"jdoe\",\n" + 
+                "    \"aud\": \""+aud+"\",\n" + 
+                "    \"exp\": "+Math.toIntExact(exp.getEpochSecond()) +",\n" + 
+                "    \"iat\": "+(double)iat.getEpochSecond()+",\n" + 
+                "    \"auth_time\": "+authTime.getEpochSecond()+",\n" + 
+                "    \"auth_result\": {\n" + 
+                "        \"status_msg\": \""+authResultMessage+"\",\n" + 
+                "        \"status\": \""+authResult+"\",\n" + 
+                "        \"result\": \""+authResult+"\"\n" + 
+                "    },\n" + 
+                "    \"auth_context\": {\n" + 
+                "        \"result\": \"success\",\n" + 
+                "        \"timestamp\": 1599749128,\n" + 
+                "        \"auth_device\": {\n" + 
+                "            \"ip\": \"82.17.89.232\",\n" + 
+                "            \"name\": \"+44 7852 119881\",\n" + 
+                "            \"location\": {\n" + 
+                "                \"state\": \"Wales\",\n" + 
+                "                \"city\": \"Cardiff\",\n" + 
+                "                \"country\": \"United Kingdom\"\n" + 
+                "            }\n" + 
+                "        },\n" + 
+                "        \"txid\": \"1684599c-bb16-4250-af85-904291bfe7cc\",\n" + 
+                "        \"event_type\": \"authentication\",\n" + 
+                "        \"reason\": \"user_approved\",\n" + 
+                "        \"access_device\": {\n" + 
+                "            \"hostname\": null,\n" + 
+                "            \"ip\": \"82.17.89.232\",\n" + 
+                "            \"location\": {\n" + 
+                "                \"state\": \"Wales\",\n" + 
+                "                \"city\": \"Cardiff\",\n" + 
+                "                \"country\": \"United Kingdom\"\n" + 
+                "            }\n" + 
+                "        },\n" + 
+                "        \"application\": {\n" + 
+                "            \"key\": \"DIU6GEFWG5LIUTVV2M3N\",\n" + 
+                "            \"name\": \"Shibboleth Integration Testing\"\n" + 
+                "        },\n" + 
+                "        \"factor\": \""+factor+"\",\n" + 
+                "        \"user\": {\n" + 
+                "            \"key\": \"DUGL8U46QGJSOUJWG59W\",\n" + 
+                "            \"name\": \"philsmart\"\n" + 
+                "        }\n" + 
+                "    }\n" + 
+                "}"; 
+        return jwtJson;
     }
     
     /**
@@ -186,6 +377,7 @@ public abstract class AbstractDuoActionTest {
     /** Add the Duo authentication context to the authentication context.*/
     protected void addDuoContext() {
         dc = new DuoOIDCAuthenticationContext();
+        dc.setUsername("jdoe");
         ac.addSubcontext(dc);
     }
     
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAudienceTest.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAudienceTest.java
deleted file mode 100644
index 8ff1a14..0000000
--- a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAudienceTest.java
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
- * Licensed to the University Corporation for Advanced Internet Development,
- * Inc. (UCAID) under one or more contributor license agreements.  See the
- * NOTICE file distributed with this work for additional information regarding
- * copyright ownership. The UCAID licenses this file to You under the Apache
- * License, Version 2.0 (the "License"); you may not use this file except in
- * compliance with the License.  You may obtain a copy of the License at
- *
- *    http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-/*
- * Licensed to the University Corporation for Advanced Internet Development,
- * Inc. (UCAID) under one or more contributor license agreements.  See the
- * NOTICE file distributed with this work for additional information regarding
- * copyright ownership. The UCAID licenses this file to You under the Apache
- * License, Version 2.0 (the "License"); you may not use this file except in
- * compliance with the License.  You may obtain a copy of the License at
- *
- *    http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package net.shibboleth.idp.plugin.authn.duo.impl;
-
-import static org.testng.Assert.assertNull;
-
-import java.time.Instant;
-import java.time.temporal.ChronoUnit;
-
-import org.springframework.webflow.execution.Event;
-import org.testng.annotations.BeforeMethod;
-import org.testng.annotations.Test;
-
-import net.shibboleth.idp.authn.AuthnEventIds;
-import net.shibboleth.idp.plugin.authn.duo.DuoOIDCAuthAPI;
-import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
-
-/**
- * Tests for the {@link ValidateDuoTokenAudience} class.
- */
-public class ValidateDuoTokenAudienceTest extends AbstractDuoActionTest {
-
-    /** The action to test. */
-    private ValidateDuoTokenAudience action;
-
-    @BeforeMethod
-    public void setUp() throws Exception {
-        super.setup();
-        action = new ValidateDuoTokenAudience();
-
-    }
-
-    /* Test Duo 2FA token response audience validation, success.*/
-    @Test
-    public final void testDoExecuteSuccess() throws ComponentInitializationException {
-        addDuoContext();
-        addDuoIntegrationToContext();
-        addAttemptedFlow("authn/DuoOIDC");
-        dc.setAuthToken(createDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID, 
-                Instant.now().plus(1,ChronoUnit.MINUTES),Instant.now(), Instant.now(), "api.duosecurity.com", "duo_push"));
-        action.initialize();
-        final Event event = action.execute(src);
-        // null event is success.
-        assertNull(event);
-
-    }
-    
-    /* Test Duo 2FA token response audience validation, wrong audience.*/
-    @Test
-    public final void testDoExecuteWrongAudience() throws ComponentInitializationException {
-        addDuoContext();
-        addDuoIntegrationToContext();
-        addAttemptedFlow("authn/DuoOIDC");
-        dc.setAuthToken(createDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful","WRONG",
-                Instant.now().plus(1,ChronoUnit.MINUTES),Instant.now(), Instant.now(), "api.duosecurity.com", "duo_push"));
-        action.initialize();
-        final Event event = action.execute(src);
-        // null event is success.
-        assertEventId(event,AuthnEventIds.NO_CREDENTIALS);
-
-    }
-
-}
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAuthenticationResultTest.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAuthenticationResultTest.java
index acb1344..9dd4804 100644
--- a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAuthenticationResultTest.java
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAuthenticationResultTest.java
@@ -24,6 +24,7 @@ import static org.testng.Assert.assertNull;
 import static org.testng.Assert.assertTrue;
 
 import java.security.Principal;
+import java.text.ParseException;
 import java.time.Instant;
 import java.time.temporal.ChronoUnit;
 import java.util.ArrayList;
@@ -72,7 +73,7 @@ public class ValidateDuoTokenAuthenticationResultTest extends AbstractDuoActionT
         addDuoContext();
         addDuoIntegrationToContext();
         addAttemptedFlow("authn/DuoOIDC");
-        dc.setAuthToken(createDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
+        dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
                 Instant.now().plus(1,ChronoUnit.MINUTES),Instant.now(), Instant.now(), "api.duosecurity.com", "duo_push"));
         dc.setUsername("jdoe");
         action.initialize();
@@ -111,7 +112,7 @@ public class ValidateDuoTokenAuthenticationResultTest extends AbstractDuoActionT
         classifiedMessages.put(AuthnEventIds.ACCOUNT_LOCKED, Arrays.asList(new String[] {"deny"}));
         action.setClassifiedMessages(classifiedMessages);
         
-        dc.setAuthToken(createDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_DENY,"Account locked",CLIENT_ID, 
+        dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_DENY,"Account locked",CLIENT_ID, 
                 Instant.now().plus(1,ChronoUnit.MINUTES),Instant.now(), Instant.now(), "api.duosecurity.com", "duo_push"));
         dc.setUsername("jdoe");
         action.initialize();
@@ -135,7 +136,7 @@ public class ValidateDuoTokenAuthenticationResultTest extends AbstractDuoActionT
         
         final Event event = action.execute(src);
      
-        assertEventId(event, AuthnEventIds.INVALID_CREDENTIALS);
+        assertEventId(event, AuthnEventIds.INVALID_AUTHN_CTX);
     }
     
     /**
@@ -151,7 +152,7 @@ public class ValidateDuoTokenAuthenticationResultTest extends AbstractDuoActionT
         addDuoIntegrationToContext();
         addAttemptedFlow("authn/DuoOIDC");
         //made up fail message, will terminate on the duo site in reality.
-        dc.setAuthToken(createDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_DENY,"Login Failed",CLIENT_ID, 
+        dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_DENY,"Login Failed",CLIENT_ID, 
                 Instant.now().plus(1,ChronoUnit.MINUTES),Instant.now(), Instant.now(), "api.duosecurity.com", "duo_push"));
         dc.setUsername("jdoe");
         action.initialize();
@@ -170,7 +171,7 @@ public class ValidateDuoTokenAuthenticationResultTest extends AbstractDuoActionT
         addDuoContext();
         addDuoIntegrationToContext();
         addAttemptedFlow("authn/DuoOIDC");
-        dc.setAuthToken(createDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
+        dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
                 Instant.now().plus(1,ChronoUnit.MINUTES),Instant.now(), Instant.now(), "api.duosecurity.com", "duo_push"));
         dc.setUsername("jdoe");
         
@@ -178,9 +179,14 @@ public class ValidateDuoTokenAuthenticationResultTest extends AbstractDuoActionT
            
            DuoOIDCAuthenticationContext duoContext = 
                    prc.getSubcontext(AuthenticationContext.class).getSubcontext(DuoOIDCAuthenticationContext.class);
-           List<Principal> p = new ArrayList<>();
-           if ("duo_push".equals(duoContext.getAuthToken().getAuthContext().getFactor())){
-               p.add(new AuthnContextClassRefPrincipal("http://example.com/duoPush"));
+           final List<Principal> p = new ArrayList<>();
+           try {
+            if ("duo_push".equals((String)duoContext.getAuthToken().getJWTClaimsSet()
+                       .getJSONObjectClaim("auth_context").get("factor"))){
+                   p.add(new AuthnContextClassRefPrincipal("http://example.com/duoPush"));
+               }
+           } catch (final ParseException e) {
+               throw new RuntimeException("Can not find duo factor in mocked duo response",e);
            }
            return p;
         });
@@ -207,7 +213,7 @@ public class ValidateDuoTokenAuthenticationResultTest extends AbstractDuoActionT
         addDuoContext();
         addDuoIntegrationToContext();
         addAttemptedFlow("authn/DuoOIDC");
-        dc.setAuthToken(createDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
+        dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
                 Instant.now().plus(1,ChronoUnit.MINUTES),Instant.now(), Instant.now(), "api.duosecurity.com", "duo_push"));
         dc.setUsername("jdoe");
         
@@ -215,10 +221,16 @@ public class ValidateDuoTokenAuthenticationResultTest extends AbstractDuoActionT
            
            DuoOIDCAuthenticationContext duoContext = 
                    prc.getSubcontext(AuthenticationContext.class).getSubcontext(DuoOIDCAuthenticationContext.class);
-           List<Principal> p = new ArrayList<>();
-           if ("sms".equals(duoContext.getAuthToken().getAuthContext().getFactor())){
-               p.add(new AuthnContextClassRefPrincipal("http://example.com/sms"));
-           }
+           final List<Principal> p = new ArrayList<>();
+           try {
+               if ("sms".equals((String)duoContext.getAuthToken().getJWTClaimsSet()
+                          .getJSONObjectClaim("auth_context").get("factor"))){
+                      p.add(new AuthnContextClassRefPrincipal("http://example.com/sms"));
+                  }
+              } catch (final ParseException e) {
+                  throw new RuntimeException("Can not find duo factor in mocked duo response",e);
+              }
+           
            return p;
         });
         
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAuthenticationTimeTest.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAuthenticationTimeTest.java
deleted file mode 100644
index b8fb569..0000000
--- a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAuthenticationTimeTest.java
+++ /dev/null
@@ -1,165 +0,0 @@
-/*
- * Licensed to the University Corporation for Advanced Internet Development,
- * Inc. (UCAID) under one or more contributor license agreements.  See the
- * NOTICE file distributed with this work for additional information regarding
- * copyright ownership. The UCAID licenses this file to You under the Apache
- * License, Version 2.0 (the "License"); you may not use this file except in
- * compliance with the License.  You may obtain a copy of the License at
- *
- *    http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-
-package net.shibboleth.idp.plugin.authn.duo.impl;
-
-
-import static org.testng.Assert.assertNull;
-
-import java.time.Duration;
-import java.time.Instant;
-import java.time.temporal.ChronoUnit;
-
-
-import org.springframework.webflow.execution.Event;
-import org.testng.annotations.BeforeMethod;
-import org.testng.annotations.Test;
-
-import net.shibboleth.idp.authn.AuthnEventIds;
-import net.shibboleth.idp.plugin.authn.duo.DuoOIDCAuthAPI;
-import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
-
-/**
- * Tests for the {@link ValidateDuoTokenAuthenticationTime} action.
- */
-public class ValidateDuoTokenAuthenticationTimeTest extends AbstractDuoActionTest {
-
-    /** The action to test. */
-    private ValidateDuoTokenAuthenticationTime action;
-
-    /**
-     * Setup. 
-     * 
-     * @throws Exception on error.
-     */
-    @BeforeMethod
-    public void setUp() throws Exception {
-        super.setup();
-        action = new ValidateDuoTokenAuthenticationTime();
-
-    }
-
-    /**
-     * Test Duo 2FA token response authentication time validation, success.
-     * 
-     * @throws ComponentInitializationException on error.
-     */
-    @Test
-    public final void testDoExecuteSuccess() throws ComponentInitializationException {
-        addDuoContext();
-        addDuoIntegrationToContext();
-        addAttemptedFlow("authn/DuoOIDC");
-        ac.setForceAuthn(true);
-        action.setClockSkew(Duration.of(1, ChronoUnit.MINUTES));
-        action.setAuthnLifetime(Duration.of(1, ChronoUnit.MINUTES));
-        dc.setAuthToken(createDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID, 
-                Instant.now(),Instant.now(), Instant.now(), "api.duosecurity.com", "duo_push"));
-        action.initialize();
-        final Event event = action.execute(src);
-        // null event is success.
-        assertNull(event);
-
-    }
-    
-    /**
-     * Test Duo 2FA token response authentication time validation, not forced auth. 
-     * 
-     * @throws ComponentInitializationException on error.
-     */
-    @Test
-    public final void testDoExecuteSuccessNotForcedAuth() throws ComponentInitializationException {
-        addDuoContext();
-        addDuoIntegrationToContext();
-        addAttemptedFlow("authn/DuoOIDC");
-        ac.setForceAuthn(false);
-        action.setClockSkew(Duration.of(1, ChronoUnit.MINUTES));
-        action.setAuthnLifetime(Duration.of(1, ChronoUnit.MINUTES));
-        dc.setAuthToken(createDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID, 
-                Instant.now(),Instant.now(), Instant.now(), "api.duosecurity.com", "duo_push"));
-        action.initialize();
-        final Event event = action.execute(src);
-        // null event is success.
-        assertNull(event);
-
-    }
-    
-    /** 
-     * Test Duo 2FA token response authentication time validation, outside window but not force authn.
-     * 
-     * @throws ComponentInitializationException on error.
-     */
-    @Test
-    public final void testDoExecuteSuccessOutsideLifetimeNotForcedAuthn() throws ComponentInitializationException {
-        addDuoContext();
-        addDuoIntegrationToContext();
-        addAttemptedFlow("authn/DuoOIDC");
-        ac.setForceAuthn(false);
-        action.setClockSkew(Duration.of(1, ChronoUnit.MINUTES));
-        action.setAuthnLifetime(Duration.of(1, ChronoUnit.MINUTES));
-        dc.setAuthToken(createDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID, 
-                Instant.now(),Instant.now(), Instant.now().plus(Duration.of(10, ChronoUnit.MINUTES)), "api.duosecurity.com", "duo_push"));
-        action.initialize();
-        final Event event = action.execute(src);
-        // null event is success.
-        assertNull(event);
-
-    }
-    
-    /**
-     * Test Duo 2FA token response authentication time validation, outside lifetime window.
-     * 
-     * @throws ComponentInitializationException on error.
-     */
-    @Test
-    public final void testDoExecuteOutsideLifetime() throws ComponentInitializationException {
-        addDuoContext();
-        addDuoIntegrationToContext();
-        addAttemptedFlow("authn/DuoOIDC");
-        ac.setForceAuthn(true);
-        action.setClockSkew(Duration.of(1, ChronoUnit.MINUTES));
-        action.setAuthnLifetime(Duration.of(1, ChronoUnit.MINUTES));
-        dc.setAuthToken(createDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID, 
-                Instant.now(),Instant.now(), Instant.now().minus(Duration.of(10, ChronoUnit.MINUTES)), "api.duosecurity.com", "duo_push"));
-        action.initialize();
-        final Event event = action.execute(src);
-        assertEventId(event, AuthnEventIds.NO_CREDENTIALS);
-
-    }
-    
-    /**
-     * Test Duo 2FA token response authentication time validation, in the future.
-     * 
-     * @throws ComponentInitializationException on error.
-     */
-    @Test
-    public final void testDoExecuteInTheFuture() throws ComponentInitializationException {
-        addDuoContext();
-        addDuoIntegrationToContext();
-        addAttemptedFlow("authn/DuoOIDC");
-        ac.setForceAuthn(true);
-        action.setClockSkew(Duration.of(1, ChronoUnit.MINUTES));
-        action.setAuthnLifetime(Duration.of(1, ChronoUnit.MINUTES));
-        dc.setAuthToken(createDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID, 
-                Instant.now(),Instant.now(), Instant.now().plus(Duration.of(10, ChronoUnit.MINUTES)), "api.duosecurity.com", "duo_push"));
-        action.initialize();
-        final Event event = action.execute(src);
-        assertEventId(event, AuthnEventIds.NO_CREDENTIALS);
-
-    }
-
-}
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenExpirationTimeTest.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenExpirationTimeTest.java
deleted file mode 100644
index 28179d8..0000000
--- a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenExpirationTimeTest.java
+++ /dev/null
@@ -1,130 +0,0 @@
-/*
- * Licensed to the University Corporation for Advanced Internet Development,
- * Inc. (UCAID) under one or more contributor license agreements.  See the
- * NOTICE file distributed with this work for additional information regarding
- * copyright ownership. The UCAID licenses this file to You under the Apache
- * License, Version 2.0 (the "License"); you may not use this file except in
- * compliance with the License.  You may obtain a copy of the License at
- *
- *    http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package net.shibboleth.idp.plugin.authn.duo.impl;
-
-import static org.testng.Assert.assertNull;
-
-import java.time.Duration;
-import java.time.Instant;
-import java.time.temporal.ChronoUnit;
-
-import org.springframework.webflow.execution.Event;
-import org.testng.annotations.BeforeMethod;
-import org.testng.annotations.Test;
-
-import net.shibboleth.idp.authn.AuthnEventIds;
-import net.shibboleth.idp.plugin.authn.duo.DuoOIDCAuthAPI;
-import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
-
-/** Tests for ValidateDuoTokenExpirationTime class.*/
-public class ValidateDuoTokenExpirationTimeTest extends AbstractDuoActionTest {
-
-    /** The action to test. */
-    private ValidateDuoTokenExpirationTime action;
-
-    @BeforeMethod
-    public void setUp() throws Exception {
-        super.setup();
-        action = new ValidateDuoTokenExpirationTime();
-
-    }
-
-
-    /**
-     * Test Duo 2FA token response expiration time validation, success.
-     *  
-     * @throws ComponentInitializationException on error.
-     */
-    @Test
-    public final void testDoExecuteSuccess() throws ComponentInitializationException {
-        addDuoContext();
-        addDuoIntegrationToContext();
-        addAttemptedFlow("authn/DuoOIDC");
-        dc.setAuthToken(createDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID, 
-                Instant.now().plus(1,ChronoUnit.MINUTES),Instant.now(), Instant.now(), "api.duosecurity.com", "duo_push"));
-        action.initialize();
-        final Event event = action.execute(src);
-        // null event is success.
-        assertNull(event);
-
-    }
-    
-    /**
-     *  Test Duo 2FA token response expiration time validation, expired token.
-     *  
-     * @throws ComponentInitializationException on error.
-     */
-    @Test
-    public final void testDoExecuteExpiredToken() throws ComponentInitializationException {
-        addDuoContext();
-        addDuoIntegrationToContext();
-        addAttemptedFlow("authn/DuoOIDC");
-        //create token, set expiry 1 day in the past.
-        dc.setAuthToken(createDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID, 
-                Instant.now().minus(1,ChronoUnit.DAYS),Instant.now(), Instant.now(), "api.duosecurity.com", "duo_push"));
-        action.initialize();
-        final Event event = action.execute(src);
-        assertEventId(event, AuthnEventIds.NO_CREDENTIALS);
-
-    }
-    
-    /**
-     * Test Duo 2FA token response expiration time validation, within clock skew, success.
-     * 
-     * @throws ComponentInitializationException on error.
-     */
-    @Test
-    public final void testDoExecuteWithinClockSkewSuccess() throws ComponentInitializationException {
-        addDuoContext();
-        addDuoIntegrationToContext();
-        addAttemptedFlow("authn/DuoOIDC");
-
-        //allow a 1 minute clock skew - should be default
-        action.setClockSkew(Duration.of(1, ChronoUnit.MINUTES));
-        //create token, set expiry 50 second in the past.
-        dc.setAuthToken(createDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID, 
-                Instant.now().minus(50,ChronoUnit.SECONDS),Instant.now(), Instant.now(), "api.duosecurity.com", "duo_push"));
-        action.initialize();
-        final Event event = action.execute(src);
-        // null event is success.
-        assertNull(event);
-
-    }
-    
-    /**
-     * Test Duo 2FA token response expiration time validation, set zero clock skew expired token.
-     * 
-     * @throws ComponentInitializationException on error.
-     */
-    @Test
-    public final void testDoExecuteSetZeroClockSkewExpiredToken() throws ComponentInitializationException {
-        addDuoContext();
-        addDuoIntegrationToContext();
-        addAttemptedFlow("authn/DuoOIDC");
-        //do not allow a clock skew on the expiry
-        action.setClockSkew(Duration.of(0, ChronoUnit.SECONDS));
-        //create token, set expiry 1 second in the past.
-        dc.setAuthToken(createDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID, 
-                Instant.now().minus(1,ChronoUnit.SECONDS),Instant.now(), Instant.now(), "api.duosecurity.com", "duo_push"));
-        action.initialize();
-        final Event event = action.execute(src);
-        assertEventId(event, AuthnEventIds.NO_CREDENTIALS);
-
-    }
-
-}
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenIssuedAtTest.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenIssuedAtTest.java
deleted file mode 100644
index 8772963..0000000
--- a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenIssuedAtTest.java
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
- * Licensed to the University Corporation for Advanced Internet Development, 
- * Inc. (UCAID) under one or more contributor license agreements.  See the 
- * NOTICE file distributed with this work for additional information regarding
- * copyright ownership. The UCAID licenses this file to You under the Apache 
- * License, Version 2.0 (the "License"); you may not use this file except in 
- * compliance with the License.  You may obtain a copy of the License at
- *
- *    http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package net.shibboleth.idp.plugin.authn.duo.impl;
-
-
-
-import static org.testng.Assert.assertNull;
-
-import java.time.Duration;
-import java.time.Instant;
-import java.time.temporal.ChronoUnit;
-
-
-import org.springframework.webflow.execution.Event;
-import org.testng.annotations.BeforeMethod;
-import org.testng.annotations.Test;
-
-import net.shibboleth.idp.authn.AuthnEventIds;
-import net.shibboleth.idp.plugin.authn.duo.DuoOIDCAuthAPI;
-import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
-
-/**
- * Test the {@link ValidateDuoTokenIssuedAt} class.
- */
-public class ValidateDuoTokenIssuedAtTest extends AbstractDuoActionTest {
-
-    /** The action to test. */
-    private ValidateDuoTokenIssuedAt action;
-
-    @BeforeMethod
-    public void setUp() throws Exception {
-        super.setup();
-        action = new ValidateDuoTokenIssuedAt();
-
-    }
-
-    /**
-     * Test Duo 2FA token response token lifetime (issuedAt) validation, success.
-     * 
-     * @throws ComponentInitializationException on error.
-     */
-    @Test
-    public final void testDoExecuteSuccess() throws ComponentInitializationException {
-        addDuoContext();
-        addDuoIntegrationToContext();
-        addAttemptedFlow("authn/DuoOIDC");
-        dc.setAuthToken(createDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID, 
-                Instant.now(),Instant.now(), Instant.now(), "api.duosecurity.com", "duo_push"));
-        action.initialize();
-        final Event event = action.execute(src);
-        // null event is success.
-        assertNull(event);
-
-    }
-    
-    /**
-     * Test Duo 2FA token response token lifetime (issuedAt) validation, to far in the past.
-     * 
-     * @throws ComponentInitializationException on error.
-     */
-    @Test
-    public final void testDoExecuteToFarInThePast() throws ComponentInitializationException {
-        addDuoContext();
-        addDuoIntegrationToContext();
-        addAttemptedFlow("authn/DuoOIDC");
-        action.setIatMaxClockSkew(Duration.of(1, ChronoUnit.MINUTES));
-        dc.setAuthToken(createDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID, 
-                Instant.now(),Instant.now().minus(3,ChronoUnit.MINUTES), Instant.now(), "api.duosecurity.com", "duo_push"));
-        action.initialize();
-        final Event event = action.execute(src);
-       
-        assertEventId(event, AuthnEventIds.NO_CREDENTIALS);
-
-    }
-    
-    /** 
-     * Test Duo 2FA token response token lifetime (issuedAt) validation, to far in the future.
-     * 
-     * @throws ComponentInitializationException on error.
-     */
-    @Test
-    public final void testDoExecuteToFarInTheFuture() throws ComponentInitializationException {
-        addDuoContext();
-        addDuoIntegrationToContext();
-        addAttemptedFlow("authn/DuoOIDC");
-        action.setIatMaxClockSkew(Duration.of(1, ChronoUnit.MINUTES));
-        dc.setAuthToken(createDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID, 
-                Instant.now(),Instant.now().plus(3,ChronoUnit.MINUTES), Instant.now(), "api.duosecurity.com", "duo_push"));
-        action.initialize();
-        final Event event = action.execute(src);
-       
-        assertEventId(event, AuthnEventIds.NO_CREDENTIALS);
-
-    }
-
-}
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenIssuerTest.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenIssuerTest.java
deleted file mode 100644
index 08978bf..0000000
--- a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenIssuerTest.java
+++ /dev/null
@@ -1,88 +0,0 @@
-package net.shibboleth.idp.plugin.authn.duo.impl;
-
-import static org.testng.Assert.assertNull;
-
-import java.time.Instant;
-import java.time.temporal.ChronoUnit;
-
-import org.springframework.webflow.execution.Event;
-import org.testng.annotations.BeforeMethod;
-import org.testng.annotations.Test;
-
-import net.shibboleth.idp.authn.AuthnEventIds;
-import net.shibboleth.idp.plugin.authn.duo.DuoOIDCAuthAPI;
-import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
-
-public class ValidateDuoTokenIssuerTest extends AbstractDuoActionTest {
-
-    /** The action to test. */
-    private ValidateDuoTokenIssuer action;
-
-    @BeforeMethod
-    public void setUp() throws Exception {
-        super.setup();
-        action = new ValidateDuoTokenIssuer();
-
-    }
-    
-    /** 
-     * Test Duo 2FA token issuer validation, success.
-     * 
-     * @throws ComponentInitializationException on error.
-     */
-    @Test
-    public final void testExecuteSuccesful() throws ComponentInitializationException {
-        addDuoContext();
-        addDuoIntegrationToContext();
-        addAttemptedFlow("authn/DuoOIDC");
-        dc.setUsername("jdoe");
-        
-        dc.setAuthToken(createDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID, 
-                Instant.now().plus(1,ChronoUnit.MINUTES),Instant.now(), Instant.now(), "api.duosecurity.com", "duo_push"));
-        action.initialize();
-        final Event event = action.execute(src);
-        // null event is success.
-        assertNull(event);
-    }
-    
-    /** 
-     * Test Duo 2FA token issuer validation, different issuer.
-     * 
-     * @throws ComponentInitializationException on error.
-     */
-    @Test
-    public final void testExecuteDifferentIssuer() throws ComponentInitializationException {
-        addDuoContext();
-        addDuoIntegrationToContext();
-        addAttemptedFlow("authn/DuoOIDC");
-        dc.setUsername("jdoe");
-        
-        dc.setAuthToken(createDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID, 
-                Instant.now().plus(1,ChronoUnit.MINUTES),Instant.now(), Instant.now(), "different.host.com", "duo_push"));
-        action.initialize();
-        final Event event = action.execute(src);
-        // null event is success.
-        assertEventId(event, AuthnEventIds.NO_CREDENTIALS);
-    }
-    
-    /** 
-     * Test Duo 2FA token issuer validation, no context.
-     * 
-     * @throws ComponentInitializationException on error.
-     */
-    @Test
-    public final void testExecuteNoIntegration() throws ComponentInitializationException {
-        addDuoContext();
-
-        addAttemptedFlow("authn/DuoOIDC");
-        dc.setUsername("jdoe");
-        
-        dc.setAuthToken(createDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID, 
-                Instant.now().plus(1,ChronoUnit.MINUTES),Instant.now(), Instant.now(), "different.host.com", "duo_push"));
-        action.initialize();
-        final Event event = action.execute(src);
-        // null event is success.
-        assertEventId(event, AuthnEventIds.NO_CREDENTIALS);
-    }
-
-}
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenSubjectTest.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenSubjectTest.java
deleted file mode 100644
index 5d09bc0..0000000
--- a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenSubjectTest.java
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * Licensed to the University Corporation for Advanced Internet Development,
- * Inc. (UCAID) under one or more contributor license agreements.  See the
- * NOTICE file distributed with this work for additional information regarding
- * copyright ownership. The UCAID licenses this file to You under the Apache
- * License, Version 2.0 (the "License"); you may not use this file except in
- * compliance with the License.  You may obtain a copy of the License at
- *
- *    http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package net.shibboleth.idp.plugin.authn.duo.impl;
-
-import static org.testng.Assert.assertNull;
-
-import java.time.Instant;
-import java.time.temporal.ChronoUnit;
-
-import org.springframework.webflow.execution.Event;
-import org.testng.annotations.BeforeMethod;
-import org.testng.annotations.Test;
-
-import net.shibboleth.idp.authn.AuthnEventIds;
-import net.shibboleth.idp.plugin.authn.duo.DuoOIDCAuthAPI;
-import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
-
-class ValidateDuoTokenSubjectTest extends AbstractDuoActionTest {
-
-    /** The action to test. */
-    private ValidateDuoTokenSubject action;
-
-    @BeforeMethod
-    public void setUp() throws Exception {
-        super.setup();
-        action = new ValidateDuoTokenSubject();
-
-    }
-
-    /** 
-     * Test Duo 2FA token subject validation, success.
-     * 
-     * @throws ComponentInitializationException on error.
-     */
-    @Test
-    public final void testExecuteSuccesful() throws ComponentInitializationException {
-        addDuoContext();
-        addDuoIntegrationToContext();
-        addAttemptedFlow("authn/DuoOIDC");
-        dc.setUsername("jdoe");
-        dc.setAuthToken(createDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID, 
-                Instant.now().plus(1,ChronoUnit.MINUTES),Instant.now(), Instant.now(), "api.duosecurity.com", "duo_push"));
-        action.initialize();
-        final Event event = action.execute(src);
-        // null event is success.
-        assertNull(event);
-    }
-    
-    /** 
-     * Test Duo 2FA token subject validation, failure.
-     * 
-     * @throws ComponentInitializationException on error.
-     */
-    @Test
-    public final void testExecuteInvalidSubject() throws ComponentInitializationException {
-        addDuoContext();
-        addDuoIntegrationToContext();
-        addAttemptedFlow("authn/DuoOIDC");
-        dc.setUsername("different-user");
-        dc.setAuthToken(createDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID, 
-                Instant.now().plus(1,ChronoUnit.MINUTES),Instant.now(), Instant.now(), "api.duosecurity.com", "duo_push"));
-        action.initialize();
-        final Event event = action.execute(src);
-        // null event is success.
-        assertEventId(event, AuthnEventIds.INVALID_CREDENTIALS);
-    }
-
-}
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateTokenClaimsTest.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateTokenClaimsTest.java
new file mode 100644
index 0000000..321a546
--- /dev/null
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateTokenClaimsTest.java
@@ -0,0 +1,351 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.authn.duo.impl;
+
+import static org.testng.Assert.assertNull;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+
+import org.springframework.webflow.execution.Event;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.plugin.authn.duo.DefaultDuoOIDCIntegration;
+import net.shibboleth.idp.plugin.authn.duo.DuoOIDCAuthAPI;
+
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.logic.ConstraintViolationException;
+
+/**
+ * Tests for the {@link ValidateDuoTokenAudience} class.
+ */
+public class ValidateTokenClaimsTest extends AbstractDuoActionTest {
+
+    /** The action to test. */
+    private ValidateTokenClaims action;
+
+    @BeforeMethod
+    public void setUp() throws Exception {
+        super.setup();
+        action = new ValidateTokenClaims();
+
+    }
+    
+    /**
+     * Test oversized clock skew. To large for the internal int used by Nimbus, fail fast.
+     * 
+     * @throws ComponentInitializationException on error.
+     */
+    @Test(expectedExceptions = ConstraintViolationException.class)
+    public final void testOversizedClockSkew() throws ComponentInitializationException {
+        action.setClockSkew(Duration.ofDays(1000000000));
+
+    }
+    
+    /**
+     * Test blank duo integration.
+     * 
+     * @throws ComponentInitializationException on error.
+     */
+    @Test
+    public final void testBlankDuoIntegration() throws ComponentInitializationException {
+        addDuoContext();
+        addAttemptedFlow("authn/DuoOIDC");
+        dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID, 
+                Instant.now(),Instant.now(), Instant.now(), 
+                "api.duosecurity.com", "duo_push"));
+        action.initialize();
+        final Event event = action.execute(src);
+        assertEventId(event, AuthnEventIds.INVALID_AUTHN_CTX);
+
+    }
+    
+   
+    
+    /**
+     * Validate a valid token.
+     * 
+     * @throws ComponentInitializationException on error.
+     */
+    @Test
+    public final void testValidToken() throws ComponentInitializationException {
+        addDuoContext();
+        addDuoIntegrationToContext();
+        addAttemptedFlow("authn/DuoOIDC");
+        dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID, 
+                Instant.now(),Instant.now(), Instant.now(), 
+                "api.duosecurity.com", "duo_push"));
+        action.initialize();
+        final Event event = action.execute(src);
+     // null event is success.
+        assertNull(event);
+
+    }
+
+    /**
+     * Validate an expired token.
+     * 
+     * @throws ComponentInitializationException on error.
+     */
+    @Test
+    public final void testExpiredToken() throws ComponentInitializationException {
+        addDuoContext();
+        addDuoIntegrationToContext();
+        addAttemptedFlow("authn/DuoOIDC");
+        dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID, 
+                Instant.now().minus(10,ChronoUnit.MINUTES),Instant.now(), Instant.now(), 
+                "api.duosecurity.com", "duo_push"));
+        action.initialize();
+        final Event event = action.execute(src);
+        assertEventId(event, AuthnEventIds.NO_CREDENTIALS);
+
+    }
+    
+    /**
+     * Validate an expired token but with a large clock skew.
+     * 
+     * @throws ComponentInitializationException on error.
+     */
+    @Test
+    public final void testExpiredTokenLargeClockSkew() throws ComponentInitializationException {
+        addDuoContext();
+        addDuoIntegrationToContext();
+        addAttemptedFlow("authn/DuoOIDC");
+        //set clock skew
+        action.setClockSkew(Duration.ofMinutes(20));
+        dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID, 
+                Instant.now().minus(10,ChronoUnit.MINUTES),Instant.now(), Instant.now(), 
+                "api.duosecurity.com", "duo_push"));
+        action.initialize();
+        final Event event = action.execute(src);
+        // null event is success.   
+        assertNull(event);
+
+    }
+    
+    
+    /**
+     * Validate a token which has the incorrect issuer.
+     * 
+     * @throws ComponentInitializationException on error.
+     */
+    @Test
+    public final void testInvalidIssuer() throws ComponentInitializationException {
+        addDuoContext();
+        addDuoIntegrationToContext();
+        addAttemptedFlow("authn/DuoOIDC");
+        dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID, 
+                Instant.now(),Instant.now(), Instant.now(), 
+                "incorrect-issuer", "duo_push"));
+        action.initialize();
+        final Event event = action.execute(src);
+        assertEventId(event, AuthnEventIds.NO_CREDENTIALS);
+
+    }
+    
+    /**
+     * Validate a token which has the incorrect audience.
+     * 
+     * @throws ComponentInitializationException on error.
+     */
+    @Test
+    public final void testInvalidAudience() throws ComponentInitializationException {
+        addDuoContext();
+        addDuoIntegrationToContext();
+        addAttemptedFlow("authn/DuoOIDC");
+        dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,
+                "Login Succesful","wrong-client-audience", 
+                Instant.now(),Instant.now(), Instant.now(), 
+                "api.duosecurity.com", "duo_push"));
+        action.initialize();
+        final Event event = action.execute(src);
+        assertEventId(event, AuthnEventIds.NO_CREDENTIALS);
+    }
+    
+    /**
+     * Validate a token which has the incorrect subject.
+     * 
+     * @throws ComponentInitializationException on error.
+     */
+    @Test
+    public final void testInvalidSubject() throws ComponentInitializationException {
+        addDuoContext();
+        addDuoIntegrationToContext();
+        addAttemptedFlow("authn/DuoOIDC");
+        
+        //override subject
+        dc.setUsername("wrong-subject");
+        
+        dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID, 
+                Instant.now(),Instant.now(), Instant.now(), 
+                "api.duosecurity.com", "duo_push"));
+        action.initialize();
+        final Event event = action.execute(src);
+        assertEventId(event, AuthnEventIds.NO_CREDENTIALS);
+
+    }
+    
+    /**
+     * Validate a token which was issued too far in the past.
+     * 
+     * @throws ComponentInitializationException on error.
+     */
+    @Test
+    public final void testInvalidIssuedAtPast() throws ComponentInitializationException {
+        addDuoContext();
+        addDuoIntegrationToContext();
+        addAttemptedFlow("authn/DuoOIDC");
+        
+        dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID, 
+                Instant.now(),Instant.now().minus(10,ChronoUnit.MINUTES), Instant.now(), 
+                "api.duosecurity.com", "duo_push"));
+        action.initialize();
+        final Event event = action.execute(src);
+        assertEventId(event, AuthnEventIds.NO_CREDENTIALS);
+
+    }
+    
+    /**
+     * Validate a token which was issued too far in the future.
+     * 
+     * @throws ComponentInitializationException on error.
+     */
+    @Test
+    public final void testInvalidIssuedAtFuture() throws ComponentInitializationException {
+        addDuoContext();
+        addDuoIntegrationToContext();
+        addAttemptedFlow("authn/DuoOIDC");
+        
+        dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID, 
+                Instant.now(),Instant.now().plus(10,ChronoUnit.MINUTES), Instant.now(), 
+                "api.duosecurity.com", "duo_push"));
+        action.initialize();
+        final Event event = action.execute(src);
+        assertEventId(event, AuthnEventIds.NO_CREDENTIALS);
+
+    }
+    
+    /**
+     * Validate a token which was outside (in the future)
+     * the authentication lifetime window when using forced authn.
+     * 
+     * @throws ComponentInitializationException on error.
+     */
+    @Test
+    public final void testInvalidAuthenticationTimeFuture() throws ComponentInitializationException {
+        addDuoContext();
+        addDuoIntegrationToContext();
+        addAttemptedFlow("authn/DuoOIDC");
+        ac.setForceAuthn(true);
+        dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID, 
+                Instant.now(),Instant.now(), Instant.now().plus(10,ChronoUnit.MINUTES), 
+                "api.duosecurity.com", "duo_push"));
+        action.initialize();
+        final Event event = action.execute(src);
+        assertEventId(event, AuthnEventIds.NO_CREDENTIALS);
+
+    }
+    
+    /**
+     * Validate a token which was outside (expired) the authentication lifetime window when using forced authn.
+     * 
+     * @throws ComponentInitializationException on error.
+     */
+    @Test
+    public final void testInvalidAuthenticationTimePast() throws ComponentInitializationException {
+        addDuoContext();
+        addDuoIntegrationToContext();
+        addAttemptedFlow("authn/DuoOIDC");
+        ac.setForceAuthn(true);
+        dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID, 
+                Instant.now(),Instant.now(), Instant.now().minus(10,ChronoUnit.MINUTES), 
+                "api.duosecurity.com", "duo_push"));
+        action.initialize();
+        final Event event = action.execute(src);
+        assertEventId(event, AuthnEventIds.NO_CREDENTIALS);
+
+    }
+    
+    /**
+     * Validate a token which is inside the authentication lifetime window when using forced authn.
+     * 
+     * @throws ComponentInitializationException on error.
+     */
+    @Test
+    public final void testInvalidAuthenticationWithinWindow() throws ComponentInitializationException {
+        addDuoContext();
+        addDuoIntegrationToContext();
+        addAttemptedFlow("authn/DuoOIDC");
+        ac.setForceAuthn(true);
+        //set a lifetime
+        action.setAuthnLifetime(Duration.ofMinutes(5));
+        dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID, 
+                Instant.now(),Instant.now(), Instant.now().minus(2,ChronoUnit.MINUTES), 
+                "api.duosecurity.com", "duo_push"));
+        action.initialize();
+        final Event event = action.execute(src);
+     // null event is success.
+        assertNull(event);
+
+    }
+    
+    /**
+     * Validate a token which is outside the authentication lifetime window but authn is not forced.
+     * 
+     * @throws ComponentInitializationException on error.
+     */
+    @Test
+    public final void testInvalidAuthenticationOutsideWindowNotForced() throws ComponentInitializationException {
+        addDuoContext();
+        addDuoIntegrationToContext();
+        addAttemptedFlow("authn/DuoOIDC");
+        ac.setForceAuthn(false);
+        dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID, 
+                Instant.now(),Instant.now(), Instant.now().minus(10,ChronoUnit.MINUTES), 
+                "api.duosecurity.com", "duo_push"));
+        action.initialize();
+        final Event event = action.execute(src);
+     // null event is success.
+        assertNull(event);
+
+    }
+    
+   
+   
+
+}
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateTokenSignatureTest.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateTokenSignatureTest.java
new file mode 100644
index 0000000..44063b0
--- /dev/null
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateTokenSignatureTest.java
@@ -0,0 +1,182 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.authn.duo.impl;
+
+import static org.testng.Assert.assertNull;
+
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+
+import org.springframework.webflow.execution.Event;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.plugin.authn.duo.DuoOIDCAuthAPI;
+import net.shibboleth.idp.saml.audit.impl.AuthnInstantAuditExtractor;
+import net.shibboleth.utilities.java.support.codec.Base64Support;
+import net.shibboleth.utilities.java.support.codec.EncodingException;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+
+/**
+ * Tests for the {@link ValidateDuoTokenAudience} class.
+ */
+public class ValidateTokenSignatureTest extends AbstractDuoActionTest {
+
+    /** The action to test. */
+    private ValidateTokenSignature action;
+
+    @BeforeMethod
+    public void setUp() throws Exception {
+        super.setup();
+        action = new ValidateTokenSignature();
+
+    }
+
+    /**
+     * Test Duo 2FA token signature validation with no signature, should fail, we need a signature.
+     * 
+     * @throws ComponentInitializationException on error.
+     */
+    @Test
+    public final void testNoneSignature() throws ComponentInitializationException {
+        addDuoContext();
+        addDuoIntegrationToContext();
+        addAttemptedFlow("authn/DuoOIDC");
+        dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID, 
+                Instant.now().plus(1,ChronoUnit.MINUTES),Instant.now(), Instant.now(), 
+                "api.duosecurity.com", "duo_push"));
+        action.initialize();
+        final Event event = action.execute(src);
+        assertEventId(event, AuthnEventIds.NO_CREDENTIALS);
+
+    }
+    
+    /**
+     * Test Duo 2FA token signature validation with an unsupported signature, should fail.
+     * 
+     * @throws ComponentInitializationException on error.
+     */
+    @Test
+    public final void testUnsuportedSignature() throws ComponentInitializationException, EncodingException {
+        addDuoContext();
+        addDuoIntegrationToContext();
+        addAttemptedFlow("authn/DuoOIDC");
+        
+        //unsupported asymmetric RSA algorithm
+        final String headerJson = "{\"typ\": \"JWT\",\"alg\": \"RS256\"}";
+        
+        dc.setAuthToken(createUnsignedSignedDummyToken(headerJson,
+                DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID, 
+                Instant.now().plus(1,ChronoUnit.MINUTES),Instant.now(), Instant.now(), 
+                "api.duosecurity.com", "duo_push"));
+        action.initialize();
+        final Event event = action.execute(src);
+       assertEventId(event, AuthnEventIds.NO_CREDENTIALS);
+    }
+   
+    /**
+     * Test Duo 2FA token signature validation with a valid signature, should succeed.
+     * 
+     * @throws ComponentInitializationException on error.
+     */
+    @Test
+    public final void testValidSignature() throws ComponentInitializationException, EncodingException {
+        addDuoContext();
+        addDuoIntegrationToContext();
+        addAttemptedFlow("authn/DuoOIDC");
+        
+        final String headerJson = "{\"typ\": \"JWT\",\"alg\": \"HS256\"}";
+        
+        dc.setAuthToken(createSignedDummyToken(headerJson,dc.getIntegration().getSecretKey(),
+                DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID, 
+                Instant.now().plus(1,ChronoUnit.MINUTES),Instant.now(), Instant.now(), 
+                "api.duosecurity.com", "duo_push"));
+        action.initialize();
+        final Event event = action.execute(src);
+        // null event is success.
+        assertNull(event);
+    }
+    
+    /**
+     * Test Duo 2FA token signature validation with an invalid signature, should fail.
+     * 
+     * @throws ComponentInitializationException on error.
+     */
+    @Test
+    public final void testInvalidSignature() throws ComponentInitializationException, EncodingException {
+        addDuoContext();
+        addDuoIntegrationToContext();
+        addAttemptedFlow("authn/DuoOIDC");
+
+        final String headerJson = "{\"typ\": \"JWT\",\"alg\": \"HS256\"}";
+        //bad sig
+        final String signature = "dGhpc2lzbm90Z29pbmd0b3dvcms=";
+        
+        dc.setAuthToken(createSignedDummyTokenFromGivenSignature(headerJson,signature,
+                DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID, 
+                Instant.now().plus(1,ChronoUnit.MINUTES),Instant.now(), Instant.now(), 
+                "api.duosecurity.com", "duo_push"));
+        action.initialize();
+        final Event event = action.execute(src);
+        assertEventId(event, AuthnEventIds.NO_CREDENTIALS);
+    }
+    
+    
+    /**
+     * The JWT header is present with a signature algorithm other than none, but the token has not been signed.
+     * 
+     * @throws ComponentInitializationException on error.
+     */
+    @Test
+    public final void testSignatureNotPresent() throws ComponentInitializationException, EncodingException {
+        addDuoContext();
+        addDuoIntegrationToContext();
+        addAttemptedFlow("authn/DuoOIDC");
+        
+        final String headerJson = "{\"typ\": \"JWT\",\"alg\": \"HS256\"}";
+        
+        dc.setAuthToken(createUnsignedSignedDummyToken(headerJson,
+                DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID, 
+                Instant.now().plus(1,ChronoUnit.MINUTES),Instant.now(), Instant.now(), 
+                "api.duosecurity.com", "duo_push"));
+        action.initialize();
+        final Event event = action.execute(src);
+        assertEventId(event, AuthnEventIds.NO_CREDENTIALS);
+    }
+    
+   
+
+}
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClient_FAIL.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClient_FAIL.java
index 0b6929a..17ca92d 100644
--- a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClient_FAIL.java
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClient_FAIL.java
@@ -19,11 +19,24 @@ package net.shibboleth.idp.plugin.authn.mock;
 
 import static java.lang.String.format;
 
+import java.text.ParseException;
 import java.time.Instant;
 import java.time.temporal.ChronoUnit;
 
 import javax.annotation.Nonnull;
 
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.JWSHeader;
+import com.nimbusds.jose.JWSSigner;
+import com.nimbusds.jose.PlainHeader;
+import com.nimbusds.jose.crypto.MACSigner;
+import com.nimbusds.jose.util.Base64URL;
+import com.nimbusds.jwt.JWT;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.PlainJWT;
+import com.nimbusds.jwt.SignedJWT;
+
 import net.shibboleth.idp.plugin.authn.duo.DuoClientException;
 import net.shibboleth.idp.plugin.authn.duo.DuoOIDCClient;
 import net.shibboleth.idp.plugin.authn.duo.DuoOIDCIntegration;
@@ -34,6 +47,8 @@ import net.shibboleth.idp.plugin.authn.duo.model.DuoAuthDevice;
 import net.shibboleth.idp.plugin.authn.duo.model.DuoAuthToken;
 import net.shibboleth.idp.plugin.authn.duo.model.DuoHealthCheck;
 import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.utilities.java.support.codec.Base64Support;
+import net.shibboleth.utilities.java.support.codec.EncodingException;
 
 /**
  * Mock a Duo client which is not available (unhealthy).
@@ -68,35 +83,72 @@ public class MockDuoOIDCClient_FAIL implements DuoOIDCClient{
     }
 
     @Override
-    public DuoAuthToken exchangeAuthorizationCodeFor2FAResult(final String code, 
+    public JWT exchangeAuthorizationCodeFor2FAResult(final String code, 
             final String username) throws DuoClientException {
-        return DuoAuthToken.builder()
-                .withIss("https://api.duosecurity.com/oauth/v1/token")
-                .withSub(SUB)
-                .withAud(integration.getClientId())
-                .withExp(Math.toIntExact(Instant.now().plus(10,ChronoUnit.MINUTES).getEpochSecond()))
-                .withIat((double)Instant.now().getEpochSecond())
-                .withAuthResultStatusMessage("Login Failed")
-                .withAuthResultStatus("fail")
-                .withAuthResult("fail")
-                .withAuthContext(DuoAuthContext.builder()
-                        .withResult("fail").
-                        withTimestamp(1590070939)
-                        .withAuthDevice(DuoAuthDevice.builder()
-                                .withIp("192.168.0.1").
-                                withName("99999999")
-                                .build())
-                        .withTxid("b1287968-1dd1-4488-bb3c-0c72fc398b8b").
-                        withEventType("authenticaiton")
-                        .withReason("user_approved").
-                        withAccessDevice(DuoAccessDevice.builder()
-                                .withIp("192.168.0.1")
-                                .withHostname("localhost")
-                                .build()).
-                        withApplication(DuoApplication.builder()
-                                .withKey("DIU6GEFXXXXXXX")
-                                .withName("Test").build()).
-                        withFactor("duo_push").withUsername("jdoe").withUserKey("XXXXXXX").build()).build();
+        
+        final String jwtJson = "{\n" + 
+                "    \"iss\": \"https://"+integration.getAPIHost()+"oauth/v1/token\",\n" + 
+                "    \"sub\": \""+SUB+"\",\n" + 
+                "    \"preferred_username\": \"philsmart\",\n" + 
+                "    \"aud\": \""+integration.getClientId()+"\",\n" + 
+                "    \"exp\": "+new String(Math.toIntExact(Instant.now().
+                        plus(10,ChronoUnit.MINUTES).getEpochSecond())+",\n" + 
+                "    \"iat\": "+Long.toString(Instant.now().getEpochSecond()))+",\n" + 
+                "    \"auth_time\": "+Math.toIntExact(Instant.now().getEpochSecond())+",\n" + 
+                "    \"auth_result\": {\n" + 
+                "        \"status_msg\": \"Login Failed\",\n" + 
+                "        \"status\": \"fail\",\n" + 
+                "        \"result\": \"fail\"\n" + 
+                "    },\n" + 
+                "    \"auth_context\": {\n" + 
+                "        \"result\": \"success\",\n" + 
+                "        \"timestamp\": 1599749128,\n" + 
+                "        \"auth_device\": {\n" + 
+                "            \"ip\": \"82.17.89.232\",\n" + 
+                "            \"name\": \"+44 7852 119881\",\n" + 
+                "            \"location\": {\n" + 
+                "                \"state\": \"Wales\",\n" + 
+                "                \"city\": \"Cardiff\",\n" + 
+                "                \"country\": \"United Kingdom\"\n" + 
+                "            }\n" + 
+                "        },\n" + 
+                "        \"txid\": \"1684599c-bb16-4250-af85-904291bfe7cc\",\n" + 
+                "        \"event_type\": \"authentication\",\n" + 
+                "        \"reason\": \"user_approved\",\n" + 
+                "        \"access_device\": {\n" + 
+                "            \"hostname\": null,\n" + 
+                "            \"ip\": \"82.17.89.232\",\n" + 
+                "            \"location\": {\n" + 
+                "                \"state\": \"Wales\",\n" + 
+                "                \"city\": \"Cardiff\",\n" + 
+                "                \"country\": \"United Kingdom\"\n" + 
+                "            }\n" + 
+                "        },\n" + 
+                "        \"application\": {\n" + 
+                "            \"key\": \"DIU6GEFWG5LIUTVV2M3N\",\n" + 
+                "            \"name\": \"Shibboleth Integration Testing\"\n" + 
+                "        },\n" + 
+                "        \"factor\": \"duo_push\",\n" + 
+                "        \"user\": {\n" + 
+                "            \"key\": \"DUGL8U46QGJSOUJWG59W\",\n" + 
+                "            \"name\": \"philsmart\"\n" + 
+                "        }\n" + 
+                "    }\n" + 
+                "}"; 
+        
+        try {
+            //re-sign the token using the client secret
+            final JWSSigner signer = new MACSigner(integration.getSecretKey());
+            //FIXME: needs to be HS512 (or not?)
+            final JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.HS256).build();
+            final JWTClaimsSet claims = JWTClaimsSet.parse(jwtJson);
+            final SignedJWT signedJWT = new SignedJWT(header,claims);
+            signedJWT.sign(signer);
+            return signedJWT;
+        } catch (final ParseException | JOSEException e) {
+            throw new DuoClientException(e);
+        }
+
     }
 
 }
\ No newline at end of file
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClient_OK.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClient_OK.java
index ac61765..8b569d3 100644
--- a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClient_OK.java
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClient_OK.java
@@ -19,11 +19,25 @@ package net.shibboleth.idp.plugin.authn.mock;
 
 import static java.lang.String.format;
 
+import java.text.ParseException;
 import java.time.Instant;
 import java.time.temporal.ChronoUnit;
 
 import javax.annotation.Nonnull;
 
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.JWSHeader;
+import com.nimbusds.jose.JWSSigner;
+import com.nimbusds.jose.KeyLengthException;
+import com.nimbusds.jose.PlainHeader;
+import com.nimbusds.jose.crypto.MACSigner;
+import com.nimbusds.jose.util.Base64URL;
+import com.nimbusds.jwt.JWT;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.PlainJWT;
+import com.nimbusds.jwt.SignedJWT;
+
 import net.shibboleth.idp.plugin.authn.duo.DuoClientException;
 import net.shibboleth.idp.plugin.authn.duo.DuoOIDCClient;
 import net.shibboleth.idp.plugin.authn.duo.DuoOIDCIntegration;
@@ -34,6 +48,8 @@ import net.shibboleth.idp.plugin.authn.duo.model.DuoAuthDevice;
 import net.shibboleth.idp.plugin.authn.duo.model.DuoAuthToken;
 import net.shibboleth.idp.plugin.authn.duo.model.DuoHealthCheck;
 import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.utilities.java.support.codec.Base64Support;
+import net.shibboleth.utilities.java.support.codec.EncodingException;
 
 /**
  * Mock a Duo client which is available and returns a valid response.
@@ -65,41 +81,72 @@ public class MockDuoOIDCClient_OK implements DuoOIDCClient{
     }
 
     @Override
-    public DuoAuthToken exchangeAuthorizationCodeFor2FAResult(final String code, 
+    public JWT exchangeAuthorizationCodeFor2FAResult(final String code, 
             final String username) throws DuoClientException {
-        return DuoAuthToken.builder()
-                .withIss("https://"+integration.getAPIHost()+"/oauth/v1/token")
-                .withSub(SUB)
-                .withAud(integration.getClientId())
-                .withExp(Math.toIntExact(Instant.now().plus(10,ChronoUnit.MINUTES).getEpochSecond()))
-                .withIat((double)Instant.now().getEpochSecond())
-                .withAuthResultStatusMessage("Login Successful")
-                .withAuthResultStatus("allow")
-                .withAuthResult("allow")
-                .withAuthContext(DuoAuthContext.builder()
-                        .withResult("success")
-                        .withTimestamp(1590070939)
-                        .withAuthDevice(DuoAuthDevice.builder()
-                                .withIp("192.168.0.1").
-                                withName("99999999")
-                                .build())
-                        .withTxid("b1287968-1dd1-4488-bb3c-0c72fc398b8b").
-                        withEventType("authenticaiton")
-                        .withReason("user_approved")
-                        .withAccessDevice(DuoAccessDevice.builder()
-                                .withIp("192.168.0.1")
-                                .withHostname("localhost")
-                                .build())
-                       .withApplication(DuoApplication.builder()
-                                .withKey("DIU6GEFXXXXXXX")
-                                .withName("Test")
-                                .build()).
-                        withFactor("duo_push")
-                        .withUsername("jdoe")
-                        .withUserKey("XXXXXXX")
-                        .build()).
-                withAuthTime(Math.toIntExact(Instant.now().getEpochSecond()))
-                .build();
+        
+           final String jwtJson = "{\n" + 
+                   "    \"iss\": \"https://"+integration.getAPIHost()+"/oauth/v1/token\",\n" + 
+                   "    \"sub\": \""+SUB+"\",\n" + 
+                   "    \"preferred_username\": \"jdoe\",\n" + 
+                   "    \"aud\": \""+integration.getClientId()+"\",\n" + 
+                   "    \"exp\": "+new String(Math.toIntExact(Instant.now().
+                           plus(10,ChronoUnit.MINUTES).getEpochSecond())+",\n" + 
+                   "    \"iat\": "+Long.toString(Instant.now().getEpochSecond()))+",\n" + 
+                   "    \"auth_time\": "+Math.toIntExact(Instant.now().getEpochSecond())+",\n" + 
+                   "    \"auth_result\": {\n" + 
+                   "        \"status_msg\": \"Login Successful\",\n" + 
+                   "        \"status\": \"allow\",\n" + 
+                   "        \"result\": \"allow\"\n" + 
+                   "    },\n" + 
+                   "    \"auth_context\": {\n" + 
+                   "        \"result\": \"success\",\n" + 
+                   "        \"timestamp\": 1599749128,\n" + 
+                   "        \"auth_device\": {\n" + 
+                   "            \"ip\": \"82.17.89.232\",\n" + 
+                   "            \"name\": \"+44 7852 119881\",\n" + 
+                   "            \"location\": {\n" + 
+                   "                \"state\": \"Wales\",\n" + 
+                   "                \"city\": \"Cardiff\",\n" + 
+                   "                \"country\": \"United Kingdom\"\n" + 
+                   "            }\n" + 
+                   "        },\n" + 
+                   "        \"txid\": \"1684599c-bb16-4250-af85-904291bfe7cc\",\n" + 
+                   "        \"event_type\": \"authentication\",\n" + 
+                   "        \"reason\": \"user_approved\",\n" + 
+                   "        \"access_device\": {\n" + 
+                   "            \"hostname\": null,\n" + 
+                   "            \"ip\": \"82.17.89.232\",\n" + 
+                   "            \"location\": {\n" + 
+                   "                \"state\": \"Wales\",\n" + 
+                   "                \"city\": \"Cardiff\",\n" + 
+                   "                \"country\": \"United Kingdom\"\n" + 
+                   "            }\n" + 
+                   "        },\n" + 
+                   "        \"application\": {\n" + 
+                   "            \"key\": \"DIU6GEFWG5LIUTVV2M3N\",\n" + 
+                   "            \"name\": \"Shibboleth Integration Testing\"\n" + 
+                   "        },\n" + 
+                   "        \"factor\": \"duo_push\",\n" + 
+                   "        \"user\": {\n" + 
+                   "            \"key\": \"DUGL8U46QGJSOUJWG59W\",\n" + 
+                   "            \"name\": \"philsmart\"\n" + 
+                   "        }\n" + 
+                   "    }\n" + 
+                   "}"; 
+           
+           try {
+               //re-sign the token using the client secret
+               final JWSSigner signer = new MACSigner(integration.getSecretKey());
+               //FIXME: needs to be HS512 (or not?)
+               final JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.HS256).build();
+               final JWTClaimsSet claims = JWTClaimsSet.parse(jwtJson);
+               final SignedJWT signedJWT = new SignedJWT(header,claims);
+               signedJWT.sign(signer);
+               
+               return signedJWT;
+           } catch (final ParseException | JOSEException e) {
+               throw new DuoClientException(e);
+           }
     }
 
 }
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClient_OK_OLD_AUTH_TIME.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClient_OK_OLD_AUTH_TIME.java
index fd4c82d..f81b732 100644
--- a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClient_OK_OLD_AUTH_TIME.java
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClient_OK_OLD_AUTH_TIME.java
@@ -19,11 +19,25 @@ package net.shibboleth.idp.plugin.authn.mock;
 
 import static java.lang.String.format;
 
+import java.text.ParseException;
 import java.time.Instant;
 import java.time.temporal.ChronoUnit;
 
 import javax.annotation.Nonnull;
 
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.JWSHeader;
+import com.nimbusds.jose.JWSSigner;
+import com.nimbusds.jose.KeyLengthException;
+import com.nimbusds.jose.PlainHeader;
+import com.nimbusds.jose.crypto.MACSigner;
+import com.nimbusds.jose.util.Base64URL;
+import com.nimbusds.jwt.JWT;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.PlainJWT;
+import com.nimbusds.jwt.SignedJWT;
+
 import net.shibboleth.idp.plugin.authn.duo.DuoClientException;
 import net.shibboleth.idp.plugin.authn.duo.DuoOIDCClient;
 import net.shibboleth.idp.plugin.authn.duo.DuoOIDCIntegration;
@@ -34,6 +48,8 @@ import net.shibboleth.idp.plugin.authn.duo.model.DuoAuthDevice;
 import net.shibboleth.idp.plugin.authn.duo.model.DuoAuthToken;
 import net.shibboleth.idp.plugin.authn.duo.model.DuoHealthCheck;
 import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.utilities.java.support.codec.Base64Support;
+import net.shibboleth.utilities.java.support.codec.EncodingException;
 
 /**
  * Mock a Duo client which is available and returns a valid response.
@@ -67,41 +83,72 @@ public class MockDuoOIDCClient_OK_OLD_AUTH_TIME implements DuoOIDCClient{
     }
 
     @Override
-    public DuoAuthToken exchangeAuthorizationCodeFor2FAResult(final String code, 
+    public JWT exchangeAuthorizationCodeFor2FAResult(final String code, 
             final String username) throws DuoClientException {
-        return DuoAuthToken.builder()
-                .withIss("https://"+integration.getAPIHost()+"/oauth/v1/token")
-                .withSub(SUB)
-                .withAud(integration.getClientId())
-                .withExp(Math.toIntExact(Instant.now().plus(10,ChronoUnit.MINUTES).getEpochSecond()))
-                .withIat((double)Instant.now().getEpochSecond())
-                .withAuthResultStatusMessage("Login Successful")
-                .withAuthResultStatus("allow")
-                .withAuthResult("allow")
-                .withAuthContext(DuoAuthContext.builder()
-                        .withResult("success")
-                        .withTimestamp(1590070939)
-                        .withAuthDevice(DuoAuthDevice.builder()
-                                .withIp("192.168.0.1").
-                                withName("99999999")
-                                .build())
-                        .withTxid("b1287968-1dd1-4488-bb3c-0c72fc398b8b").
-                        withEventType("authenticaiton")
-                        .withReason("user_approved")
-                        .withAccessDevice(DuoAccessDevice.builder()
-                                .withIp("192.168.0.1")
-                                .withHostname("localhost")
-                                .build())
-                       .withApplication(DuoApplication.builder()
-                                .withKey("DIU6GEFXXXXXXX")
-                                .withName("Test")
-                                .build()).
-                        withFactor("duo_push")
-                        .withUsername("jdoe")
-                        .withUserKey("XXXXXXX")
-                        .build())
-                .withAuthTime(Math.toIntExact(Instant.now().minus(10,ChronoUnit.MINUTES).getEpochSecond()))
-                .build();
+        
+        final String jwtJson = "{\n" + 
+                "    \"iss\": \"https://"+integration.getAPIHost()+"/oauth/v1/token\",\n" + 
+                "    \"sub\": \""+SUB+"\",\n" + 
+                "    \"preferred_username\": \"jdoe\",\n" + 
+                "    \"aud\": \""+integration.getClientId()+"\",\n" + 
+                "    \"exp\": "+new String(Math.toIntExact(Instant.now().
+                        plus(10,ChronoUnit.MINUTES).getEpochSecond())+",\n" + 
+                "    \"iat\": "+Long.toString(Instant.now().getEpochSecond()))+",\n" + 
+                "    \"auth_time\": "+Math.toIntExact(Instant.now().minus(10,ChronoUnit.MINUTES).getEpochSecond())+",\n" + 
+                "    \"auth_result\": {\n" + 
+                "        \"status_msg\": \"Login Successful\",\n" + 
+                "        \"status\": \"allow\",\n" + 
+                "        \"result\": \"allow\"\n" + 
+                "    },\n" + 
+                "    \"auth_context\": {\n" + 
+                "        \"result\": \"success\",\n" + 
+                "        \"timestamp\": 1599749128,\n" + 
+                "        \"auth_device\": {\n" + 
+                "            \"ip\": \"82.17.89.232\",\n" + 
+                "            \"name\": \"+44 7852 119881\",\n" + 
+                "            \"location\": {\n" + 
+                "                \"state\": \"Wales\",\n" + 
+                "                \"city\": \"Cardiff\",\n" + 
+                "                \"country\": \"United Kingdom\"\n" + 
+                "            }\n" + 
+                "        },\n" + 
+                "        \"txid\": \"1684599c-bb16-4250-af85-904291bfe7cc\",\n" + 
+                "        \"event_type\": \"authentication\",\n" + 
+                "        \"reason\": \"user_approved\",\n" + 
+                "        \"access_device\": {\n" + 
+                "            \"hostname\": null,\n" + 
+                "            \"ip\": \"82.17.89.232\",\n" + 
+                "            \"location\": {\n" + 
+                "                \"state\": \"Wales\",\n" + 
+                "                \"city\": \"Cardiff\",\n" + 
+                "                \"country\": \"United Kingdom\"\n" + 
+                "            }\n" + 
+                "        },\n" + 
+                "        \"application\": {\n" + 
+                "            \"key\": \"DIU6GEFWG5LIUTVV2M3N\",\n" + 
+                "            \"name\": \"Shibboleth Integration Testing\"\n" + 
+                "        },\n" + 
+                "        \"factor\": \"duo_push\",\n" + 
+                "        \"user\": {\n" + 
+                "            \"key\": \"DUGL8U46QGJSOUJWG59W\",\n" + 
+                "            \"name\": \"philsmart\"\n" + 
+                "        }\n" + 
+                "    }\n" + 
+                "}"; 
+        
+        try {
+            //re-sign the token using the client secret
+            final JWSSigner signer = new MACSigner(integration.getSecretKey());
+            //FIXME: needs to be HS512 (or not?)
+            final JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.HS256).build();
+            final JWTClaimsSet claims = JWTClaimsSet.parse(jwtJson);
+            final SignedJWT signedJWT = new SignedJWT(header,claims);
+            signedJWT.sign(signer);
+            return signedJWT;
+        } catch (final ParseException | JOSEException e) {
+            throw new DuoClientException(e);
+        }
+        
     }
 
 }
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClient_UNKNOWN.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClient_UNKNOWN.java
index a626998..c8e0fa0 100644
--- a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClient_UNKNOWN.java
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClient_UNKNOWN.java
@@ -19,11 +19,24 @@ package net.shibboleth.idp.plugin.authn.mock;
 
 import static java.lang.String.format;
 
+import java.text.ParseException;
 import java.time.Instant;
 import java.time.temporal.ChronoUnit;
 
 import javax.annotation.Nonnull;
 
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.JWSHeader;
+import com.nimbusds.jose.JWSSigner;
+import com.nimbusds.jose.PlainHeader;
+import com.nimbusds.jose.crypto.MACSigner;
+import com.nimbusds.jose.util.Base64URL;
+import com.nimbusds.jwt.JWT;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.PlainJWT;
+import com.nimbusds.jwt.SignedJWT;
+
 import net.shibboleth.idp.plugin.authn.duo.DuoClientException;
 import net.shibboleth.idp.plugin.authn.duo.DuoOIDCClient;
 import net.shibboleth.idp.plugin.authn.duo.DuoOIDCIntegration;
@@ -34,6 +47,8 @@ import net.shibboleth.idp.plugin.authn.duo.model.DuoAuthDevice;
 import net.shibboleth.idp.plugin.authn.duo.model.DuoAuthToken;
 import net.shibboleth.idp.plugin.authn.duo.model.DuoHealthCheck;
 import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.utilities.java.support.codec.Base64Support;
+import net.shibboleth.utilities.java.support.codec.EncodingException;
 
 /**
  * Mock a Duo client which returns an unknown response for the health check and a login failure in the
@@ -65,36 +80,70 @@ public class MockDuoOIDCClient_UNKNOWN implements DuoOIDCClient{
     }
 
     @Override
-    public DuoAuthToken exchangeAuthorizationCodeFor2FAResult(final String code, 
+    public JWT exchangeAuthorizationCodeFor2FAResult(final String code, 
             final String username) throws DuoClientException {
-        return DuoAuthToken.builder()
-                .withIss("https://api.duosecurity.com/oauth/v1/token")
-                .withSub(SUB)
-                .withAud(integration.getClientId())
-                .withExp(Math.toIntExact(Instant.now().plus(10,ChronoUnit.MINUTES).getEpochSecond()))
-                .withIat((double)Instant.now().getEpochSecond())
-                .withAuthResultStatusMessage("Login Failed")
-                .withAuthResultStatus("fail")
-                .withAuthResult("fail")
-                .withAuthContext(DuoAuthContext.builder()
-                        .withResult("fail").
-                        withTimestamp(1590070939)
-                        .withAuthDevice(DuoAuthDevice.builder()
-                                .withIp("192.168.0.1").
-                                withName("99999999")
-                                .build())
-                        .withTxid("b1287968-1dd1-4488-bb3c-0c72fc398b8b").
-                        withEventType("authenticaiton")
-                        .withReason("user_approved").
-                        withAccessDevice(DuoAccessDevice.builder()
-                                .withIp("192.168.0.1")
-                                .withHostname("localhost")
-                                .build()).
-                        withApplication(DuoApplication.builder()
-                                .withKey("DIU6GEFXXXXXXX")
-                                .withName("Test").build()).
-                        withFactor("duo_push").withUsername("jdoe").withUserKey("XXXXXXX").build()).
-                withAuthTime(Math.toIntExact(Instant.now().getEpochSecond())).build();
+        final String jwtJson = "{\n" + 
+                "    \"iss\": \"https://"+integration.getAPIHost()+"oauth/v1/token\",\n" + 
+                "    \"sub\": \""+SUB+"\",\n" + 
+                "    \"preferred_username\": \"philsmart\",\n" + 
+                "    \"aud\": \""+integration.getClientId()+"\",\n" + 
+                "    \"exp\": "+new String(Math.toIntExact(Instant.now().
+                        plus(10,ChronoUnit.MINUTES).getEpochSecond())+",\n" + 
+                "    \"iat\": "+Long.toString(Instant.now().getEpochSecond()))+",\n" + 
+                "    \"auth_time\": "+Math.toIntExact(Instant.now().getEpochSecond())+",\n" + 
+                "    \"auth_result\": {\n" + 
+                "        \"status_msg\": \"Login Failed\",\n" + 
+                "        \"status\": \"fail\",\n" + 
+                "        \"result\": \"fail\"\n" + 
+                "    },\n" + 
+                "    \"auth_context\": {\n" + 
+                "        \"result\": \"success\",\n" + 
+                "        \"timestamp\": 1599749128,\n" + 
+                "        \"auth_device\": {\n" + 
+                "            \"ip\": \"82.17.89.232\",\n" + 
+                "            \"name\": \"+44 7852 119881\",\n" + 
+                "            \"location\": {\n" + 
+                "                \"state\": \"Wales\",\n" + 
+                "                \"city\": \"Cardiff\",\n" + 
+                "                \"country\": \"United Kingdom\"\n" + 
+                "            }\n" + 
+                "        },\n" + 
+                "        \"txid\": \"1684599c-bb16-4250-af85-904291bfe7cc\",\n" + 
+                "        \"event_type\": \"authentication\",\n" + 
+                "        \"reason\": \"user_approved\",\n" + 
+                "        \"access_device\": {\n" + 
+                "            \"hostname\": null,\n" + 
+                "            \"ip\": \"82.17.89.232\",\n" + 
+                "            \"location\": {\n" + 
+                "                \"state\": \"Wales\",\n" + 
+                "                \"city\": \"Cardiff\",\n" + 
+                "                \"country\": \"United Kingdom\"\n" + 
+                "            }\n" + 
+                "        },\n" + 
+                "        \"application\": {\n" + 
+                "            \"key\": \"DIU6GEFWG5LIUTVV2M3N\",\n" + 
+                "            \"name\": \"Shibboleth Integration Testing\"\n" + 
+                "        },\n" + 
+                "        \"factor\": \"duo_push\",\n" + 
+                "        \"user\": {\n" + 
+                "            \"key\": \"DUGL8U46QGJSOUJWG59W\",\n" + 
+                "            \"name\": \"philsmart\"\n" + 
+                "        }\n" + 
+                "    }\n" + 
+                "}"; 
+        
+        try {
+            //re-sign the token using the client secret
+            final JWSSigner signer = new MACSigner(integration.getSecretKey());
+            //FIXME: needs to be HS512 (or not?)
+            final JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.HS256).build();
+            final JWTClaimsSet claims = JWTClaimsSet.parse(jwtJson);
+            final SignedJWT signedJWT = new SignedJWT(header,claims);
+            signedJWT.sign(signer);
+            return signedJWT;
+        } catch (final ParseException | JOSEException e) {
+            throw new DuoClientException(e);
+        }
     }
 
 }
\ No newline at end of file
diff --git a/idp-duo-native-client-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/sdk/impl/DuoSDKClientAdaptor.java b/idp-duo-native-client-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/sdk/impl/DuoSDKClientAdaptor.java
index 4f6c3d4..97fd948 100644
--- a/idp-duo-native-client-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/sdk/impl/DuoSDKClientAdaptor.java
+++ b/idp-duo-native-client-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/sdk/impl/DuoSDKClientAdaptor.java
@@ -1,25 +1,42 @@
 package net.shibboleth.idp.plugin.authn.duo.sdk.impl;
 
+import java.text.ParseException;
+import java.util.Date;
 import java.util.List;
+import java.util.function.BiFunction;
 import java.util.function.Function;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import com.duosecurity.Client;
 import com.duosecurity.exception.DuoException;
 import com.duosecurity.model.HealthCheckResponse;
 import com.duosecurity.model.Token;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.JWSHeader;
+import com.nimbusds.jose.JWSSigner;
+import com.nimbusds.jose.PlainHeader;
+import com.nimbusds.jose.crypto.MACSigner;
+import com.nimbusds.jose.util.Base64URL;
+import com.nimbusds.jwt.JWT;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.PlainJWT;
+import com.nimbusds.jwt.SignedJWT;
 
 import net.shibboleth.idp.plugin.authn.duo.DuoClientException;
 import net.shibboleth.idp.plugin.authn.duo.DuoOIDCClient;
 import net.shibboleth.idp.plugin.authn.duo.DuoOIDCIntegration;
-import net.shibboleth.idp.plugin.authn.duo.model.DuoAccessDevice;
-import net.shibboleth.idp.plugin.authn.duo.model.DuoApplication;
-import net.shibboleth.idp.plugin.authn.duo.model.DuoAuthContext;
-import net.shibboleth.idp.plugin.authn.duo.model.DuoAuthDevice;
-import net.shibboleth.idp.plugin.authn.duo.model.DuoAuthToken;
 import net.shibboleth.idp.plugin.authn.duo.model.DuoHealthCheck;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.utilities.java.support.codec.Base64Support;
+import net.shibboleth.utilities.java.support.codec.EncodingException;
 import net.shibboleth.utilities.java.support.logic.Constraint;
 
 /**
@@ -30,14 +47,20 @@ import net.shibboleth.utilities.java.support.logic.Constraint;
  */
 final class DuoSDKClientAdaptor implements DuoOIDCClient{
     
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(DuoSDKClientAdaptor.class);
+    
     /** The wrapped Duo native client.*/
     @Nonnull private Client client;
     
     /** Function to map the native Duo {@link HealthCheckResponse} object to the interface {@link DuoHealthCheck} object.*/
     @Nonnull private Function<HealthCheckResponse,DuoHealthCheck> healthCheckResponseConverter;
     
-    /** Function to map the native Duo {@link Token} object to the interface {@link DuoAuthToken} object.*/
-    @Nonnull private Function<Token, DuoAuthToken> tokenResponseConverter;    
+    /** Function to map the native Duo {@link Token} object to the interface {@link JWT} object.*/
+    @Nonnull private BiFunction<Token, DuoOIDCIntegration, JWT> tokenResponseConverter;  
+    
+    /** Save off the integration to help generate the JWT.*/
+    @Nonnull private DuoOIDCIntegration duoIntegration;
 
     
     /**
@@ -51,9 +74,10 @@ final class DuoSDKClientAdaptor implements DuoOIDCClient{
      */
     protected DuoSDKClientAdaptor(@Nonnull final DuoOIDCIntegration integration, @Nullable List<String> caCerts) 
             throws DuoClientException {  
-        Constraint.isNotNull(integration,"Duo SDK Client requires a non-null Duo Integration");
+        duoIntegration = Constraint.isNotNull(integration,"Duo SDK Client requires a non-null Duo Integration");
         healthCheckResponseConverter = new DefaultHealthCheckResponseConverter();
         tokenResponseConverter = new DefaultTokenResponseConverter();
+        log.info("Secret has size '{}'",integration.getSecretKey().getBytes().length);
         try {
             if (caCerts == null) {
                 client = new Client(integration.getClientId(), integration.getSecretKey(),
@@ -114,7 +138,7 @@ final class DuoSDKClientAdaptor implements DuoOIDCClient{
 
     /** {@inheritDoc} */
     @Override
-    @Nonnull public DuoAuthToken exchangeAuthorizationCodeFor2FAResult(@Nonnull final String code, 
+    @Nonnull public JWT exchangeAuthorizationCodeFor2FAResult(@Nonnull final String code, 
             @Nonnull final String username) 
             throws DuoClientException {
         try {
@@ -122,7 +146,11 @@ final class DuoSDKClientAdaptor implements DuoOIDCClient{
             if (token == null) {
                 throw new DuoClientException("Duo token was null");
             }
-            return tokenResponseConverter.apply(token);
+            final JWT tokenAsJWT = tokenResponseConverter.apply(token,duoIntegration);
+            if (tokenAsJWT == null) {
+                throw new DuoClientException("Duo token could not be converted to a JWT token");
+            }
+            return tokenAsJWT;
          } catch (final DuoException e) {
             //wrap duo specific exception.
             throw new DuoClientException(e);
@@ -147,49 +175,40 @@ final class DuoSDKClientAdaptor implements DuoOIDCClient{
         }
         
     }
-    //TODO: optional parts of the builder e.g. city are not being built optionally
-    //TODO: no null checks on nested classes?
-    //TODO: builders are a marginal mess?
-    /** Default Duo token converter.*/
-    private class DefaultTokenResponseConverter implements Function<Token, DuoAuthToken>{
 
+    /**
+     * Default Duo token converter. Creates a JWT by converting the Duo token (back) to a JSON String which it uses to
+     * create a **signed** JWT. As the signature is not returned from the Duo SDK, a new HMAC signature is computed
+     * using the integrations secret key - the flow requires a signed JWT.
+     */
+    private class DefaultTokenResponseConverter implements BiFunction<Token, DuoOIDCIntegration, JWT>{
+        
+        /** JSON object mapper. */
+        @Nonnull private ObjectMapper objectMapper;
+        
+        /** Constructor. */
+        private DefaultTokenResponseConverter() {
+            objectMapper = new ObjectMapper();
+        }
+    
         @Override
-        public DuoAuthToken apply(Token t) {
-            return DuoAuthToken.builder().
-            withIss(t.getIss()).
-            withSub(t.getSub()).
-            withAud(t.getAud()).
-            withExp(t.getExp()).
-            withIat(t.getIat()).
-            withAuthResultStatusMessage(t.getAuth_result().getStatus_msg()).
-            withAuthResultStatus(t.getAuth_result().getStatus()).
-            withAuthResult(t.getAuth_result().getResult())
-            .withAuthContext(DuoAuthContext.builder().
-                    withResult(t.getAuth_context().getResult()).
-                    withTimestamp(t.getAuth_context().getTimestamp()).
-                    withAuthDevice(DuoAuthDevice.builder().
-                            withIp(t.getAuth_context().getAuth_device().getIp()).
-                            withName(t.getAuth_context().getAuth_device().getName()).
-                            withCity(t.getAuth_context().getAuth_device().getLocation().getCity()).
-                            withCountry(t.getAuth_context().getAuth_device().getLocation().getCountry()).
-                            withState(t.getAuth_context().getAuth_device().getLocation().getState()).build()).
-                    withTxid(t.getAuth_context().getTxid()).
-                    withEventType(t.getAuth_context().getEvent_type()).
-                    withReason(t.getAuth_context().getReason()).
-                    withAccessDevice(DuoAccessDevice.builder().
-                            withIp(t.getAuth_context().getAccess_device().getIp()).
-                            withHostname(t.getAuth_context().getAccess_device().getHostname()).
-                            withCity(t.getAuth_context().getAccess_device().getLocation().getCity()).
-                            withCountry(t.getAuth_context().getAccess_device().getLocation().getCountry()).
-                            withState(t.getAuth_context().getAccess_device().getLocation().getState()).build()).
-                    withApplication(DuoApplication.builder().withKey(t.getAuth_context().getApplication().getKey()).
-                            withName(t.getAuth_context().getApplication().getName()).build()).
-                    withFactor(t.getAuth_context().getFactor()).
-                    withUsername(t.getAuth_context().getUser().getName()).
-                    withUserKey(t.getAuth_context().getUser().getKey()).build()).
-            withAuthTime(t.getAuth_time()).
-            withPreferredUsername(t.getPreferred_username()).build();
+        @Nullable public JWT apply(final Token t, final DuoOIDCIntegration integ) {                 
+            try {               
+                final String duoTokenAsJson = objectMapper.writeValueAsString(t);
+                //re-sign the token using the client secret
+                final JWSSigner signer = new MACSigner(integ.getSecretKey());
+                //FIXME: needs to be HS512 (or not?)
+                final JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.HS256).build();
+                final JWTClaimsSet claims = JWTClaimsSet.parse(duoTokenAsJson);
+                final SignedJWT signedJWT = new SignedJWT(header,claims);
+                signedJWT.sign(signer);
+                return signedJWT;
+            } catch (final JsonProcessingException | ParseException | JOSEException e) {
+                log.error("Could not convert Duo Token to a Nimbus JWT Token",e);
+               return null;
+            }      
         }
+
         
     }
 
diff --git a/pom.xml b/pom.xml
index e038192..78c8fff 100644
--- a/pom.xml
+++ b/pom.xml
@@ -76,6 +76,22 @@
             <artifactId>mockito-core</artifactId>
             <scope>test</scope>
         </dependency>
+        
+        <!--  TODO REMOVE -->
+        
+        <dependency>
+            <groupId>com.nimbusds</groupId>
+            <artifactId>nimbus-jose-jwt</artifactId>
+            <version>9.0</version>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.datatype</groupId>
+            <artifactId>jackson-datatype-jsr310</artifactId>
+        </dependency> 
     </dependencies>
 
     <dependencyManagement>

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


More information about the commits mailing list