KNOX-2390 - Let end-users configure SAML2 client configuration using Pac4J provider parameters (#348)

diff --git a/gateway-provider-security-pac4j/src/main/java/org/apache/knox/gateway/pac4j/config/AzureADClientConfigurationDecorator.java b/gateway-provider-security-pac4j/src/main/java/org/apache/knox/gateway/pac4j/config/AzureADClientConfigurationDecorator.java
new file mode 100644
index 0000000..5021536
--- /dev/null
+++ b/gateway-provider-security-pac4j/src/main/java/org/apache/knox/gateway/pac4j/config/AzureADClientConfigurationDecorator.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.knox.gateway.pac4j.config;
+
+import java.util.List;
+import java.util.Map;
+
+import org.pac4j.core.client.Client;
+import org.pac4j.core.http.callback.PathParameterCallbackUrlResolver;
+import org.pac4j.oidc.client.AzureAdClient;
+
+public class AzureADClientConfigurationDecorator implements ClientConfigurationDecorator {
+  private static final String AZURE_AD_CLIENT_CLASS_NAME = AzureAdClient.class.getSimpleName();
+
+  @Override
+  public void decorateClients(List<Client> clients, Map<String, String> properties) {
+    for (Client client : clients) {
+      if (AZURE_AD_CLIENT_CLASS_NAME.equalsIgnoreCase(client.getName())) {
+        // special handling for Azure AD, use path separators instead of query params
+        ((AzureAdClient) client).setCallbackUrlResolver(new PathParameterCallbackUrlResolver());
+      }
+    }
+  }
+
+}
diff --git a/gateway-provider-security-pac4j/src/main/java/org/apache/knox/gateway/pac4j/config/ClientConfigurationDecorator.java b/gateway-provider-security-pac4j/src/main/java/org/apache/knox/gateway/pac4j/config/ClientConfigurationDecorator.java
new file mode 100644
index 0000000..91bb099
--- /dev/null
+++ b/gateway-provider-security-pac4j/src/main/java/org/apache/knox/gateway/pac4j/config/ClientConfigurationDecorator.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.knox.gateway.pac4j.config;
+
+import java.util.List;
+import java.util.Map;
+
+import org.pac4j.core.client.Client;
+
+/**
+ * Defines the contract of decorating different type of Pac4J client configurations
+ */
+public interface ClientConfigurationDecorator {
+
+  /**
+   * Decorates the given clients' configuration using the given properties (if applicable)
+   *
+   * @param clients
+   *          the client, whose configuration should be decorated
+   * @param properties
+   *          the properties which may contain the required information to decorate the clients
+   */
+  void decorateClients(List<Client> clients, Map<String, String> properties);
+
+}
diff --git a/gateway-provider-security-pac4j/src/main/java/org/apache/knox/gateway/pac4j/config/Pac4jClientConfigurationDecorator.java b/gateway-provider-security-pac4j/src/main/java/org/apache/knox/gateway/pac4j/config/Pac4jClientConfigurationDecorator.java
new file mode 100644
index 0000000..8039c46
--- /dev/null
+++ b/gateway-provider-security-pac4j/src/main/java/org/apache/knox/gateway/pac4j/config/Pac4jClientConfigurationDecorator.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.knox.gateway.pac4j.config;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+import org.pac4j.core.client.Client;
+
+public class Pac4jClientConfigurationDecorator implements ClientConfigurationDecorator {
+
+  private static final List<ClientConfigurationDecorator> DEFAULT_DECORATORS = Arrays.asList(new SAML2ClientConfigurationDecorator(), new AzureADClientConfigurationDecorator());
+  private final List<ClientConfigurationDecorator> decorators;
+
+  public Pac4jClientConfigurationDecorator() {
+    this(DEFAULT_DECORATORS);
+  }
+
+  // package protected so that it's visible in unit tests
+  Pac4jClientConfigurationDecorator(List<ClientConfigurationDecorator> decorators) {
+    this.decorators = decorators;
+  }
+
+  @Override
+  public void decorateClients(List<Client> clients, Map<String, String> properties) {
+    decorators.forEach(decorator -> decorator.decorateClients(clients, properties));
+  }
+
+}
diff --git a/gateway-provider-security-pac4j/src/main/java/org/apache/knox/gateway/pac4j/config/SAML2ClientConfigurationDecorator.java b/gateway-provider-security-pac4j/src/main/java/org/apache/knox/gateway/pac4j/config/SAML2ClientConfigurationDecorator.java
new file mode 100644
index 0000000..1444890
--- /dev/null
+++ b/gateway-provider-security-pac4j/src/main/java/org/apache/knox/gateway/pac4j/config/SAML2ClientConfigurationDecorator.java
@@ -0,0 +1,75 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.knox.gateway.pac4j.config;
+
+import java.util.List;
+import java.util.Map;
+
+import org.apache.commons.lang3.StringUtils;
+import org.pac4j.core.client.Client;
+import org.pac4j.saml.client.SAML2Client;
+
+public class SAML2ClientConfigurationDecorator implements ClientConfigurationDecorator {
+
+  private static final String SAML2_CLIENT_CLASS_NAME = SAML2Client.class.getSimpleName();
+  private static final String CONFIG_NAME_USE_NAME_QUALIFIER = "useNameQualifier";
+  private static final String CONFIG_NAME_USE_FORCE_AUTH = "forceAuth";
+  private static final String CONFIG_NAME_USE_PASSIVE = "passive";
+  private static final String CONFIG_NAME_NAMEID_POLICY_FORMAT = "nameIdPolicyFormat";
+
+  @Override
+  public void decorateClients(List<Client> clients, Map<String, String> properties) {
+    for (Client client : clients) {
+      if (SAML2_CLIENT_CLASS_NAME.equalsIgnoreCase(client.getName())) {
+        final SAML2Client saml2Client = (SAML2Client) client;
+        setUseNameQualifierFlag(properties, saml2Client);
+        setForceAuthFlag(properties, saml2Client);
+        setPassiveFlag(properties, saml2Client);
+        setNameIdPolicyFormat(properties, saml2Client);
+      }
+    }
+  }
+
+  private void setUseNameQualifierFlag(Map<String, String> properties, final SAML2Client saml2Client) {
+    final String useNameQualifier = properties.get(CONFIG_NAME_USE_NAME_QUALIFIER);
+    if (StringUtils.isNotBlank(useNameQualifier)) {
+      saml2Client.getConfiguration().setUseNameQualifier(Boolean.valueOf(useNameQualifier));
+    }
+  }
+
+  private void setForceAuthFlag(Map<String, String> properties, final SAML2Client saml2Client) {
+    final String forceAuth = properties.get(CONFIG_NAME_USE_FORCE_AUTH);
+    if (StringUtils.isNotBlank(forceAuth)) {
+      saml2Client.getConfiguration().setForceAuth(Boolean.valueOf(forceAuth));
+    }
+  }
+
+  private void setPassiveFlag(Map<String, String> properties, final SAML2Client saml2Client) {
+    final String passive = properties.get(CONFIG_NAME_USE_PASSIVE);
+    if (StringUtils.isNotBlank(passive)) {
+      saml2Client.getConfiguration().setPassive(Boolean.valueOf(passive));
+    }
+  }
+
+  private void setNameIdPolicyFormat(Map<String, String> properties, final SAML2Client saml2Client) {
+    final String nameIdPolicyFormat = properties.get(CONFIG_NAME_NAMEID_POLICY_FORMAT);
+    if (StringUtils.isNotBlank(nameIdPolicyFormat)) {
+      saml2Client.getConfiguration().setNameIdPolicyFormat(nameIdPolicyFormat);
+    }
+  }
+}
diff --git a/gateway-provider-security-pac4j/src/main/java/org/apache/knox/gateway/pac4j/filter/Pac4jDispatcherFilter.java b/gateway-provider-security-pac4j/src/main/java/org/apache/knox/gateway/pac4j/filter/Pac4jDispatcherFilter.java
index 4eaabaa..cd6beaf 100644
--- a/gateway-provider-security-pac4j/src/main/java/org/apache/knox/gateway/pac4j/filter/Pac4jDispatcherFilter.java
+++ b/gateway-provider-security-pac4j/src/main/java/org/apache/knox/gateway/pac4j/filter/Pac4jDispatcherFilter.java
@@ -20,6 +20,8 @@
 import org.apache.commons.lang3.StringUtils;
 import org.apache.knox.gateway.i18n.messages.MessagesFactory;
 import org.apache.knox.gateway.pac4j.Pac4jMessages;
+import org.apache.knox.gateway.pac4j.config.ClientConfigurationDecorator;
+import org.apache.knox.gateway.pac4j.config.Pac4jClientConfigurationDecorator;
 import org.apache.knox.gateway.pac4j.session.KnoxSessionStore;
 import org.apache.knox.gateway.services.ServiceType;
 import org.apache.knox.gateway.services.GatewayServices;
@@ -34,7 +36,6 @@
 import org.pac4j.core.config.Config;
 import org.pac4j.core.context.session.J2ESessionStore;
 import org.pac4j.core.context.session.SessionStore;
-import org.pac4j.core.http.callback.PathParameterCallbackUrlResolver;
 import org.pac4j.core.util.CommonHelper;
 import org.pac4j.http.client.indirect.IndirectBasicAuthClient;
 import org.pac4j.http.credentials.authenticator.test.SimpleTestUsernamePasswordAuthenticator;
@@ -74,6 +75,8 @@
 
   private static Pac4jMessages log = MessagesFactory.get(Pac4jMessages.class);
 
+  private static final ClientConfigurationDecorator PAC4J_CLIENT_CONFIGURATION_DECORATOR = new Pac4jClientConfigurationDecorator();
+
   public static final String TEST_BASIC_AUTH = "testBasicAuth";
 
   public static final String PAC4J_CALLBACK_URL = "pac4j.callbackUrl";
@@ -181,19 +184,11 @@
         log.atLeastOnePac4jClientMustBeDefined();
         throw new ServletException("At least one pac4j client must be defined.");
       }
-      if (CommonHelper.isBlank(clientNameParameter)) {
-        clientName = clients.get(0).getName();
-      } else {
-        clientName = clientNameParameter;
-      }
 
-      /* special handling for Azure AD, use path separators instead of query params */
-      clients.forEach( client -> {
-        if(client.getName().equalsIgnoreCase(AzureAdClient.class.getSimpleName())) {
-          ((AzureAdClient)client).setCallbackUrlResolver(new PathParameterCallbackUrlResolver());
-        }
-      });
+      clientName = CommonHelper.isBlank(clientNameParameter) ? clients.get(0).getName() : clientNameParameter;
 
+      //decorating client configuration (if needed)
+      PAC4J_CLIENT_CONFIGURATION_DECORATOR.decorateClients(clients, properties);
     }
 
 
diff --git a/gateway-provider-security-pac4j/src/test/java/org/apache/knox/gateway/pac4j/config/AzureADClientConfigurationDecoratorTest.java b/gateway-provider-security-pac4j/src/test/java/org/apache/knox/gateway/pac4j/config/AzureADClientConfigurationDecoratorTest.java
new file mode 100644
index 0000000..0605025
--- /dev/null
+++ b/gateway-provider-security-pac4j/src/test/java/org/apache/knox/gateway/pac4j/config/AzureADClientConfigurationDecoratorTest.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.knox.gateway.pac4j.config;
+
+import static org.junit.Assert.assertTrue;
+
+import java.util.Collections;
+
+import org.junit.Test;
+import org.pac4j.core.http.callback.PathParameterCallbackUrlResolver;
+import org.pac4j.oidc.client.AzureAdClient;
+import org.pac4j.oidc.config.AzureAdOidcConfiguration;
+
+public class AzureADClientConfigurationDecoratorTest {
+
+  @Test
+  public void testAzureADClientConfigurationDecoration() throws Exception {
+    final AzureAdOidcConfiguration azureAdConfig = new AzureAdOidcConfiguration();
+    final AzureAdClient client = new AzureAdClient(azureAdConfig);
+    final AzureADClientConfigurationDecorator azureConfigDecorator = new AzureADClientConfigurationDecorator();
+    azureConfigDecorator.decorateClients(Collections.singletonList(client), null);
+    assertTrue(client.getCallbackUrlResolver() instanceof PathParameterCallbackUrlResolver);
+  }
+
+}
diff --git a/gateway-provider-security-pac4j/src/test/java/org/apache/knox/gateway/pac4j/config/Pac4jClientConfigurationDecoratorTest.java b/gateway-provider-security-pac4j/src/test/java/org/apache/knox/gateway/pac4j/config/Pac4jClientConfigurationDecoratorTest.java
new file mode 100644
index 0000000..f40aacf
--- /dev/null
+++ b/gateway-provider-security-pac4j/src/test/java/org/apache/knox/gateway/pac4j/config/Pac4jClientConfigurationDecoratorTest.java
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.knox.gateway.pac4j.config;
+
+import static org.junit.Assert.assertEquals;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.easymock.EasyMock;
+import org.junit.Test;
+import org.pac4j.core.client.Client;
+
+public class Pac4jClientConfigurationDecoratorTest {
+
+  @Test
+  public void testClientConfigDecoration() throws Exception {
+    final AtomicInteger tested = new AtomicInteger(0);
+    final AtomicInteger decorated = new AtomicInteger(0);
+
+    final ClientConfigurationDecorator passiveDecorator = new TestClientConfigurationDecorator(tested, decorated, false);
+    final ClientConfigurationDecorator activeDecorator = new TestClientConfigurationDecorator(tested, decorated, true);
+    final Pac4jClientConfigurationDecorator pac4jConfigurationDecorator = new Pac4jClientConfigurationDecorator(Arrays.asList(passiveDecorator, activeDecorator));
+    final Client client = EasyMock.createNiceMock(Client.class);
+    pac4jConfigurationDecorator.decorateClients(Collections.singletonList(client), null);
+    assertEquals(2, tested.get());
+    assertEquals(1, decorated.get());
+  }
+
+  private static class TestClientConfigurationDecorator implements ClientConfigurationDecorator {
+
+    private final AtomicInteger tested;
+    private final AtomicInteger decorated;
+    private final boolean decorate;
+
+    TestClientConfigurationDecorator(AtomicInteger testedClientsNum, AtomicInteger decoratedClientsNum, boolean decorate) {
+      this.tested = testedClientsNum;
+      this.decorated = decoratedClientsNum;
+      this.decorate = decorate;
+    }
+
+    @Override
+    public void decorateClients(List<Client> clients, Map<String, String> properties) {
+      clients.forEach(client -> {
+        tested.incrementAndGet();
+        if (decorate) {
+          decorated.incrementAndGet();
+        }
+      });
+    }
+  }
+
+}
diff --git a/gateway-provider-security-pac4j/src/test/java/org/apache/knox/gateway/pac4j/config/SAML2ClientConfigurationDecoratorTest.java b/gateway-provider-security-pac4j/src/test/java/org/apache/knox/gateway/pac4j/config/SAML2ClientConfigurationDecoratorTest.java
new file mode 100644
index 0000000..c78c6fc
--- /dev/null
+++ b/gateway-provider-security-pac4j/src/test/java/org/apache/knox/gateway/pac4j/config/SAML2ClientConfigurationDecoratorTest.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.knox.gateway.pac4j.config;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.junit.Test;
+import org.pac4j.saml.client.SAML2Client;
+import org.pac4j.saml.config.SAML2Configuration;
+
+public class SAML2ClientConfigurationDecoratorTest {
+
+  @Test
+  public void testSaml2ClientConfigurationDecoration() throws Exception {
+    final SAML2Configuration saml2Configuration = new SAML2Configuration();
+    final SAML2Client client = new SAML2Client(saml2Configuration);
+    final Map<String, String> properties = new HashMap<>();
+    properties.put("useNameQualifier", "true");
+    properties.put("forceAuth", "true");
+    properties.put("passive", "true");
+    properties.put("nameIdPolicyFormat", "testPolicyFormat");
+
+    final SAML2ClientConfigurationDecorator saml2ConfigurationDecorator = new SAML2ClientConfigurationDecorator();
+    saml2ConfigurationDecorator.decorateClients(Collections.singletonList(client), properties);
+    assertTrue(saml2Configuration.isUseNameQualifier());
+    assertTrue(saml2Configuration.isForceAuth());
+    assertTrue(saml2Configuration.isPassive());
+    assertEquals("testPolicyFormat", saml2Configuration.getNameIdPolicyFormat());
+  }
+
+}