Repository: hyperledger/quilt
Pull request #408 (2019-12-27 20:45:54)

align ilp over http custom settings
use new customSetting property names as defined in #383 
refactored the incoming/outgoingSettings such that simple or jwt auth settings are provided as separate objects
add JWT_RS_256 as an authType and added validation to require subject/audience/issuer when using JWT_RS_256
changed tokenAudience to be a String rather than HttpUrl because although it is often a URL, the JWT RFC allows for non-url audiences and in the case of Auth0 the audience is not a URL by default

Fixes issue #383 


Patch:
diff --git a/examples-parent/send-money/src/main/java/org/interledger/examples/SendMoneyExample.java b/examples-parent/send-money/src/main/java/org/interledger/examples/SendMoneyExample.java
index 4f9fc44..89c9cf0 100644
--- a/examples-parent/send-money/src/main/java/org/interledger/examples/SendMoneyExample.java
+++ b/examples-parent/send-money/src/main/java/org/interledger/examples/SendMoneyExample.java
@@ -7,9 +7,6 @@ import org.interledger.core.InterledgerAddress;
 import org.interledger.core.SharedSecret;
 import org.interledger.link.Link;
 import org.interledger.link.http.IlpOverHttpLink;
-import org.interledger.link.http.IlpOverHttpLinkSettings;
-import org.interledger.link.http.IncomingLinkSettings;
-import org.interledger.link.http.OutgoingLinkSettings;
 import org.interledger.link.http.auth.SimpleBearerTokenSupplier;
 import org.interledger.spsp.PaymentPointer;
 import org.interledger.spsp.StreamConnectionDetails;
@@ -19,6 +16,7 @@ import org.interledger.spsp.client.rust.InterledgerRustNodeClient;
 import org.interledger.stream.Denominations;
 import org.interledger.stream.SendMoneyRequest;
 import org.interledger.stream.SendMoneyResult;
+import org.interledger.stream.SenderAmountMode;
 import org.interledger.stream.sender.FixedSenderAmountPaymentTracker;
 import org.interledger.stream.sender.SimpleStreamSender;
 
@@ -74,6 +72,7 @@ public class SendMoneyExample {
     SendMoneyResult result = simpleStreamSender.sendMoney(
         SendMoneyRequest.builder()
             .sourceAddress(SENDER_ADDRESS)
+            .senderAmountMode(SenderAmountMode.SENDER_AMOUNT)
             .amount(UnsignedLong.valueOf(100000))
             .denomination(Denominations.XRP)
             .destinationAddress(connectionDetails.destinationAddress())
@@ -88,23 +87,9 @@ public class SendMoneyExample {
   }
 
   private static Link newIlpOverHttpLink() {
-    String sharedSecret = "some random secret here";
-    final IlpOverHttpLinkSettings linkSettings = IlpOverHttpLinkSettings.builder()
-        .incomingLinkSettings(IncomingLinkSettings.builder()
-            .authType(IlpOverHttpLinkSettings.AuthType.SIMPLE)
-            .encryptedTokenSharedSecret(sharedSecret)
-            .build())
-        .outgoingLinkSettings(OutgoingLinkSettings.builder()
-            .authType(IlpOverHttpLinkSettings.AuthType.SIMPLE)
-            .tokenSubject(SENDER_ACCOUNT_USERNAME)
-            .url(HttpUrl.parse(TESTNET_URI + "/ilp"))
-            .encryptedTokenSharedSecret(sharedSecret)
-            .build())
-        .build();
-
     return new IlpOverHttpLink(
         () -> SENDER_ADDRESS,
-        linkSettings,
+        HttpUrl.parse(TESTNET_URI + "/ilp"),
         newHttpClient(),
         new ObjectMapper(),
         InterledgerCodecContextFactory.oer(),
diff --git a/link-parent/link-ilp-over-http/src/main/java/org/interledger/link/http/AuthenticatedLinkSettings.java b/link-parent/link-ilp-over-http/src/main/java/org/interledger/link/http/AuthenticatedLinkSettings.java
new file mode 100644
index 0000000..25a3bec
--- /dev/null
+++ b/link-parent/link-ilp-over-http/src/main/java/org/interledger/link/http/AuthenticatedLinkSettings.java
@@ -0,0 +1,32 @@
+package org.interledger.link.http;
+
+import java.util.Optional;
+
+/**
+ * Common auth settings for incoming and outgoing links.
+ */
+public interface AuthenticatedLinkSettings {
+
+  /**
+   * Auth settings if using SIMPLE scheme.
+   *
+   * @return settings
+   */
+  Optional<SimpleAuthSettings> simpleAuthSettings();
+
+  /**
+   * Auth settings if using JWT scheme.
+   *
+   * @return settings
+   */
+  Optional<JwtAuthSettings> jwtAuthSettings();
+
+
+  /**
+   * The type of Auth to support for this link.
+   *
+   * @return A {@link IlpOverHttpLinkSettings.AuthType} for this link
+   */
+  IlpOverHttpLinkSettings.AuthType authType();
+
+}
diff --git a/link-parent/link-ilp-over-http/src/main/java/org/interledger/link/http/IlpOverHttpLink.java b/link-parent/link-ilp-over-http/src/main/java/org/interledger/link/http/IlpOverHttpLink.java
index ffdf7a3..c6fa688 100644
--- a/link-parent/link-ilp-over-http/src/main/java/org/interledger/link/http/IlpOverHttpLink.java
+++ b/link-parent/link-ilp-over-http/src/main/java/org/interledger/link/http/IlpOverHttpLink.java
@@ -27,6 +27,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
 import com.google.common.io.CharStreams;
 import com.google.common.net.HttpHeaders;
 import okhttp3.Headers;
+import okhttp3.HttpUrl;
 import okhttp3.OkHttpClient;
 import okhttp3.Request;
 import okhttp3.Request.Builder;
@@ -77,6 +78,8 @@ public class IlpOverHttpLink extends AbstractLink<IlpOverHttpLinkSettings> imple
    */
   private final Supplier<String> authTokenSupplier;
 
+  private final HttpUrl outgoingUrl;
+
   /**
    * Required-args Constructor.
    *
@@ -84,7 +87,7 @@ public class IlpOverHttpLink extends AbstractLink<IlpOverHttpLinkSettings> imple
    *                                uninitialized, for example, in cases where the Link obtains its address from a
    *                                parent node using IL-DCP. If an ILP address has not been assigned, or it has not
    *                                been obtained via IL-DCP, then this value will by default be {@link Link#SELF}.
-   * @param ilpOverHttpLinkSettings A {@link IlpOverHttpLinkSettings} that specified ledger link options.
+   * @param outgoingUrl             A {@link HttpUrl} to the connector.
    * @param okHttpClient            A {@link OkHttpClient} to use to communicate with the remote ILP-over-HTTP
    *                                endpoint.
    * @param objectMapper            A {@link ObjectMapper} for reading error responses from the remote ILP-over-HTTP
@@ -95,13 +98,17 @@ public class IlpOverHttpLink extends AbstractLink<IlpOverHttpLinkSettings> imple
    */
   public IlpOverHttpLink(
       final Supplier<InterledgerAddress> operatorAddressSupplier,
-      final IlpOverHttpLinkSettings ilpOverHttpLinkSettings,
+      final HttpUrl outgoingUrl,
       final OkHttpClient okHttpClient,
       final ObjectMapper objectMapper,
       final CodecContext ilpCodecContext,
       final BearerTokenSupplier bearerTokenSupplier
   ) {
-    super(operatorAddressSupplier, ilpOverHttpLinkSettings);
+    // IlpOverHttpLink does not require a full link settings, but the parent class requires one (even though it also
+    // does not use it for anything). For backwards compatibility pass a bare bones setting but ideally the abstract
+    // class could be refactored to not require it.
+    super(operatorAddressSupplier, IlpOverHttpLinkSettings.builder().build());
+    this.outgoingUrl = outgoingUrl;
     this.okHttpClient = Objects.requireNonNull(okHttpClient);
     this.objectMapper = Objects.requireNonNull(objectMapper);
     this.ilpCodecContext = Objects.requireNonNull(ilpCodecContext);
@@ -189,15 +196,7 @@ public class IlpOverHttpLink extends AbstractLink<IlpOverHttpLinkSettings> imple
    * the endpoint does not support ILP-over-HTTP requests, then we expect a 415 UNSUPPORTED_MEDIA_TYPE.</p>
    */
   public void testConnection() {
-    final OutgoingLinkSettings outgoingLinkSettings = this.getLinkSettings().outgoingLinkSettings()
-      .orElseGet(() -> {
-        logger.warn("Falling back to deprecated means of loading outgoing settings. " +
-          "Please switch to using outgoingLinkSettings() instead");
-        return this.getLinkSettings().outgoingHttpLinkSettings();
-      });
-    final String tokenSubject = outgoingLinkSettings.tokenSubject();
     try {
-
       final Request okHttpRequest = this.constructSendPacketRequest(UNFULFILLABLE_PACKET);
 
       try (Response response = okHttpClient.newCall(okHttpRequest).execute()) {
@@ -211,26 +210,26 @@ public class IlpOverHttpLink extends AbstractLink<IlpOverHttpLinkSettings> imple
               .isPresent();
 
           if (responseHasOctetStreamContentType) {
-            logger.info("Remote peer-link supports ILP-over-HTTP. tokenSubject={} url={} responseHeaders={}",
-                outgoingLinkSettings.tokenSubject(), outgoingLinkSettings.url(), response
+            logger.info("Remote peer-link supports ILP-over-HTTP. url={} responseHeaders={}",
+                outgoingUrl, response
             );
           } else {
-            logger.warn("Remote peer-link supports ILP-over-HTTP but uses wrong ContentType. tokenSubject={} url={} "
+            logger.warn("Remote peer-link supports ILP-over-HTTP but uses wrong ContentType. url={} "
                     + "response={}",
-                outgoingLinkSettings.tokenSubject(), outgoingLinkSettings.url(), response
+                outgoingUrl, response
             );
           }
         } else {
           if (response.code() == 406 || response.code() == 415) { // NOT_ACCEPTABLE || UNSUPPORTED_MEDIA_TYPE
             throw new LinkException(
-                String.format("Remote peer-link DOES NOT support ILP-over-HTTP. tokenSubject=%s url=%s response=%s",
-                    tokenSubject, outgoingLinkSettings.url(), response
+                String.format("Remote peer-link DOES NOT support ILP-over-HTTP. url=%s response=%s",
+                    outgoingUrl, response
                 ), getLinkId()
             );
           } else {
             throw new LinkException(
-                String.format("Unable to connect to ILP-over-HTTP. tokenSubject=%s url=%s response=%s",
-                    tokenSubject, outgoingLinkSettings.url(), response
+                String.format("Unable to connect to ILP-over-HTTP. url=%s response=%s",
+                    outgoingUrl, response
                 ), getLinkId()
             );
           }
@@ -300,12 +299,6 @@ public class IlpOverHttpLink extends AbstractLink<IlpOverHttpLinkSettings> imple
    */
   private Request constructSendPacketRequest(final InterledgerPreparePacket preparePacket) {
     Objects.requireNonNull(preparePacket);
-    final OutgoingLinkSettings outgoingLinkSettings = this.getLinkSettings().outgoingLinkSettings()
-      .orElseGet(() -> {
-        logger.warn("Falling back to deprecated means of loading outgoing settings. " +
-          "Please switch to using outgoingLinkSettings() instead");
-        return this.getLinkSettings().outgoingHttpLinkSettings();
-      });
 
     try {
       final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
@@ -313,7 +306,7 @@ public class IlpOverHttpLink extends AbstractLink<IlpOverHttpLinkSettings> imple
 
       return new Builder()
           .headers(constructHttpRequestHeaders())
-          .url(outgoingLinkSettings.url())
+          .url(outgoingUrl)
           .post(
               RequestBody.create(byteArrayOutputStream.toByteArray(), APPLICATION_OCTET_STREAM)
           )
diff --git a/link-parent/link-ilp-over-http/src/main/java/org/interledger/link/http/IlpOverHttpLinkFactory.java b/link-parent/link-ilp-over-http/src/main/java/org/interledger/link/http/IlpOverHttpLinkFactory.java
index e2d8494..fdeed24 100644
--- a/link-parent/link-ilp-over-http/src/main/java/org/interledger/link/http/IlpOverHttpLinkFactory.java
+++ b/link-parent/link-ilp-over-http/src/main/java/org/interledger/link/http/IlpOverHttpLinkFactory.java
@@ -82,16 +82,12 @@ public class IlpOverHttpLinkFactory implements LinkFactory {
 
     final BearerTokenSupplier bearerTokenSupplier;
     OutgoingLinkSettings outgoingLinkSettings = ilpOverHttpLinkSettings.outgoingLinkSettings()
-      .orElseGet(() -> {
-        logger.warn("Falling back to deprecated means of loading outgoing settings. " +
-          "Please switch to using outgoingLinkSettings() instead");
-        return ilpOverHttpLinkSettings.outgoingHttpLinkSettings();
-      });
+      .orElseThrow(() -> new IllegalArgumentException("No outgoing link settings configured for this link"));
     if (outgoingLinkSettings.authType().equals(IlpOverHttpLinkSettings.AuthType.SIMPLE)) {
       // Decrypt whatever is inside of the encryptedTokenSharedSecret. For the SIMPLE profile, this will decrypt to the
       // actual bearer token.
       bearerTokenSupplier = new SimpleBearerTokenSupplier(new String(
-        decryptor.decrypt(outgoingLinkSettings.encryptedTokenSharedSecret().getBytes())
+        decryptor.decrypt(outgoingLinkSettings.simpleAuthSettings().get().authToken().getBytes())
       ));
     } else {
       // TODO: For now, we assume the bytes are a String that conform to the Crypt CLI. However, this should be made
@@ -101,7 +97,7 @@ public class IlpOverHttpLinkFactory implements LinkFactory {
       // NOTE: This supplier will always create a copy of the decrypted bytes so that the consumer of each call can
       // safely wipe the bytes from memory without affecting other callers.
       final SharedSecretBytesSupplier sharedSecretSupplier = () -> decryptor
-        .decrypt(outgoingLinkSettings.encryptedTokenSharedSecret().getBytes());
+        .decrypt(outgoingLinkSettings.jwtAuthSettings().get().encryptedTokenSharedSecret().get().getBytes());
 
       bearerTokenSupplier = new JwtHs256BearerTokenSupplier(
         sharedSecretSupplier, outgoingLinkSettings
@@ -110,7 +106,7 @@ public class IlpOverHttpLinkFactory implements LinkFactory {
 
     final IlpOverHttpLink ilpOverHttpLink = new IlpOverHttpLink(
         operatorAddressSupplier,
-        ModifiableIlpOverHttpLinkSettings.create().from(ilpOverHttpLinkSettings), // Modifiable for testing
+        outgoingLinkSettings.url(),
         okHttpClient,
         objectMapper,
         ilpCodecContext,
diff --git a/link-parent/link-ilp-over-http/src/main/java/org/interledger/link/http/IlpOverHttpLinkSettings.java b/link-parent/link-ilp-over-http/src/main/java/org/interledger/link/http/IlpOverHttpLinkSettings.java
index b2c7dcc..a10bb8f 100644
--- a/link-parent/link-ilp-over-http/src/main/java/org/interledger/link/http/IlpOverHttpLinkSettings.java
+++ b/link-parent/link-ilp-over-http/src/main/java/org/interledger/link/http/IlpOverHttpLinkSettings.java
@@ -3,6 +3,7 @@ package org.interledger.link.http;
 import org.interledger.link.LinkSettings;
 import org.interledger.link.LinkType;
 
+import com.google.common.collect.ImmutableMap;
 import org.immutables.value.Value;
 import org.immutables.value.Value.Derived;
 import org.immutables.value.Value.Modifiable;
@@ -10,7 +11,6 @@ import org.immutables.value.Value.Modifiable;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
-import javax.annotation.Nullable;
 
 /**
  * An extension of {@link LinkSettings} for ILP-over-HTTP links.
@@ -24,6 +24,8 @@ public interface IlpOverHttpLinkSettings extends LinkSettings {
   String INCOMING = "incoming";
 
   String AUTH_TYPE = "auth_type";
+  String SIMPLE = "simple";
+  String JWT = "jwt";
 
   String TOKEN_ISSUER = "token_issuer";
   String TOKEN_AUDIENCE = "token_audience";
@@ -32,6 +34,7 @@ public interface IlpOverHttpLinkSettings extends LinkSettings {
 
   // Used to grab the auth credential from custom settings...
   String SHARED_SECRET = "shared_secret";
+  String AUTH_TOKEN = "auth_token";
   String URL = "url";
 
   static ImmutableIlpOverHttpLinkSettings.Builder builder() {
@@ -64,19 +67,12 @@ public interface IlpOverHttpLinkSettings extends LinkSettings {
     Objects.requireNonNull(builder);
     Objects.requireNonNull(customSettings);
 
-    final ImmutableIncomingLinkSettings.Builder incomingLinkSettingsBuilder =
-        IncomingLinkSettings.fromCustomSettings(customSettings);
-    final ImmutableOutgoingLinkSettings.Builder outgoingLinkSettingsBuilder =
-        OutgoingLinkSettings.fromCustomSettings(customSettings);
-
-    ImmutableIncomingLinkSettings incoming = incomingLinkSettingsBuilder.build();
-    builder.incomingLinkSettings(incoming);
-    ImmutableOutgoingLinkSettings outgoing = outgoingLinkSettingsBuilder.build();
-    builder.outgoingLinkSettings(outgoing);
-
-    // FIXME initialize in deprecated as well until removed
-    builder.incomingHttpLinkSettings(incoming);
-    builder.outgoingHttpLinkSettings(outgoing);
+    if (LinkSettingsUtils.getIncomingAuthType(customSettings).isPresent()) {
+      builder.incomingLinkSettings(IncomingLinkSettings.fromCustomSettings(customSettings).build());
+    }
+    if (LinkSettingsUtils.getOutgoingAuthType(customSettings).isPresent()) {
+      builder.outgoingLinkSettings(OutgoingLinkSettings.fromCustomSettings(customSettings).build());
+    }
 
     builder.customSettings(customSettings);
 
@@ -88,26 +84,6 @@ public interface IlpOverHttpLinkSettings extends LinkSettings {
     return IlpOverHttpLink.LINK_TYPE;
   }
 
-  /**
-   * @deprecated use {@link #incomingLinkSettings()}. To be removed in 1.0.4
-   * Link settings for the incoming HTTP link.
-   *
-   * @return A {@link IncomingLinkSettings}.
-   */
-  @Deprecated
-  @Nullable
-  IncomingLinkSettings incomingHttpLinkSettings();
-
-  /**
-   * @deprecated use {@link #outgoingLinkSettings()}. To be removed in 1.0.4
-   * Link settings for the outgoing HTTP link.
-   *
-   * @return A {@link OutgoingLinkSettings}.
-   */
-  @Deprecated
-  @Nullable
-  OutgoingLinkSettings outgoingHttpLinkSettings();
-
   /**
    * Optional link settings for the incoming HTTP link.
    *
@@ -140,9 +116,17 @@ public interface IlpOverHttpLinkSettings extends LinkSettings {
     JWT_HS_256,
 
     /**
-     * Use RSA asymmetric keys to create aand verify JWT_RS_256 tokens.
+     * Use RSA asymmetric keys to create and verify JWT_RS_256 tokens.
      */
-    //JWT_RS_256
+    JWT_RS_256
+  }
+
+  @Value.Auxiliary
+  default Map<String, Object> toCustomSettingsMap() {
+    ImmutableMap.Builder<String, Object> mapBuilder = ImmutableMap.builder();
+    incomingLinkSettings().ifPresent(settings -> mapBuilder.putAll(settings.toCustomSettingsMap()));
+    outgoingLinkSettings().ifPresent(settings -> mapBuilder.putAll(settings.toCustomSettingsMap()));
+    return mapBuilder.build();
   }
 
   @Value.Immutable
diff --git a/link-parent/link-ilp-over-http/src/main/java/org/interledger/link/http/IncomingLinkSettings.java b/link-parent/link-ilp-over-http/src/main/java/org/interledger/link/http/IncomingLinkSettings.java
index fcf1932..073deba 100644
--- a/link-parent/link-ilp-over-http/src/main/java/org/interledger/link/http/IncomingLinkSettings.java
+++ b/link-parent/link-ilp-over-http/src/main/java/org/interledger/link/http/IncomingLinkSettings.java
@@ -1,13 +1,18 @@
 package org.interledger.link.http;
 
+import static org.interledger.link.http.IlpOverHttpLinkSettings.AUTH_TOKEN;
 import static org.interledger.link.http.IlpOverHttpLinkSettings.AUTH_TYPE;
 import static org.interledger.link.http.IlpOverHttpLinkSettings.DOT;
 import static org.interledger.link.http.IlpOverHttpLinkSettings.ILP_OVER_HTTP;
 import static org.interledger.link.http.IlpOverHttpLinkSettings.INCOMING;
+import static org.interledger.link.http.IlpOverHttpLinkSettings.JWT;
 import static org.interledger.link.http.IlpOverHttpLinkSettings.SHARED_SECRET;
+import static org.interledger.link.http.IlpOverHttpLinkSettings.SIMPLE;
 import static org.interledger.link.http.IlpOverHttpLinkSettings.TOKEN_AUDIENCE;
 import static org.interledger.link.http.IlpOverHttpLinkSettings.TOKEN_ISSUER;
+import static org.interledger.link.http.IlpOverHttpLinkSettings.TOKEN_SUBJECT;
 
+import com.google.common.collect.ImmutableMap;
 import okhttp3.HttpUrl;
 import org.immutables.value.Value;
 import org.immutables.value.Value.Modifiable;
@@ -19,26 +24,59 @@ import java.util.Objects;
 import java.util.Optional;
 
 /**
- * An extension of {@link SharedSecretTokenSettings} for incoming ILP-over-HTTP Links. Note that this interface
+ * Settings for incoming ILP-over-HTTP Links. Note that this interface
  * purposefully does not define a `sub` claim because the `accountId` for any given account should always be used
  * instead for verification purposes.
  */
-public interface IncomingLinkSettings extends SharedSecretTokenSettings {
+@Value.Immutable
+@Modifiable
+public interface IncomingLinkSettings extends AuthenticatedLinkSettings {
 
   String HTTP_INCOMING_AUTH_TYPE = ILP_OVER_HTTP + DOT + INCOMING + DOT + AUTH_TYPE;
 
-  String HTTP_INCOMING_TOKEN_ISSUER = ILP_OVER_HTTP + DOT + INCOMING + DOT + TOKEN_ISSUER;
-  String HTTP_INCOMING_TOKEN_AUDIENCE = ILP_OVER_HTTP + DOT + INCOMING + DOT + TOKEN_AUDIENCE;
-  String HTTP_INCOMING_SHARED_SECRET = ILP_OVER_HTTP + DOT + INCOMING + DOT + SHARED_SECRET;
+  String INCOMING_JWT_SETINGS_PREFIX = ILP_OVER_HTTP + DOT + INCOMING + DOT + JWT;
+  String INCOMING_SIMPLE_SETINGS_PREFIX = ILP_OVER_HTTP + DOT + INCOMING + DOT + SIMPLE;
+  String HTTP_INCOMING_TOKEN_ISSUER = INCOMING_JWT_SETINGS_PREFIX + DOT + TOKEN_ISSUER;
+  String HTTP_INCOMING_TOKEN_AUDIENCE = INCOMING_JWT_SETINGS_PREFIX + DOT + TOKEN_AUDIENCE;
+  String HTTP_INCOMING_SHARED_SECRET = INCOMING_JWT_SETINGS_PREFIX + DOT + SHARED_SECRET;
+  String HTTP_INCOMING_TOKEN_SUBJECT = INCOMING_JWT_SETINGS_PREFIX + DOT + TOKEN_SUBJECT;
+  String HTTP_INCOMING_SIMPLE_AUTH_TOKEN = INCOMING_SIMPLE_SETINGS_PREFIX + DOT + AUTH_TOKEN;
 
   static ImmutableIncomingLinkSettings.Builder builder() {
     return ImmutableIncomingLinkSettings.builder();
   }
 
   /**
-   * Constructs a new builder with the correct custom settings, as found in {@code customSettings}.
+   * Constructs a new builder with the correct custom settings, as found in {@code customSettings}. Custom settings
+   * are expected to follow 1 of the following sets of properties.
+   * <p>
+   *   Simple
+   * </p>
+   * <pre>
+   *   "ilpOverHttp.incoming.auth_type": "SIMPLE"
+   *   "ilpOverHttp.incoming.simple.auth_token": "password"
+   * </pre>
    *
-   * @param customSettings A {@link Map} of custom settings to apply.
+   * <p>
+   *   JWT (using HS256)
+   * </p>
+   * <pre>
+   *   "ilpOverHttp.incoming.auth_type": "JWT_HS_256"
+   *   "ilpOverHttp.incoming.jwt.shared_secret": "some-key-used-for-symmetric-encryption",
+   *   "ilpOverHttp.incoming.jwt.subject": "foo@me"
+   * </pre>
+   *
+   * <p>
+   *   JWT (using RS256)
+   * </p>
+   * <pre>
+   *   "ilpOverHttp.incoming.auth_type": "JWT_RS_256"
+   *   "ilpOverHttp.incoming.jwt.issuer": "http://them.example.com",
+   *   "ilpOverHttp.incoming.jwt.audience": "https://me.example.com",
+   *   "ilpOverHttp.incoming.jwt.subject": "foo@me"
+   * </pre>
+   *
+   * * @param customSettings A {@link Map} of custom settings to apply.
    *
    * @return A {@link ImmutableIncomingLinkSettings.Builder} with applied custom settings.
    */
@@ -62,66 +100,56 @@ public interface IncomingLinkSettings extends SharedSecretTokenSettings {
     Objects.requireNonNull(builder);
     Objects.requireNonNull(customSettings);
 
-    // When loaded from a properties file, the properties are hierarchical in a Map. However, in Java, they are not, so
-    // consider both options. Generally only one will be present, but if for some reason both are present, the String
-    // values will win.
-    Optional.ofNullable(customSettings.get(ILP_OVER_HTTP))
-        .map(val -> (Map<String, Object>) val)
-        .ifPresent(blastSettings -> Optional.ofNullable(blastSettings.get(INCOMING))
-            .map(val -> (Map<String, Object>) val)
-            .ifPresent(incomingSettings -> {
-
-              Optional.ofNullable(incomingSettings.get(AUTH_TYPE))
-                  .map(Object::toString)
-                  .map(String::toUpperCase)
-                  .map(IlpOverHttpLinkSettings.AuthType::valueOf)
+    Optional.ofNullable(LinkSettingsUtils.flattenSettings(customSettings))
+        .ifPresent(settings ->
+              LinkSettingsUtils.getIncomingAuthType(settings)
                   .ifPresent((authType) -> {
                     builder.authType(authType);
+                    switch (authType) {
+                      case SIMPLE: {
+                        builder.simpleAuthSettings(buildSettingsForSimple(settings));
+                        break;
+                      }
+                      case JWT_HS_256:
+                      case JWT_RS_256: {
+                        builder.jwtAuthSettings(buildSettingsForJwt(settings));
+                        break;
+                      }
+                      default: throw new IllegalArgumentException("unsupported authType " + authType);
+                    }
+                  })
+            );
 
-                    if (authType != IlpOverHttpLinkSettings.AuthType.SIMPLE) {
-                      Optional.ofNullable(incomingSettings.get(TOKEN_ISSUER))
-                          .map(Object::toString)
-                          .map(HttpUrl::parse)
-                          .ifPresent(builder::tokenIssuer);
+    return builder;
+  }
 
-                      Optional.ofNullable(incomingSettings.get(TOKEN_AUDIENCE))
-                          .map(Object::toString)
-                          .map(HttpUrl::parse)
-                          .ifPresent(builder::tokenAudience);
-                    }
-                  });
+  static SimpleAuthSettings buildSettingsForSimple(Map<String, Object> settings) {
+    return Optional.ofNullable(settings.get(HTTP_INCOMING_SIMPLE_AUTH_TOKEN))
+        .map(Object::toString)
+        .map(SimpleAuthSettings::forAuthToken)
+        .orElseThrow(() -> new IllegalArgumentException(HTTP_INCOMING_SIMPLE_AUTH_TOKEN + " required"));
+  }
 
-              Optional.ofNullable(incomingSettings.get(SHARED_SECRET))
-                  .map(Object::toString)
-                  .ifPresent(builder::encryptedTokenSharedSecret);
+  static JwtAuthSettings buildSettingsForJwt(Map<String, Object> settings) {
+    ImmutableJwtAuthSettings.Builder authSettingsBuilder = JwtAuthSettings.builder();
+    Optional.ofNullable(settings.get(HTTP_INCOMING_TOKEN_ISSUER))
+        .map(Object::toString)
+        .map(HttpUrl::parse)
+        .map(authSettingsBuilder::tokenIssuer);
 
-            }));
+    Optional.ofNullable(settings.get(HTTP_INCOMING_TOKEN_AUDIENCE))
+        .map(Object::toString)
+        .map(authSettingsBuilder::tokenAudience);
 
-    Optional.ofNullable(customSettings.get(HTTP_INCOMING_AUTH_TYPE))
+    Optional.ofNullable(settings.get(HTTP_INCOMING_TOKEN_SUBJECT))
         .map(Object::toString)
-        .map(String::toUpperCase)
-        .map(IlpOverHttpLinkSettings.AuthType::valueOf)
-        .ifPresent((authType) -> {
-          builder.authType(authType);
-
-          if (authType != IlpOverHttpLinkSettings.AuthType.SIMPLE) {
-            Optional.ofNullable(customSettings.get(HTTP_INCOMING_TOKEN_ISSUER))
-                .map(Object::toString)
-                .map(HttpUrl::parse)
-                .ifPresent(builder::tokenIssuer);
-
-            Optional.ofNullable(customSettings.get(HTTP_INCOMING_TOKEN_AUDIENCE))
-                .map(Object::toString)
-                .map(HttpUrl::parse)
-                .ifPresent(builder::tokenAudience);
-          }
-        });
-
-    Optional.ofNullable(customSettings.get(HTTP_INCOMING_SHARED_SECRET))
+        .map(authSettingsBuilder::tokenSubject);
+
+    Optional.ofNullable(settings.get(HTTP_INCOMING_SHARED_SECRET))
         .map(Object::toString)
-        .ifPresent(builder::encryptedTokenSharedSecret);
+        .map(authSettingsBuilder::encryptedTokenSharedSecret);
 
-    return builder;
+    return authSettingsBuilder.build();
   }
 
   /**
@@ -133,23 +161,56 @@ public interface IncomingLinkSettings extends SharedSecretTokenSettings {
    *
    * @return A {@link Duration}.
    */
+  @Value.Default
   default Duration getMinMessageWindow() {
     return Duration.of(2500, ChronoUnit.MILLIS);
   }
 
-  @Value.Immutable
-  @Modifiable
-  abstract class AbstractIncomingLinkSettings implements IncomingLinkSettings {
+  /**
+   * The type of Auth to support for outgoing HTTP connections.
+   *
+   * @return A {@link IlpOverHttpLinkSettings.AuthType} for incoming link
+   */
+  IlpOverHttpLinkSettings.AuthType authType();
+
+  /**
+   * Auth settings if using SIMPLE scheme.
+   *
+   * @return settings
+   */
+  Optional<SimpleAuthSettings> simpleAuthSettings();
 
-    @Override
-    @Value.Default
-    public Duration getMinMessageWindow() {
-      return Duration.of(2500, ChronoUnit.MILLIS);
-    }
+  /**
+   * Auth settings if using JWT scheme.
+   *
+   * @return settings
+   */
+  Optional<JwtAuthSettings> jwtAuthSettings();
 
-    @Override
-    @Value.Redacted
-    public abstract String encryptedTokenSharedSecret();
+  /**
+   * Generates a custom settings map representation of this incoming link settings such that
+   * {@code IncomingLinkSettings.fromCustomSettings(someSettings.toCustomSettingsMap()).equals(someSettings); }
+   *
+   * @return map with custom settings for this incoming link settings instance
+   */
+  @Value.Auxiliary
+  default Map<String, Object> toCustomSettingsMap() {
+    ImmutableMap.Builder<String, Object> mapBuilder = ImmutableMap.builder();
+    mapBuilder.put(HTTP_INCOMING_AUTH_TYPE, authType().toString());
+    simpleAuthSettings().ifPresent(settings -> mapBuilder.put(HTTP_INCOMING_SIMPLE_AUTH_TOKEN, settings.authToken()));
+    jwtAuthSettings().ifPresent(settings -> {
+      mapBuilder.put(HTTP_INCOMING_TOKEN_SUBJECT, settings.tokenSubject());
+      settings.encryptedTokenSharedSecret().ifPresent(value -> mapBuilder.put(HTTP_INCOMING_SHARED_SECRET, value));
+      settings.tokenAudience().ifPresent(value -> mapBuilder.put(HTTP_INCOMING_TOKEN_AUDIENCE, value.toString()));
+      settings.tokenIssuer().ifPresent(value -> mapBuilder.put(HTTP_INCOMING_TOKEN_ISSUER, value.toString()));
+    });
+    return mapBuilder.build();
+  }
 
+  @Value.Check
+  default IncomingLinkSettings validate() {
+    LinkSettingsUtils.validate(this);
+    return this;
   }
+
 }
diff --git a/link-parent/link-ilp-over-http/src/main/java/org/interledger/link/http/JwtAuthSettings.java b/link-parent/link-ilp-over-http/src/main/java/org/interledger/link/http/JwtAuthSettings.java
new file mode 100644
index 0000000..29ebe32
--- /dev/null
+++ b/link-parent/link-ilp-over-http/src/main/java/org/interledger/link/http/JwtAuthSettings.java
@@ -0,0 +1,61 @@
+package org.interledger.link.http;
+
+import okhttp3.HttpUrl;
+import org.immutables.value.Value;
+
+import java.time.Duration;
+import java.util.Optional;
+
+/**
+ * Settings for authenticating use JWT with HS256 or RS256
+ */
+@Value.Immutable
+public interface JwtAuthSettings {
+
+  static ImmutableJwtAuthSettings.Builder builder() {
+    return ImmutableJwtAuthSettings.builder();
+  }
+
+  /**
+   * The expected `sub` value of a JWT token. This values identifies the principal that the
+   * token is authenticating.
+   *
+   * @return A {@link String} with the token subject.
+   */
+  String tokenSubject();
+
+  /**
+   * An encrypted shared `secret`, encoded per `EncryptedSecret`, that can be used to authenticate an incoming
+   * ILP-over-HTTP (BLAST) connection.
+   *
+   * @return A {@link String} containing the shared secret in base64 encoding. Required for HS256.
+   */
+  @Value.Redacted
+  Optional<String> encryptedTokenSharedSecret();
+
+  /**
+   * The expected `iss` value of the issuer of a JWT_RS_256 token. This value should always be a URL so that it can be rooted
+   * in Internet PKI and compared against the TLS certificate issued by the other side of a blast connection (i.e., the
+   * remote peer). It is optional in order to support node-wide issuance, or account-level issuance.
+   *
+   * @return An optionally present {@link HttpUrl} for the token issuer. Required for RS256.
+   */
+  Optional<HttpUrl> tokenIssuer();
+
+  /**
+   * The expected `aud` claim value of an incoming JWT_RS_256 token. In general, this value should be the URL of the
+   * Connector operating this BLAST link, since the remote will want to narrow the scope of its token to only be valid
+   * on this endpoint.
+   *
+   * @return An optionally present String for the token audience. Required for RS256.
+   */
+  Optional<String> tokenAudience();
+
+  /**
+   * If present, determines how often to sign a new token for auth. Optional to support the shared-secret use-case.
+   *
+   * @return An optionally present {@link Duration} representing the token expiry.
+   */
+  Optional<Duration> tokenExpiry();
+
+}
diff --git a/link-parent/link-ilp-over-http/src/main/java/org/interledger/link/http/LinkSettingsUtils.java b/link-parent/link-ilp-over-http/src/main/java/org/interledger/link/http/LinkSettingsUtils.java
new file mode 100644
index 0000000..c850da1
--- /dev/null
+++ b/link-parent/link-ilp-over-http/src/main/java/org/interledger/link/http/LinkSettingsUtils.java
@@ -0,0 +1,102 @@
+package org.interledger.link.http;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableMap;
+
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * Utility methods for the customSettings Map objects used by {@link IlpOverHttpLinkSettings}
+ */
+public class LinkSettingsUtils {
+
+  /**
+   * Flattens a {@code Map<String, Object || Map<String, Object>>} to a {@code Map<String, Object>} such that
+   * a map with values {@code ["foo" -> [ "bar" -> "baz", "fizz" -> "buzz" ] } would become
+   * {@code ["foo.bar" -> "baz" , "foo.fizz" -> "buzz" ] }. This is to deal with differences between Java property parsers
+   * that generate hierarchical Map of Maps (where keys are single segments of a property name) instead a flattened
+   * Map where all keys are the full property name.
+   *
+   * @param settings map to flatten
+   * @return flattened map
+   */
+  public static Map<String, Object> flattenSettings(Map<String, Object> settings) {
+    return flattenSettings("", settings);
+  }
+
+  private static Map<String, Object> flattenSettings(String prefix, Map<String, Object> settings) {
+    ImmutableMap.Builder<String, Object> flattened = ImmutableMap.builder();
+    settings.forEach((key, value) -> {
+      if (value instanceof Map) {
+        try {
+          Map<String, Object> typedValue = (Map<String, Object>) value;
+          flattened.putAll(flattenSettings(prefix + key + ".", typedValue));
+        } catch (ClassCastException e) {
+          throw new IllegalArgumentException("Found value with a key that is not a String");
+        }
+      } else {
+        flattened.put(prefix + key, value);
+      }
+    });
+    return flattened.build();
+  }
+
+  /**
+   * Determines the incoming {@link org.interledger.link.http.IlpOverHttpLinkSettings.AuthType}
+   * from custom settings values.
+   *
+   * @param customSettings
+   * @return auth type or null if no auth settings founds
+   */
+  public static Optional<IlpOverHttpLinkSettings.AuthType> getIncomingAuthType(Map<String, Object> customSettings) {
+    return Optional.ofNullable(flattenSettings(customSettings).get(IncomingLinkSettings.HTTP_INCOMING_AUTH_TYPE))
+        .map(Object::toString)
+        .map(String::toUpperCase)
+        .map(IlpOverHttpLinkSettings.AuthType::valueOf);
+  }
+
+  /**
+   * Determines the outgoing {@link org.interledger.link.http.IlpOverHttpLinkSettings.AuthType}
+   * from custom settings values.
+   *
+   * @param customSettings
+   * @return auth type or null if no auth settings founds
+   */
+  public static Optional<IlpOverHttpLinkSettings.AuthType> getOutgoingAuthType(Map<String, Object> customSettings) {
+    return Optional.ofNullable(flattenSettings(customSettings).get(OutgoingLinkSettings.HTTP_OUTGOING_AUTH_TYPE))
+        .map(Object::toString)
+        .map(String::toUpperCase)
+        .map(IlpOverHttpLinkSettings.AuthType::valueOf);
+  }
+
+  public static void validate(AuthenticatedLinkSettings linkSettings) {
+    switch (linkSettings.authType()) {
+      case SIMPLE: {
+        Preconditions.checkArgument(linkSettings.simpleAuthSettings().isPresent(), "simpleAuthSettings required");
+        break;
+      }
+      case JWT_HS_256: {
+        Preconditions.checkArgument(linkSettings.jwtAuthSettings().isPresent(), "jwtAuthSettings required");
+        JwtAuthSettings jwtAuthSettings = linkSettings.jwtAuthSettings().get();
+        Preconditions.checkArgument(jwtAuthSettings.encryptedTokenSharedSecret().isPresent(),
+            "encryptedTokenSharedSecret required for HS256");
+        break;
+      }
+      case JWT_RS_256:
+      {
+        Preconditions.checkArgument(linkSettings.jwtAuthSettings().isPresent(), "jwtAuthSettings required");
+        JwtAuthSettings jwtAuthSettings = linkSettings.jwtAuthSettings().get();
+        Preconditions.checkArgument(jwtAuthSettings.tokenAudience().isPresent(),
+            "tokenAudience required for RS256");
+        Preconditions.checkArgument(jwtAuthSettings.tokenIssuer().isPresent(),
+            "tokenIssuer required for RS256");
+        break;
+      }
+      default:
+        throw new IllegalStateException("unknown authType " + linkSettings.authType());
+    }
+
+  }
+
+}
diff --git a/link-parent/link-ilp-over-http/src/main/java/org/interledger/link/http/OutgoingLinkSettings.java b/link-parent/link-ilp-over-http/src/main/java/org/interledger/link/http/OutgoingLinkSettings.java
index f3c2ec7..cc9371d 100644
--- a/link-parent/link-ilp-over-http/src/main/java/org/interledger/link/http/OutgoingLinkSettings.java
+++ b/link-parent/link-ilp-over-http/src/main/java/org/interledger/link/http/OutgoingLinkSettings.java
@@ -1,16 +1,20 @@
 package org.interledger.link.http;
 
+import static org.interledger.link.http.IlpOverHttpLinkSettings.AUTH_TOKEN;
 import static org.interledger.link.http.IlpOverHttpLinkSettings.AUTH_TYPE;
 import static org.interledger.link.http.IlpOverHttpLinkSettings.DOT;
 import static org.interledger.link.http.IlpOverHttpLinkSettings.ILP_OVER_HTTP;
+import static org.interledger.link.http.IlpOverHttpLinkSettings.JWT;
 import static org.interledger.link.http.IlpOverHttpLinkSettings.OUTGOING;
 import static org.interledger.link.http.IlpOverHttpLinkSettings.SHARED_SECRET;
+import static org.interledger.link.http.IlpOverHttpLinkSettings.SIMPLE;
 import static org.interledger.link.http.IlpOverHttpLinkSettings.TOKEN_AUDIENCE;
 import static org.interledger.link.http.IlpOverHttpLinkSettings.TOKEN_EXPIRY;
 import static org.interledger.link.http.IlpOverHttpLinkSettings.TOKEN_ISSUER;
 import static org.interledger.link.http.IlpOverHttpLinkSettings.TOKEN_SUBJECT;
 import static org.interledger.link.http.IlpOverHttpLinkSettings.URL;
 
+import com.google.common.collect.ImmutableMap;
 import okhttp3.HttpUrl;
 import org.immutables.value.Value;
 import org.immutables.value.Value.Modifiable;
@@ -20,16 +24,18 @@ import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
 
-public interface OutgoingLinkSettings extends SharedSecretTokenSettings {
+@Value.Immutable
+@Modifiable
+public interface OutgoingLinkSettings extends AuthenticatedLinkSettings {
 
   String HTTP_OUTGOING_AUTH_TYPE = ILP_OVER_HTTP + DOT + OUTGOING + DOT + AUTH_TYPE;
+  String HTTP_OUTGOING_TOKEN_ISSUER = ILP_OVER_HTTP + DOT + OUTGOING + DOT + JWT + DOT + TOKEN_ISSUER;
+  String HTTP_OUTGOING_TOKEN_AUDIENCE = ILP_OVER_HTTP + DOT + OUTGOING + DOT + JWT + DOT + TOKEN_AUDIENCE;
+  String HTTP_OUTGOING_TOKEN_SUBJECT = ILP_OVER_HTTP + DOT + OUTGOING + DOT + JWT + DOT + TOKEN_SUBJECT;
+  String HTTP_OUTGOING_TOKEN_EXPIRY = ILP_OVER_HTTP + DOT + OUTGOING + DOT + JWT + DOT + TOKEN_EXPIRY;
+  String HTTP_OUTGOING_SHARED_SECRET = ILP_OVER_HTTP + DOT + OUTGOING + DOT + JWT + DOT + SHARED_SECRET;
 
-  String HTTP_OUTGOING_TOKEN_ISSUER = ILP_OVER_HTTP + DOT + OUTGOING + DOT + TOKEN_ISSUER;
-  String HTTP_OUTGOING_TOKEN_AUDIENCE = ILP_OVER_HTTP + DOT + OUTGOING + DOT + TOKEN_AUDIENCE;
-  String HTTP_OUTGOING_TOKEN_SUBJECT = ILP_OVER_HTTP + DOT + OUTGOING + DOT + TOKEN_SUBJECT;
-  String HTTP_OUTGOING_TOKEN_EXPIRY = ILP_OVER_HTTP + DOT + OUTGOING + DOT + TOKEN_EXPIRY;
-
-  String HTTP_OUTGOING_SHARED_SECRET = ILP_OVER_HTTP + DOT + OUTGOING + DOT + SHARED_SECRET;
+  String HTTP_OUTGOING_SIMPLE_AUTH_TOKEN = ILP_OVER_HTTP + DOT + OUTGOING + DOT + SIMPLE + DOT + AUTH_TOKEN;
   String HTTP_OUTGOING_URL = ILP_OVER_HTTP + DOT + OUTGOING + DOT + URL;
 
   static ImmutableOutgoingLinkSettings.Builder builder() {
@@ -37,10 +43,40 @@ public interface OutgoingLinkSettings extends SharedSecretTokenSettings {
   }
 
   /**
-   * Constructs a new builder with the correct custom settings, as found in {@code customSettings}.
+   * Constructs a new builder with the correct custom settings, as found in {@code customSettings}. Custom settings
+   * are expected to follow 1 of the following sets of properties.
+   * <p>
+   * Simple
+   * </p>
+   * <pre>
+   *   "ilpOverHttp.outgoing.auth_type": "SIMPLE"
+   *   "ilpOverHttp.outgoing.simple.auth_token": "password",
+   *   "ilpOverHttp.outgoing.url": "http://java1.connector.com/accounts/foo/ilp"
+   * </pre>
    *
-   * @param customSettings A {@link Map} of custom settings to apply.
+   * <p>
+   * JWT (using HS256)
+   * </p>
+   * <pre>
+   *   "ilpOverHttp.outgoing.auth_type": "JWT_HS_256"
+   *   "ilpOverHttp.incoming.jwt.shared_secret": "some-key-used-for-symmetric-encryption",
+   *   "ilpOverHttp.outgoing.jwt.subject": "foo@them",
+   *   "ilpOverHttp.outgoing.jwt.expiryDuration": "pt500s",
+   *   "ilpOverHttp.outgoing.url": "http://java1.connector.com/accounts/foo/ilp"
+   * </pre>
+   *
+   * <p>
+   * JWT (using RS256)
+   * </p>
+   * <pre>
+   *   "ilpOverHttp.outgoing.auth_type": "JWT_RS_256"
+   *   "ilpOverHttp.outgoing.jwt.issuer": "http://me.example.com",
+   *   "ilpOverHttp.outgoing.jwt.audience": "https://me.example.com",
+   *   "ilpOverHttp.outgoing.jwt.subject": "foo@me",
+   *   "ilpOverHttp.outgoing.url": "http://java1.connector.com/accounts/foo/ilp"
+   * </pre>
    *
+   * @param customSettings A {@link Map} of custom settings to apply.
    * @return A {@link ImmutableOutgoingLinkSettings.Builder} with applied custom settings.
    */
   static ImmutableOutgoingLinkSettings.Builder fromCustomSettings(Map<String, Object> customSettings) {
@@ -52,9 +88,8 @@ public interface OutgoingLinkSettings extends SharedSecretTokenSettings {
   /**
    * Populate a supplied builder with the correct custom settings, as found in {@code customSettings}.
    *
-   * @param builder        A {@link ImmutableIncomingLinkSettings.Builder} to apply custom settings to.
+   * @param builder        A {@link ImmutableOutgoingLinkSettings.Builder} to apply custom settings to.
    * @param customSettings A {@link Map} of custom settings to apply.
-   *
    * @return A {@link ImmutableOutgoingLinkSettings.Builder} with applied custom settings.
    */
   static ImmutableOutgoingLinkSettings.Builder applyCustomSettings(
@@ -63,112 +98,71 @@ public interface OutgoingLinkSettings extends SharedSecretTokenSettings {
     Objects.requireNonNull(builder);
     Objects.requireNonNull(customSettings);
 
-    // When loaded from a properties file, the properties are hierarchical in a Map. However, in Java, they are not, so
-    // consider both options. Generally only one will be present, but if for some reason both are present, the String
-    // values will win.
-    Optional.ofNullable(customSettings.get(ILP_OVER_HTTP))
-        .map(val -> (Map<String, Object>) val)
-        .ifPresent(blastSettings -> {
-
-          Optional.ofNullable(blastSettings.get(OUTGOING))
-              .map(val -> (Map<String, Object>) val)
-              .ifPresent(outgoingSettings -> {
-
-                Optional.ofNullable(outgoingSettings.get(TOKEN_SUBJECT))
-                    .map(Object::toString)
-                    .ifPresent(builder::tokenSubject);
-
-                Optional.ofNullable(outgoingSettings.get(AUTH_TYPE))
-                  .map(Object::toString)
-                  .map(String::toUpperCase)
-                  .map(IlpOverHttpLinkSettings.AuthType::valueOf)
-                  .ifPresent((authType) -> {
-                    builder.authType(authType);
-
-                    if (authType != IlpOverHttpLinkSettings.AuthType.SIMPLE) {
-                      Optional.ofNullable(outgoingSettings.get(TOKEN_ISSUER))
-                        .map(Object::toString)
-                        .map(HttpUrl::parse)
-                        .ifPresent(builder::tokenIssuer);
-
-                      Optional.ofNullable(outgoingSettings.get(TOKEN_AUDIENCE))
-                        .map(Object::toString)
-                        .map(HttpUrl::parse)
-                        .ifPresent(builder::tokenAudience);
+    Optional.ofNullable(LinkSettingsUtils.flattenSettings(customSettings))
+        .ifPresent(settings ->
+            LinkSettingsUtils.getOutgoingAuthType(settings)
+                .ifPresent((authType) -> {
+
+                  builder.authType(authType);
+
+                  Optional.ofNullable(settings.get(HTTP_OUTGOING_URL))
+                      .map(Object::toString)
+                      .map(HttpUrl::parse)
+                      .map(builder::url)
+                      .orElseThrow(() -> new IllegalArgumentException(HTTP_OUTGOING_URL + " is required"));
+
+                  switch (authType) {
+                    case SIMPLE: {
+                      builder.simpleAuthSettings(buildSettingsForSimple(settings));
+                      break;
+                    }
+                    case JWT_HS_256:
+                    case JWT_RS_256: {
+                      builder.jwtAuthSettings(buildSettingsForJwt(settings));
+                      break;
                     }
-                  });
-
-                Optional.ofNullable(outgoingSettings.get(SHARED_SECRET))
-                    .map(Object::toString)
-                    .ifPresent(builder::encryptedTokenSharedSecret);
-
-                Optional.ofNullable(outgoingSettings.get(TOKEN_EXPIRY))
-                    .map(Object::toString)
-                    .map(Duration::parse)
-                    .ifPresent(builder::tokenExpiry);
-
-                Optional.ofNullable(outgoingSettings.get(URL))
-                    .map(Object::toString)
-                    .map(HttpUrl::parse)
-                    .ifPresent(builder::url);
-
-              });
-        });
-
-    Optional.ofNullable(customSettings.get(HTTP_OUTGOING_AUTH_TYPE))
-      .map(Object::toString)
-      .map(String::toUpperCase)
-      .map(IlpOverHttpLinkSettings.AuthType::valueOf)
-      .ifPresent((authType) -> {
-        builder.authType(authType);
-        if (authType != IlpOverHttpLinkSettings.AuthType.SIMPLE) {
-          Optional.ofNullable(customSettings.get(HTTP_OUTGOING_TOKEN_ISSUER))
-            .map(Object::toString)
-            .map(HttpUrl::parse)
-            .ifPresent(builder::tokenIssuer);
-
-          Optional.ofNullable(customSettings.get(HTTP_OUTGOING_TOKEN_AUDIENCE))
-            .map(Object::toString)
-            .map(HttpUrl::parse)
-            .ifPresent(builder::tokenAudience);
-        }
-      });
-
-    Optional.ofNullable(customSettings.get(HTTP_OUTGOING_TOKEN_SUBJECT))
+                    default: throw new IllegalArgumentException("unsupported authType " + authType);
+                  }
+                })
+        );
+
+    return builder;
+  }
+
+  static SimpleAuthSettings buildSettingsForSimple(Map<String, Object> settings) {
+    return Optional.ofNullable(settings.get(HTTP_OUTGOING_SIMPLE_AUTH_TOKEN))
         .map(Object::toString)
-        .ifPresent(builder::tokenSubject);
+        .map(SimpleAuthSettings::forAuthToken)
+        .orElseThrow(() -> new IllegalArgumentException(HTTP_OUTGOING_SIMPLE_AUTH_TOKEN + " required"));
+  }
 
-    Optional.ofNullable(customSettings.get(HTTP_OUTGOING_SHARED_SECRET))
+  static JwtAuthSettings buildSettingsForJwt(Map<String, Object> settings) {
+    ImmutableJwtAuthSettings.Builder authSettingsBuilder = JwtAuthSettings.builder();
+
+    Optional.ofNullable(settings.get(HTTP_OUTGOING_TOKEN_ISSUER))
         .map(Object::toString)
-        .ifPresent(builder::encryptedTokenSharedSecret);
+        .map(HttpUrl::parse)
+        .map(authSettingsBuilder::tokenIssuer);
 
-    Optional.ofNullable(customSettings.get(HTTP_OUTGOING_TOKEN_EXPIRY))
+    Optional.ofNullable(settings.get(HTTP_OUTGOING_TOKEN_AUDIENCE))
         .map(Object::toString)
-        .map(Duration::parse)
-        .ifPresent(builder::tokenExpiry);
+        .map(authSettingsBuilder::tokenAudience);
 
-    Optional.ofNullable(customSettings.get(HTTP_OUTGOING_URL))
+    Optional.ofNullable(settings.get(HTTP_OUTGOING_TOKEN_SUBJECT))
         .map(Object::toString)
-        .map(HttpUrl::parse)
-        .ifPresent(builder::url);
+        .map(authSettingsBuilder::tokenSubject);
 
-    return builder;
-  }
+    Optional.ofNullable(settings.get(HTTP_OUTGOING_TOKEN_EXPIRY))
+        .map(Object::toString)
+        .map(Duration::parse)
+        .map(authSettingsBuilder::tokenExpiry);
 
-  /**
-   * The expected `sub` value of an ILP-over-HTTP authentication token. This values identifies the principal that the
-   * token is authenticating.
-   *
-   * @return A {@link String} with the token subject.
-   */
-  String tokenSubject();
+    Optional.ofNullable(settings.get(HTTP_OUTGOING_SHARED_SECRET))
+        .map(Object::toString)
+        .map(authSettingsBuilder::encryptedTokenSharedSecret);
 
-  /**
-   * If present, determines how often to sign a new token for auth. Optional to support the shared-secret use-case.
-   *
-   * @return An optionally present {@link Duration} representing the token expiry.
-   */
-  Optional<Duration> tokenExpiry();
+    return authSettingsBuilder.build();
+  }
 
   /**
    * endpoint to POST packets to. If url contains a percent and the link is in `multi` mode, then the segment after this
@@ -178,13 +172,32 @@ public interface OutgoingLinkSettings extends SharedSecretTokenSettings {
    */
   HttpUrl url();
 
-  @Value.Immutable
-  @Modifiable
-  abstract class AbstractOutgoingLinkSettings implements OutgoingLinkSettings {
-
-    @Override
-    @Value.Redacted
-    public abstract String encryptedTokenSharedSecret();
+  /**
+   * Generates a custom settings map representation of this outgoing link settings such that
+   * {@code OutgoingLinkSettings.fromCustomSettings(someSettings.toCustomSettingsMap()).equals(someSettings); }
+   *
+   * @return map with custom settings for this outgoing link settings instance
+   */
+  @Value.Auxiliary
+  default Map<String, Object> toCustomSettingsMap() {
+    ImmutableMap.Builder<String, Object> mapBuilder = ImmutableMap.builder();
+    mapBuilder.put(HTTP_OUTGOING_URL, url().toString());
+    mapBuilder.put(HTTP_OUTGOING_AUTH_TYPE, authType().toString());
+    simpleAuthSettings().ifPresent(settings -> mapBuilder.put(HTTP_OUTGOING_SIMPLE_AUTH_TOKEN, settings.authToken()));
+    jwtAuthSettings().ifPresent(settings -> {
+      mapBuilder.put(HTTP_OUTGOING_TOKEN_SUBJECT, settings.tokenSubject());
+      settings.encryptedTokenSharedSecret().ifPresent(value -> mapBuilder.put(HTTP_OUTGOING_SHARED_SECRET, value));
+      settings.tokenAudience().ifPresent(value -> mapBuilder.put(HTTP_OUTGOING_TOKEN_AUDIENCE, value.toString()));
+      settings.tokenIssuer().ifPresent(value -> mapBuilder.put(HTTP_OUTGOING_TOKEN_ISSUER, value.toString()));
+      settings.tokenExpiry().ifPresent(value -> mapBuilder.put(HTTP_OUTGOING_TOKEN_EXPIRY, value.toString()));
+    });
+    return mapBuilder.build();
+  }
 
+  @Value.Check
+  default OutgoingLinkSettings validate() {
+    LinkSettingsUtils.validate(this);
+    return this;
   }
+
 }
diff --git a/link-parent/link-ilp-over-http/src/main/java/org/interledger/link/http/SharedSecretTokenSettings.java b/link-parent/link-ilp-over-http/src/main/java/org/interledger/link/http/SharedSecretTokenSettings.java
deleted file mode 100644
index a00ee24..0000000
--- a/link-parent/link-ilp-over-http/src/main/java/org/interledger/link/http/SharedSecretTokenSettings.java
+++ /dev/null
@@ -1,45 +0,0 @@
-package org.interledger.link.http;
-
-import okhttp3.HttpUrl;
-
-import java.util.Optional;
-
-/**
- * Defines settings for a link that uses JWT_HS_256 with HMAC-SHA256 bearer tokens employing a shared secret.
- */
-public interface SharedSecretTokenSettings {
-
-  /**
-   * The type of Auth to support for outgoing HTTP connections.
-   *
-   * @return A {@link IlpOverHttpLinkSettings.AuthType} for this shared secret token settings.
-   */
-  IlpOverHttpLinkSettings.AuthType authType();
-
-  /**
-   * The expected `iss` value of the issuer of a Blast token. This value should always be a URL so that it can be rooted
-   * in Internet PKI and compared against the TLS certificate issued by the other side of a blast connection (i.e., the
-   * remote peer). It is optional in order to support node-wide issuance, or account-level issuance.
-   *
-   * @return An optionally present {@link HttpUrl} for the token issuer.
-   */
-  Optional<HttpUrl> tokenIssuer();
-
-  /**
-   * The expected `aud` claim value of an incoming JWT_HS_256 token. In general, this value should be the URL of the
-   * Connector operating this BLAST link, since the remote will want to narrow the scope of its token to only be valid
-   * on this endpoint.
-   *
-   * @return An optionally present {@link HttpUrl} for the token audience.
-   */
-  Optional<HttpUrl> tokenAudience();
-
-  /**
-   * An encrypted shared `secret`, encoded per `EncryptedSecret`, that can be used to authenticate an incoming
-   * ILP-over-HTTP (BLAST) connection.
-   *
-   * @return A {@link String} containing the shared secret in base64 encoding.
-   */
-  String encryptedTokenSharedSecret();
-
-}
diff --git a/link-parent/link-ilp-over-http/src/main/java/org/interledger/link/http/SimpleAuthSettings.java b/link-parent/link-ilp-over-http/src/main/java/org/interledger/link/http/SimpleAuthSettings.java
new file mode 100644
index 0000000..33885af
--- /dev/null
+++ b/link-parent/link-ilp-over-http/src/main/java/org/interledger/link/http/SimpleAuthSettings.java
@@ -0,0 +1,22 @@
+package org.interledger.link.http;
+
+import org.immutables.value.Value;
+
+/**
+ * Settings for simple auth scheme
+ */
+@Value.Immutable
+public interface SimpleAuthSettings {
+
+  static ImmutableSimpleAuthSettings forAuthToken(String authToken) {
+    return ImmutableSimpleAuthSettings.builder().authToken(authToken).build();
+  }
+
+  /**
+   * authentication token for authenticating to remote system
+   * @return token
+   */
+  @Value.Redacted
+  String authToken();
+
+}
diff --git a/link-parent/link-ilp-over-http/src/main/java/org/interledger/link/http/auth/JwtHs256BearerTokenSupplier.java b/link-parent/link-ilp-over-http/src/main/java/org/interledger/link/http/auth/JwtHs256BearerTokenSupplier.java
index 63e4d59..2293cf0 100644
--- a/link-parent/link-ilp-over-http/src/main/java/org/interledger/link/http/auth/JwtHs256BearerTokenSupplier.java
+++ b/link-parent/link-ilp-over-http/src/main/java/org/interledger/link/http/auth/JwtHs256BearerTokenSupplier.java
@@ -1,6 +1,7 @@
 package org.interledger.link.http.auth;
 
 import org.interledger.core.DateUtils;
+import org.interledger.link.http.JwtAuthSettings;
 import org.interledger.link.http.OutgoingLinkSettings;
 
 import com.auth0.jwt.JWT;
@@ -17,6 +18,7 @@ import java.time.temporal.ChronoUnit;
 import java.util.Arrays;
 import java.util.Date;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
 import java.util.function.Supplier;
@@ -57,10 +59,7 @@ public class JwtHs256BearerTokenSupplier implements BearerTokenSupplier {
         // There should only ever be 1 or 2 tokens in-memory for a given client instance.
         .maximumSize(3)
         // Expire after this duration, which will correspond to the last incoming request from the peer.
-        .expireAfterAccess(
-            this.outgoingLinkSettings.tokenExpiry().orElse(Duration.of(30, ChronoUnit.MINUTES)).toMinutes(),
-            TimeUnit.MINUTES
-        )
+        .expireAfterAccess(getExpiryInMinutes(Duration.of(30, ChronoUnit.MINUTES)).toMinutes(), TimeUnit.MINUTES)
         .removalListener((RemovalListener<String, String>) notification ->
             logger.debug("Removing IlpOverHttp AuthToken from Cache for Principal: {}", notification.getKey())
         )
@@ -71,18 +70,10 @@ public class JwtHs256BearerTokenSupplier implements BearerTokenSupplier {
             final byte[] sharedSecretBytes = sharedSecretBytesSupplier.get();
             try {
               return JWT.create()
-                  //.withIssuedAt(new Date())
-                  //      .withIssuer(getOutgoingLinkSettings()
-                  //        .tokenIssuer()
-                  //        .map(HttpUrl::toString)
-                  //        .orElseThrow(() -> new RuntimeException("JWT Blast Senders require an Outgoing Issuer!"))
-                  //      )
-                  .withSubject(outgoingLinkSettings.tokenSubject()) // account identifier at the remote server.
+                  .withSubject(outgoingLinkSettings.jwtAuthSettings().get().tokenSubject()) // account identifier at the remote server.
                   // Expire at the appointed time, or else after 15 minutes.
                   .withExpiresAt(
-                      JwtHs256BearerTokenSupplier.this.outgoingLinkSettings.tokenExpiry()
-                      .map(expiry -> Date.from(DateUtils.now().plus(expiry)))
-                      .orElseGet(() -> Date.from(DateUtils.now().plus(15, ChronoUnit.MINUTES)))
+                      Date.from(DateUtils.now().plus(getExpiryInMinutes(Duration.of(15, ChronoUnit.MINUTES))))
                   )
                   .sign(Algorithm.HMAC256(sharedSecretBytes));
             } finally {
@@ -93,6 +84,14 @@ public class JwtHs256BearerTokenSupplier implements BearerTokenSupplier {
         });
   }
 
+  private Duration getExpiryInMinutes(Duration defaultExpiry) {
+    return this.outgoingLinkSettings.jwtAuthSettings()
+        .map(JwtAuthSettings::tokenExpiry)
+        .filter(Optional::isPresent)
+        .map(Optional::get)
+        .orElse(defaultExpiry);
+  }
+
   /**
    * Gets an ILP-over-HTTP bearer token which can be used to make requests to a remote HTTP endpoint supporting
    * ILP-over-HTTP.
@@ -102,7 +101,7 @@ public class JwtHs256BearerTokenSupplier implements BearerTokenSupplier {
   @Override
   public String get() {
     try {
-      return this.ilpOverHttpAuthTokens.get(outgoingLinkSettings.tokenSubject());
+      return this.ilpOverHttpAuthTokens.get(outgoingLinkSettings.jwtAuthSettings().get().tokenSubject());
     } catch (ExecutionException e) {
       throw new RuntimeException(e);
     }


Test patch:
diff --git a/link-parent/link-ilp-over-http/src/test/java/org/interledger/link/http/AbstractHttpLinkSettingsTest.java b/link-parent/link-ilp-over-http/src/test/java/org/interledger/link/http/AbstractHttpLinkSettingsTest.java
index a822caf..d55f601 100644
--- a/link-parent/link-ilp-over-http/src/test/java/org/interledger/link/http/AbstractHttpLinkSettingsTest.java
+++ b/link-parent/link-ilp-over-http/src/test/java/org/interledger/link/http/AbstractHttpLinkSettingsTest.java
@@ -1,10 +1,13 @@
 package org.interledger.link.http;
 
+import static org.interledger.link.http.IlpOverHttpLinkSettings.AUTH_TOKEN;
 import static org.interledger.link.http.IlpOverHttpLinkSettings.AUTH_TYPE;
 import static org.interledger.link.http.IlpOverHttpLinkSettings.ILP_OVER_HTTP;
 import static org.interledger.link.http.IlpOverHttpLinkSettings.INCOMING;
+import static org.interledger.link.http.IlpOverHttpLinkSettings.JWT;
 import static org.interledger.link.http.IlpOverHttpLinkSettings.OUTGOING;
 import static org.interledger.link.http.IlpOverHttpLinkSettings.SHARED_SECRET;
+import static org.interledger.link.http.IlpOverHttpLinkSettings.SIMPLE;
 import static org.interledger.link.http.IlpOverHttpLinkSettings.TOKEN_AUDIENCE;
 import static org.interledger.link.http.IlpOverHttpLinkSettings.TOKEN_EXPIRY;
 import static org.interledger.link.http.IlpOverHttpLinkSettings.TOKEN_ISSUER;
@@ -12,10 +15,13 @@ import static org.interledger.link.http.IlpOverHttpLinkSettings.TOKEN_SUBJECT;
 import static org.interledger.link.http.IlpOverHttpLinkSettings.URL;
 import static org.interledger.link.http.IncomingLinkSettings.HTTP_INCOMING_AUTH_TYPE;
 import static org.interledger.link.http.IncomingLinkSettings.HTTP_INCOMING_SHARED_SECRET;
+import static org.interledger.link.http.IncomingLinkSettings.HTTP_INCOMING_SIMPLE_AUTH_TOKEN;
 import static org.interledger.link.http.IncomingLinkSettings.HTTP_INCOMING_TOKEN_AUDIENCE;
 import static org.interledger.link.http.IncomingLinkSettings.HTTP_INCOMING_TOKEN_ISSUER;
+import static org.interledger.link.http.IncomingLinkSettings.HTTP_INCOMING_TOKEN_SUBJECT;
 import static org.interledger.link.http.OutgoingLinkSettings.HTTP_OUTGOING_AUTH_TYPE;
 import static org.interledger.link.http.OutgoingLinkSettings.HTTP_OUTGOING_SHARED_SECRET;
+import static org.interledger.link.http.OutgoingLinkSettings.HTTP_OUTGOING_SIMPLE_AUTH_TOKEN;
 import static org.interledger.link.http.OutgoingLinkSettings.HTTP_OUTGOING_TOKEN_AUDIENCE;
 import static org.interledger.link.http.OutgoingLinkSettings.HTTP_OUTGOING_TOKEN_EXPIRY;
 import static org.interledger.link.http.OutgoingLinkSettings.HTTP_OUTGOING_TOKEN_ISSUER;
@@ -30,50 +36,77 @@ import java.util.Map;
 
 public abstract class AbstractHttpLinkSettingsTest {
 
-  protected Map<String, Object> customSettingsFlat() {
-    return customSettingsFlat(IlpOverHttpLinkSettings.AuthType.JWT_HS_256, IlpOverHttpLinkSettings.AuthType.JWT_HS_256);
-  }
-
-  protected Map<String, Object> customSettingsFlat(IlpOverHttpLinkSettings.AuthType incomingAuthType,
-                                                   IlpOverHttpLinkSettings.AuthType outgoingAuthType) {
+  static Map<String, Object> customSettingsJwtFlat(IlpOverHttpLinkSettings.AuthType authType) {
     return ImmutableMap.<String, Object>builder()
-        .put(HTTP_INCOMING_AUTH_TYPE, incomingAuthType.name())
+        .put(HTTP_INCOMING_AUTH_TYPE, authType.toString())
         .put(HTTP_INCOMING_TOKEN_ISSUER, "https://incoming-issuer.example.com/")
         .put(HTTP_INCOMING_SHARED_SECRET, "incoming-credential")
+        .put(HTTP_INCOMING_TOKEN_SUBJECT, "incoming-subject")
         .put(HTTP_INCOMING_TOKEN_AUDIENCE, "https://incoming-audience.example.com/")
-
-        .put(HTTP_OUTGOING_AUTH_TYPE, outgoingAuthType.name())
+        .put(HTTP_OUTGOING_AUTH_TYPE, authType.toString())
         .put(HTTP_OUTGOING_TOKEN_SUBJECT, "outgoing-subject")
         .put(HTTP_OUTGOING_SHARED_SECRET, "outgoing-credential")
         .put(HTTP_OUTGOING_TOKEN_ISSUER, "https://outgoing-issuer.example.com/")
         .put(HTTP_OUTGOING_TOKEN_AUDIENCE, "https://outgoing-audience.example.com/")
         .put(HTTP_OUTGOING_TOKEN_EXPIRY, Duration.ofDays(1).toString())
-        .put(HTTP_OUTGOING_URL, "https://outgoing.example.com")
+        .put(HTTP_OUTGOING_URL, "https://outgoing.example.com/")
 
         .build();
   }
 
-  protected Map<String, Object> customSettingsHierarchical() {
-    return customSettingsHierarchical(IlpOverHttpLinkSettings.AuthType.JWT_HS_256,
-      IlpOverHttpLinkSettings.AuthType.JWT_HS_256);
+  static Map<String, Object> customSettingsJwtHierarchical(IlpOverHttpLinkSettings.AuthType authType) {
+    final Map<String, Object> incomingMap = new HashMap<>();
+    final Map<String, Object> incomingAuthMap = new HashMap<>();
+    incomingAuthMap.put(TOKEN_SUBJECT, "incoming-subject");
+    incomingAuthMap.put(SHARED_SECRET, "incoming-credential");
+    incomingAuthMap.put(TOKEN_ISSUER, "https://incoming-issuer.example.com/");
+    incomingAuthMap.put(TOKEN_AUDIENCE, "https://incoming-audience.example.com/");
+    incomingMap.put(AUTH_TYPE, authType.toString());
+    incomingMap.put(JWT, incomingAuthMap);
+
+    final Map<String, Object> outgoingMap = new HashMap<>();
+    final Map<String, Object> outgoingAuthMap = new HashMap<>();
+    outgoingAuthMap.put(TOKEN_SUBJECT, "outgoing-subject");
+    outgoingAuthMap.put(SHARED_SECRET, "outgoing-credential");
+    outgoingAuthMap.put(TOKEN_ISSUER, "https://outgoing-issuer.example.com/");
+    outgoingAuthMap.put(TOKEN_AUDIENCE, "https://outgoing-audience.example.com/");
+    outgoingAuthMap.put(TOKEN_EXPIRY, Duration.ofDays(2).toString());
+    outgoingMap.put(JWT, outgoingAuthMap);
+    outgoingMap.put(AUTH_TYPE, authType.toString());
+    outgoingMap.put(URL, "https://outgoing.example.com/");
+
+    final Map<String, Object> httpMap = new HashMap<>();
+    httpMap.put(INCOMING, incomingMap);
+    httpMap.put(OUTGOING, outgoingMap);
+
+    final Map<String, Object> customSettings = new HashMap<>();
+    customSettings.put(ILP_OVER_HTTP, httpMap);
+
+    return customSettings;
   }
 
-  protected Map<String, Object> customSettingsHierarchical(IlpOverHttpLinkSettings.AuthType incomingAuthType,
-                                                           IlpOverHttpLinkSettings.AuthType outgoingAuthType) {
+  static Map<String, Object> customSettingsSimpleFlat() {
+    return ImmutableMap.<String, Object>builder()
+        .put(HTTP_INCOMING_AUTH_TYPE, "SIMPLE")
+        .put(HTTP_INCOMING_SIMPLE_AUTH_TOKEN, "incoming-secret")
+        .put(HTTP_OUTGOING_AUTH_TYPE, "SIMPLE")
+        .put(HTTP_OUTGOING_SIMPLE_AUTH_TOKEN, "outgoing-secret")
+        .put(HTTP_OUTGOING_URL, "https://outgoing.example.com/")
+        .build();
+  }
+
+  static Map<String, Object> customSettingsSimpleHierarchical() {
     final Map<String, Object> incomingMap = new HashMap<>();
-    incomingMap.put(AUTH_TYPE, incomingAuthType.name());
-    incomingMap.put(TOKEN_SUBJECT, "incoming-subject");
-    incomingMap.put(SHARED_SECRET, "incoming-credential");
-    incomingMap.put(TOKEN_ISSUER, "https://incoming-issuer.example.com/");
-    incomingMap.put(TOKEN_AUDIENCE, "https://incoming-audience.example.com/");
+    final Map<String, Object> incomingAuthMap = new HashMap<>();
+    incomingAuthMap.put(AUTH_TOKEN, "incoming-secret");
+    incomingMap.put(AUTH_TYPE, "SIMPLE");
+    incomingMap.put(SIMPLE, incomingAuthMap);
 
     final Map<String, Object> outgoingMap = new HashMap<>();
-    outgoingMap.put(AUTH_TYPE, outgoingAuthType.name());
-    outgoingMap.put(TOKEN_SUBJECT, "outgoing-subject");
-    outgoingMap.put(SHARED_SECRET, "outgoing-credential");
-    outgoingMap.put(TOKEN_ISSUER, "https://outgoing-issuer.example.com/");
-    outgoingMap.put(TOKEN_AUDIENCE, "https://outgoing-audience.example.com/");
-    outgoingMap.put(TOKEN_EXPIRY, Duration.ofDays(2).toString());
+    final Map<String, Object> outgoingAuthMap = new HashMap<>();
+    outgoingAuthMap.put(AUTH_TOKEN, "outgoing-secret");
+    outgoingMap.put(AUTH_TYPE, "SIMPLE");
+    outgoingMap.put(SIMPLE, outgoingAuthMap);
     outgoingMap.put(URL, "https://outgoing.example.com/");
 
     final Map<String, Object> httpMap = new HashMap<>();
@@ -85,5 +118,4 @@ public abstract class AbstractHttpLinkSettingsTest {
 
     return customSettings;
   }
-
 }
diff --git a/link-parent/link-ilp-over-http/src/test/java/org/interledger/link/http/IlpOverHttpLinkSettingsTest.java b/link-parent/link-ilp-over-http/src/test/java/org/interledger/link/http/IlpOverHttpLinkSettingsTest.java
index 09c9f35..5856855 100644
--- a/link-parent/link-ilp-over-http/src/test/java/org/interledger/link/http/IlpOverHttpLinkSettingsTest.java
+++ b/link-parent/link-ilp-over-http/src/test/java/org/interledger/link/http/IlpOverHttpLinkSettingsTest.java
@@ -7,6 +7,7 @@ import org.junit.Test;
 
 import java.time.Duration;
 import java.util.Map;
+import java.util.Optional;
 
 /**
  * Unit tests for {@link IlpOverHttpLinkSettings}.
@@ -17,8 +18,62 @@ public class IlpOverHttpLinkSettingsTest extends AbstractHttpLinkSettingsTest {
    * Tests the builder when customAttributes is a flat collection of key/value pairs using dotted-notation.
    */
   @Test
-  public void applyCustomSettingsWithFlatDottedNotation() {
-    final Map<String, Object> flattenedCustomSettings = this.customSettingsFlat();
+  public void applyCustomSettingsSimpleWithFlatDottedNotation() {
+    final Map<String, Object> flattenedCustomSettings = this.customSettingsSimpleFlat();
+
+    final ImmutableIlpOverHttpLinkSettings.Builder builder = IlpOverHttpLinkSettings.builder();
+    final IlpOverHttpLinkSettings ilpOverHttpLinkSettings =
+        IlpOverHttpLinkSettings.applyCustomSettings(builder, flattenedCustomSettings).build();
+
+    assertThat(ilpOverHttpLinkSettings.incomingLinkSettings().get().authType())
+        .isEqualTo(IlpOverHttpLinkSettings.AuthType.SIMPLE);
+    SimpleAuthSettings incomingSimpleSettings =
+        ilpOverHttpLinkSettings.incomingLinkSettings().get().simpleAuthSettings().get();
+    assertThat(incomingSimpleSettings.authToken()).isEqualTo("incoming-secret");
+
+    SimpleAuthSettings outgoingSimpleSettings =
+        ilpOverHttpLinkSettings.outgoingLinkSettings().get().simpleAuthSettings().get();
+    assertThat(outgoingSimpleSettings.authToken()).isEqualTo("outgoing-secret");
+    assertThat(ilpOverHttpLinkSettings.outgoingLinkSettings().get().url())
+        .isEqualTo(HttpUrl.parse("https://outgoing.example.com/"));
+
+    assertThat(ilpOverHttpLinkSettings.toCustomSettingsMap()).isEqualTo(flattenedCustomSettings);
+  }
+
+  /**
+   * Tests the builder when customAttributes is a flat collection of key/value pairs using dotted-notation.
+   */
+  @Test
+  public void applyCustomSettingsSimpleWithMapHeirarchy() {
+    final Map<String, Object> hierchicalSettings = this.customSettingsSimpleHierarchical();
+
+    final ImmutableIlpOverHttpLinkSettings.Builder builder = IlpOverHttpLinkSettings.builder();
+    final IlpOverHttpLinkSettings ilpOverHttpLinkSettings =
+        IlpOverHttpLinkSettings.applyCustomSettings(builder, hierchicalSettings).build();
+
+    assertThat(ilpOverHttpLinkSettings.incomingLinkSettings().get().authType())
+        .isEqualTo(IlpOverHttpLinkSettings.AuthType.SIMPLE);
+    SimpleAuthSettings incomingSimpleSettings =
+        ilpOverHttpLinkSettings.incomingLinkSettings().get().simpleAuthSettings().get();
+    assertThat(incomingSimpleSettings.authToken()).isEqualTo("incoming-secret");
+
+    SimpleAuthSettings outgoingSimpleSettings =
+        ilpOverHttpLinkSettings.outgoingLinkSettings().get().simpleAuthSettings().get();
+    assertThat(outgoingSimpleSettings.authToken()).isEqualTo("outgoing-secret");
+    assertThat(ilpOverHttpLinkSettings.outgoingLinkSettings().get().url())
+        .isEqualTo(HttpUrl.parse("https://outgoing.example.com/"));
+
+    assertThat(ilpOverHttpLinkSettings.toCustomSettingsMap())
+        .isEqualTo(LinkSettingsUtils.flattenSettings(hierchicalSettings));
+  }
+
+  /**
+   * Tests the builder when customAttributes is a flat collection of key/value pairs using dotted-notation.
+   */
+  @Test
+  public void applyCustomSettingsJwtWithFlatDottedNotation() {
+    final Map<String, Object> flattenedCustomSettings =
+        this.customSettingsJwtFlat(IlpOverHttpLinkSettings.AuthType.JWT_HS_256);
 
     final ImmutableIlpOverHttpLinkSettings.Builder builder = IlpOverHttpLinkSettings.builder();
     final IlpOverHttpLinkSettings ilpOverHttpLinkSettings =
@@ -26,36 +81,41 @@ public class IlpOverHttpLinkSettingsTest extends AbstractHttpLinkSettingsTest {
 
     assertThat(ilpOverHttpLinkSettings.incomingLinkSettings().get().authType())
         .isEqualTo(IlpOverHttpLinkSettings.AuthType.JWT_HS_256);
-    assertThat(ilpOverHttpLinkSettings.incomingLinkSettings().get().tokenIssuer().get())
+    JwtAuthSettings incomingJwtSettings = ilpOverHttpLinkSettings.incomingLinkSettings().get().jwtAuthSettings().get();
+    assertThat(incomingJwtSettings.tokenIssuer().get())
         .isEqualTo(HttpUrl.parse("https://incoming-issuer.example.com/"));
-    assertThat(ilpOverHttpLinkSettings.incomingLinkSettings().get().tokenAudience().get())
-        .isEqualTo(HttpUrl.parse("https://incoming-audience.example.com/"));
-    assertThat(ilpOverHttpLinkSettings.incomingLinkSettings().get().encryptedTokenSharedSecret())
-        .isEqualTo("incoming-credential");
+    assertThat(incomingJwtSettings.tokenAudience().get()).isEqualTo("https://incoming-audience.example.com/");
+
+    JwtAuthSettings outgoingJwtSettings = ilpOverHttpLinkSettings.outgoingLinkSettings().get().jwtAuthSettings().get();
+    assertThat(outgoingJwtSettings.tokenSubject()).isEqualTo("outgoing-subject");
+    assertThat(incomingJwtSettings.encryptedTokenSharedSecret())
+        .isEqualTo(Optional.of("incoming-credential"));
     assertThat(ilpOverHttpLinkSettings.incomingLinkSettings().get().getMinMessageWindow())
         .isEqualTo(Duration.ofMillis(2500));
-
     assertThat(ilpOverHttpLinkSettings.outgoingLinkSettings().get().authType())
         .isEqualTo(IlpOverHttpLinkSettings.AuthType.JWT_HS_256);
-    assertThat(ilpOverHttpLinkSettings.outgoingLinkSettings().get().tokenIssuer().get())
+    assertThat(outgoingJwtSettings.tokenIssuer().get())
         .isEqualTo(HttpUrl.parse("https://outgoing-issuer.example.com/"));
-    assertThat(ilpOverHttpLinkSettings.outgoingLinkSettings().get().tokenAudience().get())
-        .isEqualTo(HttpUrl.parse("https://outgoing-audience.example.com/"));
-    assertThat(ilpOverHttpLinkSettings.outgoingLinkSettings().get().tokenSubject()).isEqualTo("outgoing-subject");
-    assertThat(ilpOverHttpLinkSettings.outgoingLinkSettings().get().encryptedTokenSharedSecret())
-        .isEqualTo("outgoing-credential");
-    assertThat(ilpOverHttpLinkSettings.outgoingLinkSettings().get().tokenExpiry().get())
+    assertThat(outgoingJwtSettings.tokenAudience().get()).isEqualTo("https://outgoing-audience.example.com/");
+    assertThat(outgoingJwtSettings.tokenSubject()).isEqualTo("outgoing-subject");
+    assertThat(outgoingJwtSettings.encryptedTokenSharedSecret())
+        .isEqualTo(Optional.of("outgoing-credential"));
+    assertThat(outgoingJwtSettings.tokenExpiry().get())
       .isEqualTo(Duration.ofHours(24));
     assertThat(ilpOverHttpLinkSettings.outgoingLinkSettings().get().url())
         .isEqualTo(HttpUrl.parse("https://outgoing.example.com/"));
+
+    assertThat(ilpOverHttpLinkSettings.toCustomSettingsMap()).isEqualTo(flattenedCustomSettings);
   }
 
   /**
    * Tests the builder when customAttributes is a Map of Maps.
    */
   @Test
-  public void applyCustomSettingsWithMapHeirarchy() {
-    final Map<String, Object> customSettings = this.customSettingsHierarchical();
+  public void applyCustomSettingsJwtWithMapHeirarchy() {
+    final Map<String, Object> customSettings =
+        this.customSettingsJwtFlat(IlpOverHttpLinkSettings.AuthType.JWT_HS_256);
+
 
     final ImmutableIlpOverHttpLinkSettings.Builder builder = IlpOverHttpLinkSettings.builder();
     final ImmutableIlpOverHttpLinkSettings httpLinkSettings =
@@ -63,47 +123,50 @@ public class IlpOverHttpLinkSettingsTest extends AbstractHttpLinkSettingsTest {
 
     assertThat(httpLinkSettings.incomingLinkSettings().get().authType())
         .isEqualTo(IlpOverHttpLinkSettings.AuthType.JWT_HS_256);
-    assertThat(httpLinkSettings.incomingLinkSettings().get().tokenIssuer().get())
+    JwtAuthSettings incomingJwtAuthSettings = httpLinkSettings.incomingLinkSettings().get().jwtAuthSettings().get();
+    assertThat(incomingJwtAuthSettings.tokenIssuer().get())
         .isEqualTo(HttpUrl.parse("https://incoming-issuer.example.com/"));
-    assertThat(httpLinkSettings.incomingLinkSettings().get().tokenAudience().get())
-        .isEqualTo(HttpUrl.parse("https://incoming-audience.example.com/"));
-    assertThat(httpLinkSettings.incomingLinkSettings().get().encryptedTokenSharedSecret())
-        .isEqualTo("incoming-credential");
+    assertThat(incomingJwtAuthSettings.tokenAudience().get())
+        .isEqualTo("https://incoming-audience.example.com/");
+    assertThat(incomingJwtAuthSettings.encryptedTokenSharedSecret())
+        .isEqualTo(Optional.of("incoming-credential"));
     assertThat(httpLinkSettings.incomingLinkSettings().get().getMinMessageWindow()).isEqualTo(Duration.ofMillis(2500));
 
     assertThat(httpLinkSettings.outgoingLinkSettings().get().authType())
         .isEqualTo(IlpOverHttpLinkSettings.AuthType.JWT_HS_256);
-    assertThat(httpLinkSettings.outgoingLinkSettings().get().tokenIssuer().get())
+    JwtAuthSettings outgoingJwtAuthSettings = httpLinkSettings.outgoingLinkSettings().get().jwtAuthSettings().get();
+    assertThat(outgoingJwtAuthSettings.tokenIssuer().get())
         .isEqualTo(HttpUrl.parse("https://outgoing-issuer.example.com/"));
-    assertThat(httpLinkSettings.outgoingLinkSettings().get().tokenAudience().get())
-        .isEqualTo(HttpUrl.parse("https://outgoing-audience.example.com/"));
-    assertThat(httpLinkSettings.outgoingLinkSettings().get().tokenSubject()).isEqualTo("outgoing-subject");
-    assertThat(httpLinkSettings.outgoingLinkSettings().get().encryptedTokenSharedSecret())
-        .isEqualTo("outgoing-credential");
-    assertThat(httpLinkSettings.outgoingLinkSettings().get().tokenExpiry().get()).isEqualTo(Duration.ofHours(48));
+    assertThat(outgoingJwtAuthSettings.tokenAudience().get())
+        .isEqualTo("https://outgoing-audience.example.com/");
+    assertThat(outgoingJwtAuthSettings.tokenSubject()).isEqualTo("outgoing-subject");
+    assertThat(outgoingJwtAuthSettings.encryptedTokenSharedSecret())
+        .isEqualTo(Optional.of("outgoing-credential"));
+    assertThat(outgoingJwtAuthSettings.tokenExpiry().get()).isEqualTo(Duration.ofHours(24));
     assertThat(httpLinkSettings.outgoingLinkSettings().get().url())
         .isEqualTo(HttpUrl.parse("https://outgoing.example.com/"));
+
+    assertThat(httpLinkSettings.toCustomSettingsMap())
+        .isEqualTo(LinkSettingsUtils.flattenSettings(customSettings));
   }
 
   @Test
   public void testWithoutCustomSettings() {
     final IncomingLinkSettings incomingLinksettings =
         IncomingLinkSettings.builder()
-            .authType(IlpOverHttpLinkSettings.AuthType.SIMPLE)
-            .tokenIssuer(HttpUrl.parse("https://incoming-issuer.example.com/"))
-            .tokenAudience(HttpUrl.parse("https://incoming-audience.example.com/"))
             .minMessageWindow(Duration.ofMillis(30))
-            .encryptedTokenSharedSecret("incoming-credential")
+            .authType(IlpOverHttpLinkSettings.AuthType.SIMPLE)
+            .simpleAuthSettings(
+                SimpleAuthSettings.forAuthToken("incoming-credential")
+            )
             .build();
 
     final OutgoingLinkSettings outgoingLinksettings =
         OutgoingLinkSettings.builder()
             .authType(IlpOverHttpLinkSettings.AuthType.SIMPLE)
-            .tokenSubject("outgoing-subject")
-            .tokenIssuer(HttpUrl.parse("https://outgoing-issuer.example.com/"))
-            .tokenAudience(HttpUrl.parse("https://outgoing-audience.example.com/"))
-            .encryptedTokenSharedSecret("outgoing-credential")
-            .tokenExpiry(Duration.ofMillis(40))
+            .simpleAuthSettings(
+                SimpleAuthSettings.forAuthToken("outgoing-credential")
+            )
             .url(HttpUrl.parse("https://outgoing.example.com/"))
             .build();
 
@@ -116,27 +179,17 @@ public class IlpOverHttpLinkSettingsTest extends AbstractHttpLinkSettingsTest {
 
     assertThat(ilpOverHttpLinkSettings.incomingLinkSettings().get().authType())
         .isEqualTo(IlpOverHttpLinkSettings.AuthType.SIMPLE);
-    assertThat(ilpOverHttpLinkSettings.incomingLinkSettings().get().tokenIssuer().get())
-        .isEqualTo(HttpUrl.parse("https://incoming-issuer.example.com/"));
-    assertThat(ilpOverHttpLinkSettings.incomingLinkSettings().get().tokenAudience().get())
-        .isEqualTo(HttpUrl.parse("https://incoming-audience.example.com/"));
-    assertThat(ilpOverHttpLinkSettings.incomingLinkSettings().get().encryptedTokenSharedSecret())
+    assertThat(ilpOverHttpLinkSettings.incomingLinkSettings().get().simpleAuthSettings().get().authToken())
         .isEqualTo("incoming-credential");
     assertThat(ilpOverHttpLinkSettings.incomingLinkSettings().get().getMinMessageWindow())
         .isEqualTo(Duration.ofMillis(30));
 
     assertThat(ilpOverHttpLinkSettings.outgoingLinkSettings().get().authType())
         .isEqualTo(IlpOverHttpLinkSettings.AuthType.SIMPLE);
-    assertThat(ilpOverHttpLinkSettings.outgoingLinkSettings().get().tokenIssuer().get())
-        .isEqualTo(HttpUrl.parse("https://outgoing-issuer.example.com/"));
-    assertThat(ilpOverHttpLinkSettings.outgoingLinkSettings().get().tokenAudience().get())
-        .isEqualTo(HttpUrl.parse("https://outgoing-audience.example.com/"));
-    assertThat(ilpOverHttpLinkSettings.outgoingLinkSettings().get().tokenSubject()).isEqualTo("outgoing-subject");
-    assertThat(ilpOverHttpLinkSettings.outgoingLinkSettings().get().encryptedTokenSharedSecret())
+    assertThat(ilpOverHttpLinkSettings.outgoingLinkSettings().get().simpleAuthSettings().get().authToken())
         .isEqualTo("outgoing-credential");
-    assertThat(ilpOverHttpLinkSettings.outgoingLinkSettings().get().tokenExpiry().get())
-      .isEqualTo(Duration.ofMillis(40));
     assertThat(ilpOverHttpLinkSettings.outgoingLinkSettings().get().url())
         .isEqualTo(HttpUrl.parse("https://outgoing.example.com/"));
   }
+
 }
diff --git a/link-parent/link-ilp-over-http/src/test/java/org/interledger/link/http/IlpOverHttpLinkTest.java b/link-parent/link-ilp-over-http/src/test/java/org/interledger/link/http/IlpOverHttpLinkTest.java
index 2a77c42..7128b92 100644
--- a/link-parent/link-ilp-over-http/src/test/java/org/interledger/link/http/IlpOverHttpLinkTest.java
+++ b/link-parent/link-ilp-over-http/src/test/java/org/interledger/link/http/IlpOverHttpLinkTest.java
@@ -35,7 +35,6 @@ import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
 
 import java.io.IOException;
-import java.util.Optional;
 
 /**
  * Unit tests for {@link IlpOverHttpLink}.
@@ -47,12 +46,6 @@ public class IlpOverHttpLinkTest {
   @Rule
   public MockitoRule mockitoRule = MockitoJUnit.rule();
   @Mock
-  IlpOverHttpLinkSettings settings;
-  @Mock
-  OutgoingLinkSettings outgoingLinkSettings;
-  @Mock
-  IncomingLinkSettings incomingLinkSettings;
-  @Mock
   OkHttpClient httpClient;
   @Mock
   ObjectMapper objectMapper;
@@ -69,16 +62,13 @@ public class IlpOverHttpLinkTest {
   public void setup() {
     link = new IlpOverHttpLink(
         () -> InterledgerAddress.of("example.destination"),
-        settings,
+        HttpUrl.get("https://cannotspellsurgerywithouturges.com"),
         httpClient,
         objectMapper,
         codecContext,
         bearerTokenSupplier
     );
     link.setLinkId(LinkId.of("pepe silvia"));
-    when(settings.outgoingLinkSettings()).thenReturn(Optional.of(outgoingLinkSettings));
-    when(settings.incomingLinkSettings()).thenReturn(Optional.of(incomingLinkSettings));
-    when(outgoingLinkSettings.url()).thenReturn(HttpUrl.get("https://cannotspellsurgerywithouturges.com"));
     packet = mock(InterledgerPreparePacket.class);
   }
 
@@ -159,7 +149,6 @@ public class IlpOverHttpLinkTest {
     mockCall(406);
     expectedException.expect(LinkException.class);
     expectedException.expectMessage("Remote peer-link DOES NOT support ILP-over-HTTP. "
-        + "tokenSubject=null "
         + "url=https://cannotspellsurgerywithouturges.com/ "
         + "response=Response{"
         + "protocol=h2, "
@@ -175,7 +164,6 @@ public class IlpOverHttpLinkTest {
     mockCall(415);
     expectedException.expect(LinkException.class);
     expectedException.expectMessage("Remote peer-link DOES NOT support ILP-over-HTTP. "
-        + "tokenSubject=null "
         + "url=https://cannotspellsurgerywithouturges.com/ "
         + "response=Response{"
         + "protocol=h2, "
@@ -191,7 +179,6 @@ public class IlpOverHttpLinkTest {
     mockCall(422);
     expectedException.expect(LinkException.class);
     expectedException.expectMessage("Unable to connect to ILP-over-HTTP. "
-        + "tokenSubject=null "
         + "url=https://cannotspellsurgerywithouturges.com/ "
         + "response=Response{"
         + "protocol=h2, "
diff --git a/link-parent/link-ilp-over-http/src/test/java/org/interledger/link/http/IncomingIlpOverHttpLinkSettingsSettingsTest.java b/link-parent/link-ilp-over-http/src/test/java/org/interledger/link/http/IncomingIlpOverHttpLinkSettingsSettingsTest.java
index 770ed4a..774cf72 100644
--- a/link-parent/link-ilp-over-http/src/test/java/org/interledger/link/http/IncomingIlpOverHttpLinkSettingsSettingsTest.java
+++ b/link-parent/link-ilp-over-http/src/test/java/org/interledger/link/http/IncomingIlpOverHttpLinkSettingsSettingsTest.java
@@ -7,6 +7,7 @@ import org.junit.Test;
 
 import java.time.Duration;
 import java.util.Map;
+import java.util.Optional;
 
 /**
  * Unit tests for {@link IncomingLinkSettings}.
@@ -23,15 +24,16 @@ public class IncomingIlpOverHttpLinkSettingsSettingsTest extends AbstractHttpLin
    */
   @Test
   public void applyCustomSettingsWithFlatDottedNotation() {
-    final Map<String, Object> customSettings = this.customSettingsFlat();
+    IlpOverHttpLinkSettings.AuthType authType = IlpOverHttpLinkSettings.AuthType.JWT_HS_256;
+    final Map<String, Object> customSettings = this.customSettingsJwtFlat(authType);
     final IncomingLinkSettings incomingLinksettings = IncomingLinkSettings.fromCustomSettings(customSettings).build();
 
-    assertThat(incomingLinksettings.authType()).isEqualTo(IlpOverHttpLinkSettings.AuthType.JWT_HS_256);
-    assertThat(incomingLinksettings.tokenIssuer().get())
-      .isEqualTo(HttpUrl.parse("https://incoming-issuer.example.com/"));
-    assertThat(incomingLinksettings.tokenAudience().get())
-      .isEqualTo(HttpUrl.parse("https://incoming-audience.example.com/"));
-    assertThat(incomingLinksettings.encryptedTokenSharedSecret()).isEqualTo("incoming-credential");
+    assertThat(incomingLinksettings.authType()).isEqualTo(authType);
+    JwtAuthSettings jwtAuthSettings = incomingLinksettings.jwtAuthSettings().get();
+    assertThat(jwtAuthSettings.tokenIssuer().get())
+        .isEqualTo(HttpUrl.parse("https://incoming-issuer.example.com/"));
+    assertThat(jwtAuthSettings.tokenAudience().get()).isEqualTo("https://incoming-audience.example.com/");
+    assertThat(jwtAuthSettings.encryptedTokenSharedSecret()).isEqualTo(Optional.of("incoming-credential"));
     assertThat(incomingLinksettings.getMinMessageWindow()).isEqualTo(Duration.ofMillis(2500));
   }
 
@@ -41,15 +43,11 @@ public class IncomingIlpOverHttpLinkSettingsSettingsTest extends AbstractHttpLin
    */
   @Test
   public void applyCustomSettingsWithFlatDottedNotationSimpleAuth() {
-    final Map<String, Object> customSettings = this.customSettingsFlat(IlpOverHttpLinkSettings.AuthType.SIMPLE,
-      IlpOverHttpLinkSettings.AuthType.SIMPLE);
+    final Map<String, Object> customSettings = this.customSettingsSimpleFlat();
     final IncomingLinkSettings incomingLinksettings = IncomingLinkSettings.fromCustomSettings(customSettings).build();
 
     assertThat(incomingLinksettings.authType()).isEqualTo(IlpOverHttpLinkSettings.AuthType.SIMPLE);
-    assertThat(incomingLinksettings.tokenIssuer()).isEmpty();
-    assertThat(incomingLinksettings.tokenAudience()).isEmpty();
-    assertThat(incomingLinksettings.encryptedTokenSharedSecret()).isEqualTo("incoming-credential");
-    assertThat(incomingLinksettings.getMinMessageWindow()).isEqualTo(Duration.ofMillis(2500));
+    assertThat(incomingLinksettings.simpleAuthSettings().get().authToken()).isEqualTo("incoming-secret");
   }
 
   /**
@@ -57,15 +55,15 @@ public class IncomingIlpOverHttpLinkSettingsSettingsTest extends AbstractHttpLin
    */
   @Test
   public void applyCustomSettingsWithMapHierarchy() {
-    final Map<String, Object> customSettings = this.customSettingsHierarchical();
+    IlpOverHttpLinkSettings.AuthType authType = IlpOverHttpLinkSettings.AuthType.JWT_HS_256;
+    final Map<String, Object> customSettings = this.customSettingsJwtHierarchical(authType);
     final IncomingLinkSettings incomingLinksettings = IncomingLinkSettings.fromCustomSettings(customSettings).build();
 
-    assertThat(incomingLinksettings.authType()).isEqualTo(IlpOverHttpLinkSettings.AuthType.JWT_HS_256);
-    assertThat(incomingLinksettings.tokenIssuer().get())
-      .isEqualTo(HttpUrl.parse("https://incoming-issuer.example.com/"));
-    assertThat(incomingLinksettings.tokenAudience().get())
-      .isEqualTo(HttpUrl.parse("https://incoming-audience.example.com/"));
-    assertThat(incomingLinksettings.encryptedTokenSharedSecret()).isEqualTo("incoming-credential");
+    assertThat(incomingLinksettings.authType()).isEqualTo(authType);
+    JwtAuthSettings jwtAuthSettings = incomingLinksettings.jwtAuthSettings().get();
+    assertThat(jwtAuthSettings.tokenIssuer().get()).isEqualTo(HttpUrl.parse("https://incoming-issuer.example.com/"));
+    assertThat(jwtAuthSettings.tokenAudience().get()).isEqualTo("https://incoming-audience.example.com/");
+    assertThat(jwtAuthSettings.encryptedTokenSharedSecret()).isEqualTo(Optional.of("incoming-credential"));
     assertThat(incomingLinksettings.getMinMessageWindow()).isEqualTo(Duration.ofMillis(2500));
   }
 
@@ -74,15 +72,11 @@ public class IncomingIlpOverHttpLinkSettingsSettingsTest extends AbstractHttpLin
    */
   @Test
   public void applyCustomSettingsWithMapHierarchySimpleAuth() {
-    final Map<String, Object> customSettings = this.customSettingsHierarchical(IlpOverHttpLinkSettings.AuthType.SIMPLE,
-      IlpOverHttpLinkSettings.AuthType.SIMPLE);
+    final Map<String, Object> customSettings = this.customSettingsSimpleHierarchical();
     final IncomingLinkSettings incomingLinksettings = IncomingLinkSettings.fromCustomSettings(customSettings).build();
 
     assertThat(incomingLinksettings.authType()).isEqualTo(IlpOverHttpLinkSettings.AuthType.SIMPLE);
-    assertThat(incomingLinksettings.tokenIssuer()).isEmpty();
-    assertThat(incomingLinksettings.tokenAudience()).isEmpty();
-    assertThat(incomingLinksettings.encryptedTokenSharedSecret()).isEqualTo("incoming-credential");
-    assertThat(incomingLinksettings.getMinMessageWindow()).isEqualTo(Duration.ofMillis(2500));
+    assertThat(incomingLinksettings.simpleAuthSettings().get().authToken()).isEqualTo("incoming-secret");
   }
 
   @Test
@@ -90,18 +84,11 @@ public class IncomingIlpOverHttpLinkSettingsSettingsTest extends AbstractHttpLin
     final IncomingLinkSettings incomingLinksettings =
         IncomingLinkSettings.builder()
             .authType(IlpOverHttpLinkSettings.AuthType.SIMPLE)
-            .tokenIssuer(HttpUrl.parse("https://incoming-issuer.example.com"))
-            .tokenAudience(HttpUrl.parse("https://incoming-audience.example.com/"))
-            .encryptedTokenSharedSecret(SHH)
+            .simpleAuthSettings(SimpleAuthSettings.forAuthToken(SHH))
             .minMessageWindow(Duration.ofMillis(30))
             .build();
 
     assertThat(incomingLinksettings.authType()).isEqualTo(IlpOverHttpLinkSettings.AuthType.SIMPLE);
-    assertThat(incomingLinksettings.tokenIssuer().get())
-        .isEqualTo(HttpUrl.parse("https://incoming-issuer.example.com/"));
-    assertThat(incomingLinksettings.tokenAudience().get())
-        .isEqualTo(HttpUrl.parse("https://incoming-audience.example.com/"));
-    assertThat(incomingLinksettings.encryptedTokenSharedSecret()).isEqualTo(SHH);
-    assertThat(incomingLinksettings.getMinMessageWindow()).isEqualTo(Duration.ofMillis(30));
+    assertThat(incomingLinksettings.simpleAuthSettings().get().authToken()).isEqualTo(SHH);
   }
 }
diff --git a/link-parent/link-ilp-over-http/src/test/java/org/interledger/link/http/IncomingLinkSettingsTest.java b/link-parent/link-ilp-over-http/src/test/java/org/interledger/link/http/IncomingLinkSettingsTest.java
new file mode 100644
index 0000000..df71c06
--- /dev/null
+++ b/link-parent/link-ilp-over-http/src/test/java/org/interledger/link/http/IncomingLinkSettingsTest.java
@@ -0,0 +1,36 @@
+package org.interledger.link.http;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+public class IncomingLinkSettingsTest {
+
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  @Test
+  public void validateSimpleAuthSettingsRequired() {
+    expectedException.expectMessage("simpleAuthSettings required");
+    IncomingLinkSettings.builder()
+        .authType(IlpOverHttpLinkSettings.AuthType.SIMPLE)
+        .build();
+  }
+
+  @Test
+  public void validateJwtSettingsRequiredForHS256() {
+    expectedException.expectMessage("jwtAuthSettings required");
+    IncomingLinkSettings.builder()
+        .authType(IlpOverHttpLinkSettings.AuthType.JWT_HS_256)
+        .build();
+  }
+
+  @Test
+  public void validateJwtSettingsRequiredForRS256() {
+    expectedException.expectMessage("jwtAuthSettings required");
+    IncomingLinkSettings.builder()
+        .authType(IlpOverHttpLinkSettings.AuthType.JWT_RS_256)
+        .build();
+  }
+
+}
\ No newline at end of file
diff --git a/link-parent/link-ilp-over-http/src/test/java/org/interledger/link/http/LinkSettingsUtilsTest.java b/link-parent/link-ilp-over-http/src/test/java/org/interledger/link/http/LinkSettingsUtilsTest.java
new file mode 100644
index 0000000..dc29084
--- /dev/null
+++ b/link-parent/link-ilp-over-http/src/test/java/org/interledger/link/http/LinkSettingsUtilsTest.java
@@ -0,0 +1,116 @@
+package org.interledger.link.http;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import com.google.common.collect.ImmutableMap;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+public class LinkSettingsUtilsTest {
+
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  ImmutableMap<String, Object> flattenedSettings = ImmutableMap.<String, Object>builder()
+      .put("parent.name", "foo")
+      .put("parent.age", 100)
+      .put("parent.son.name", "bar")
+      .put("parent.son.age", 50)
+      .put("parent.son.grandson.name", "fizz")
+      .put("parent.son.grandson.age", 15)
+      .put("parent.son.granddaughter.name", "buzz")
+      .put("parent.son.granddaughter.age", 20)
+      .build();
+
+  @Test
+  public void flattenSettingsAlreadyFlattened() {
+    assertThat(LinkSettingsUtils.flattenSettings(flattenedSettings)).isEqualTo(flattenedSettings);
+  }
+
+  @Test
+  public void flattenSettings() {
+    Map<String, Object> grandsonSettings = ImmutableMap.of("name", "fizz", "age", 15);
+    Map<String, Object> grandDaughterSettings = ImmutableMap.of("name", "buzz", "age", 20);
+    Map<String, Object> sonSettings = ImmutableMap.of("name", "bar", "age", 50,
+        "grandson", grandsonSettings,
+        "granddaughter", grandDaughterSettings);
+    Map<String, Object> parentSettings = ImmutableMap.of("name", "foo", "age", 100,
+        "son", sonSettings);
+
+    Map<String, Object> settings = ImmutableMap.of("parent", parentSettings);
+
+    assertThat(LinkSettingsUtils.flattenSettings(settings)).isEqualTo(flattenedSettings);
+  }
+
+  @Test
+  public void getIncomingAuthTypeWithSimple() {
+    assertThat(LinkSettingsUtils.getIncomingAuthType(AbstractHttpLinkSettingsTest.customSettingsSimpleFlat()))
+        .isEqualTo(Optional.of(IlpOverHttpLinkSettings.AuthType.SIMPLE));
+    assertThat(LinkSettingsUtils.getIncomingAuthType(AbstractHttpLinkSettingsTest.customSettingsSimpleHierarchical()))
+        .isEqualTo(Optional.of(IlpOverHttpLinkSettings.AuthType.SIMPLE));
+  }
+
+  @Test
+  public void getOutgoingAuthTypeWithSimple() {
+    assertThat(LinkSettingsUtils.getOutgoingAuthType(AbstractHttpLinkSettingsTest.customSettingsSimpleFlat()))
+        .isEqualTo(Optional.of(IlpOverHttpLinkSettings.AuthType.SIMPLE));
+    assertThat(LinkSettingsUtils.getOutgoingAuthType(AbstractHttpLinkSettingsTest.customSettingsSimpleHierarchical()))
+        .isEqualTo(Optional.of(IlpOverHttpLinkSettings.AuthType.SIMPLE));
+  }
+
+  @Test
+  public void getIncomingAuthTypeWithJwtHs256() {
+    IlpOverHttpLinkSettings.AuthType authType = IlpOverHttpLinkSettings.AuthType.JWT_HS_256;
+    assertThat(
+        LinkSettingsUtils.getIncomingAuthType(AbstractHttpLinkSettingsTest.customSettingsJwtFlat(authType)))
+        .isEqualTo(Optional.of(authType));
+    assertThat(
+        LinkSettingsUtils.getIncomingAuthType(AbstractHttpLinkSettingsTest.customSettingsJwtHierarchical(authType)))
+        .isEqualTo(Optional.of(authType));
+  }
+
+  @Test
+  public void getOutgoingAuthTypeWithJwtHs256() {
+    IlpOverHttpLinkSettings.AuthType authType = IlpOverHttpLinkSettings.AuthType.JWT_HS_256;
+    assertThat(
+        LinkSettingsUtils.getOutgoingAuthType(AbstractHttpLinkSettingsTest.customSettingsJwtFlat(authType)))
+        .isEqualTo(Optional.of(authType));
+    assertThat(
+        LinkSettingsUtils.getOutgoingAuthType(AbstractHttpLinkSettingsTest.customSettingsJwtHierarchical(authType)))
+        .isEqualTo(Optional.of(authType));
+  }
+
+  @Test
+  public void getIncomingAuthTypeWithJwtRs256() {
+    IlpOverHttpLinkSettings.AuthType authType = IlpOverHttpLinkSettings.AuthType.JWT_RS_256;
+    assertThat(
+        LinkSettingsUtils.getIncomingAuthType(AbstractHttpLinkSettingsTest.customSettingsJwtFlat(authType)))
+        .isEqualTo(Optional.of(authType));
+    assertThat(
+        LinkSettingsUtils.getIncomingAuthType(AbstractHttpLinkSettingsTest.customSettingsJwtHierarchical(authType)))
+        .isEqualTo(Optional.of(authType));
+  }
+
+  @Test
+  public void getOutgoingAuthTypeWithJwtRs256() {
+    IlpOverHttpLinkSettings.AuthType authType = IlpOverHttpLinkSettings.AuthType.JWT_RS_256;
+    assertThat(
+        LinkSettingsUtils.getOutgoingAuthType(AbstractHttpLinkSettingsTest.customSettingsJwtFlat(authType)))
+        .isEqualTo(Optional.of(authType));
+    assertThat(
+        LinkSettingsUtils.getOutgoingAuthType(AbstractHttpLinkSettingsTest.customSettingsJwtHierarchical(authType)))
+        .isEqualTo(Optional.of(authType));
+  }
+
+  @Test
+  public void getAuthTypeWithMissing() {
+    assertThat(LinkSettingsUtils.getIncomingAuthType(new HashMap<>())).isEqualTo(Optional.empty());
+    assertThat(LinkSettingsUtils.getOutgoingAuthType(new HashMap<>())).isEqualTo(Optional.empty());
+  }
+
+}
\ No newline at end of file
diff --git a/link-parent/link-ilp-over-http/src/test/java/org/interledger/link/http/OutgoingIlpOverHttpLinkSettingsSettingsTest.java b/link-parent/link-ilp-over-http/src/test/java/org/interledger/link/http/OutgoingIlpOverHttpLinkSettingsSettingsTest.java
index c2b8281..956b993 100644
--- a/link-parent/link-ilp-over-http/src/test/java/org/interledger/link/http/OutgoingIlpOverHttpLinkSettingsSettingsTest.java
+++ b/link-parent/link-ilp-over-http/src/test/java/org/interledger/link/http/OutgoingIlpOverHttpLinkSettingsSettingsTest.java
@@ -7,6 +7,7 @@ import org.junit.Test;
 
 import java.time.Duration;
 import java.util.Map;
+import java.util.Optional;
 
 /**
  * Unit tests for {@link OutgoingLinkSettings}.
@@ -18,17 +19,19 @@ public class OutgoingIlpOverHttpLinkSettingsSettingsTest extends AbstractHttpLin
    */
   @Test
   public void applyCustomSettingsWithFlatDottedNotation() {
-    final Map<String, Object> customSettings = this.customSettingsFlat();
+    IlpOverHttpLinkSettings.AuthType authType = IlpOverHttpLinkSettings.AuthType.JWT_HS_256;
+
+    final Map<String, Object> customSettings = this.customSettingsJwtFlat(authType);
     final OutgoingLinkSettings outgoingLinksettings = OutgoingLinkSettings.fromCustomSettings(customSettings).build();
 
-    assertThat(outgoingLinksettings.authType()).isEqualTo(IlpOverHttpLinkSettings.AuthType.JWT_HS_256);
-    assertThat(outgoingLinksettings.tokenIssuer().get())
+    assertThat(outgoingLinksettings.authType()).isEqualTo(authType);
+    JwtAuthSettings jwtAuthSettings = outgoingLinksettings.jwtAuthSettings().get();
+    assertThat(jwtAuthSettings.tokenIssuer().get())
         .isEqualTo(HttpUrl.parse("https://outgoing-issuer.example.com/"));
-    assertThat(outgoingLinksettings.tokenAudience().get())
-        .isEqualTo(HttpUrl.parse("https://outgoing-audience.example.com/"));
-    assertThat(outgoingLinksettings.tokenSubject()).isEqualTo("outgoing-subject");
-    assertThat(outgoingLinksettings.encryptedTokenSharedSecret()).isEqualTo("outgoing-credential");
-    assertThat(outgoingLinksettings.tokenExpiry().get()).isEqualTo(Duration.ofHours(24));
+    assertThat(jwtAuthSettings.tokenAudience().get()).isEqualTo("https://outgoing-audience.example.com/");
+    assertThat(jwtAuthSettings.tokenSubject()).isEqualTo("outgoing-subject");
+    assertThat(jwtAuthSettings.encryptedTokenSharedSecret()).isEqualTo(Optional.of("outgoing-credential"));
+    assertThat(jwtAuthSettings.tokenExpiry().get()).isEqualTo(Duration.ofHours(24));
     assertThat(outgoingLinksettings.url()).isEqualTo(HttpUrl.parse("https://outgoing.example.com/"));
   }
 
@@ -38,16 +41,11 @@ public class OutgoingIlpOverHttpLinkSettingsSettingsTest extends AbstractHttpLin
    */
   @Test
   public void applyCustomSettingsWithFlatDottedNotationWithSimpleAuth() {
-    final Map<String, Object> customSettings = this.customSettingsFlat(IlpOverHttpLinkSettings.AuthType.SIMPLE,
-      IlpOverHttpLinkSettings.AuthType.SIMPLE);
+    final Map<String, Object> customSettings = this.customSettingsSimpleFlat();
     final OutgoingLinkSettings outgoingLinksettings = OutgoingLinkSettings.fromCustomSettings(customSettings).build();
 
     assertThat(outgoingLinksettings.authType()).isEqualTo(IlpOverHttpLinkSettings.AuthType.SIMPLE);
-    assertThat(outgoingLinksettings.tokenIssuer()).isEmpty();
-    assertThat(outgoingLinksettings.tokenAudience()).isEmpty();
-    assertThat(outgoingLinksettings.tokenSubject()).isEqualTo("outgoing-subject");
-    assertThat(outgoingLinksettings.encryptedTokenSharedSecret()).isEqualTo("outgoing-credential");
-    assertThat(outgoingLinksettings.tokenExpiry().get()).isEqualTo(Duration.ofHours(24));
+    assertThat(outgoingLinksettings.simpleAuthSettings().get().authToken()).isEqualTo("outgoing-secret");
     assertThat(outgoingLinksettings.url()).isEqualTo(HttpUrl.parse("https://outgoing.example.com/"));
   }
 
@@ -56,17 +54,18 @@ public class OutgoingIlpOverHttpLinkSettingsSettingsTest extends AbstractHttpLin
    */
   @Test
   public void applyCustomSettingsWithMapHierarchy() {
-    final Map<String, Object> customSettings = this.customSettingsHierarchical();
+    IlpOverHttpLinkSettings.AuthType authType = IlpOverHttpLinkSettings.AuthType.JWT_HS_256;
+    final Map<String, Object> customSettings = this.customSettingsJwtHierarchical(authType);
     final OutgoingLinkSettings outgoingLinksettings = OutgoingLinkSettings.fromCustomSettings(customSettings).build();
 
-    assertThat(outgoingLinksettings.authType()).isEqualTo(IlpOverHttpLinkSettings.AuthType.JWT_HS_256);
-    assertThat(outgoingLinksettings.tokenIssuer().get())
+    assertThat(outgoingLinksettings.authType()).isEqualTo(authType);
+    JwtAuthSettings jwtAuthSettings = outgoingLinksettings.jwtAuthSettings().get();
+    assertThat(jwtAuthSettings.tokenIssuer().get())
         .isEqualTo(HttpUrl.parse("https://outgoing-issuer.example.com/"));
-    assertThat(outgoingLinksettings.tokenAudience().get())
-        .isEqualTo(HttpUrl.parse("https://outgoing-audience.example.com/"));
-    assertThat(outgoingLinksettings.tokenSubject()).isEqualTo("outgoing-subject");
-    assertThat(outgoingLinksettings.encryptedTokenSharedSecret()).isEqualTo("outgoing-credential");
-    assertThat(outgoingLinksettings.tokenExpiry().get()).isEqualTo(Duration.ofHours(48));
+    assertThat(jwtAuthSettings.tokenAudience().get()).isEqualTo("https://outgoing-audience.example.com/");
+    assertThat(jwtAuthSettings.tokenSubject()).isEqualTo("outgoing-subject");
+    assertThat(jwtAuthSettings.encryptedTokenSharedSecret()).isEqualTo(Optional.of("outgoing-credential"));
+    assertThat(jwtAuthSettings.tokenExpiry().get()).isEqualTo(Duration.ofHours(48));
     assertThat(outgoingLinksettings.url()).isEqualTo(HttpUrl.parse("https://outgoing.example.com"));
   }
 
@@ -75,40 +74,40 @@ public class OutgoingIlpOverHttpLinkSettingsSettingsTest extends AbstractHttpLin
    */
   @Test
   public void applyCustomSettingsWithMapHierarchyWithSimpleAuth() {
-    final Map<String, Object> customSettings = this.customSettingsHierarchical(IlpOverHttpLinkSettings.AuthType.SIMPLE,
-      IlpOverHttpLinkSettings.AuthType.SIMPLE);
+    final Map<String, Object> customSettings = this.customSettingsSimpleHierarchical();
     final OutgoingLinkSettings outgoingLinksettings = OutgoingLinkSettings.fromCustomSettings(customSettings).build();
 
     assertThat(outgoingLinksettings.authType()).isEqualTo(IlpOverHttpLinkSettings.AuthType.SIMPLE);
-    assertThat(outgoingLinksettings.tokenIssuer()).isEmpty();
-    assertThat(outgoingLinksettings.tokenAudience()).isEmpty();
-    assertThat(outgoingLinksettings.tokenSubject()).isEqualTo("outgoing-subject");
-    assertThat(outgoingLinksettings.encryptedTokenSharedSecret()).isEqualTo("outgoing-credential");
-    assertThat(outgoingLinksettings.tokenExpiry().get()).isEqualTo(Duration.ofHours(48));
+    assertThat(outgoingLinksettings.simpleAuthSettings().get().authToken()).isEqualTo("outgoing-secret");
     assertThat(outgoingLinksettings.url()).isEqualTo(HttpUrl.parse("https://outgoing.example.com"));
   }
 
   @Test
   public void testWithoutCustomSettings() {
+    IlpOverHttpLinkSettings.AuthType authType = IlpOverHttpLinkSettings.AuthType.JWT_HS_256;
+
     final OutgoingLinkSettings outgoingLinksettings =
         OutgoingLinkSettings.builder()
-            .authType(IlpOverHttpLinkSettings.AuthType.SIMPLE)
-            .tokenIssuer(HttpUrl.parse("https://outgoing-issuer.example.com"))
-            .tokenAudience(HttpUrl.parse("https://outgoing-audience.example.com"))
-            .tokenSubject("outgoing-subject")
-            .tokenExpiry(Duration.ofMillis(30))
-            .encryptedTokenSharedSecret("outgoing-credential")
+            .authType(IlpOverHttpLinkSettings.AuthType.JWT_HS_256)
+            .jwtAuthSettings(JwtAuthSettings.builder()
+                .tokenIssuer(HttpUrl.parse("https://outgoing-issuer.example.com"))
+                .tokenAudience("https://outgoing-audience.example.com")
+                .tokenSubject("outgoing-subject")
+                .tokenExpiry(Duration.ofMillis(30))
+                .encryptedTokenSharedSecret("outgoing-credential")
+                .build()
+            )
             .url(HttpUrl.parse("https://outgoing.example.com"))
             .build();
 
-    assertThat(outgoingLinksettings.authType()).isEqualTo(IlpOverHttpLinkSettings.AuthType.SIMPLE);
-    assertThat(outgoingLinksettings.tokenIssuer().get())
+    assertThat(outgoingLinksettings.authType()).isEqualTo(authType);
+    JwtAuthSettings jwtAuthSettings = outgoingLinksettings.jwtAuthSettings().get();
+    assertThat(jwtAuthSettings.tokenIssuer().get())
         .isEqualTo(HttpUrl.parse("https://outgoing-issuer.example.com"));
-    assertThat(outgoingLinksettings.tokenAudience().get())
-        .isEqualTo(HttpUrl.parse("https://outgoing-audience.example.com"));
-    assertThat(outgoingLinksettings.tokenSubject()).isEqualTo("outgoing-subject");
-    assertThat(outgoingLinksettings.tokenExpiry().get()).isEqualTo(Duration.ofMillis(30));
-    assertThat(outgoingLinksettings.encryptedTokenSharedSecret()).isEqualTo("outgoing-credential");
+    assertThat(jwtAuthSettings.tokenAudience().get()).isEqualTo("https://outgoing-audience.example.com");
+    assertThat(jwtAuthSettings.tokenSubject()).isEqualTo("outgoing-subject");
+    assertThat(jwtAuthSettings.tokenExpiry().get()).isEqualTo(Duration.ofMillis(30));
+    assertThat(jwtAuthSettings.encryptedTokenSharedSecret()).isEqualTo(Optional.of("outgoing-credential"));
     assertThat(outgoingLinksettings.url()).isEqualTo(HttpUrl.parse("https://outgoing.example.com"));
   }
 }
diff --git a/link-parent/link-ilp-over-http/src/test/java/org/interledger/link/http/OutgoingLinkSettingsTest.java b/link-parent/link-ilp-over-http/src/test/java/org/interledger/link/http/OutgoingLinkSettingsTest.java
new file mode 100644
index 0000000..3dfa69c
--- /dev/null
+++ b/link-parent/link-ilp-over-http/src/test/java/org/interledger/link/http/OutgoingLinkSettingsTest.java
@@ -0,0 +1,40 @@
+package org.interledger.link.http;
+
+import okhttp3.HttpUrl;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+public class OutgoingLinkSettingsTest {
+
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  @Test
+  public void validateSimpleAuthSettingsRequired() {
+    expectedException.expectMessage("simpleAuthSettings required");
+    OutgoingLinkSettings.builder()
+        .url(HttpUrl.parse("http://test.com"))
+        .authType(IlpOverHttpLinkSettings.AuthType.SIMPLE)
+        .build();
+  }
+
+  @Test
+  public void validateJwtSettingsRequiredForHS256() {
+    expectedException.expectMessage("jwtAuthSettings required");
+    OutgoingLinkSettings.builder()
+        .url(HttpUrl.parse("http://test.com"))
+        .authType(IlpOverHttpLinkSettings.AuthType.JWT_HS_256)
+        .build();
+  }
+
+  @Test
+  public void validateJwtSettingsRequiredForRS256() {
+    expectedException.expectMessage("jwtAuthSettings required");
+    OutgoingLinkSettings.builder()
+        .url(HttpUrl.parse("http://test.com"))
+        .authType(IlpOverHttpLinkSettings.AuthType.JWT_RS_256)
+        .build();
+  }
+
+}
\ No newline at end of file
diff --git a/link-parent/link-ilp-over-http/src/test/java/org/interledger/link/http/auth/JwtHs256BearerTokenSupplierTest.java b/link-parent/link-ilp-over-http/src/test/java/org/interledger/link/http/auth/JwtHs256BearerTokenSupplierTest.java
index 27211e5..dad24e3 100644
--- a/link-parent/link-ilp-over-http/src/test/java/org/interledger/link/http/auth/JwtHs256BearerTokenSupplierTest.java
+++ b/link-parent/link-ilp-over-http/src/test/java/org/interledger/link/http/auth/JwtHs256BearerTokenSupplierTest.java
@@ -5,6 +5,7 @@ import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import org.interledger.link.http.IlpOverHttpLinkSettings;
+import org.interledger.link.http.JwtAuthSettings;
 import org.interledger.link.http.OutgoingLinkSettings;
 
 import okhttp3.HttpUrl;
@@ -62,11 +63,15 @@ public class JwtHs256BearerTokenSupplierTest {
   private OutgoingLinkSettings createOutgoingSettings(Duration duration) {
     return OutgoingLinkSettings.builder()
         .authType(IlpOverHttpLinkSettings.AuthType.JWT_HS_256)
-        .encryptedTokenSharedSecret("i am a terrible bowler")
-        .tokenAudience(HttpUrl.get("https://www.ripple.com"))
-        .tokenExpiry(duration)
-        .tokenIssuer(HttpUrl.get("https://www.ripple.com"))
-        .tokenSubject("about last night")
+        .jwtAuthSettings(
+            JwtAuthSettings.builder()
+                .encryptedTokenSharedSecret("i am a terrible bowler")
+                .tokenAudience("https://www.ripple.com")
+                .tokenExpiry(duration)
+                .tokenIssuer(HttpUrl.get("https://www.ripple.com"))
+                .tokenSubject("about last night")
+                .build()
+        )
         .url(HttpUrl.get("https://www.ripple.com"))
         .build();
   }
diff --git a/stream-parent/stream-client/src/test/java/org/interledger/stream/sender/SimpleStreamSenderIT.java b/stream-parent/stream-client/src/test/java/org/interledger/stream/sender/SimpleStreamSenderIT.java
index f3e4519..3b066c9 100644
--- a/stream-parent/stream-client/src/test/java/org/interledger/stream/sender/SimpleStreamSenderIT.java
+++ b/stream-parent/stream-client/src/test/java/org/interledger/stream/sender/SimpleStreamSenderIT.java
@@ -8,10 +8,6 @@ import org.interledger.core.InterledgerAddress;
 import org.interledger.link.Link;
 import org.interledger.link.LinkId;
 import org.interledger.link.http.IlpOverHttpLink;
-import org.interledger.link.http.IlpOverHttpLinkSettings;
-import org.interledger.link.http.IlpOverHttpLinkSettings.AuthType;
-import org.interledger.link.http.IncomingLinkSettings;
-import org.interledger.link.http.OutgoingLinkSettings;
 import org.interledger.link.http.auth.SimpleBearerTokenSupplier;
 import org.interledger.spsp.PaymentPointer;
 import org.interledger.spsp.StreamConnectionDetails;
@@ -96,9 +92,9 @@ public class SimpleStreamSenderIT {
       .withExposedPorts(7770)
       .withNetwork(network)
       //.withLogConsumer(new org.testcontainers.containers.output.Slf4jLogConsumer (logger)) // uncomment to see logs
-      .withEnv("ILP_REDIS_URL", "redis://redis:6379")
       .withCommand(""
           + "--admin_auth_token " + AUTH_TOKEN + " "
+          + "--database_url redis://redis:6379 "
           + "--ilp_address " + HOST_ADDRESS.getValue() + " "
           + "--secret_seed 9dce76b1a20ec8d3db05ad579f3293402743767692f935a0bf06b30d2728439d "
           + "--http_bind_address 0.0.0.0:7770"
@@ -130,22 +126,9 @@ public class SimpleStreamSenderIT {
         SpspClientDefaults.MAPPER
     );
 
-    final IlpOverHttpLinkSettings linkSettings = IlpOverHttpLinkSettings.builder()
-        .incomingHttpLinkSettings(IncomingLinkSettings.builder()
-            .authType(AuthType.SIMPLE)
-            .encryptedTokenSharedSecret(AUTH_TOKEN)
-            .build())
-        .outgoingHttpLinkSettings(OutgoingLinkSettings.builder()
-            .authType(AuthType.SIMPLE)
-            .tokenSubject(SENDER_ACCOUNT_USERNAME)
-            .url(this.constructIlpOverHttpUrl(SENDER_ACCOUNT_USERNAME))
-            .encryptedTokenSharedSecret(AUTH_TOKEN)
-            .build())
-        .build();
-
     this.link = new IlpOverHttpLink(
         () -> SENDER_ADDRESS,
-        linkSettings,
+        this.constructIlpOverHttpUrl(SENDER_ACCOUNT_USERNAME),
         httpClient,
         SpspClientDefaults.MAPPER,
         InterledgerCodecContextFactory.oer(),
@@ -516,22 +499,10 @@ public class SimpleStreamSenderIT {
     final String connectorAccountUsername = UUID.randomUUID().toString().replace("-", "");
 
     final OkHttpClient httpClient = this.constructOkHttpClient();
-    final IlpOverHttpLinkSettings linkSettings = IlpOverHttpLinkSettings.builder()
-        .incomingHttpLinkSettings(IncomingLinkSettings.builder()
-            .authType(AuthType.SIMPLE)
-            .encryptedTokenSharedSecret(AUTH_TOKEN)
-            .build())
-        .outgoingHttpLinkSettings(OutgoingLinkSettings.builder()
-            .authType(AuthType.SIMPLE)
-            .tokenSubject(connectorAccountUsername)
-            .url(this.constructIlpOverHttpUrl(SENDER_ACCOUNT_USERNAME))
-            .encryptedTokenSharedSecret("wrong-password")
-            .build())
-        .build();
 
     this.link = new IlpOverHttpLink(
         () -> SENDER_ADDRESS,
-        linkSettings,
+        this.constructIlpOverHttpUrl(SENDER_ACCOUNT_USERNAME),
         httpClient,
         SpspClientDefaults.MAPPER,
         InterledgerCodecContextFactory.oer(),