[java-idp-plugin-duo] branch main updated: JDUO-23 and JDUO-22

Phil Smart philip.smart at jisc.ac.uk
Fri Dec 4 14:21:20 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=fff59cb93f4c4208148312d4766ba7ce988dcfd0

The following commit(s) were added to refs/heads/main by this push:
       new  fff59cb   JDUO-23 and JDUO-22
fff59cb is described below

commit fff59cb93f4c4208148312d4766ba7ce988dcfd0
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Fri Dec 4 14:13:06 2020 +0000

    JDUO-23 and JDUO-22
    
     - JDUO-23 - Allow injection of the JWT claims verifier used to validate
    the id_token
     - JDUO-22 - Add OIDC nonce to the authorisation request
    
     https://issues.shibboleth.net/jira/browse/JDUO-23
     https://issues.shibboleth.net/jira/browse/JDUO-22
---
 idp-duo-api/pom.xml                                |   6 +
 .../plugin/authn/duo/AbstractDuoOIDCClient.java    |  48 +++
 .../idp/plugin/authn/duo/DuoOIDCAuthAPI.java       |  57 +++-
 .../idp/plugin/authn/duo/DuoOIDCClient.java        |  29 +-
 .../authn/duo/DuoOIDCClientCapabilities.java       |  35 +++
 .../duo/context/DuoOIDCAuthenticationContext.java  |  24 ++
 idp-duo-impl/pom.xml                               |   5 +
 .../authn/duo/impl/BasicJWTClaimsVerifier.java     | 265 ++++++++++++++++
 .../authn/duo/impl/DuoJWTClaimsVerifier.java       | 195 ------------
 .../authn/duo/impl/DuoOIDCAuthnController.java     |  15 +-
 .../idp/plugin/authn/duo/impl/DuoSupport.java      |   1 +
 .../impl/DynamicDuoContextJWTClaimsVerifier.java   | 344 +++++++++++++++++++++
 .../plugin/authn/duo/impl/ValidateTokenClaims.java | 179 ++---------
 .../flows/authn/DuoOIDC/duo-oidc-authn-beans.xml   |  22 +-
 .../impl/AbstractAuthnXmlFlowExecutionTests.java   |  30 +-
 .../authn/duo/impl/AbstractDuoActionTest.java      | 213 ++++++++++++-
 .../authn/duo/impl/BasicJWTClaimsVerifierTest.java | 251 +++++++++++++++
 .../authn/duo/impl/DuoOIDCAuthnControllerTest.java |   3 +-
 ...=> DynamicDuoContextJWTClaimsVerifierTest.java} | 288 +++++++++--------
 .../authn/duo/impl/ValidateTokenClaimsTest.java    | 271 +++-------------
 .../plugin/authn/mock/MockDuoOIDCClient_FAIL.java  |  19 +-
 .../plugin/authn/mock/MockDuoOIDCClient_OK.java    |  19 +-
 .../mock/MockDuoOIDCClient_OK_OLD_AUTH_TIME.java   |  19 +-
 .../authn/mock/MockDuoOIDCClient_UNKNOWN.java      |  19 +-
 idp-duo-native-client-impl/pom.xml                 |  13 +-
 .../authn/duo/sdk/impl/DuoSDKClientAdaptor.java    |  26 +-
 .../authn/duo/sdk/impl/DuoSDKClientFactory.java    |   2 -
 .../authn/duo/sdk/conf/authn/duo-oidc.properties   |  10 +-
 idp-duo-nimbus-client-impl/pom.xml                 |   1 -
 .../plugin/authn/duo/nimbus/impl/NimbusClient.java |  19 +-
 .../duo/nimbus/conf/authn/duo-oidc.properties      |  10 +-
 .../authn/duo/nimbus/impl/NimbusClientTest.java    |   3 +-
 pom.xml                                            |  33 +-
 33 files changed, 1683 insertions(+), 791 deletions(-)

diff --git a/idp-duo-api/pom.xml b/idp-duo-api/pom.xml
index 4c9e9a6..507f229 100644
--- a/idp-duo-api/pom.xml
+++ b/idp-duo-api/pom.xml
@@ -19,6 +19,12 @@
 
 
     <dependencies>
+        <!-- Compile time dependencies -->
+        <dependency>
+            <groupId>com.nimbusds</groupId>
+            <artifactId>nimbus-jose-jwt</artifactId>
+            <scope>compile</scope>
+        </dependency>
         <!-- Provided dependencies -->
         <dependency>
             <groupId>com.google.code.findbugs</groupId>
diff --git a/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/AbstractDuoOIDCClient.java b/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/AbstractDuoOIDCClient.java
new file mode 100644
index 0000000..011bf1e
--- /dev/null
+++ b/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/AbstractDuoOIDCClient.java
@@ -0,0 +1,48 @@
+/*
+ * 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;
+
+import java.util.UUID;
+
+import javax.annotation.Nonnull;
+
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+
+/**
+ * Abstract base class for {@link DuoOIDCClient} implementations. Handles the clientId and
+ * retrieval of the client's capabilities.
+ */
+public abstract class AbstractDuoOIDCClient implements DuoOIDCClient{
+    
+    /** The client instance UUID for identification.*/
+    @Nonnull @NotEmpty private final String clientId;
+    
+    /** Constructor.*/
+    public AbstractDuoOIDCClient() {
+        clientId = UUID.randomUUID().toString();
+    }
+    
+    @Override @Nonnull public String getClientId() {
+        return clientId;
+    }
+    
+    @Override @Nonnull public DuoOIDCClientCapabilities getCapabilities() {
+        return this;
+    }
+
+}
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 745b068..e0d6f7d 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
@@ -20,6 +20,8 @@ package net.shibboleth.idp.plugin.authn.duo;
 import javax.annotation.Nonnull;
 
 import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+import net.shibboleth.utilities.java.support.primitive.StringSupport;
 
 /**
  * Constants defined in the Duo OIDC Auth API.
@@ -54,8 +56,59 @@ public final class DuoOIDCAuthAPI {
     /** The name of the JSON result status message property.*/
     @Nonnull @NotEmpty public static final String DUO_AUTH_RESULT_STATUS_MSG_JSON_OBJECT = "status_msg";
     
-    /** The name of the authentication time claim.*/
-    @Nonnull @NotEmpty public static final String DUO_AUTH_TIME_CLAIM_NAME = "auth_time";
+    //TODO MOVE these to a registered OIDC claims set in commons? 
+    
+    //There are others.
+    /** Core ID Token Claims, see OpenID Connect core 1.0 section 2.*/
+    @Nonnull public enum OIDC_ID_TOKEN_CLAIMS{
+        
+        /**Issuer Identifier for the Issuer of the response.*/
+        ISSUER_CLAIM ("iss"),
+        
+        /** Subject Identifier. A locally unique and never reassigned identifier within the 
+         * Issuer for the End-User.*/
+        SUBJECT_CLAIM ("sub"),
+        
+        /** Audience(s) that this ID Token is intended for.*/
+        AUDIENCE_CLAIM ("aud"),
+        
+        /**  Expiration time on or after which the ID Token MUST NOT be accepted for processing.*/
+        EXPIRATION_TIME_CLAIM ("exp"),
+        
+        /**The "nbf" (not before) claim identifies the time before which the JWT MUST NOT be accepted for processing.*/
+        NOT_BEFORE_CLAIM ("nbf"),
+        
+        /**  Time at which the JWT was issued.*/
+        ISSUED_AT_CLAIM ("iat"),
+        
+        /** Time when the End-User authentication occurred.*/
+        AUTHENTICATION_TIME("auth_time"),
+        
+        /** String value used to associate a Client session with an ID Token, and to mitigate replay attacks.*/
+        NONCE("nonce"),
+        
+        /** Authentication Context Class Reference.*/
+        AUTHENTICATION_CONTEXT_CLASS_REFERENCE("acr"),
+        
+        /** Authentication Methods References.*/
+        AUTHENTICATION_METHODS_REFERENCES("amr"),
+        
+        /** Authorized party - the party to which the ID Token was issued.*/
+        AUTHORIZED_PARTY("azp"),
+        
+        /**  The "jti" (JWT ID) claim provides a unique identifier for the JWT.*/
+        JWT_ID_CLAIM ("jti");
+        
+        @Nonnull @NotEmpty private final String claimName;
+        
+        private OIDC_ID_TOKEN_CLAIMS(@Nonnull @NotEmpty final String name) {
+            claimName = Constraint.isNotNull(StringSupport.trimOrNull(name), "Claim name can not be null or empt");
+        }
+                
+        @Nonnull public String getClaimName() {
+            return claimName;
+        }
+    }
 
     /** 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 056a647..96dc7c9 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
@@ -18,15 +18,24 @@
 package net.shibboleth.idp.plugin.authn.duo;
 
 import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
 
 import com.nimbusds.jwt.JWT;
 
 import net.shibboleth.idp.plugin.authn.duo.model.DuoHealthCheck;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
 
 /**
  * A client for handling Duo OIDC 2FA interactions.
  */
-public interface DuoOIDCClient {
+public interface DuoOIDCClient extends DuoOIDCClientCapabilities{
+    
+    /**
+     * Returns the capabilities of the underlying client.
+     *
+     * @return interface to access the service's capabilities
+     */
+    @Nonnull DuoOIDCClientCapabilities getCapabilities();
     
     /**
      * Check the health of the Duo 2FA endpoint. 
@@ -38,16 +47,22 @@ public interface DuoOIDCClient {
     @Nonnull DuoHealthCheck healthCheck() throws DuoClientException;    
     
     /**
-     * Constructs a redirection URL string with the query parameters required to initiate a Duo 2FA request.
+     * Constructs an authorization redirection URL string with the query parameters required to initiate 
+     * a Duo 2FA request.
      * 
-     * @param username The user to be authenticated by Duo.
-     * @param state A randomly generated minimum 22 character String, which is relayed back to the client.
+     * @param username The user to be authenticated by Duo, never {@code null}.
+     * @param state A randomly generated minimum 22 character String, which is relayed back to the client, 
+     *          never {@code null}.
+     * @param nonce a randomly generated (minimum 22 character) cryptographically secure nonce that is 
+     *              replayed in the id_token. Can be {@code null} if not supported by the client as indicated
+     *              by the clients described capabilities.
      * 
      * @return the redirect URL as a string, never {@code null}.
      * 
      * @throws DuoClientException if there is an error creating the authentication URL.
      */
-    @Nonnull String createAuthUrl(@Nonnull final String username, @Nonnull final String state) throws DuoClientException;
+    @Nonnull String createAuthUrl(@Nonnull @NotEmpty final String username, @Nonnull @NotEmpty final String state, 
+            @Nullable final String nonce) throws DuoClientException;
     
     /**
      * Exchanges the authorizaton code for a signed Json Web Token (JWT) which contains information pertaining to
@@ -56,8 +71,8 @@ public interface DuoOIDCClient {
      *
      * @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.
+     *              about the authentication, never {@code null}.
+     * @param username The user to be authenticated by Duo, never {@code null}.
      *
      * @return the **signed** JWT, never {@code null}.
      * 
diff --git a/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/DuoOIDCClientCapabilities.java b/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/DuoOIDCClientCapabilities.java
new file mode 100644
index 0000000..9f13ac1
--- /dev/null
+++ b/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/DuoOIDCClientCapabilities.java
@@ -0,0 +1,35 @@
+/*
+ * 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;
+
+/**
+ * Exposes capabilities of a {@link DuoOIDCClient}.
+ */
+public interface DuoOIDCClientCapabilities {
+    
+    /**
+     * <p>Does this client support the OIDC nonce parameter.</p>
+     * 
+     * <p>If the client does support a nonce, it <b>must</b> be included by the client in the authorisation request URL,
+     * where it should then be returned by the OP in the id_token as part of the 2FA result.</p>
+     * 
+     * @return true iff the client supports the nonce parameter, false otherwise.
+     */
+    boolean isSupportsNonce();
+
+}
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 2522d0f..d9bc15b 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
@@ -49,6 +49,9 @@ public final class DuoOIDCAuthenticationContext extends BaseContext {
     /** A randomly generated 32 character minimum String returned in the Duo 2FA authorization response.*/
     @Nullable private String responseState;
     
+    /** String value used to associate a Client session with an ID Token, and to mitigate replay attacks.*/ 
+    @Nullable private String nonce;
+    
     /** The authorization code return from the Duo authorization request.*/
     @Nullable private String authCode;
     
@@ -63,6 +66,27 @@ public final class DuoOIDCAuthenticationContext extends BaseContext {
         
     }
     
+    /** 
+     * Get the nonce.
+     * 
+     * @return the nonce.
+     */
+    @Nullable public String getNonce() {
+        return nonce;
+    }
+    
+    /**
+     * Set the OIDC nonce.
+     * 
+     * @param oidcNonce the nonce.
+     * 
+     * @return this context.
+     */
+    @Nonnull public DuoOIDCAuthenticationContext setNonce(@Nonnull final String oidcNonce) {
+        nonce = oidcNonce;
+        return this;
+    }
+    
     /**
      * Get the authorization code.
      * 
diff --git a/idp-duo-impl/pom.xml b/idp-duo-impl/pom.xml
index 6df0189..12f8808 100644
--- a/idp-duo-impl/pom.xml
+++ b/idp-duo-impl/pom.xml
@@ -25,6 +25,11 @@
             <groupId>net.shibboleth.idp.plugin</groupId>
             <artifactId>idp-plugin-duo-api</artifactId>
         </dependency>
+        <dependency>
+            <groupId>com.nimbusds</groupId>
+            <artifactId>nimbus-jose-jwt</artifactId>
+            <scope>compile</scope>
+        </dependency>        
 
         <!-- Provided dependencies -->
         <dependency>
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/BasicJWTClaimsVerifier.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/BasicJWTClaimsVerifier.java
new file mode 100644
index 0000000..f5e15b4
--- /dev/null
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/BasicJWTClaimsVerifier.java
@@ -0,0 +1,265 @@
+
+package net.shibboleth.idp.plugin.authn.duo.impl;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import com.nimbusds.jose.proc.SecurityContext;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.proc.BadJWTException;
+import com.nimbusds.jwt.proc.JWTClaimsSetVerifier;
+
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
+import net.shibboleth.utilities.java.support.annotation.constraint.ThreadSafeAfterInit;
+import net.shibboleth.utilities.java.support.component.AbstractInitializableComponent;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+import net.shibboleth.utilities.java.support.primitive.StringSupport;
+
+/**
+ * <p>
+ * {@link JWTClaimsSetVerifier JWT claims verifier} implementation that only checks statically configurable claims. 
+ * That is, not those that are determined at runtime (for instance the issuer and audience), as these are taken 
+ * from the Security Context - see the {@link DynamicDuoContextJWTClaimsVerifier}.
+ * </p>
+ * <p>
+ * This class is thread-safe once it has been initialised.
+ * </p>
+ *
+ * <p>
+ * Performs the following checks:
+ * <ol>
+ * <li>Checks all required claims are present.</li>
+ * <li>Checks prohibited claims are not present.</li>
+ * <li>If an issued-at time (iat) claim is present, 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>If an expiration time (exp) claim is present, makes sure it is ahead of the current time, else the JWT claims set
+ * is rejected. A few minutes of {@code clockSkew} is allowed. See section 3.1.3.7 of OpenID Connect core 1.0.</li>
+ * <li>If a not-before-time (nbf) claim is present, makes sure it is before the current time, else the JWT claims set is
+ * rejected.</li>
+ * </ol>
+ * <p>To make JWT verification OIDC v1.0 complaint for verifying id_tokens, the set of required claims needs to contain
+ * {'iss','sub','aud','exp','iat'}. This verifier does not check 'auth_time', which maybe required if requested 
+ * or 'max_age' was set. 
+ * </p>
+ * <p>
+ * This class may be extended to perform additional checks.
+ * </p>
+ * 
+ * @param <ContextType> the {@link SecurityContext} type.
+ */
+ at ThreadSafeAfterInit
+public class BasicJWTClaimsVerifier<ContextType extends SecurityContext> extends AbstractInitializableComponent
+                            implements JWTClaimsSetVerifier<ContextType> {
+
+    /** 
+     * Positive clock skew adjustment to consider when checking JWT not before and expiration 
+     * (Default value: 60 seconds). 
+     */
+    @Nonnull private Duration clockSkew;
+    
+    /**
+     *  Maximum amount (in either direction from now) of duration for which a token is valid after 
+     *  it is issued (Default value: 60 seconds). 
+     */
+    @Nonnull private Duration iatWindow;
+
+    /**
+     * The names of the JWT claims that must be present, empty set if none.
+     */
+    @Nonnull @NonnullElements private Set<String> requiredClaims;
+
+    /**
+     * The names of the JWT claims that must not be present, empty set if none.
+     */ 
+    @Nonnull @NonnullElements private Set<String> prohibitedClaims;
+
+
+    /** Constructor. */
+    public BasicJWTClaimsVerifier() {        
+        clockSkew = Duration.ofSeconds(60);
+        iatWindow = Duration.ofSeconds(60);
+        requiredClaims = Collections.emptySet();
+        prohibitedClaims = Collections.emptySet();
+    }
+    
+    /**
+     * 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");
+    }
+    
+    /**
+     * 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 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");
+
+        iatWindow = window;
+    }
+    
+    /**
+     * Set the required claims.
+     * 
+     * @param claims the required claims.
+     */
+    public void setRequiredClaims(@Nullable final Set<String> claims) {   
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        
+        if (claims !=null) {
+            requiredClaims = Set.copyOf(StringSupport.normalizeStringCollection(claims));
+        } else {
+            requiredClaims = Collections.emptySet();
+        }
+    }
+    
+    /**
+     * Set the prohibited claims.
+     * 
+     * @param claims the prohibited claims.
+     */
+    public void setProhibitedClaims(@Nullable final Set<String> claims) {   
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        
+        if (claims !=null) {
+            prohibitedClaims = Set.copyOf(StringSupport.normalizeStringCollection(claims));
+        } else {
+            prohibitedClaims = Collections.emptySet();
+        }
+    }
+
+    @Override
+    public void verify(final JWTClaimsSet claimsSet, final ContextType context)
+            throws BadJWTException {
+
+        verifyRequiredClaims(claimsSet);
+        verifyProhibitedClaims(claimsSet);
+        verifyNotBefore(claimsSet);
+        verifyExpiration(claimsSet);
+        verifyIat(claimsSet);
+        
+    }
+    
+    /**
+     * Verify the claims set contains the given, required, claims.
+     * 
+     * @param claimsSet the claims set.
+     * 
+     * @throws BadJWTException if a required claim is not present.
+     */
+    private void verifyRequiredClaims(@Nonnull final JWTClaimsSet claimsSet) throws BadJWTException {
+
+        if (!claimsSet.getClaims().keySet().containsAll(requiredClaims)) {
+            final Set<String> missingClaims = new HashSet<>(requiredClaims);
+            missingClaims.removeAll(claimsSet.getClaims().keySet());
+            throw new BadJWTException("JWT missing required claims: " + missingClaims);
+        }
+    }
+    
+    /**
+     * Verify the claims set does not contain the given, prohibited, claims.
+     * 
+     * @param claimsSet the claims set.
+     * 
+     * @throws BadJWTException if a prohibited claim is present.
+     */
+    private void verifyProhibitedClaims(@Nonnull final JWTClaimsSet claimsSet) throws BadJWTException {
+
+        final Set<String> presentProhibitedClaims = new HashSet<>();
+        for (final String prohibited : prohibitedClaims) {
+            if (claimsSet.getClaims().containsKey(prohibited)) {
+                presentProhibitedClaims.add(prohibited);
+            }
+            if (!presentProhibitedClaims.isEmpty()) {
+                throw new BadJWTException("JWT has prohibited claims: " + presentProhibitedClaims);
+            }
+        }
+    }
+    
+    /**
+     * If present, verifies the not-before-time has passed.
+     * 
+     * @param claimsSet the claims set.
+     * 
+     * @throws BadJWTException if the not-before-time is in the future.
+     */
+    private void verifyNotBefore(@Nonnull final JWTClaimsSet claimsSet) throws BadJWTException {
+        
+        final Instant now = Instant.now();  
+        final Date nbf = claimsSet.getNotBeforeTime();
+        if (nbf != null) {
+            final Instant nbfInstant = nbf.toInstant();
+            final Instant nbfInstantMinusSkew = nbfInstant.minus(clockSkew);
+            if (!nbfInstantMinusSkew.isBefore(now)) {
+                throw new BadJWTException("JWT before use time");
+            }
+        }
+       
+    }
+    
+    /**
+     * If present, verifies the expiration time has not passed (including the given clock skew).
+     * 
+     * @param claimsSet the claims set.
+     * 
+     * @throws BadJWTException if the token has expired.
+     */
+    private void verifyExpiration(@Nonnull final JWTClaimsSet claimsSet) throws BadJWTException {
+        
+        final Instant now = Instant.now();        
+
+        final Date exp = claimsSet.getExpirationTime();
+        if (exp != null) {
+            final Instant expInstant = exp.toInstant();
+            final Instant expirationPlusSkew = expInstant.plus(clockSkew);
+                            
+            if (now.isAfter(expirationPlusSkew)) {
+                throw new BadJWTException("Expired JWT");
+            }
+        }
+    }
+    
+    /**
+     * If present, verifies the IssuedAt claim exists and is within a specified window from the current time.
+     * 
+     * @param claimsSet the claims set.
+     * 
+     * @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) {           
+            final Instant iat = iatDate.toInstant();        
+            final Instant now = Instant.now();        
+            final Duration iatDifference = Duration.between(now, iat).abs();
+            
+            if (iatWindow.compareTo(iatDifference)  < 0) {
+                throw new BadJWTException("JWT issued-at time is too far away from the current time. "
+                        + "Token issued at '"+iat+"' was too far away from the current time '"+now+"' with acceptable deviation of "
+                                + "'"+iatWindow+"', difference is '"+iatDifference+"'");
+            }
+        }
+        
+    }
+
+    
+
+}
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
deleted file mode 100644
index 56641c5..0000000
--- a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoJWTClaimsVerifier.java
+++ /dev/null
@@ -1,195 +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.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.idp.plugin.authn.duo.DuoOIDCAuthAPI;
-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 applies when forced authentication is requested.</li>
- *  </ol>
- */
- at ThreadSafe
-public class DuoJWTClaimsVerifier extends DefaultJWTClaimsVerifier<ProfileRequestJWTSecurityContext>{
-       
-    /** 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 ProfileRequestJWTSecurityContext context)
-        throws BadJWTException {
-        if (context == null) {
-            throw new BadJWTException("Duo claims verifier requires the ProfileRequest security context");
-        }
-        super.verify(claimsSet,context);        
-        verifyIat(claimsSet);
-        verifyAuthenticationTime(claimsSet,context);      
-    }
-    
-    /**
-     * Verifies if the auth_time (when the End-User authentication took place) is within a 
-     * valid expiration window. Only applies to forced authentications. 
-     * 
-     * @param claimsSet the JWT claimset.
-     * @param context the profile request security context.
-     * 
-     * @throws BadJWTException if the auth_time is invalid.
-     */
-    private void verifyAuthenticationTime(@Nonnull final JWTClaimsSet claimsSet, 
-            @Nonnull final ProfileRequestJWTSecurityContext context) throws BadJWTException {
-        
-        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(DuoOIDCAuthAPI.DUO_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/DuoOIDCAuthnController.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoOIDCAuthnController.java
index 579c17a..2c70cd9 100644
--- a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoOIDCAuthnController.java
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoOIDCAuthnController.java
@@ -127,8 +127,19 @@ public class DuoOIDCAuthnController extends AbstractInitializableComponent{
             final String nonce = DuoSupport.generateNonce(32);
             final String state = DuoSupport.generateState(nonce, key);
             //store only the nonce component as the request state. The key component is only used to resume the flow.
-            duoContext.setRequestState(nonce);            
-            final String authURL = client.createAuthUrl(duoContext.getUsername(), state);
+            duoContext.setRequestState(nonce);
+            
+            String authURL;
+            //if an OIDC id_token nonce is supported, add it to the authz request
+            if (client.getCapabilities().isSupportsNonce()) {
+                final String oidcNonce = DuoSupport.generateNonce(36);
+                authURL = client.createAuthUrl(duoContext.getUsername(), state, oidcNonce);
+                duoContext.setNonce(oidcNonce);
+               
+            } else {
+                authURL = client.createAuthUrl(duoContext.getUsername(), state, null);
+            }
+
             httpResponse.sendRedirect(authURL);
 
         } catch (final DuoClientException e) {
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoSupport.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoSupport.java
index 0229bbd..774e006 100644
--- a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoSupport.java
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoSupport.java
@@ -37,6 +37,7 @@ public final class DuoSupport {
     private DuoSupport() {
 
     }
+    
    
     /**
      * Generates a random identifier to be used as a nonce.
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DynamicDuoContextJWTClaimsVerifier.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DynamicDuoContextJWTClaimsVerifier.java
new file mode 100644
index 0000000..464ba87
--- /dev/null
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DynamicDuoContextJWTClaimsVerifier.java
@@ -0,0 +1,344 @@
+/*
+ * 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.text.ParseException;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Date;
+import java.util.List;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.nimbusds.jose.proc.SecurityContext;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.proc.BadJWTException;
+import com.nimbusds.jwt.proc.JWTClaimsSetVerifier;
+
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.plugin.authn.duo.DuoOIDCAuthAPI;
+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.annotation.constraint.ThreadSafeAfterInit;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
+/**
+ * <p>
+ * {@link JWTClaimsSetVerifier JWT claims verifier} implementation. Extends the {@link BasicJWTClaimsVerifier}
+ * to include claim checks constructed from values inside the {@link DuoOIDCIntegration}
+ * stashed in the {@link SecurityContext} at runtime, namely; the issuer and audience.
+ * </p>
+ * <p>
+ * This class is thread-safe once it has been initialised.
+ * </p>
+ *
+ * <p>
+ * In addition to the verification checks in the {@link BasicJWTClaimsVerifier}, verifies:
+ <ul>
+ * <li>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>The auth_time (when the End-User authentication took place) is within a valid expiration window. Only
+ * for forced authentications.</li>
+ * <li>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>The preferred username standard claim in the token matches the user who is currently authenticating</li>
+ * <li>If present, the nonce matches that sent in the authorization request.</li>
+ * </ul>
+ *
+ * <p>
+ * This class may be extended to perform additional checks.
+ * </p>
+ */
+ at ThreadSafeAfterInit
+public class DynamicDuoContextJWTClaimsVerifier extends BasicJWTClaimsVerifier<ProfileRequestJWTSecurityContext> {
+    
+    /** 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"; 
+    
+    /** The default name of the username claim (an OIDC standard claims).*/
+    @Nonnull @NotEmpty public static final String DEFAULT_USERNAME_CLAIM = "preferred_username";
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(DynamicDuoContextJWTClaimsVerifier.class);
+    
+    /** 
+     * If forced authentication, amount of time for which a token is valid 
+     * after if it was issued. (Default value: 60 seconds) 
+     */
+    @Nonnull private Duration authnLifetime;
+    
+    /** The URL path component of the issuer.*/
+    @Nonnull @NotEmpty private String issuerPath;
+    
+    /** The name of the username claim.*/
+    @Nonnull @NotEmpty private String usernameClaim;
+    
+    /** Lookup strategy to locate the Duo authentication context. */
+    @Nonnull
+    private Function<ProfileRequestContext, DuoOIDCAuthenticationContext> duoContextLookupStrategy;
+    
+    /** Lookup strategy to locate the authentication context. */
+    @Nonnull
+    private Function<ProfileRequestContext, AuthenticationContext> authenticationContextLookupStrategy;
+
+    /** Constructor.*/
+    public DynamicDuoContextJWTClaimsVerifier() {      
+        super();        
+        // prc -> ac -> dc        
+        duoContextLookupStrategy = new ChildContextLookup<>(DuoOIDCAuthenticationContext.class)
+                .compose(new ChildContextLookup<>(AuthenticationContext.class));        
+        // prc -> ac
+        authenticationContextLookupStrategy = new ChildContextLookup<>(AuthenticationContext.class);
+        authnLifetime = Duration.ofSeconds(60);
+        issuerPath = DEFAULT_ISSUER_PATH;
+        usernameClaim = DEFAULT_USERNAME_CLAIM;
+    }
+    
+    /**
+     * Set Duo authentication context lookup strategy to use.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setDuoContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,DuoOIDCAuthenticationContext> strategy) {     
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        
+        duoContextLookupStrategy = Constraint.isNotNull(strategy, "DuoContextLookup strategy cannot be null");
+    }
+    
+    /**
+     * Set Duo authentication context lookup strategy to use.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setAuthenticationContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,AuthenticationContext> strategy) {    
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        
+        authenticationContextLookupStrategy = Constraint.isNotNull(strategy, 
+                "AuthenticationContextLookup strategy cannot be null");
+    }
+    
+    /**
+     * 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 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;
+    }
+        
+    /**
+     * Sets the issuer URL path component.
+     * 
+     * @param path the issuer path
+     */
+    public void setIssuerPath(@Nonnull @NotEmpty final String path) {    
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        
+        issuerPath = Constraint.isNotEmpty(path, "Issuer URL path cannot be null or empty");       
+    }
+    
+    /**
+     * Sets the name of the username claim.
+     * 
+     * @param claimName the username claim name
+     */
+    public void setUsernameClaim(@Nonnull @NotEmpty final String claimName) {   
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        
+        usernameClaim = Constraint.isNotEmpty(claimName, "Username claim name can not be null or empty");       
+    }
+    
+
+    @Override
+    public void verify(@Nonnull final JWTClaimsSet claimsSet, @Nonnull final ProfileRequestJWTSecurityContext context)
+            throws BadJWTException {
+        
+        //do basic checks first.
+        super.verify(claimsSet,context);
+        
+        //when used within the validation action, it is unlikely these could ever be null.
+        if (context == null) {
+            throw new BadJWTException("Duo claims verifier requires the ProfileRequest security context");
+        }
+
+        final AuthenticationContext authnContext = authenticationContextLookupStrategy.apply(context.getPrc());
+        if (authnContext == null) {
+            throw new BadJWTException("No authentication context returned by lookup strategy, audience and issuer"
+                    + " could not be verified");
+        }
+
+        final DuoOIDCAuthenticationContext duoContext = duoContextLookupStrategy.apply(context.getPrc());
+        if (duoContext == null) {
+            throw new BadJWTException("No Duo context returned by lookup strategy, audience and issuer"
+                    + " could not be verified");
+        }
+        
+        final DuoOIDCIntegration duoIntegration = duoContext.getIntegration();
+        if (duoIntegration == null) {
+            throw new BadJWTException("No Duo integration contained within the Duo context");
+        }
+            
+        verifyAudience(claimsSet,duoIntegration);     
+        verifyAuthenticationTime(claimsSet,authnContext);   
+        
+        //if the nonce exists in the id_token it must be validated alongside the issuer and username.
+        if (claimsSet.getClaim(DuoOIDCAuthAPI.OIDC_ID_TOKEN_CLAIMS.NONCE.getClaimName()) != null) {
+            //verify with nonce if present
+            verifyExactMatchClaims(claimsSet, new JWTClaimsSet.Builder()
+                    .issuer(HTTPS+duoContext.getIntegration().getAPIHost()+issuerPath)
+                    .claim(usernameClaim, duoContext.getUsername())
+                    .claim(DuoOIDCAuthAPI.OIDC_ID_TOKEN_CLAIMS.NONCE.getClaimName(),duoContext.getNonce())
+                    .build());           
+        } else {
+           //verify without a nonce, only issuer and username
+            verifyExactMatchClaims(claimsSet, new JWTClaimsSet.Builder()
+                    .issuer(HTTPS+duoContext.getIntegration().getAPIHost()+issuerPath)
+                    .claim(usernameClaim, duoContext.getUsername())
+                    .build());
+        }        
+    }
+
+    /**
+     * Verify the claims in <code>exactMatchClaims</code> exactly match (by Object equality) those
+     * in the <code>claimsSet</code>.
+     * 
+     * @param claimsSet the JWT claimset.
+     * @param exactMatchClaims the claims to match exactly.
+     * 
+     * @throws BadJWTException if the claim does not exist or does not match the given claim value.
+     */
+    private void verifyExactMatchClaims(@Nonnull final JWTClaimsSet claimsSet, 
+            @Nonnull final JWTClaimsSet exactMatchClaims) throws BadJWTException {
+        
+        for (final String exactMatch: exactMatchClaims.getClaims().keySet()) {
+            final Object actualClaim = claimsSet.getClaim(exactMatch);
+            if (actualClaim == null) {
+                throw new BadJWTException("Claim '"+exactMatch+"' does not exist");
+            }
+            final Object expectedClaim = exactMatchClaims.getClaim(exactMatch);
+            if (! actualClaim.equals(expectedClaim)) {
+                throw new BadJWTException("JWT \"" + exactMatch + "\" claim has value " 
+                            + actualClaim + " but should be " + expectedClaim);
+            }
+        }
+        
+    }
+    
+    /**
+     * Verify one of the audience values in the 'aud' claim matches with the Duo clientId.
+     * 
+     * @param claimsSet the JWT claimset.
+     * @param duoIntegration the Duo integration information.
+     * 
+     * @throws BadJWTException if the accepted audience (Duo ClientId) can not be found in 
+     *                          the JWT's audience list.
+     */
+    private void verifyAudience(@Nonnull final JWTClaimsSet claimsSet, 
+            @Nonnull final DuoOIDCIntegration duoIntegration) throws BadJWTException {
+        
+        // Check audience based on that specified in the duo integration
+        final String acceptedAudience = duoIntegration.getClientId();
+        
+        final List<String> audList = claimsSet.getAudience();
+        if (audList != null && !audList.isEmpty()) {
+            boolean audMatch = false;
+            for (final String aud : audList) {
+                if (acceptedAudience.equals(aud)) {
+                    audMatch = true;
+                    break;
+                }
+            }
+            if (!audMatch) {
+                throw new BadJWTException("JWT audience rejected: " + audList);
+            }
+        } else {
+            throw new BadJWTException("JWT missing required audience");
+        }
+        
+    }
+    
+    /**
+     * Verifies if the auth_time (when the End-User authentication took place) is within a 
+     * valid expiration window. Only applies to forced authentications. 
+     * 
+     * @param claimsSet the JWT claimset.
+     * @param ac the authentication context.
+     * 
+     * @throws BadJWTException if the auth_time is invalid.
+     */
+    private void verifyAuthenticationTime(@Nonnull final JWTClaimsSet claimsSet, 
+            @Nonnull final AuthenticationContext ac) throws BadJWTException {
+        
+        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(DuoOIDCAuthAPI.OIDC_ID_TOKEN_CLAIMS.
+                        AUTHENTICATION_TIME.getClaimName());
+                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(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);
+            }
+        }
+    }          
+
+}
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
index 48a78f5..8e14b4d 100644
--- 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
@@ -18,9 +18,6 @@
 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;
@@ -33,33 +30,23 @@ import org.slf4j.LoggerFactory;
 import com.nimbusds.jwt.JWT;
 import com.nimbusds.jwt.JWTClaimsSet;
 import com.nimbusds.jwt.proc.BadJWTException;
+import com.nimbusds.jwt.proc.JWTClaimsSetVerifier;
 
 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.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
 import net.shibboleth.utilities.java.support.component.ComponentSupport;
 import net.shibboleth.utilities.java.support.logic.Constraint;
-import net.shibboleth.utilities.java.support.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>
+ * Action that validates the claims of the Duo id_token using the supplied {@link JWTClaimsSetVerifier}. The 
+ * verifier <b>must</b> be thread-safe and validate the claims set against the OpenID Connect core 1.0 
+ * section 3.1.3.7 specification, and those required by Duo. However, the action can not guarantee the 
+ * verifier behaves in this way.
  * 
  * @pre
  * 
@@ -88,17 +75,7 @@ import net.shibboleth.utilities.java.support.logic.ConstraintViolationException;
  * @event {@link net.shibboleth.idp.authn.AuthnEventIds#AUTHN_EXCEPTION}
  * @event {@link net.shibboleth.idp.authn.AuthnEventIds#NO_CREDENTIALS}
  */
-//TODO: maybe the claims checker could be injected here?
 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);
@@ -106,106 +83,27 @@ public class ValidateTokenClaims extends AbstractDuoAuthenticationAction {
     /** 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;
-    }
+    /** The JWT claims verify use to verify the claimsset.*/
+    @NonnullAfterInit private JWTClaimsSetVerifier<ProfileRequestJWTSecurityContext> claimsVerifier;
     
-    /**
-     * 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");
+    /** {@inheritDoc} */
+    @Override protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+
+        if (claimsVerifier ==  null) {
+            throw new ComponentInitializationException("Duo ClaimSet Verifier cannot be null");
         }
     }
     
     /**
-     * Sets the issuer URL path component.
+     * Set the JWT claims verifier to use.
      * 
-     * @param path the issuer path
+     * @param verifier the claims verifier.
      */
-    public synchronized void setIssuerPath(@Nonnull final String path) {
+    public synchronized void setClaimsVerifier(
+            @Nonnull final JWTClaimsSetVerifier<ProfileRequestJWTSecurityContext> verifier) {
         ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
-        issuerPath = Constraint.isNotNull(path, "Issuer URL path cannot be null");       
+        claimsVerifier = Constraint.isNotNull(verifier, "Claims verifier cannot be null");
     }
     
     @Override
@@ -229,14 +127,7 @@ public class ValidateTokenClaims extends AbstractDuoAuthenticationAction {
             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;
     }
     
@@ -247,33 +138,11 @@ public class ValidateTokenClaims extends AbstractDuoAuthenticationAction {
             @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 ProfileRequestJWTSecurityContext(profileRequestContext));
+            //blank the nonce even if not used
+            duoContext.setNonce(null);
         } catch (final BadJWTException e) {
             log.error("{} Token verification failed for subject '{}'", getLogPrefix(),claimsSet.getSubject(),e);
             ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
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 f8161f3..e5ad12b 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
@@ -69,7 +69,27 @@
         class="net.shibboleth.idp.plugin.authn.duo.impl.ValidateTokenSignature" />
         
     <bean id="ValidateTokenClaims" scope="prototype"
-        class="net.shibboleth.idp.plugin.authn.duo.impl.ValidateTokenClaims" />
+        class="net.shibboleth.idp.plugin.authn.duo.impl.ValidateTokenClaims" 
+        p:claimsVerifier="#{getObject('shibboleth.authn.DuoOIDC.DuoTokenClaimsVerifier') ?: getObject('shibboleth.authn.DuoOIDC.DefaultDuoTokenClaimsVerifier')}"/>
+
+    <bean id="shibboleth.authn.DuoOIDC.DefaultDuoTokenClaimsVerifier"
+        class="net.shibboleth.idp.plugin.authn.duo.impl.DynamicDuoContextJWTClaimsVerifier"
+        p:clockSkew="%{idp.duo.oidc.jwt.verifier.clockSkew:PT60S}"
+        p:iatWindow="%{idp.duo.oidc.jwt.verifier.iatWindow:PT60S}"
+        p:authnLifetime="%{idp.duo.oidc.jwt.verifier.authLifetime:PT60S}"
+        p:issuerPath="%{idp.duo.oidc.jwt.verifier.issuerPath:/oauth/v1/token}"
+        p:usernameClaim="%{idp.duo.oidc.jwt.verifier.preferredUsername:preferred_username}"
+        p:requiredClaims="#{getObject('shibboleth.authn.DuoOIDC.RequiredOIDCClaims') ?: getObject('shibboleth.authn.DuoOIDC.DefaultRequiredOIDCClaims')}"
+        />
+     
+    <!-- These represent the default set of id_token claims which are **required** by OIDC -->   
+    <util:set id="shibboleth.authn.DuoOIDC.DefaultRequiredOIDCClaims">
+        <value>iss</value>
+        <value>sub</value>
+        <value>aud</value>
+        <value>exp</value>
+        <value>iat</value>
+    </util:set>
 
     <bean id="ExchangeCodeForDuoToken" scope="prototype"
         class="net.shibboleth.idp.plugin.authn.duo.impl.ExchangeCodeForDuoToken" />
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/AbstractAuthnXmlFlowExecutionTests.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/AbstractAuthnXmlFlowExecutionTests.java
index 8f49e1a..3d12a4a 100644
--- a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/AbstractAuthnXmlFlowExecutionTests.java
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/AbstractAuthnXmlFlowExecutionTests.java
@@ -17,10 +17,12 @@
 
 package net.shibboleth.idp.plugin.authn.duo.impl;
 
+import java.time.Duration;
 import java.util.Base64;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -33,10 +35,12 @@ import org.springframework.beans.factory.BeanFactory;
 import org.springframework.beans.factory.config.BeanDefinition;
 import org.springframework.beans.factory.support.BeanDefinitionBuilder;
 import org.springframework.beans.factory.support.DefaultListableBeanFactory;
+
 import org.springframework.context.ApplicationContext;
 import org.springframework.context.ConfigurableApplicationContext;
-
+import org.springframework.context.support.ConversionServiceFactoryBean;
 import org.springframework.context.support.StaticApplicationContext;
+import org.springframework.core.convert.converter.Converter;
 import org.springframework.core.env.Environment;
 import org.springframework.core.env.StandardEnvironment;
 import org.springframework.mock.env.MockPropertySource;
@@ -52,6 +56,7 @@ import org.springframework.webflow.test.execution.AbstractExternalizedFlowExecut
 
 import com.google.common.net.HttpHeaders;
 
+import net.shibboleth.ext.spring.config.StringToDurationConverter;
 import net.shibboleth.idp.authn.AuthenticationFlowDescriptor;
 import net.shibboleth.idp.authn.context.AuthenticationContext;
 import net.shibboleth.idp.authn.context.SubjectCanonicalizationContext;
@@ -65,6 +70,7 @@ import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElemen
 import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
 import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
 import net.shibboleth.utilities.java.support.security.impl.SecureRandomIdentifierGenerationStrategy;
+import net.shibboleth.utilities.java.support.xml.DOMTypeSupport;
 
 
 /**
@@ -275,6 +281,28 @@ public abstract class AbstractAuthnXmlFlowExecutionTests extends CustomAbstractX
         //create a real JSON parser
         builderContext.registerBean("shibboleth.JSONObjectMapper", new com.fasterxml.jackson.databind.ObjectMapper());
         
+        
+        
+        
+        //create converter for Durations
+        ConversionServiceFactoryBean conversationService = new ConversionServiceFactoryBean();
+        //unable to register the spring-ext converter even though it is the same!
+        final Converter<String,Duration> durationConverter = new Converter<String,Duration>(){
+
+            @Override
+            public Duration convert(String source) {
+                if (source.startsWith("P") || source.startsWith("-P")) {
+                    return DOMTypeSupport.stringToDuration(source.trim());
+                }                
+                // Treat as milliseconds.
+                return Duration.ofMillis(Long.valueOf(source));
+            }
+            
+        };        
+        conversationService.setConverters(Set.of(durationConverter));
+        conversationService.afterPropertiesSet();
+        builderContext.registerBean("conversionService", conversationService.getObject());
+        
         //register shibboleth function constants.
         addBeanDefinition(builderContext, "shibboleth.Functions.Constant",BeanDefinitionBuilder.
                 genericBeanDefinition(net.shibboleth.utilities.java.support.logic.FunctionSupport.class)
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 9fc31de..83a9a17 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
@@ -105,8 +105,8 @@ public abstract class AbstractDuoActionTest {
      * @param exp expiration time.
      * @param iat issued at.
      * @param authTime the auth time.
-     * @param apiHost TODO
-     * @param factor TODO
+     * @param apiHost the api host
+     * @param factor the duo factor
      * @return the duo auth token.
      */
     protected JWT createPlainDummyToken(@Nonnull final String authResult, 
@@ -132,6 +132,77 @@ public abstract class AbstractDuoActionTest {
         return null;
     }
     
+    /**
+     * Create a dummy Duo plain (no sig or enc) JWT token which is missing the 'exp' and 'aud' claims.
+     * 
+     * @param authResult the authentication result e.g. allow.
+     * @param authResultMessage a user friendly result message.
+     * @param iat issued at.
+     * @param authTime the auth time.
+     * @param apiHost the api host
+     * @param factor the duo factor
+     * @return the duo auth token.
+     */
+    protected JWT createPlainDummyToken(@Nonnull final String authResult, 
+            @Nonnull final String authResultMessage, @Nonnull final Instant iat, 
+            @Nonnull final Instant authTime, @Nonnull final String apiHost, 
+            @Nonnull final String factor) {
+        
+        final String jwtJson = createJWTJsonMissingAudAndExpClaims(authResult,authResultMessage,
+                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 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 nbf not before time.
+     * @param apiHost the api host
+     * @param factor the duo factor
+     * @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 Instant nbf,
+            @Nonnull final String apiHost, 
+            @Nonnull final String factor) {
+        
+        final String jwtJson = createJWTJsonWithNbf(authResult,authResultMessage,aud,
+            exp, iat, authTime, nbf, 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.
      * 
@@ -321,6 +392,144 @@ public abstract class AbstractDuoActionTest {
         return jwtJson;
     }
     
+    /**
+     * Create a JWT token in JSON using the claims arguments present, but missing the 'aud' and
+     * 'exp' claims.
+     * 
+     * @param authResult the authentication result e.g. allow.
+     * @param authResultMessage a user friendly result message.
+     * @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 createJWTJsonMissingAudAndExpClaims(@Nonnull final String authResult, 
+            @Nonnull final String authResultMessage, @Nonnull final Instant iat, 
+            @Nonnull final Instant authTime, @Nonnull final String apiHost, 
+            @Nonnull final String factor) {
+        final String jwtJson = "{\n" + 
+                "    \"iss\": \"https://"+apiHost+"/oauth/v1/token\",\n" + 
+                "    \"sub\": \"jdoe\",\n" + 
+                "    \"preferred_username\": \"jdoe\",\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;
+    }
+    
+    /**
+     * Create a JWT token in JSON using the claims arguments. Includes the
+     * not before claim which is not part of the normal Duo (or OIDC) response, but is part
+     * of the JWT spec.
+     * 
+     * @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 nbf the not before time.
+     * @param apiHost the api host.
+     * @param factor the factor.
+     * @return the duo auth token.
+     */
+    private String createJWTJsonWithNbf(@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 Instant nbf, @Nonnull final String apiHost, 
+            @Nonnull final String factor) {
+        final String jwtJson = "{\n" + 
+                "    \"nbf\": "+Math.toIntExact(nbf.getEpochSecond())+",\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;
+    }
+    
     /**
      * Add an {@link AuthenticationFlowDescriptor} to the {@link AuthenticationContext} 
      * with the specified flow ID.
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/BasicJWTClaimsVerifierTest.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/BasicJWTClaimsVerifierTest.java
new file mode 100644
index 0000000..fcd8d0d
--- /dev/null
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/BasicJWTClaimsVerifierTest.java
@@ -0,0 +1,251 @@
+package net.shibboleth.idp.plugin.authn.duo.impl;
+
+import java.text.ParseException;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.Set;
+
+import javax.annotation.Nonnull;
+
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.nimbusds.jwt.proc.BadJWTException;
+
+import net.shibboleth.idp.plugin.authn.duo.DuoOIDCAuthAPI;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.component.UnmodifiableComponentException;
+
+/**
+ * Tests for the {@link BasicJWTClaimsVerifier}.
+ */
+public class BasicJWTClaimsVerifierTest  extends AbstractDuoActionTest{
+    
+    /** The verifier to test.*/
+    @Nonnull private BasicJWTClaimsVerifier<ProfileRequestJWTSecurityContext> verifier;
+    
+    @BeforeMethod
+    public void setUp() throws Exception {
+        super.setup();
+        verifier = new BasicJWTClaimsVerifier<ProfileRequestJWTSecurityContext>();
+        
+    }
+
+    @Test(expectedExceptions = UnmodifiableComponentException.class)
+    public final void testSetupAfterInit() throws ComponentInitializationException {
+        verifier.initialize();
+        verifier.setClockSkew(Duration.ofMillis(100));
+    }
+    
+    @Test(expectedExceptions = BadJWTException.class)
+    public final void testNotBeforeTimeInFuture() throws Exception {
+        addDuoContext();
+        addDuoIntegrationToContext();
+        addAttemptedFlow("authn/DuoOIDC");
+        dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID, 
+                Instant.now(),Instant.now(), Instant.now(), Instant.now().plus(Duration.ofMinutes(2)),
+                "api.duosecurity.com", "duo_push"));       
+        
+        verifier.initialize();
+        verifier.verify(dc.getAuthToken().getJWTClaimsSet(), new ProfileRequestJWTSecurityContext(prc));
+        
+    }
+    
+    @Test
+    public final void testNotBeforeTimeInFutureButWithinSkew() throws Exception {
+        addDuoContext();
+        addDuoIntegrationToContext();
+        addAttemptedFlow("authn/DuoOIDC");
+        verifier.setClockSkew(Duration.ofMinutes(3));
+        dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID, 
+                Instant.now(),Instant.now(), Instant.now(), Instant.now().plus(Duration.ofMinutes(2)),
+                "api.duosecurity.com", "duo_push"));       
+        verifier.initialize();
+        verifier.verify(dc.getAuthToken().getJWTClaimsSet(), new ProfileRequestJWTSecurityContext(prc));
+        
+    }
+    
+    @Test(expectedExceptions = BadJWTException.class)
+    public final void testNotBeforeTimeInFutureBeyondSkew() throws Exception {
+        addDuoContext();
+        addDuoIntegrationToContext();
+        addAttemptedFlow("authn/DuoOIDC");
+        verifier.setClockSkew(Duration.ofMinutes(1));
+        dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID, 
+                Instant.now(),Instant.now(), Instant.now(), Instant.now().plus(Duration.ofMinutes(2)),
+                "api.duosecurity.com", "duo_push"));       
+        verifier.initialize();
+        verifier.verify(dc.getAuthToken().getJWTClaimsSet(), new ProfileRequestJWTSecurityContext(prc));
+        
+    }
+    
+    @Test
+    public final void testNotBeforeTimeInPast() throws Exception {
+        addDuoContext();
+        addDuoIntegrationToContext();
+        addAttemptedFlow("authn/DuoOIDC");
+        dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID, 
+                Instant.now(),Instant.now(), Instant.now(), Instant.now().minus(Duration.ofMinutes(1)),
+                "api.duosecurity.com", "duo_push"));       
+        verifier.initialize();
+        verifier.verify(dc.getAuthToken().getJWTClaimsSet(), new ProfileRequestJWTSecurityContext(prc));
+        
+    }
+    
+    /**
+     * Test the nbf time is equal to the current time - which should be allowed per JWT spec (rfc7519)
+     * 
+     * @throws BadJWTException on error.
+     * @throws ParseException on error.
+     */
+    @Test
+    public final void testNotBeforeTimeIsNow() throws Exception {
+        addDuoContext();
+        addDuoIntegrationToContext();
+        addAttemptedFlow("authn/DuoOIDC");
+        dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID, 
+                Instant.now(),Instant.now(), Instant.now(), Instant.now(),
+                "api.duosecurity.com", "duo_push"));       
+        verifier.initialize();
+        verifier.verify(dc.getAuthToken().getJWTClaimsSet(), new ProfileRequestJWTSecurityContext(prc));
+        
+    }
+    
+    /**
+     * Validate an expired token.
+     * 
+     * @throws ParseException on error.
+     * @throws BadJWTException on error. Expected.
+     */
+    @Test(expectedExceptions = BadJWTException.class)
+    public final void testExpiredToken() throws Exception {
+        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"));
+        verifier.initialize();
+        verifier.verify(dc.getAuthToken().getJWTClaimsSet(), new ProfileRequestJWTSecurityContext(prc));
+
+    }
+    
+    /**
+     * Validate an expired token but with a large clock skew.
+     * 
+     * @throws ParseException on error.
+     * @throws BadJWTException on error.
+     */
+    @Test
+    public final void testExpiredTokenLargeClockSkew() throws Exception {
+        addDuoContext();
+        addDuoIntegrationToContext();
+        addAttemptedFlow("authn/DuoOIDC");
+        //set clock skew
+        verifier.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"));
+        verifier.initialize();
+        verifier.verify(dc.getAuthToken().getJWTClaimsSet(), new ProfileRequestJWTSecurityContext(prc));
+
+    }
+    
+    @Test
+    public final void testAllRequiredOIDCClaimsPresent() throws Exception {
+        addDuoContext();
+        addDuoIntegrationToContext();
+        addAttemptedFlow("authn/DuoOIDC");
+       
+        verifier.setRequiredClaims(Set.of("iss","exp","sub","aud","iat"));
+        
+        dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID, 
+                Instant.now(),Instant.now(), Instant.now(), 
+                "api.duosecurity.com", "duo_push"));
+        verifier.initialize();
+        verifier.verify(dc.getAuthToken().getJWTClaimsSet(), new ProfileRequestJWTSecurityContext(prc));
+    }
+    
+    @Test(expectedExceptions = BadJWTException.class)
+    public final void testAllRequiredOIDCClaimsPresentSomeMissing() throws Exception {
+        addDuoContext();
+        addDuoIntegrationToContext();
+        addAttemptedFlow("authn/DuoOIDC");
+       
+        verifier.setRequiredClaims(Set.of("iss","exp","sub","aud","iat"));
+        
+        dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",
+                Instant.now(), Instant.now(), 
+                "api.duosecurity.com", "duo_push"));
+        verifier.initialize();
+        verifier.verify(dc.getAuthToken().getJWTClaimsSet(), new ProfileRequestJWTSecurityContext(prc));
+    }
+    
+
+    @Test
+    public final void testRequireClaimsValid() throws Exception {
+        addDuoContext();
+        addDuoIntegrationToContext();
+        addAttemptedFlow("authn/DuoOIDC");
+       
+        verifier.setRequiredClaims(Set.of("iss","exp","sub"));
+        
+        dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID, 
+                Instant.now(),Instant.now(), Instant.now(), 
+                "api.duosecurity.com", "duo_push"));
+        verifier.initialize();
+        verifier.verify(dc.getAuthToken().getJWTClaimsSet(), new ProfileRequestJWTSecurityContext(prc));
+
+    }
+    
+    @Test(expectedExceptions = BadJWTException.class)
+    public final void testRequireClaimsInvalid() throws Exception {
+        addDuoContext();
+        addDuoIntegrationToContext();
+        addAttemptedFlow("authn/DuoOIDC");
+       
+        verifier.setRequiredClaims(Set.of("iss","exp","sub","not_in_jwt"));
+        
+        dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID, 
+                Instant.now(),Instant.now(), Instant.now(), 
+                "api.duosecurity.com", "duo_push"));
+        verifier.initialize();
+        verifier.verify(dc.getAuthToken().getJWTClaimsSet(), new ProfileRequestJWTSecurityContext(prc));
+
+    }
+    
+    @Test(expectedExceptions = BadJWTException.class)
+    public final void testProhibitedClaims() throws Exception {
+        addDuoContext();
+        addDuoIntegrationToContext();
+        addAttemptedFlow("authn/DuoOIDC");
+       
+        verifier.setProhibitedClaims(Set.of("iss"));
+        
+        dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID, 
+                Instant.now(),Instant.now(), Instant.now(), 
+                "api.duosecurity.com", "duo_push"));
+        verifier.initialize();
+        verifier.verify(dc.getAuthToken().getJWTClaimsSet(), new ProfileRequestJWTSecurityContext(prc));
+
+    }
+    
+    @Test
+    public final void testProhibitedClaimsDoNotExist() throws Exception {
+        addDuoContext();
+        addDuoIntegrationToContext();
+        addAttemptedFlow("authn/DuoOIDC");
+       
+        //should be fine, as not in JWT
+        verifier.setProhibitedClaims(Set.of("not_in_jwt"));
+        
+        dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID, 
+                Instant.now(),Instant.now(), Instant.now(), 
+                "api.duosecurity.com", "duo_push"));
+        verifier.initialize();
+        verifier.verify(dc.getAuthToken().getJWTClaimsSet(), new ProfileRequestJWTSecurityContext(prc));
+
+    }
+
+}
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoOIDCAuthnControllerTest.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoOIDCAuthnControllerTest.java
index be4f144..5dd0741 100644
--- a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoOIDCAuthnControllerTest.java
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoOIDCAuthnControllerTest.java
@@ -129,7 +129,7 @@ public class DuoOIDCAuthnControllerTest extends AbstractTestNGSpringContextTests
     }
 
     /**
-     * Start a Duo 2FA request. Make sure the webflow execution key is in the HttpSession.
+     * Start a Duo 2FA request. 
      * 
      * @throws Exception on exception.
      */
@@ -139,6 +139,7 @@ public class DuoOIDCAuthnControllerTest extends AbstractTestNGSpringContextTests
         MvcResult result = mockMvc.perform(get("/Authn/Duo/2FA/authorize").param("conversation", "e1s1")).andDo(print())
                 .andExpect(status().is3xxRedirection()).andReturn();
         assertNotNull(result.getResponse().getHeader("Location"));
+        //basic check of the URL - contains the API host
         assertTrue(result.getResponse().getHeader("Location").contains(API_HOST));
 
     }
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/DynamicDuoContextJWTClaimsVerifierTest.java
similarity index 51%
copy from idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateTokenClaimsTest.java
copy to idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DynamicDuoContextJWTClaimsVerifierTest.java
index bdd45cf..d68e912 100644
--- 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/DynamicDuoContextJWTClaimsVerifierTest.java
@@ -15,175 +15,168 @@
  * 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.text.ParseException;
 import java.time.Duration;
 import java.time.Instant;
 import java.time.temporal.ChronoUnit;
 
-import org.springframework.webflow.execution.Event;
+import javax.annotation.Nonnull;
+
 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 com.nimbusds.jwt.JWT;
+import com.nimbusds.jwt.proc.BadJWTException;
 
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+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 ValidateTokenClaims} class.
- */
-public class ValidateTokenClaimsTest extends AbstractDuoActionTest {
-
-    /** The action to test. */
-    private ValidateTokenClaims action;
+import net.shibboleth.utilities.java.support.component.UnmodifiableComponentException;
 
+public class DynamicDuoContextJWTClaimsVerifierTest extends AbstractDuoActionTest{
+    
+    @Nonnull private DynamicDuoContextJWTClaimsVerifier verifier;
+    
     @BeforeMethod
     public void setUp() throws Exception {
         super.setup();
-        action = new ValidateTokenClaims();
-
+        verifier = new DynamicDuoContextJWTClaimsVerifier();
+        
     }
     
     /**
-     * Test oversized clock skew. To large for the internal int used by Nimbus, fail fast.
+     * Test component can not be altered after initialisation
      * 
      * @throws ComponentInitializationException on error.
      */
-    @Test(expectedExceptions = ConstraintViolationException.class)
-    public final void testOversizedClockSkew() throws ComponentInitializationException {
-        action.setClockSkew(Duration.ofDays(1000000000));
-
+    @Test(expectedExceptions = UnmodifiableComponentException.class)
+    public final void testSetupAfterInit() throws ComponentInitializationException {
+        verifier.initialize();
+        verifier.setIssuerPath("/test");
     }
+
     
     /**
      * Test blank duo integration.
      * 
-     * @throws ComponentInitializationException on error.
+     * @throws ParseException on error.
+     * @throws BadJWTException on error. Expected.
      */
-    @Test
-    public final void testBlankDuoIntegration() throws ComponentInitializationException {
+    @Test(expectedExceptions = BadJWTException.class)
+    public final void testBlankDuoIntegration() throws BadJWTException, ParseException {
         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);
+        
+        verifier.verify(dc.getAuthToken().getJWTClaimsSet(), new ProfileRequestJWTSecurityContext(prc));
 
     }
     
-   
     
     /**
-     * Validate a valid token.
+     * Blank the Authentication Context, this is fatal and should result in a bad JWT exception.
      * 
-     * @throws ComponentInitializationException on error.
+     * @throws ParseException on error.
+     * @throws BadJWTException 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);
-
+    @Test(expectedExceptions = BadJWTException.class)
+    public final void testBlankAuthenticationContext() throws BadJWTException, ParseException {
+       
+        final JWT duoToken = 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");
+        
+        //blank Authentication Context, should throw.
+        prc.removeSubcontext(AuthenticationContext.class);
+     
+        verifier.verify(duoToken.getJWTClaimsSet(), new ProfileRequestJWTSecurityContext(prc));
     }
-
+    
     /**
-     * Validate an expired token.
+     * Blank the Duo Context, this is fatal and should result in a bad JWT exception.
      * 
-     * @throws ComponentInitializationException on error.
+     * @throws ParseException on error.
+     * @throws BadJWTException on error.
      */
-    @Test
-    public final void testExpiredToken() throws ComponentInitializationException {
+    @Test(expectedExceptions = BadJWTException.class)
+    public final void testBlankDuoContext() throws BadJWTException, ParseException {
+       
+        final JWT duoToken = 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");
+     
+        verifier.verify(duoToken.getJWTClaimsSet(), new ProfileRequestJWTSecurityContext(prc));
+    }
+    
+    /**
+     * Test if no security context is input - and is required.
+     * 
+     * @throws ParseException on error.
+     * @throws BadJWTException on error.
+     */
+    @Test(expectedExceptions = BadJWTException.class)
+    public final void testBlankSecurityContext() throws BadJWTException, ParseException {
         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);
-
+                Instant.now(),Instant.now(), Instant.now(), 
+                "incorrect-issuer", "duo_push"));
+     
+        verifier.verify(dc.getAuthToken().getJWTClaimsSet(), null);
     }
     
     /**
-     * Validate an expired token but with a large clock skew.
+     * Validate a token which has the incorrect issuer.
      * 
-     * @throws ComponentInitializationException on error.
+     * @throws ParseException on error.
+     * @throws BadJWTException on error. Expected.
      */
-    @Test
-    public final void testExpiredTokenLargeClockSkew() throws ComponentInitializationException {
+    @Test(expectedExceptions = BadJWTException.class)
+    public final void testInvalidIssuer() throws BadJWTException, ParseException {
         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);
+                Instant.now(),Instant.now(), Instant.now(), 
+                "incorrect-issuer", "duo_push"));
+        
+        verifier.verify(dc.getAuthToken().getJWTClaimsSet(), new ProfileRequestJWTSecurityContext(prc));
 
     }
     
-    
     /**
-     * Validate a token which has the incorrect issuer.
+     * If the username field does not exist in the claimsset, expect and error.
      * 
-     * @throws ComponentInitializationException on error.
+     * @throws ParseException on error.
+     * @throws BadJWTException on error. Expected.
      */
-    @Test
-    public final void testInvalidIssuer() throws ComponentInitializationException {
+    @Test(expectedExceptions = BadJWTException.class)
+    public final void testClaimDoesNotExist() throws BadJWTException, ParseException {
         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);
-
+                "api.duosecurity.com", "duo_push"));
+        
+        //set the preferred name to a claim that does not exist
+        verifier.setUsernameClaim("not_a_claim");
+        verifier.verify(dc.getAuthToken().getJWTClaimsSet(), new ProfileRequestJWTSecurityContext(prc));
     }
     
     /**
      * Validate a token which has the incorrect audience.
      * 
-     * @throws ComponentInitializationException on error.
+     * @throws ParseException on error.
+     * @throws BadJWTException on error. Expected.
      */
-    @Test
-    public final void testInvalidAudience() throws ComponentInitializationException {
+    @Test(expectedExceptions = BadJWTException.class)
+    public final void testInvalidAudience() throws BadJWTException, ParseException {
         addDuoContext();
         addDuoIntegrationToContext();
         addAttemptedFlow("authn/DuoOIDC");
@@ -191,18 +184,20 @@ public class ValidateTokenClaimsTest extends AbstractDuoActionTest {
                 "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);
+        
+        verifier.verify(dc.getAuthToken().getJWTClaimsSet(), new ProfileRequestJWTSecurityContext(prc));
     }
     
+    
+    
     /**
      * Validate a token which has the incorrect subject.
      * 
-     * @throws ComponentInitializationException on error.
+     * @throws ParseException on error.
+     * @throws BadJWTException on error. Expected.
      */
-    @Test
-    public final void testInvalidSubject() throws ComponentInitializationException {
+    @Test(expectedExceptions = BadJWTException.class)
+    public final void testInvalidSubject() throws BadJWTException, ParseException {
         addDuoContext();
         addDuoIntegrationToContext();
         addAttemptedFlow("authn/DuoOIDC");
@@ -213,19 +208,19 @@ public class ValidateTokenClaimsTest extends AbstractDuoActionTest {
         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);
+        
+        verifier.verify(dc.getAuthToken().getJWTClaimsSet(), new ProfileRequestJWTSecurityContext(prc));
 
     }
     
     /**
      * Validate a token which was issued too far in the past.
      * 
-     * @throws ComponentInitializationException on error.
+     * @throws ParseException on error.
+     * @throws BadJWTException on error. Expected.
      */
-    @Test
-    public final void testInvalidIssuedAtPast() throws ComponentInitializationException {
+    @Test(expectedExceptions = BadJWTException.class)
+    public final void testInvalidIssuedAtPast() throws BadJWTException, ParseException {
         addDuoContext();
         addDuoIntegrationToContext();
         addAttemptedFlow("authn/DuoOIDC");
@@ -233,19 +228,19 @@ public class ValidateTokenClaimsTest extends AbstractDuoActionTest {
         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);
+        
+        verifier.verify(dc.getAuthToken().getJWTClaimsSet(), new ProfileRequestJWTSecurityContext(prc));
 
     }
     
     /**
      * Validate a token which was issued too far in the future.
      * 
-     * @throws ComponentInitializationException on error.
+     * @throws ParseException on error.
+     * @throws BadJWTException on error. Expected.
      */
-    @Test
-    public final void testInvalidIssuedAtFuture() throws ComponentInitializationException {
+    @Test(expectedExceptions = BadJWTException.class)
+    public final void testInvalidIssuedAtFuture() throws BadJWTException, ParseException {
         addDuoContext();
         addDuoIntegrationToContext();
         addAttemptedFlow("authn/DuoOIDC");
@@ -253,9 +248,8 @@ public class ValidateTokenClaimsTest extends AbstractDuoActionTest {
         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);
+        
+        verifier.verify(dc.getAuthToken().getJWTClaimsSet(), new ProfileRequestJWTSecurityContext(prc));
 
     }
     
@@ -263,10 +257,11 @@ public class ValidateTokenClaimsTest extends AbstractDuoActionTest {
      * Validate a token which was outside (in the future)
      * the authentication lifetime window when using forced authn.
      * 
-     * @throws ComponentInitializationException on error.
+     * @throws ParseException on error.
+     * @throws BadJWTException on error. Expected.
      */
-    @Test
-    public final void testInvalidAuthenticationTimeFuture() throws ComponentInitializationException {
+    @Test(expectedExceptions = BadJWTException.class)
+    public final void testInvalidAuthenticationTimeFuture() throws BadJWTException, ParseException {
         addDuoContext();
         addDuoIntegrationToContext();
         addAttemptedFlow("authn/DuoOIDC");
@@ -274,19 +269,19 @@ public class ValidateTokenClaimsTest extends AbstractDuoActionTest {
         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);
+        
+        verifier.verify(dc.getAuthToken().getJWTClaimsSet(), new ProfileRequestJWTSecurityContext(prc));
 
     }
     
     /**
      * Validate a token which was outside (expired) the authentication lifetime window when using forced authn.
      * 
-     * @throws ComponentInitializationException on error.
+     * @throws ParseException on error.
+     * @throws BadJWTException on error. Expected.
      */
-    @Test
-    public final void testInvalidAuthenticationTimePast() throws ComponentInitializationException {
+    @Test(expectedExceptions = BadJWTException.class)
+    public final void testInvalidAuthenticationTimePast() throws BadJWTException, ParseException {
         addDuoContext();
         addDuoIntegrationToContext();
         addAttemptedFlow("authn/DuoOIDC");
@@ -294,42 +289,41 @@ public class ValidateTokenClaimsTest extends AbstractDuoActionTest {
         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);
+        
+        verifier.verify(dc.getAuthToken().getJWTClaimsSet(), new ProfileRequestJWTSecurityContext(prc));
 
     }
     
     /**
-     * Validate a token which is inside the authentication lifetime window when using forced authn.
+     * Validate a token which is inside the authentication lifetime window when using forced authn
+     * even though it was performed 2 minutes in the past.
      * 
-     * @throws ComponentInitializationException on error.
+     * @throws ParseException on error.
+     * @throws BadJWTException on error.
      */
     @Test
-    public final void testInvalidAuthenticationWithinWindow() throws ComponentInitializationException {
+    public final void testValidAuthenticationWithinWindow() throws BadJWTException, ParseException {
         addDuoContext();
         addDuoIntegrationToContext();
         addAttemptedFlow("authn/DuoOIDC");
         ac.setForceAuthn(true);
         //set a lifetime
-        action.setAuthnLifetime(Duration.ofMinutes(5));
+        verifier.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);
-
+        
+        verifier.verify(dc.getAuthToken().getJWTClaimsSet(), new ProfileRequestJWTSecurityContext(prc));
     }
     
     /**
      * Validate a token which is outside the authentication lifetime window but authn is not forced.
      * 
-     * @throws ComponentInitializationException on error.
+     * @throws ParseException on error.
+     * @throws BadJWTException on error.
      */
     @Test
-    public final void testInvalidAuthenticationOutsideWindowNotForced() throws ComponentInitializationException {
+    public final void testInvalidAuthenticationOutsideWindowNotForced() throws BadJWTException, ParseException {
         addDuoContext();
         addDuoIntegrationToContext();
         addAttemptedFlow("authn/DuoOIDC");
@@ -337,14 +331,12 @@ public class ValidateTokenClaimsTest extends AbstractDuoActionTest {
         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);
-
+        
+        verifier.verify(dc.getAuthToken().getJWTClaimsSet(), new ProfileRequestJWTSecurityContext(prc));
     }
     
-   
-   
+    
+    
+    
 
 }
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
index bdd45cf..b4685b9 100644
--- 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
@@ -36,19 +36,20 @@ 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.mockito.Mockito;
 import org.springframework.webflow.execution.Event;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.proc.BadJWTException;
+import com.nimbusds.jwt.proc.JWTClaimsSetVerifier;
+
 import net.shibboleth.idp.authn.AuthnEventIds;
 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 ValidateTokenClaims} class.
@@ -62,47 +63,23 @@ public class ValidateTokenClaimsTest extends AbstractDuoActionTest {
     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.
+     * Validate a valid token. The mocked verifier always returns without throwing a validation exception
+     * irrespective of the input.
      * 
      * @throws ComponentInitializationException on error.
      */
     @Test
     public final void testValidToken() throws ComponentInitializationException {
+        
+        //default mock will just return
+        final JWTClaimsSetVerifier<ProfileRequestJWTSecurityContext> verifier = 
+                Mockito.mock(JWTClaimsSetVerifier.class);       
+        action.setClaimsVerifier(verifier);
+        
         addDuoContext();
         addDuoIntegrationToContext();
         addAttemptedFlow("authn/DuoOIDC");
@@ -113,237 +90,69 @@ public class ValidateTokenClaimsTest extends AbstractDuoActionTest {
         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.
+     * Validate an invalid token. The mocked verifier always throw a {@link BadJWTException}
+     * irrespective of the input.
      * 
      * @throws ComponentInitializationException on error.
+     * @throws BadJWTException on error.
      */
     @Test
-    public final void testInvalidSubject() throws ComponentInitializationException {
-        addDuoContext();
-        addDuoIntegrationToContext();
-        addAttemptedFlow("authn/DuoOIDC");
+    public final void testInValidToken() throws ComponentInitializationException, BadJWTException {
         
-        //override subject
-        dc.setUsername("wrong-subject");
+        //default mock will always throw a validation exception
+        final JWTClaimsSetVerifier<ProfileRequestJWTSecurityContext> verifier = 
+                Mockito.mock(JWTClaimsSetVerifier.class); 
+        Mockito.doThrow(new BadJWTException("Invalid JWT")).when(verifier).
+                verify(Mockito.any(JWTClaimsSet.class), Mockito.any());
+        action.setClaimsVerifier(verifier);
         
-        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(), 
+                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 future.
+     * Test when no JWT is present in the context.
      * 
      * @throws ComponentInitializationException on error.
      */
     @Test
-    public final void testInvalidIssuedAtFuture() throws ComponentInitializationException {
-        addDuoContext();
-        addDuoIntegrationToContext();
-        addAttemptedFlow("authn/DuoOIDC");
+    public final void testBlankJWT() throws ComponentInitializationException {
+        
+        //default mock will just return
+        final JWTClaimsSetVerifier<ProfileRequestJWTSecurityContext> verifier = 
+                Mockito.mock(JWTClaimsSetVerifier.class);       
+        action.setClaimsVerifier(verifier);
         
-        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);
-
+        assertEventId(event, AuthnEventIds.INVALID_AUTHN_CTX);
     }
     
     /**
-     * Validate a token which is outside the authentication lifetime window but authn is not forced.
+     * Test when no claims verifier has been set.
      * 
      * @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"));
+    @Test(expectedExceptions = ComponentInitializationException.class)
+    public final void testNoClaimsVerifierSet() throws ComponentInitializationException {        
         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/mock/MockDuoOIDCClient_FAIL.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClient_FAIL.java
index d665f85..701d1e4 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
@@ -36,6 +36,7 @@ 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.DuoOIDCClientCapabilities;
 import net.shibboleth.idp.plugin.authn.duo.DuoOIDCIntegration;
 import net.shibboleth.idp.plugin.authn.duo.model.DuoHealthCheck;
 import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
@@ -66,7 +67,7 @@ public class MockDuoOIDCClient_FAIL implements DuoOIDCClient{
     }
 
     @Override
-    public String createAuthUrl(final String username, final String state) throws DuoClientException {
+    public String createAuthUrl(final String username, final String state, final String nonce) throws DuoClientException {
         return format("https://%s%s?scope=openid&response_type=code&redirect_uri=%s&client_id=%s&request=%s",
                 integration.getAPIHost(),"/oauth/v1/authorize", integration.getRedirectURI(),
                 integration.getClientId(), "JWT");
@@ -144,5 +145,21 @@ public class MockDuoOIDCClient_FAIL implements DuoOIDCClient{
     public String getClientId() {
         return "MockDuoClient_FAIL";
     }
+    
+    @Override
+    public boolean isSupportsNonce() {        
+        return false;
+    }
+
+    @Override
+    public DuoOIDCClientCapabilities getCapabilities() {
+        return new DuoOIDCClientCapabilities() {
+            
+            @Override
+            public boolean isSupportsNonce() {
+                return false;
+            }
+        };
+    }
 
 }
\ 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 bb7018c..f479252 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
@@ -36,6 +36,7 @@ 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.DuoOIDCClientCapabilities;
 import net.shibboleth.idp.plugin.authn.duo.DuoOIDCIntegration;
 import net.shibboleth.idp.plugin.authn.duo.model.DuoHealthCheck;
 import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
@@ -63,7 +64,7 @@ public class MockDuoOIDCClient_OK implements DuoOIDCClient{
     }
 
     @Override
-    public String createAuthUrl(final String username, final String state) throws DuoClientException {
+    public String createAuthUrl(final String username, final String state,final String nonce) throws DuoClientException {
         return format("https://%s%s?scope=openid&response_type=code&redirect_uri=%s&client_id=%s&request=%s",
                 integration.getAPIHost(),"/oauth/v1/authorize", integration.getRedirectURI(), 
                 integration.getClientId(), "JWT");
@@ -141,5 +142,21 @@ public class MockDuoOIDCClient_OK implements DuoOIDCClient{
     public String getClientId() {
         return "MockDuoClient_OK";
     }
+    
+    @Override
+    public boolean isSupportsNonce() {        
+        return false;
+    }
+
+    @Override
+    public DuoOIDCClientCapabilities getCapabilities() {
+        return new DuoOIDCClientCapabilities() {
+            
+            @Override
+            public boolean isSupportsNonce() {
+                return false;
+            }
+        };
+    }
 
 }
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 480e4e3..6e47901 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
@@ -36,6 +36,7 @@ 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.DuoOIDCClientCapabilities;
 import net.shibboleth.idp.plugin.authn.duo.DuoOIDCIntegration;
 import net.shibboleth.idp.plugin.authn.duo.model.DuoHealthCheck;
 import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
@@ -65,7 +66,7 @@ public class MockDuoOIDCClient_OK_OLD_AUTH_TIME implements DuoOIDCClient{
     }
 
     @Override
-    public String createAuthUrl(final String username, final String state) throws DuoClientException {
+    public String createAuthUrl(final String username, final String state,final String nonce) throws DuoClientException {
         return format("https://%s%s?scope=openid&response_type=code&redirect_uri=%s&client_id=%s&request=%s",
                 integration.getAPIHost(),"/oauth/v1/authorize", integration.getRedirectURI(), 
                 integration.getClientId(), "JWT");
@@ -143,5 +144,21 @@ public class MockDuoOIDCClient_OK_OLD_AUTH_TIME implements DuoOIDCClient{
     public String getClientId() {
         return "MockDuoClient_OLD_AUTH_TIME";
     }
+    
+    @Override
+    public boolean isSupportsNonce() {        
+        return false;
+    }
+
+    @Override
+    public DuoOIDCClientCapabilities getCapabilities() {
+        return new DuoOIDCClientCapabilities() {
+            
+            @Override
+            public boolean isSupportsNonce() {
+                return false;
+            }
+        };
+    }
 
 }
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 a934585..07215dc 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
@@ -36,6 +36,7 @@ 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.DuoOIDCClientCapabilities;
 import net.shibboleth.idp.plugin.authn.duo.DuoOIDCIntegration;
 import net.shibboleth.idp.plugin.authn.duo.model.DuoHealthCheck;
 import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
@@ -63,7 +64,7 @@ public class MockDuoOIDCClient_UNKNOWN implements DuoOIDCClient{
     }
 
     @Override
-    public String createAuthUrl(final String username, final String state) throws DuoClientException {
+    public String createAuthUrl(final String username, final String state,final String nonce) throws DuoClientException {
         return format("https://%s%s?scope=openid&response_type=code&redirect_uri=%s&client_id=%s&request=%s",
                 integration.getAPIHost(),"/oauth/v1/authorize", integration.getRedirectURI(),
                 integration.getClientId(), "JWT");
@@ -140,4 +141,20 @@ public class MockDuoOIDCClient_UNKNOWN implements DuoOIDCClient{
         return "MockDuoClient_UNKNOWN";
     }
 
+    @Override
+    public boolean isSupportsNonce() {        
+        return false;
+    }
+
+    @Override
+    public DuoOIDCClientCapabilities getCapabilities() {
+        return new DuoOIDCClientCapabilities() {
+            
+            @Override
+            public boolean isSupportsNonce() {
+                return false;
+            }
+        };
+    }
+
 }
\ No newline at end of file
diff --git a/idp-duo-native-client-impl/pom.xml b/idp-duo-native-client-impl/pom.xml
index 4e7bbbd..74dccda 100644
--- a/idp-duo-native-client-impl/pom.xml
+++ b/idp-duo-native-client-impl/pom.xml
@@ -34,11 +34,16 @@
             <artifactId>jackson-databind</artifactId>
             <scope>compile</scope>
         </dependency>
-         <dependency>
+        <dependency>
             <groupId>com.fasterxml.jackson.core</groupId>
             <artifactId>jackson-core</artifactId>
             <scope>compile</scope>
         </dependency>
+        <dependency>
+            <groupId>com.nimbusds</groupId>
+            <artifactId>nimbus-jose-jwt</artifactId>
+            <scope>compile</scope>
+        </dependency>
         <!-- indirectly referenced (transitive) dependencies -->
         <dependency>
             <groupId>com.squareup.retrofit2</groupId>
@@ -57,7 +62,7 @@
             <artifactId>lombok</artifactId>
             <version>1.18.10</version>
             <scope>runtime</scope>
-        </dependency>       
+        </dependency>
         <dependency>
             <groupId>com.auth0</groupId>
             <artifactId>java-jwt</artifactId>
@@ -81,7 +86,7 @@
             <artifactId>okio</artifactId>
             <version>1.15.0</version>
             <scope>runtime</scope>
-        </dependency>        
+        </dependency>
         <dependency>
             <groupId>com.fasterxml.jackson.core</groupId>
             <artifactId>jackson-annotations</artifactId>
@@ -114,7 +119,7 @@
             <groupId>${idp.groupId}</groupId>
             <artifactId>idp-admin-impl</artifactId>
             <scope>provided</scope>
-        </dependency>        
+        </dependency>
     </dependencies>
 
     <build>
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 8056847..fcef7fd 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
@@ -20,7 +20,6 @@ package net.shibboleth.idp.plugin.authn.duo.sdk.impl;
 
 import java.text.ParseException;
 import java.util.List;
-import java.util.UUID;
 import java.util.function.BiFunction;
 import java.util.function.Function;
 
@@ -45,11 +44,13 @@ import com.nimbusds.jwt.JWT;
 import com.nimbusds.jwt.JWTClaimsSet;
 import com.nimbusds.jwt.SignedJWT;
 
+import net.shibboleth.idp.plugin.authn.duo.AbstractDuoOIDCClient;
 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.DuoHealthCheck;
 import net.shibboleth.idp.plugin.authn.duo.model.DuoHealthCheckResponse;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
 import net.shibboleth.utilities.java.support.logic.Constraint;
 
 /**
@@ -58,7 +59,7 @@ import net.shibboleth.utilities.java.support.logic.Constraint;
  * 
  * <p>This is package private, and can/should only be instantiated by the {@link DuoSDKClientFactory}.</p>
  */
-final class DuoSDKClientAdaptor implements DuoOIDCClient{
+final class DuoSDKClientAdaptor extends AbstractDuoOIDCClient{
     
     /** Class logger. */
     @Nonnull private Logger log = LoggerFactory.getLogger(DuoSDKClientAdaptor.class);
@@ -78,10 +79,6 @@ final class DuoSDKClientAdaptor implements DuoOIDCClient{
     /** Save off the integration to help generate the JWT.*/
     @Nonnull private final DuoOIDCIntegration duoIntegration;
     
-    /** The client instance UUID for identification.*/
-    @Nonnull private final String clientId;
-
-    
     /**
      * 
      * Constructor. Initialises the native Duo SDK client.
@@ -93,12 +90,11 @@ final class DuoSDKClientAdaptor implements DuoOIDCClient{
      */
     protected DuoSDKClientAdaptor(@Nonnull final DuoOIDCIntegration integration, 
             @Nullable final List<String> caCerts) throws DuoClientException {
-        
+        super();
         duoIntegration = Constraint.isNotNull(integration,"Duo SDK Client requires a non-null Duo Integration");
         healthCheckResponseConverter = new DefaultHealthCheckResponseConverter();
         tokenResponseConverter = new DefaultTokenResponseConverter();
-        clientId = UUID.randomUUID().toString();
-        log.info("Secret has size '{}'",integration.getSecretKey().getBytes().length);
+
         try {
             if (caCerts == null) {
                 client = new Client(integration.getClientId(), integration.getSecretKey(),
@@ -147,8 +143,12 @@ final class DuoSDKClientAdaptor implements DuoOIDCClient{
 
     /** {@inheritDoc} */
     @Override
-    @Nonnull public String createAuthUrl(@Nonnull final String username, @Nonnull final String state) 
+    @Nonnull public String createAuthUrl(@Nonnull @NotEmpty final String username, 
+            @Nonnull @NotEmpty final String state, @Nullable final String nonce) 
             throws DuoClientException {
+        Constraint.isNotEmpty(username, "Username can not be null or empty");
+        Constraint.isNotEmpty(state, "State can not be null or empty");
+        //does not support the nonce.
         try {
             return client.createAuthUrl(username, state);
         } catch (final DuoException e) {
@@ -162,6 +162,7 @@ final class DuoSDKClientAdaptor implements DuoOIDCClient{
     @Nonnull public JWT exchangeAuthorizationCodeFor2FAResult(@Nonnull final String code, 
             @Nonnull final String username) 
             throws DuoClientException {
+        Constraint.isNotEmpty(code, "Auth_code can not be null");
         try {
             final Token token = client.exchangeAuthorizationCodeFor2FAResult(code,username);
             if (token == null) {
@@ -233,9 +234,10 @@ final class DuoSDKClientAdaptor implements DuoOIDCClient{
         
     }
 
+
     @Override
-    public String getClientId() {
-        return clientId;
+    public boolean isSupportsNonce() {
+        return false;
     }
 
 
diff --git a/idp-duo-native-client-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/sdk/impl/DuoSDKClientFactory.java b/idp-duo-native-client-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/sdk/impl/DuoSDKClientFactory.java
index 2304121..94a1f7c 100644
--- a/idp-duo-native-client-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/sdk/impl/DuoSDKClientFactory.java
+++ b/idp-duo-native-client-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/sdk/impl/DuoSDKClientFactory.java
@@ -15,8 +15,6 @@
  * limitations under the License.
  */
 
-
-
 package net.shibboleth.idp.plugin.authn.duo.sdk.impl;
 
 import java.util.List;
diff --git a/idp-duo-native-client-impl/src/main/resources/net/shibboleth/idp/plugin/authn/duo/sdk/conf/authn/duo-oidc.properties b/idp-duo-native-client-impl/src/main/resources/net/shibboleth/idp/plugin/authn/duo/sdk/conf/authn/duo-oidc.properties
index 2f3f053..d1a87b9 100644
--- a/idp-duo-native-client-impl/src/main/resources/net/shibboleth/idp/plugin/authn/duo/sdk/conf/authn/duo-oidc.properties
+++ b/idp-duo-native-client-impl/src/main/resources/net/shibboleth/idp/plugin/authn/duo/sdk/conf/authn/duo-oidc.properties
@@ -28,4 +28,12 @@ idp.duo.oidc.clientFactoryBean = shibboleth.authn.DuoOIDC.native.sdk.clientFacto
 # If you want to enable certificate revocation checking with the Nimbus "client",
 # you need to enable this property in addition to configuring a suitable means to 
 # locate a revocation response, see the plugin documentation.
-#idp.duo.oidc.nimbus.checkRevocation=false
+#idp.duo.oidc.nimbus.checkRevocation = false
+
+# Shouldn't need changing, controls the OIDC id_token claims verification
+#idp.duo.oidc.jwt.verifier.clockSkew = PT60S
+#idp.duo.oidc.jwt.verifier.iatWindow = PT60S
+#idp.duo.oidc.jwt.verifier.issuerPath = /oauth/v1/token 
+#idp.duo.oidc.jwt.verifier.preferredUsername = preferred_username
+# Applies only to forced authentication
+#idp.duo.oidc.jwt.verifier.authLifetime = PT60S
\ No newline at end of file
diff --git a/idp-duo-nimbus-client-impl/pom.xml b/idp-duo-nimbus-client-impl/pom.xml
index 7650cfb..f4d992f 100644
--- a/idp-duo-nimbus-client-impl/pom.xml
+++ b/idp-duo-nimbus-client-impl/pom.xml
@@ -26,7 +26,6 @@
         <dependency>
             <groupId>com.nimbusds</groupId>
             <artifactId>nimbus-jose-jwt</artifactId>
-            <version>9.0</version>
             <scope>compile</scope>
         </dependency>
 
diff --git a/idp-duo-nimbus-client-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/nimbus/impl/NimbusClient.java b/idp-duo-nimbus-client-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/nimbus/impl/NimbusClient.java
index c3124b1..81c5ebc 100644
--- a/idp-duo-nimbus-client-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/nimbus/impl/NimbusClient.java
+++ b/idp-duo-nimbus-client-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/nimbus/impl/NimbusClient.java
@@ -21,7 +21,6 @@ import java.io.IOException;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.text.ParseException;
-import java.util.UUID;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -44,8 +43,8 @@ import com.nimbusds.jose.util.IOUtils;
 import com.nimbusds.jwt.JWT;
 import com.nimbusds.jwt.SignedJWT;
 
+import net.shibboleth.idp.plugin.authn.duo.AbstractDuoOIDCClient;
 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.DuoHealthCheck;
 import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
@@ -54,7 +53,7 @@ import net.shibboleth.utilities.java.support.logic.Constraint;
 /**
  * A Duo client using the Nimbus OIDC library. Can/should only be instantiated by the corresponding factory method.
  */
-final class NimbusClient implements DuoOIDCClient{
+final class NimbusClient extends AbstractDuoOIDCClient{
 
     /** The only supported client assertion type.*/
     @Nonnull @NotEmpty private static final String CLIENT_ASSERTION_TYPE = 
@@ -77,9 +76,7 @@ final class NimbusClient implements DuoOIDCClient{
     
     /** JSON object mapper. */
     @Nonnull private ObjectMapper objectMapper;
-    
-    /** The client instance UUID.*/
-    @Nonnull @NotEmpty private final String clientId;
+
 
     /**
      * Constructor.
@@ -91,11 +88,11 @@ final class NimbusClient implements DuoOIDCClient{
      */
     protected NimbusClient(@Nonnull final DuoOIDCIntegration integration, @Nonnull final HttpClient client,
             @Nullable final HttpClientSecurityParameters params, @Nonnull final ObjectMapper oMapper) {
+        super();
         duoIntegration = Constraint.isNotNull(integration,"Nimbus Client requires a non-null Duo Integration");
         httpClient =  Constraint.isNotNull(client,"Nimbus Client requires a non-null http client");
         httpClientSecurityParameters = params;
         objectMapper = oMapper;
-        clientId = UUID.randomUUID().toString();
         //We do not validate the integration parameters e.g. because we know Duo use an invalid secret key
     }
 
@@ -123,9 +120,10 @@ final class NimbusClient implements DuoOIDCClient{
     /** {@inheritDoc} */
     @Override
     @Nonnull public String createAuthUrl(@Nonnull @NotEmpty final String username, 
-            @Nonnull @NotEmpty final String state) throws DuoClientException {
+            @Nonnull @NotEmpty final String state, @Nonnull final String nonce) throws DuoClientException {
         Constraint.isNotEmpty(username, "Username can not be null or empty");
         Constraint.isNotEmpty(state, "State can not be null or empty");
+        Constraint.isNotEmpty(nonce, "Nonce can not be null or empty for this client");
         Constraint.isGreaterThan(21, state.length(), "State must be at least 22 characters");
         Constraint.isLessThan(1025, state.length(),"State must be at maximum 1024 characters");
         
@@ -137,6 +135,7 @@ final class NimbusClient implements DuoOIDCClient{
                     .setHost(duoIntegration.getAPIHost())
                     .setPath(duoIntegration.getAuthorizeEndpoint())
                     .setParameter("scope", "openid")
+                    .setParameter("nonce", nonce)
                     .setParameter("response_type", "code")
                     .setParameter("redirect_uri", duoIntegration.getRedirectURI())
                     .setParameter("client_id", duoIntegration.getClientId())
@@ -229,8 +228,8 @@ final class NimbusClient implements DuoOIDCClient{
     }
 
     @Override
-    public String getClientId() {
-        return clientId;
+    public boolean isSupportsNonce() {
+        return true;
     }
 
 }
diff --git a/idp-duo-nimbus-client-impl/src/main/resources/net/shibboleth/idp/plugin/authn/duo/nimbus/conf/authn/duo-oidc.properties b/idp-duo-nimbus-client-impl/src/main/resources/net/shibboleth/idp/plugin/authn/duo/nimbus/conf/authn/duo-oidc.properties
index 4b90f32..5218962 100644
--- a/idp-duo-nimbus-client-impl/src/main/resources/net/shibboleth/idp/plugin/authn/duo/nimbus/conf/authn/duo-oidc.properties
+++ b/idp-duo-nimbus-client-impl/src/main/resources/net/shibboleth/idp/plugin/authn/duo/nimbus/conf/authn/duo-oidc.properties
@@ -28,4 +28,12 @@ idp.duo.oidc.clientFactoryBean = shibboleth.authn.DuoOIDC.nimbus.clientFactory
 # If you want to enable certificate revocation checking with the Nimbus "client",
 # you need to enable this property in addition to configuring a suitable means to 
 # locate a revocation response, see the plugin documentation.
-#idp.duo.oidc.nimbus.checkRevocation=false
+#idp.duo.oidc.nimbus.checkRevocation = false
+
+# Shouldn't need changing, controls the OIDC id_token claims verification
+#idp.duo.oidc.jwt.verifier.clockSkew = PT60S
+#idp.duo.oidc.jwt.verifier.iatWindow = PT60S
+#idp.duo.oidc.jwt.verifier.issuerPath = /oauth/v1/token 
+#idp.duo.oidc.jwt.verifier.preferredUsername = preferred_username
+# Applies only to forced authentication
+#idp.duo.oidc.jwt.verifier.authLifetime = PT60S
\ No newline at end of file
diff --git a/idp-duo-nimbus-client-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/nimbus/impl/NimbusClientTest.java b/idp-duo-nimbus-client-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/nimbus/impl/NimbusClientTest.java
index f5a8392..5b5d448 100644
--- a/idp-duo-nimbus-client-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/nimbus/impl/NimbusClientTest.java
+++ b/idp-duo-nimbus-client-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/nimbus/impl/NimbusClientTest.java
@@ -316,13 +316,14 @@ public class NimbusClientTest {
         final HttpClient httpClient = Mockito.mock(HttpClient.class);
         // create new client with mock response
         client = new NimbusClient(integ, httpClient, null, new ObjectMapper());
-        final String authUrl = client.createAuthUrl("jdoe", "RRFSFERGERGSDDSXXXXQWERG");
+        final String authUrl = client.createAuthUrl("jdoe", "RRFSFERGERGSDDSXXXXQWERG","a_nonce");
         assertNotNull(authUrl);
         assertTrue(authUrl.contains("response_type"));
         assertTrue(authUrl.contains("client_id"));
         assertTrue(authUrl.contains("request"));
         assertTrue(authUrl.contains("redirect_uri"));
         assertTrue(authUrl.contains("scope"));
+        assertTrue(authUrl.contains("nonce"));
       
     }
     
diff --git a/pom.xml b/pom.xml
index 307389d..79d1ab4 100644
--- a/pom.xml
+++ b/pom.xml
@@ -26,6 +26,8 @@
         <opensaml.groupId>org.opensaml</opensaml.groupId>
         <opensaml.version>4.1.0-SNAPSHOT</opensaml.version>
         <java-support.version>8.2.0-SNAPSHOT</java-support.version>
+        <odic.common.crypto.version>0.0.1-SNAPSHOT</odic.common.crypto.version>
+        <nimbus.version>9.0</nimbus.version>
         <checkstyle.configLocation>${project.basedir}/checkstyle.xml</checkstyle.configLocation>
     </properties>
 
@@ -66,7 +68,7 @@
         <!-- Project wide test Dependencies -->
         <dependency>
             <groupId>org.testng</groupId>
-            <artifactId>testng</artifactId>       
+            <artifactId>testng</artifactId>
             <scope>test</scope>
         </dependency>
         <dependency>
@@ -78,15 +80,9 @@
             <artifactId>mockito-core</artifactId>
             <scope>test</scope>
         </dependency>
-        
-        <!-- review this dep when we move to central oidc-commons-lib -->
-        <dependency>
-            <groupId>com.nimbusds</groupId>
-            <artifactId>nimbus-jose-jwt</artifactId>
-            <version>9.0</version>
-        </dependency>
-      
-      
+
+
+
     </dependencies>
 
     <dependencyManagement>
@@ -122,6 +118,23 @@
                 <artifactId>idp-plugin-duo-native-client-impl</artifactId>
                 <version>${project.version}</version>
             </dependency>
+            <!-- commons OIDC -->
+            <dependency>
+                <groupId>net.shibboleth.oidc</groupId>
+                <artifactId>oidc-common-crypto-api</artifactId>
+                <version>${odic.common.crypto.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>net.shibboleth.oidc</groupId>
+                <artifactId>oidc-common-crypto-impl</artifactId>
+                <version>${odic.common.crypto.version}</version>
+            </dependency>
+            <!-- review this dep when we move to central oidc-commons-lib -->
+            <dependency>
+                <groupId>com.nimbusds</groupId>
+                <artifactId>nimbus-jose-jwt</artifactId>
+                <version>${nimbus.version}</version>
+            </dependency>
             <!-- test bom dependencies -->
             <dependency>
                 <groupId>${idp.groupId}</groupId>

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


More information about the commits mailing list