[java-idp-plugin-duo] 09/16: Working version of the native SDK replacement using Nimbus

Phil Smart philip.smart at jisc.ac.uk
Fri Oct 2 10:41:00 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=c2a4c940ae6c4ca3a3a00fa880b7b36be856f9f6

commit c2a4c940ae6c4ca3a3a00fa880b7b36be856f9f6
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Thu Sep 24 16:29:32 2020 +0100

    Working version of the native SDK replacement using Nimbus
    
     - Although auth0 is temporarily used to sign the token as the issue
    with the Duo secret key continues.
---
 .../authn/duo/DefaultDuoOIDCIntegration.java       |  68 ++++++-
 .../idp/plugin/authn/duo/DuoOIDCIntegration.java   |  24 ++-
 .../idp/plugin/authn/duo/model/DuoHealthCheck.java |  43 +++--
 .../authn/duo/model/DuoHealthCheckResponse.java    |  30 +++
 .../flows/authn/DuoOIDC/duo-oidc-authn-beans.xml   |   6 +-
 .../authn/duo/sdk/impl/DuoSDKClientAdaptor.java    |   4 +-
 idp-plugin-duo-nimbus-client/pom.xml               |  37 +++-
 .../idp/plugin/authn/duo/nimbus/NimbusClient.java  | 209 +++++++++++++++++++-
 .../authn/duo/nimbus/NimbusClientFactory.java      |  81 +++++++-
 .../idp/plugin/authn/duo/nimbus/NimbusUtils.java   |  99 ++++++++++
 .../idp/plugin/authn/duo/nimbus/TokenResponse.java | 212 +++++++++++++++++++++
 .../src/main/resources/duo-client-factory-bean.xml |  46 +++++
 .../idp/plugin/authn/duo/nimbus/.gitignore         |   1 +
 .../plugin/authn/duo/nimbus/NimbusClientTest.java  | 125 ++++++++++++
 pom.xml                                            |   2 +-
 15 files changed, 951 insertions(+), 36 deletions(-)

diff --git a/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/DefaultDuoOIDCIntegration.java b/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/DefaultDuoOIDCIntegration.java
index d8b8966..6b22ba5 100644
--- a/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/DefaultDuoOIDCIntegration.java
+++ b/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/DefaultDuoOIDCIntegration.java
@@ -53,6 +53,15 @@ public class DefaultDuoOIDCIntegration extends AbstractInitializableComponent im
     /** The redirect_uri to send the client after authorisation .*/
     @NonnullAfterInit @NotEmpty private String redirectURI; 
     
+    /** The URL path to the health endpoint.*/
+    @NonnullAfterInit @NotEmpty private String healthEndpoint;
+    
+    /** The URL path to the authorization endpoint.*/
+    @NonnullAfterInit @NotEmpty private String authorizeEndpoint;
+    
+    /** The URL path to the token endpoint.*/
+    @NonnullAfterInit @NotEmpty private String tokenEndpoint;
+    
     /** Container for supported principals. */
     @Nonnull private final Subject supportedPrincipals;
     
@@ -66,6 +75,57 @@ public class DefaultDuoOIDCIntegration extends AbstractInitializableComponent im
         return apiHost;
     }
     
+    @Override
+    @Nonnull @NotEmpty public String getHealthCheckEndpoint() {
+        return healthEndpoint;
+    }
+    
+    /**
+     * Set the health check endpoint URL path.
+     * 
+     * @param endpoint the endpoint.
+     */
+    public void setHealthCheckEndpoint(@Nonnull @NotEmpty final String endpoint) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        
+        healthEndpoint = Constraint.isNotNull(StringSupport.trimOrNull(endpoint), 
+                "Health check endpoint cannot be null or empty");
+    }
+
+    @Override
+    @Nonnull @NotEmpty public String getAuthorizeEndpoint() {
+        return authorizeEndpoint;
+    }
+    
+    /**
+     * Set the authorize endpoint URL path.
+     * 
+     * @param endpoint the endpoint.
+     */
+    public void setAuthorizeEndpoint(@Nonnull @NotEmpty final String endpoint) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        
+        authorizeEndpoint = Constraint.isNotNull(StringSupport.trimOrNull(endpoint), 
+                "Authorize endpoint cannot be null or empty");
+    }
+
+    @Override
+    @Nonnull @NotEmpty public String getTokenEndpoint() {
+        return tokenEndpoint;
+    }
+    
+    /**
+     * Set the token endpoint URL path.
+     * 
+     * @param endpoint the endpoint.
+     */
+    public void setTokenEndpoint(@Nonnull @NotEmpty final String endpoint) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        
+        tokenEndpoint = Constraint.isNotNull(StringSupport.trimOrNull(endpoint), 
+                "Token endpoint cannot be null or empty");
+    }
+    
     /**
      * Set the API host to use.
      * 
@@ -156,8 +216,10 @@ public class DefaultDuoOIDCIntegration extends AbstractInitializableComponent im
     @Override
     protected void doInitialize() throws ComponentInitializationException {
         if (apiHost == null || clientId == null || secretKey == null 
-                || redirectURI == null) {
-            throw new ComponentInitializationException("API host, clientId, secret key, and redirect_uri must be set");
+                || redirectURI == null || healthEndpoint == null || authorizeEndpoint == null
+                || tokenEndpoint == null) {
+            throw new ComponentInitializationException("API host, clientId, secret key, redirect_uri,"
+                    + "token endpoint, health check endpoint and authorization endpoint must be set");
         }
     }
 
@@ -196,6 +258,8 @@ public class DefaultDuoOIDCIntegration extends AbstractInitializableComponent im
         builder.append("]");
         return builder.toString();
     }
+
+    
     
     
 
diff --git a/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/DuoOIDCIntegration.java b/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/DuoOIDCIntegration.java
index b9e3c12..9eaecfc 100644
--- a/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/DuoOIDCIntegration.java
+++ b/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/DuoOIDCIntegration.java
@@ -23,7 +23,8 @@ import net.shibboleth.idp.authn.principal.PrincipalSupportingComponent;
 import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
 
 /**
- * Interface to a particular Duo OIDC integration point.
+ * Interface to a particular Duo OIDC integration point. In part replaces any
+ * OIDC metadata, as that is not supported by Duo.
  */
 public interface DuoOIDCIntegration extends PrincipalSupportingComponent {
     
@@ -54,6 +55,27 @@ public interface DuoOIDCIntegration extends PrincipalSupportingComponent {
      * @return the redirect_uri
      */
     @Nonnull @NotEmpty String getRedirectURI();
+    
+    /**
+     * Get the path of the health check endpoint.
+     * 
+     * @return the path of the health check endpoint
+     */
+    @Nonnull @NotEmpty String getHealthCheckEndpoint();
+    
+    /**
+     * Get the path of the authorization endpoint.
+     * 
+     * @return the path of the authorization endpoint
+     */
+    @Nonnull @NotEmpty String getAuthorizeEndpoint();
+    
+    /**
+     * Get the path of the token endpoint.
+     * 
+     * @return the path of the token endpoint;
+     */
+    @Nonnull @NotEmpty String getTokenEndpoint();
 
 
 }
diff --git a/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/model/DuoHealthCheck.java b/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/model/DuoHealthCheck.java
index 988e656..b489552 100644
--- a/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/model/DuoHealthCheck.java
+++ b/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/model/DuoHealthCheck.java
@@ -4,6 +4,11 @@ import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
+
 import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
 
 
@@ -11,18 +16,21 @@ import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
 /**
  * <p>Represents a health check response from Duo's 2FA endpoint.</p>
  * 
- * <p>Includes a staged builder for fluent generation.</p>
+ * <p>Includes a staged builder for fluent generation. Compatible with Jackson
+ * deserialization.</p>
  * 
  * <p>Is immutable, can only be built using the builder.</p>
  */
 @Immutable
+ at JsonDeserialize(builder=DuoHealthCheck.Builder.class)
+ at JsonIgnoreProperties(ignoreUnknown = true)
 public final class DuoHealthCheck {
     
     /** A successful 'OK' or unsuccessful 'FAIL' response.*/
     @Nonnull @NotEmpty private final String status;
 
     /** When a successful response was issued, as seconds since Unix EPOCH.*/
-    @Nullable private final Integer responseTimestamp;
+    @Nullable private final DuoHealthCheckResponse response;
 
     /** The error response code.*/
     @Nullable @NotEmpty private final String code;
@@ -47,12 +55,12 @@ public final class DuoHealthCheck {
     }
 
     /**
-     * Get the timestamp of when the response was issued.
+     * Get the response object.
      * 
-     * @return Returns the responseTimestamp.
+     * @return Returns the response.
      */
-    @Nullable public Integer getResponseTimestamp() {
-        return responseTimestamp;
+    @Nullable public DuoHealthCheckResponse getResponse() {
+        return response;
     }
 
     /**
@@ -95,8 +103,8 @@ public final class DuoHealthCheck {
     @Override
     public String toString() {
         return String.format(
-                "DuoHealthCheck [status=%s, responseTimestamp=%s, code=%s, timestamp=%s, message=%s, messageDetail=%s]",
-                status, responseTimestamp, code, timestamp, message, messageDetail);
+                "DuoHealthCheck [status=%s, response=%s, code=%s, timestamp=%s, message=%s, messageDetail=%s]",
+                status, response, code, timestamp, message, messageDetail);
     }
 
     
@@ -108,7 +116,7 @@ public final class DuoHealthCheck {
      */
     private DuoHealthCheck(Builder builder) {
         this.status = builder.status;
-        this.responseTimestamp = builder.responseTimestamp;
+        this.response = builder.response;
         this.code = builder.code;
         this.timestamp = builder.timestamp;
         this.message = builder.message;
@@ -131,7 +139,7 @@ public final class DuoHealthCheck {
 
     
     public interface IBuildStage {
-        public IBuildStage withResponseTimestamp(Integer responseTimestamp);
+        public IBuildStage withResponse(DuoHealthCheckResponse response);
 
         public IBuildStage withCode(String code);
 
@@ -146,11 +154,12 @@ public final class DuoHealthCheck {
 
     /**
      * Builder to build {@link DuoHealthCheck}.
-     */    
+     */   
+    @JsonPOJOBuilder(buildMethodName = "build",withPrefix = "with")
     public static final class Builder implements IStatusStage, IBuildStage {
         private String status;
 
-        private Integer responseTimestamp;
+        private DuoHealthCheckResponse response;
 
         private String code;
 
@@ -164,36 +173,42 @@ public final class DuoHealthCheck {
         }
 
         @Override
+        @JsonProperty("stat")
         public IBuildStage withStatus(String status) {
             this.status = status;
             return this;
         }
 
         @Override
-        public IBuildStage withResponseTimestamp(Integer responseTimestamp) {
-            this.responseTimestamp = responseTimestamp;
+        @JsonProperty("response")
+        public IBuildStage withResponse(DuoHealthCheckResponse response) {
+            this.response = response;
             return this;
         }
 
         @Override
+        @JsonProperty("code")
         public IBuildStage withCode(String code) {
             this.code = code;
             return this;
         }
 
         @Override
+        @JsonProperty("timestamp")
         public IBuildStage withTimestamp(String timestamp) {
             this.timestamp = timestamp;
             return this;
         }
 
         @Override
+        @JsonProperty("message")
         public IBuildStage withMessage(String message) {
             this.message = message;
             return this;
         }
 
         @Override
+        @JsonProperty("message_detail")
         public IBuildStage withMessageDetail(String messageDetail) {
             this.messageDetail = messageDetail;
             return this;
diff --git a/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/model/DuoHealthCheckResponse.java b/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/model/DuoHealthCheckResponse.java
new file mode 100644
index 0000000..8624b91
--- /dev/null
+++ b/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/model/DuoHealthCheckResponse.java
@@ -0,0 +1,30 @@
+package net.shibboleth.idp.plugin.authn.duo.model;
+
+import javax.annotation.Nonnull;
+import javax.annotation.concurrent.Immutable;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+ at Immutable
+public final class DuoHealthCheckResponse {
+    
+    /** The response timestamp in seconds since Unix EPOCH.*/
+    private final Integer timestamp;
+    
+    public DuoHealthCheckResponse(@Nonnull @JsonProperty("timestamp") final Integer time) {
+        timestamp = time;
+    }
+    /**
+     * Get the response timestamp.
+     * 
+     * @return the response timestamp.
+     */
+    public Integer getTimestamp() {
+        return timestamp;
+    }
+    @Override
+    public String toString() {
+        return "DuoHealthCheckResponse [timestamp=" + timestamp + "]";
+    }
+
+}
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 cc5d62d..ab03186 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
@@ -27,7 +27,11 @@
     <bean id="shibboleth.authn.duo.OIDC.DuoIntegration"
         class="net.shibboleth.idp.plugin.authn.duo.DefaultDuoOIDCIntegration" p:APIHost="%{idp.duo.oidc.apiHost:none}"
         p:clientId="%{idp.duo.oidc.clientId:none}" p:secretKey="%{idp.duo.oidc.secretKey:none}"
-        p:redirectURI="%{idp.duo.oidc.redirectUri:none}" />
+        p:redirectURI="%{idp.duo.oidc.redirectUri:none}" 
+        p:healthCheckEndpoint="%{idp.duo.oidc.endpoint.health:/oauth/v1/health_check}"
+        p:tokenEndpoint="%{idp.duo.oidc.endpoint.token:/oauth/v1/token}"
+        p:authorizeEndpoint="%{idp.duo.oidc.endpoint.authorize:/oauth/v1/authorize}"
+        />
     <bean id="shibboleth.authn.duo.OIDC.DuoIntegrationStrategy" parent="shibboleth.Functions.Constant"
         c:target-ref="shibboleth.authn.duo.OIDC.DuoIntegration" />
 
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 97fd948..7dd4086 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
@@ -34,6 +34,7 @@ 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.NonnullAfterInit;
 import net.shibboleth.utilities.java.support.codec.Base64Support;
 import net.shibboleth.utilities.java.support.codec.EncodingException;
@@ -171,7 +172,8 @@ final class DuoSDKClientAdaptor implements DuoOIDCClient{
             }
             return DuoHealthCheck.builder().withStatus(response.getStat()).withCode(response.getCode())
                     .withMessage(response.getMessage()).withMessageDetail(response.getMessage_detail())
-                    .withResponseTimestamp(responseTimestamp).withTimestamp(response.getTimestamp()).build();
+                    .withResponse(new DuoHealthCheckResponse(responseTimestamp))
+                    .withTimestamp(response.getTimestamp()).build();
         }
         
     }
diff --git a/idp-plugin-duo-nimbus-client/pom.xml b/idp-plugin-duo-nimbus-client/pom.xml
index add6f0f..9f5fc68 100644
--- a/idp-plugin-duo-nimbus-client/pom.xml
+++ b/idp-plugin-duo-nimbus-client/pom.xml
@@ -8,7 +8,7 @@
         <version>0.0.1-SNAPSHOT</version>
     </parent>
 
-    <artifactId>idp-plugin-duo-nimbus-client</artifactId>
+    <artifactId>idp-plugin-duo-nimbus-client-impl</artifactId>
     <name>Shibboleth IdP :: Plugins :: Duo 2FA Nimbus client implementation</name>
     <description>IdP Duo OIDC 2FA Nimbus client implementation.</description>
     <packaging>jar</packaging>
@@ -36,10 +36,41 @@
             <groupId>com.fasterxml.jackson.datatype</groupId>
             <artifactId>jackson-datatype-jsr310</artifactId>
         </dependency>
+        <!-- provided dependencies -->
+        <dependency>
+            <groupId>com.google.code.findbugs</groupId>
+            <artifactId>jsr305</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>net.shibboleth.utilities</groupId>
+            <artifactId>java-support</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency> <!-- required for the DuoIntegration -->
+            <groupId>${idp.groupId}</groupId>
+            <artifactId>idp-authn-api</artifactId>
+            <scope>provided</scope>
+        </dependency>
+         <dependency>
+            <groupId>ch.qos.logback</groupId>
+            <artifactId>logback-classic</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <!-- Test dependencies -->
+        
+        <!-- REMOVE THESE FOR NIMBUS, JUST TO GET IT WORKING -->
+         <dependency>
+            <groupId>com.auth0</groupId>
+            <artifactId>java-jwt</artifactId>
+            <version>3.10.3</version>
+             <scope>provided</scope>
+        </dependency>
+       
 
     </dependencies>
-    
-     <build>
+
+    <build>
         <plugins>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
diff --git a/idp-plugin-duo-nimbus-client/src/main/java/net/shibboleth/idp/plugin/authn/duo/nimbus/NimbusClient.java b/idp-plugin-duo-nimbus-client/src/main/java/net/shibboleth/idp/plugin/authn/duo/nimbus/NimbusClient.java
index 20e2a5d..78f0b3a 100644
--- a/idp-plugin-duo-nimbus-client/src/main/java/net/shibboleth/idp/plugin/authn/duo/nimbus/NimbusClient.java
+++ b/idp-plugin-duo-nimbus-client/src/main/java/net/shibboleth/idp/plugin/authn/duo/nimbus/NimbusClient.java
@@ -1,29 +1,218 @@
 package net.shibboleth.idp.plugin.authn.duo.nimbus;
 
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.text.ParseException;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.client.methods.RequestBuilder;
+import org.apache.http.client.protocol.HttpClientContext;
+import org.apache.http.client.utils.URIBuilder;
+import org.opensaml.security.httpclient.HttpClientSecurityParameters;
+import org.opensaml.security.httpclient.HttpClientSecuritySupport;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.nimbusds.jose.JWSObject;
+import com.nimbusds.jose.util.IOUtils;
 import com.nimbusds.jwt.JWT;
+import com.nimbusds.jwt.SignedJWT;
 
 import net.shibboleth.idp.plugin.authn.duo.DuoClientException;
 import net.shibboleth.idp.plugin.authn.duo.DuoOIDCClient;
+import net.shibboleth.idp.plugin.authn.duo.DuoOIDCIntegration;
 import net.shibboleth.idp.plugin.authn.duo.model.DuoHealthCheck;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
+/**
+ * A Duo client using the Nimbus OIDC library. Can only be instantiated by the corresponding factory method.
+ */
+final class NimbusClient implements DuoOIDCClient{
+
+    /** The only supported client assertion type.*/
+    @Nonnull @NotEmpty private static final String CLIENT_ASSERTION_TYPE = 
+            "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
+    
+    /** The HTTPS scheme.*/
+    @Nonnull @NotEmpty private static final String HTTPS = "https";
+    
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(NimbusClient.class);
+    
+    /** The integration to help generate the JWT.*/
+    @Nonnull private DuoOIDCIntegration duoIntegration;
+    
+    /** HttpClient for contacting Duo. */
+    @Nonnull private HttpClient httpClient;
 
-public class NimbusClient implements DuoOIDCClient{
+    /** HTTP client security parameters. */
+    @Nullable private HttpClientSecurityParameters httpClientSecurityParameters;
+    
+    /** JSON object mapper. */
+    @Nonnull private ObjectMapper objectMapper;
 
+    /**
+     * Constructor.
+     *
+     * @param integration the integration to create the client for, never {@code null}
+     * @param client the Http client to use to execute HTTP requests, never {@code null}
+     * @param params any security parameters to use for the Http client, can be {@code null}.
+     */
+    public NimbusClient(@Nonnull final DuoOIDCIntegration integration, @Nonnull final HttpClient client,
+            @Nullable final HttpClientSecurityParameters params) {
+        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 = new ObjectMapper();
+        //TODO: should we validate the integration here e.g. secret key length, or when being set etc.
+    }
+    
+    /**
+     * Set the JSON {@link ObjectMapper}.
+     * 
+     * @param mapper object mapper
+     */
+    public void setObjectMapper(@Nonnull final ObjectMapper mapper) {
+        objectMapper = Constraint.isNotNull(mapper, "Object mapper cannot be null");
+    }
+
+    /** {@inheritDoc} */
     @Override
-    public DuoHealthCheck healthCheck() throws DuoClientException {
-        // TODO Auto-generated method stub
-        return null;
+    @Nonnull public DuoHealthCheck healthCheck() throws DuoClientException {        
+        try {         
+            final URI uri = new URIBuilder().setScheme(HTTPS).setHost(duoIntegration.getAPIHost())
+                    .setPath(duoIntegration.getHealthCheckEndpoint()).build();            
+            log.trace("Using health check endpoint and audience '{}'",uri);
+            
+            final RequestBuilder rb =
+                    RequestBuilder.post().setUri(uri).addParameter("client_id",duoIntegration.getClientId())
+                    .addParameter("client_assertion",
+                            NimbusUtils.createJWS(uri.toString(), duoIntegration));
+
+            return executeRequest(rb.build(), new TypeReference<DuoHealthCheck>() {});
+           
+        } catch (final URISyntaxException e) {
+            log.error("Error performing a Duo health check",e);
+            throw new DuoClientException(e);
+        }        
     }
 
+    /** {@inheritDoc} */
     @Override
-    public String createAuthUrl(String username, String state) throws DuoClientException {
-        // TODO Auto-generated method stub
-        return null;
+    public String createAuthUrl(@Nonnull @NotEmpty final String username, 
+            @Nonnull final String state) throws DuoClientException {
+        Constraint.isNotEmpty(username, "Username can not be null or empty");
+        Constraint.isGreaterThan(21, state.length(), "State must be at least 22 characters");
+        Constraint.isLessThan(1025, state.length(),"State must be at maximum 1024 characters");
+        
+        try {
+            final String request = NimbusUtils.createJWSForAuthEndpoint(duoIntegration,state, username);
+            
+            final URI uri = new URIBuilder()
+                    .setScheme(HTTPS)
+                    .setHost(duoIntegration.getAPIHost())
+                    .setPath(duoIntegration.getAuthorizeEndpoint())
+                    .setParameter("scope", "openid")
+                    .setParameter("response_type", "code")
+                    .setParameter("redirect_uri", duoIntegration.getRedirectURI())
+                    .setParameter("client_id", duoIntegration.getClientId())
+                    .setParameter("request", request).build(); 
+            
+            return uri.toString();
+        } catch (final URISyntaxException e) {
+            log.error("Error performing a Duo health check",e);
+            throw new DuoClientException(e);
+        }  
+        
     }
 
+    /** {@inheritDoc} */
     @Override
-    public JWT exchangeAuthorizationCodeFor2FAResult(String code, String username) throws DuoClientException {
-        // TODO Auto-generated method stub
-        return null;
+    public JWT exchangeAuthorizationCodeFor2FAResult(@Nonnull final String code, 
+            @Nonnull final String username) throws DuoClientException {
+        Constraint.isNotEmpty(code, "Auth_code can not be null");
+        
+        try { 
+            final URI uri = new URIBuilder().setScheme(HTTPS).setHost(duoIntegration.getAPIHost())
+                    .setPath(duoIntegration.getTokenEndpoint()).build();            
+            log.trace("Using authorization endpoint and audience '{}'",uri);
+            
+            final RequestBuilder rb =
+                    RequestBuilder.post().setUri(uri)
+                    .addParameter("grant_type","authorization_code")
+                    .addParameter("code",code)
+                    .addParameter("redirect_uri",duoIntegration.getRedirectURI())
+                    .addParameter("client_assertion_type",CLIENT_ASSERTION_TYPE)
+                    .addParameter("client_assertion",
+                            NimbusUtils.createJWS(uri.toString(),duoIntegration));
+            
+            final TokenResponse response = executeRequest(rb.build(),new TypeReference<TokenResponse>() {});
+            log.trace("Duo token response: '{}'",response);
+            //Accepts only a JWS, not a JWE
+            return SignedJWT.parse(response.getIdToken());            
+            
+        } catch (final URISyntaxException | ParseException e) {
+            log.error("Unable to swap auth_code for id_token",e);
+            throw new DuoClientException(e);
+        }  
+    }
+    
+    
+    /**
+     * Performs a call to a Duo OIDC endpoint. Iff successful, the JSON response is mapped into the appropriate
+     * type.
+     * 
+     * @param <T> the response type
+     * @param request the prepared HTTP request
+     * @param wrapperTypeRef the type to deserialise the JSON into
+     * 
+     * @return the response type, never {@code null}.
+     * 
+     * @throws DuoClientException if there is an error producing a response
+     */
+    private <T> T executeRequest(@Nonnull final HttpUriRequest request, 
+            @Nonnull final TypeReference<T> wrapperTypeRef) throws DuoClientException{
+        
+        try {
+            final HttpClientContext clientContext = HttpClientContext.create();
+            HttpClientSecuritySupport.marshalSecurityParameters(clientContext, httpClientSecurityParameters, true);
+            HttpClientSecuritySupport.addDefaultTLSTrustEngineCriteria(clientContext, request);
+            final HttpResponse httpResponse = httpClient.execute(request, clientContext);
+            HttpClientSecuritySupport.checkTLSCredentialEvaluated(clientContext, request.getURI().getScheme());
+            
+            final int httpStatusCode = httpResponse.getStatusLine().getStatusCode();
+            if (httpStatusCode != HttpStatus.SC_OK) {
+                //dump the body for logging - if one exists
+                if (httpResponse.getEntity() != null && httpResponse.getEntity().getContent() != null) {
+                    final String errorContent = IOUtils.readInputStreamToString(httpResponse.getEntity().getContent());
+                    log.error("Duo returned a Non-ok message of '{}'",errorContent);
+                }
+                throw new DuoClientException("Non-ok status code (" + httpStatusCode + ") returned from Duo: "
+                        + httpResponse.getStatusLine().getReasonPhrase());
+            } else if (httpResponse.getEntity() == null) {
+                throw new DuoClientException("No response body returned from Duo");
+            }
+          
+            // Parse the JSON response.
+            final T duoResponse = objectMapper.readValue(httpResponse.getEntity().getContent(),wrapperTypeRef);
+            if (duoResponse == null) {
+                throw new DuoClientException("Unable to parse JSON response");
+            } 
+            return duoResponse;
+            
+        } catch (final IOException e) {
+            throw new DuoClientException("Could not execute Duo HTTP request",e);
+        }
     }
 
 }
diff --git a/idp-plugin-duo-nimbus-client/src/main/java/net/shibboleth/idp/plugin/authn/duo/nimbus/NimbusClientFactory.java b/idp-plugin-duo-nimbus-client/src/main/java/net/shibboleth/idp/plugin/authn/duo/nimbus/NimbusClientFactory.java
index 96a20c6..77eb7a4 100644
--- a/idp-plugin-duo-nimbus-client/src/main/java/net/shibboleth/idp/plugin/authn/duo/nimbus/NimbusClientFactory.java
+++ b/idp-plugin-duo-nimbus-client/src/main/java/net/shibboleth/idp/plugin/authn/duo/nimbus/NimbusClientFactory.java
@@ -1,18 +1,93 @@
 package net.shibboleth.idp.plugin.authn.duo.nimbus;
 
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.apache.http.client.HttpClient;
+import org.opensaml.security.httpclient.HttpClientSecurityParameters;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
 import net.shibboleth.idp.plugin.authn.duo.DuoClientException;
 import net.shibboleth.idp.plugin.authn.duo.DuoOIDCClient;
 import net.shibboleth.idp.plugin.authn.duo.DuoOIDCClientFactory;
 import net.shibboleth.idp.plugin.authn.duo.DuoOIDCIntegration;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.utilities.java.support.component.AbstractInitializableComponent;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
 
 
 /** Abstract factory implementation for the {@link DuoSDKClientAdaptor} for creating clients based
  * on the Nimbus library. */
-public class NimbusClientFactory implements DuoOIDCClientFactory{
+public class NimbusClientFactory extends AbstractInitializableComponent implements DuoOIDCClientFactory {
+    
+    //FIXME: check threadsafety here, as shared with all clients.
+    
+    /** HttpClient for contacting Duo. */
+    @NonnullAfterInit private HttpClient httpClient;
+
+    /** HTTP client security parameters. */
+    @Nullable private HttpClientSecurityParameters httpClientSecurityParameters;
+    
+    /** JSON object mapper. */
+    @NonnullAfterInit private ObjectMapper objectMapper;
+
 
     @Override
-    public DuoOIDCClient createInstance(DuoOIDCIntegration integration) throws DuoClientException {
-       return new NimbusClient();
+    public DuoOIDCClient createInstance(@Nonnull final DuoOIDCIntegration integration) throws DuoClientException {
+       return new NimbusClient(integration, httpClient, httpClientSecurityParameters);
+    }
+    
+    /** {@inheritDoc} */
+    @Override protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        
+        
+        if (httpClient == null) {
+            throw new ComponentInitializationException("HttpClient cannot be null");
+        }
+
+        if (objectMapper == null) {
+            throw new ComponentInitializationException("ObjectMapper cannot be null");
+        }
+    }
+    
+    /**
+     * Set the {@link HttpClient} to use for contacting Duo.
+     * 
+     * @param client HttpClient
+     */
+    public void setHttpClient(@Nonnull final HttpClient client) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+
+        httpClient = Constraint.isNotNull(client, "HTTP client cannot be null");
+    }
+
+    /**
+     * Set the optional client security parameters.
+     * 
+     * @param params the new client security parameters
+     */
+    public void setHttpClientSecurityParameters(@Nullable final HttpClientSecurityParameters params) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+
+        httpClientSecurityParameters = params;
+    }
+    
+    /**
+     * Set the JSON {@link ObjectMapper}.
+     * 
+     * @param mapper object mapper
+     */
+    public void setObjectMapper(@Nonnull final ObjectMapper mapper) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+
+        objectMapper = Constraint.isNotNull(mapper, "Object mapper cannot be null");
     }
 
 }
diff --git a/idp-plugin-duo-nimbus-client/src/main/java/net/shibboleth/idp/plugin/authn/duo/nimbus/NimbusUtils.java b/idp-plugin-duo-nimbus-client/src/main/java/net/shibboleth/idp/plugin/authn/duo/nimbus/NimbusUtils.java
new file mode 100644
index 0000000..759c231
--- /dev/null
+++ b/idp-plugin-duo-nimbus-client/src/main/java/net/shibboleth/idp/plugin/authn/duo/nimbus/NimbusUtils.java
@@ -0,0 +1,99 @@
+package net.shibboleth.idp.plugin.authn.duo.nimbus;
+
+import java.io.UnsupportedEncodingException;
+import java.security.SecureRandom;
+import java.time.Duration;
+import java.util.Collections;
+import java.util.Date;
+
+import javax.annotation.Nonnull;
+
+import com.auth0.jwt.JWT;
+import com.auth0.jwt.algorithms.Algorithm;
+
+import net.shibboleth.idp.plugin.authn.duo.DuoOIDCIntegration;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
+/** 
+ * Helper methods for working with Duo and Nimbus.
+ */
+public final class NimbusUtils {
+    
+    /** private constructor.*/
+    private NimbusUtils() {
+        
+    }
+    
+    /**
+     * Generate a cryptographically strong random JWT identifier. 
+     * 
+     * @param length the length of the ID.
+     * 
+     * @return a cryptographically strong random JWT identifier.
+     */
+    static String generateJWTId(@Nonnull final Integer length){
+        Constraint.isNotNull(length, "JWT length can not be null");
+        final SecureRandom secureRandom = new SecureRandom();
+        final StringBuilder sb = new StringBuilder();
+        while(sb.length() < length){
+            sb.append(Integer.toHexString(secureRandom.nextInt()));
+        }
+        return sb.toString().substring(0, length);
+    }
+    
+    /**
+     * Create a signed JWT using the given parameters suitable for the token endpoint.
+     * 
+     * @param duoIntegration the integration used to construct the JWT
+     * @param state the state
+     * @param username the subject of the authentication
+     * 
+     * @return a signed JWT
+     */
+    //TODO this method and the below should be nimbus, inside oidc-commons, and merged into a single API
+    static String createJWSForAuthEndpoint(@Nonnull final DuoOIDCIntegration duoIntegration, 
+            @Nonnull final String state, @Nonnull final String username) {
+        final Date expiration = new Date();
+        expiration.setTime(expiration.getTime() + Duration.ofHours(1).toMillis());
+
+        return JWT.create()
+                    .withHeader(Collections.singletonMap("alg", "HS512"))
+                    .withExpiresAt(expiration)
+                    .withClaim("scope", "openid")
+                    .withClaim("client_id", duoIntegration.getClientId())
+                    .withClaim("redirect_uri", duoIntegration.getRedirectURI())
+                    .withClaim("state", state)
+                    .withClaim("duo_uname", username)
+                    .withClaim("response_type", "code")
+                    .sign(Algorithm.HMAC512(duoIntegration.getSecretKey()));
+ 
+    }
+    
+    /**
+     * Create a signed JWT using the aud and duo integration supplied. 
+     * 
+     * @param aud the audience of the JWT
+     * @param duoIntegration the integration used to construct the JWT.
+     * 
+     * @return a signed JWT.
+     * 
+     */
+    //TODO: replace with nimbus method inside of commons 
+    static String createJWS(@Nonnull final String aud, 
+            @Nonnull final DuoOIDCIntegration duoIntegration) {
+        
+        final Date expiration = new Date();
+        expiration.setTime(expiration.getTime() + Duration.ofHours(1).toMillis());
+       
+        return com.auth0.jwt.JWT.create()
+                    .withHeader(Collections.singletonMap("alg", "HS512"))
+                    .withIssuer(duoIntegration.getClientId())
+                    .withSubject(duoIntegration.getClientId())
+                    .withAudience(aud)
+                    .withExpiresAt(expiration)
+                    .withJWTId(NimbusUtils.generateJWTId(32))
+                    .sign(Algorithm.HMAC512(duoIntegration.getSecretKey()));        
+        
+    }
+
+}
diff --git a/idp-plugin-duo-nimbus-client/src/main/java/net/shibboleth/idp/plugin/authn/duo/nimbus/TokenResponse.java b/idp-plugin-duo-nimbus-client/src/main/java/net/shibboleth/idp/plugin/authn/duo/nimbus/TokenResponse.java
new file mode 100644
index 0000000..755e91a
--- /dev/null
+++ b/idp-plugin-duo-nimbus-client/src/main/java/net/shibboleth/idp/plugin/authn/duo/nimbus/TokenResponse.java
@@ -0,0 +1,212 @@
+package net.shibboleth.idp.plugin.authn.duo.nimbus;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
+
+
+/** A token response, see RFC6749 section 5.1.*/
+ at Immutable
+ at JsonDeserialize(builder=TokenResponse.Builder.class)
+ at JsonIgnoreProperties(ignoreUnknown = true)
+public final class TokenResponse {
+    
+    /** The OIDC ID token string base64 encoded.*/
+    @Nonnull private final String idToken;
+
+    /** The access token issued by the authorization server.*/
+    @Nonnull private final String accessToken;
+    
+    /** The refresh token, which can be used to obtain new access tokens using the same authorization grant.*/
+    @Nullable private final String refreshToken;
+    
+    /** The token type e.g. Bearer. Value is case insensitive*/
+    @Nonnull private final String tokenType;
+    
+    /** The lifetime in seconds of the access token.*/
+    @Nullable private final Integer expiresIn;
+    
+    /** The scope requested by the client.*/
+    @Nullable private final String scope;
+    
+    /**
+     * Get the ID token.
+     * 
+     * @return Returns the idToken.
+     */
+    public final String getIdToken() {
+        return idToken;
+    }
+
+    /**
+     * Get the access token.
+     * 
+     * @return Returns the accessToken.
+     */
+    public final String getAccessToken() {
+        return accessToken;
+    }
+
+    /**
+     * Get the refresh token.
+     * 
+     * @return Returns the refreshToken.
+     */
+    public final String getRefreshToken() {
+        return refreshToken;
+    }
+
+    /**
+     * Get the token type.
+     * 
+     * @return Returns the tokenType.
+     */
+    public final String getTokenType() {
+        return tokenType;
+    }
+
+    /**
+     * Get how long the access token is valid for in seconds.
+     * 
+     * @return Returns the expiresIn.
+     */
+    public final Integer getExpiresIn() {
+        return expiresIn;
+    }
+
+    /**
+     * Get the requested scope.
+     * 
+     * @return Returns the scope.
+     */
+    public final String getScope() {
+        return scope;
+    }
+    
+
+    
+    @Override
+    public String toString() {
+        return "TokenResponse [idToken=" + idToken + ", accessToken=" + accessToken + ", refreshToken=" + refreshToken
+                + ", tokenType=" + tokenType + ", expiresIn=" + expiresIn + ", scope=" + scope + "]";
+    }
+
+    private TokenResponse(final Builder builder) {
+        this.idToken = builder.idToken;
+        this.accessToken = builder.accessToken;
+        this.tokenType = builder.tokenType;
+        this.refreshToken = builder.refreshToken;
+        this.expiresIn = builder.expiresIn;
+        this.scope = builder.scope;
+    }
+
+    /**
+     * Creates builder to build {@link TokenResponse}.
+     * @return created builder
+     */
+    
+    public static IIdTokenStage builder() {
+        return new Builder();
+    }
+
+    
+    public interface IIdTokenStage {
+        public IAccessTokenStage withIdToken(final String idToken);
+    }
+
+    
+    public interface IAccessTokenStage {
+        public ITokenTypeStage withAccessToken(final String accessToken);
+    }
+
+    
+    public interface ITokenTypeStage {
+        public IBuildStage withTokenType(final String tokenType);
+    }
+
+    
+    public interface IBuildStage {
+        public IBuildStage withRefreshToken(final String refreshToken);
+
+        public IBuildStage withExpiresIn(final Integer expiresIn);
+
+        public IBuildStage withScope(final String scope);
+
+        public TokenResponse build();
+    }
+
+    /**
+     * Builder to build {@link TokenResponse}.
+     */  
+    @JsonPOJOBuilder(buildMethodName = "build",withPrefix = "with")
+    public static final class Builder implements IIdTokenStage, IAccessTokenStage, ITokenTypeStage, IBuildStage {
+        private String idToken;
+
+        private String accessToken;
+
+        private String tokenType;
+
+        private String refreshToken;
+
+        private Integer expiresIn;
+
+        private String scope;
+
+        private Builder() {
+        }
+
+        @Override
+        @JsonProperty("id_token")
+        public IAccessTokenStage withIdToken(final String idToken) {
+            this.idToken = idToken;
+            return this;
+        }
+
+        @Override
+        @JsonProperty("access_token")
+        public ITokenTypeStage withAccessToken(final String accessToken) {
+            this.accessToken = accessToken;
+            return this;
+        }
+
+        @Override
+        @JsonProperty("token_type")
+        public IBuildStage withTokenType(final String tokenType) {
+            this.tokenType = tokenType;
+            return this;
+        }
+
+        @Override
+        @JsonProperty("refresh_token")
+        public IBuildStage withRefreshToken(final String refreshToken) {
+            this.refreshToken = refreshToken;
+            return this;
+        }
+
+        @Override
+        @JsonProperty("expires_in")
+        public IBuildStage withExpiresIn(final Integer expiresIn) {
+            this.expiresIn = expiresIn;
+            return this;
+        }
+
+        @Override
+        @JsonProperty("scope")
+        public IBuildStage withScope(final String scope) {
+            this.scope = scope;
+            return this;
+        }
+
+        @Override
+        public TokenResponse build() {
+            return new TokenResponse(this);
+        }
+    }
+    
+    
+}
diff --git a/idp-plugin-duo-nimbus-client/src/main/resources/duo-client-factory-bean.xml b/idp-plugin-duo-nimbus-client/src/main/resources/duo-client-factory-bean.xml
new file mode 100644
index 0000000..31e5fc9
--- /dev/null
+++ b/idp-plugin-duo-nimbus-client/src/main/resources/duo-client-factory-bean.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+    xmlns:context="http://www.springframework.org/schema/context"
+    xmlns:util="http://www.springframework.org/schema/util" xmlns:p="http://www.springframework.org/schema/p"
+    xmlns:c="http://www.springframework.org/schema/c" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
+                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
+                           http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
+
+    default-init-method="initialize" default-destroy-method="destroy">
+
+    <!-- TODO: do not default to the internal HTTP client -->
+    <bean id="shibboleth.authn.duo.OIDC.nimbus.clientFactory"
+        class="net.shibboleth.idp.plugin.authn.duo.nimbus.NimbusClientFactory" scope="singleton"
+        p:httpClient="#{getObject('shibboleth.authn.Duo.OIDC') ?: getObject('shibboleth.InternalHttpClient')}"
+        p:objectMapper-ref="shibboleth.authn.duo.OIDC.JSONObjectMapper">
+
+    </bean>
+    
+    <bean id="shibboleth.authn.duo.OIDC.JSONObjectMapper" class="com.fasterxml.jackson.databind.ObjectMapper" />
+    
+    <bean class="org.springframework.beans.factory.config.MethodInvokingBean"
+            p:targetObject-ref="shibboleth.authn.duo.OIDC.JSONObjectMapper"
+            p:targetMethod="setSerializationInclusion">
+        <property name="arguments">
+            <util:constant static-field="com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL" />
+        </property>
+    </bean>
+    
+    <bean class="org.springframework.beans.factory.config.MethodInvokingBean"
+            p:targetObject-ref="shibboleth.authn.duo.OIDC.JSONObjectMapper"
+            p:targetMethod="registerModule">
+        <property name="arguments">
+            <bean class="com.fasterxml.jackson.datatype.jsr310.JavaTimeModule" />
+        </property>
+    </bean>
+
+    <bean class="org.springframework.beans.factory.config.MethodInvokingBean"
+            p:targetObject-ref="shibboleth.authn.duo.OIDC.JSONObjectMapper"
+            p:targetMethod="setDateFormat">
+        <property name="arguments">
+            <bean class="java.text.SimpleDateFormat" c:_0="YYYY-MM-dd'T'HH:mm:ss.SSSZZ" />
+        </property>
+    </bean>
+
+</beans>
\ No newline at end of file
diff --git a/idp-plugin-duo-nimbus-client/src/test/java/net/shibboleth/idp/plugin/authn/duo/nimbus/.gitignore b/idp-plugin-duo-nimbus-client/src/test/java/net/shibboleth/idp/plugin/authn/duo/nimbus/.gitignore
new file mode 100644
index 0000000..39224a5
--- /dev/null
+++ b/idp-plugin-duo-nimbus-client/src/test/java/net/shibboleth/idp/plugin/authn/duo/nimbus/.gitignore
@@ -0,0 +1 @@
+/NimbusClientTestReal.java
diff --git a/idp-plugin-duo-nimbus-client/src/test/java/net/shibboleth/idp/plugin/authn/duo/nimbus/NimbusClientTest.java b/idp-plugin-duo-nimbus-client/src/test/java/net/shibboleth/idp/plugin/authn/duo/nimbus/NimbusClientTest.java
new file mode 100644
index 0000000..8682871
--- /dev/null
+++ b/idp-plugin-duo-nimbus-client/src/test/java/net/shibboleth/idp/plugin/authn/duo/nimbus/NimbusClientTest.java
@@ -0,0 +1,125 @@
+
+package net.shibboleth.idp.plugin.authn.duo.nimbus;
+
+import static org.testng.Assert.assertEquals;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.text.SimpleDateFormat;
+import java.time.Duration;
+
+import javax.annotation.Nonnull;
+
+import org.apache.http.client.ClientProtocolException;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.protocol.HttpContext;
+import org.apache.http.HttpResponse;
+import org.apache.http.StatusLine;
+import org.mockito.Mockito;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.nimbusds.jwt.JWT;
+
+import net.shibboleth.ext.spring.factory.HttpClientFactoryBean;
+import net.shibboleth.idp.plugin.authn.duo.DefaultDuoOIDCIntegration;
+import net.shibboleth.idp.plugin.authn.duo.DuoClientException;
+import net.shibboleth.idp.plugin.authn.duo.model.DuoHealthCheck;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+
+public class NimbusClientTest {
+
+    @Nonnull
+    private NimbusClient client;
+
+    @Nonnull
+    private HttpClient httpClient;
+
+    @Nonnull
+    private DefaultDuoOIDCIntegration integ;
+
+    @Nonnull
+    @NotEmpty
+    private final String ID_TOKEN_RESPONSE = "{\n" + "   \"access_token\": \"SlAV32hkKG\",\n"
+            + "   \"token_type\": \"Bearer\",\n" + "   \"refresh_token\": \"8xLOxBtZp8\",\n"
+            + "   \"expires_in\": 3600,\n" + "   \"id_token\": \"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9."
+            + "eyJpYXQiOjE2MDA5NTUxNDgsImV4cCI6MTYwMDk1ODc0OCwiYXV0aF90aW1lIjoxNjAwOTU1MTQ4"
+            + "LCJhdXRoX2NvbnRleHQiOnsiZW1haWwiOiJwaGlsaXAuc21hcnRAamlzYy5hYy51ayIsImFwcGxp"
+            + "Y2F0aW9uIjp7ImtleSI6IkRJVTZHRUZXRzVMSVVUVlYyTTNOIiwibmFtZSI6IlNoaWJib2xldGggS"
+            + "W50ZWdyYXRpb24gVGVzdGluZyJ9LCJhY2Nlc3NfZGV2aWNlIjp7InNlY3VyaXR5X2FnZW50cyI6In"
+            + "Vua25vd24iLCJpc19lbmNyeXB0aW9uX2VuYWJsZWQiOiJ1bmtub3duIiwibG9jYXRpb24iOnsiY2l"
+            + "0eSI6IkNhcmRpZmYiLCJjb3VudHJ5IjoiVW5pdGVkIEtpbmdkb20iLCJzdGF0ZSI6IldhbGVzIn0s"
+            + "Im9zIjoiTWFjIE9TIFgiLCJpc19wYXNzd29yZF9zZXQiOiJ1bmtub3duIiwiZmxhc2hfdmVyc2lvb"
+            + "iI6InVuaW5zdGFsbGVkIiwib3NfdmVyc2lvbiI6IjEwLjE1IiwiYnJvd3Nlcl92ZXJzaW9uIjoiODA"
+            + "uMCIsImphdmFfdmVyc2lvbiI6InVuaW5zdGFsbGVkIiwiaG9zdG5hbWUiOm51bGwsImlzX2ZpcmV3"
+            + "YWxsX2VuYWJsZWQiOiJ1bmtub3duIiwiaXAiOiI4Mi4xNy44OS4yMzIiLCJicm93c2VyIjoiRmlyZ"
+            + "WZveCJ9LCJ0aW1lc3RhbXAiOjE2MDA5NTUxNDgsImZhY3RvciI6ImR1b19wdXNoIiwiaXNvdGltZXN"
+            + "0YW1wIjoiMjAyMC0wOS0yNFQxMzo0NTo0OC4wMjg2ODgrMDA6MDAiLCJ0cnVzdGVkX2VuZHBvaW50"
+            + "X3N0YXR1cyI6InVua25vd24iLCJ1c2VyIjp7Imdyb3VwcyI6W10sImtleSI6IkRVR0w4VTQ2UUdKU"
+            + "09VSldHNTlXIiwibmFtZSI6InBoaWxzbWFydCJ9LCJvb2Rfc29mdHdhcmUiOm51bGwsInR4aWQiOi"
+            + "I4NzAwMDhmZi1iMmVjLTRjNGItYmY3Yi01ZWU2OWVhMDMwM2UiLCJyZXN1bHQiOiJzdWNjZXNzIiw"
+            + "iZXZlbnRfdHlwZSI6ImF1dGhlbnRpY2F0aW9uIiwiYXV0aF9kZXZpY2UiOnsibG9jYXRpb24iOnsi"
+            + "Y2l0eSI6IkNhcmRpZmYiLCJjb3VudHJ5IjoiVW5pdGVkIEtpbmdkb20iLCJzdGF0ZSI6IldhbGVzI"
+            + "n0sIm5hbWUiOiIrNDQgNzg1MiAxMTk4ODEiLCJpcCI6IjgyLjE3Ljg5LjIzMiJ9LCJhbGlhcyI6Ii"
+            + "IsInJlYXNvbiI6InVzZXJfYXBwcm92ZWQifSwiYXVkIjoiRElVNkdFRldHNUxJVVRWVjJNM04iLCJ"
+            + "hdXRoX3Jlc3VsdCI6eyJzdGF0dXNfbXNnIjoiTG9naW4gU3VjY2Vzc2Z1bCIsInN0YXR1cyI6ImFs"
+            + "bG93IiwicmVzdWx0IjoiYWxsb3cifSwicHJlZmVycmVkX3VzZXJuYW1lIjoicGhpbHNtYXJ0Iiwia"
+            + "XNzIjoiaHR0cHM6Ly9hcGktYzlmMjRjNWEuZHVvc2VjdXJpdHkuY29tL29hdXRoL3YxL3Rva2VuIi"
+            + "wic3ViIjoicGhpbHNtYXJ0In0.8-WYizEC_i1T4wXf1nh7f0RY4XmLp7bg7pq_-JRPeme40LwrPcr"
+            + "dQ9f2TtnBpiKjsD8MEkWYbthUurlyFWe5iQ\"\n" + "  }";
+
+    @Nonnull @NotEmpty private final String ID_TOKEN_RESPONSE_ERROR ="\"{\"error\": \"invalid_grant\","
+            + " \"error_description\": \"The provided authorization grant (e.g., authorization code) or refresh token is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client.\"}";
+
+    @BeforeMethod
+    public void setup() throws Exception {
+        integ = new DefaultDuoOIDCIntegration();
+        integ.setAPIHost("api-c9f24c5a.duosecurity.com");
+        integ.setClientId("DIU6GEFWG5LIUTVV2M3N");
+        integ.setRedirectURI("http://localhost/");
+        integ.setSecretKey("TeXvZxKul47v1Wew2zb6xRPzAJewJ34MP2w8Uith");
+        integ.setAuthorizeEndpoint("/oauth/v1/authorize");
+        integ.setTokenEndpoint("/oauth/v1/token");
+        integ.setHealthCheckEndpoint("/oauth/v1/health_check");
+
+        final HttpClientFactoryBean factory = new HttpClientFactoryBean();
+        factory.setConnectionDisregardTLSCertificate(false);
+        factory.setConnectionTimeout(Duration.ofMinutes(1));
+        factory.setSocketTimeout(Duration.ofMinutes(1));
+        factory.setMaxConnectionsTotal(100);
+        factory.setMaxConnectionsPerRoute(100);
+        // TODO set the TLS socket factory?
+        httpClient = factory.buildClient();
+
+        final ObjectMapper mapper = new ObjectMapper();
+        mapper.setDateFormat(new SimpleDateFormat("YYYY-MM-dd'T'HH:mm:ss.SSSZZ"));
+        client = new NimbusClient(integ, httpClient, null);
+        client.setObjectMapper(mapper);
+
+    }
+
+    @Test
+    public void testTokenExchange() throws DuoClientException, ClientProtocolException, IOException {
+        final HttpClient httpClient = Mockito.mock(HttpClient.class);
+        final HttpResponse httpResponse = Mockito.mock(HttpResponse.class);
+        final StatusLine statusLine = Mockito.mock(StatusLine.class);
+
+        Mockito.when(httpResponse.getStatusLine()).thenReturn(statusLine);
+
+        Mockito.when(statusLine.getStatusCode()).thenReturn(200);
+        Mockito.when(httpResponse.getEntity()).thenReturn(new StringEntity(ID_TOKEN_RESPONSE));
+        Mockito.when(httpClient.execute((HttpUriRequest) Mockito.any(), (HttpContext) Mockito.any()))
+                .thenReturn(httpResponse);
+
+        // create new client with mock response
+        client = new NimbusClient(integ, httpClient, null);
+
+        final JWT jwt = client.exchangeAuthorizationCodeFor2FAResult("code", "jdoe");
+        System.out.println("id_token: " + jwt);
+
+    }
+
+}
diff --git a/pom.xml b/pom.xml
index 0d00db8..164228e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -33,7 +33,7 @@
         <module>idp-duo-impl</module>
         <module>idp-duo-native-client-impl</module>
         <module>idp-duo-distribution</module>
-        <module>idp-plugin-duo-nimbus-client</module>
+        <module>idp-plugin-duo-nimbus-client-impl</module>
     </modules>
 
     <distributionManagement>

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


More information about the commits mailing list