This is an automated email from the ASF dual-hosted git repository.

ilgrosso pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/syncope.git


The following commit(s) were added to refs/heads/master by this push:
     new cf54140  [SYNCOPE-1455] Cleaning up + more tests + some useful Filters
cf54140 is described below

commit cf54140d85cf3489f67de5aabc4a3e468b013998
Author: Francesco Chicchiriccò <[email protected]>
AuthorDate: Tue Jun 23 14:20:47 2020 +0200

    [SYNCOPE-1455] Cleaning up + more tests + some useful Filters
---
 .../console/panels/GatewayRoutePredicatePanel.java |   6 +-
 .../console/panels/GatewayRouteWizardBuilder.java  |   8 -
 .../panels/GatewayRouteWizardBuilder$Profile.html  |   1 -
 .../syncope/common/lib/to/GatewayRouteTO.java      |  13 -
 .../syncope/common/lib/types/FilterFactory.java    |   3 +
 .../common/lib/types/GatewayRoutePredicate.java    |   8 +-
 ...ateCond.java => GatewayRoutePredicateCond.java} |   2 +-
 .../core/persistence/api/entity/GatewayRoute.java  |   5 -
 .../src/test/resources/domains/MasterContent.xml   |   3 +-
 .../persistence/jpa/entity/JPAGatewayRoute.java    |  17 -
 .../persistence/jpa/inner/GatewayRouteTest.java    |   3 -
 .../src/test/resources/domains/MasterContent.xml   |   2 +-
 .../java/data/GatewayRouteDataBinderImpl.java      |   3 -
 .../syncope/fit/core/GatewayRouteITCase.java       |   8 -
 pom.xml                                            |   6 +
 sra/pom.xml                                        |  17 +
 .../syncope/sra/ApplicationContextUtils.java       |  57 ++
 .../java/org/apache/syncope/sra/RouteProvider.java |  98 ++--
 ...copeSRAApplication.java => SecurityConfig.java} |  44 +-
 .../apache/syncope/sra/SyncopeSRAApplication.java  |  49 +-
 .../sra/filters/AddRefererFilterFactory.java       |  18 +-
 .../ClientCertsToRequestHeaderFilterFactory.java   |  74 +++
 .../{ => filters}/CustomGatewayFilterFactory.java  |   9 +-
 .../filters/LinkRewriteGatewayFilterFactory.java   |  88 +++
 .../ModifyResponseGatewayFilterFactory.java        | 155 +++++
 .../QueryParamToRequestHeaderFilterFactory.java    |  56 ++
 .../CustomRoutePredicateFactory.java               |   2 +-
 .../java/org/apache/syncope/sra/AbstractTest.java  |  65 +++
 .../java/org/apache/syncope/sra/ActuatorTest.java  |  64 ++
 .../org/apache/syncope/sra/RouteProviderTest.java  | 647 +++++++++++++++++++++
 .../syncope/sra/SyncopeCoreTestingServer.java      |  32 +-
 .../org/apache/syncope/sra/SyncopeSRATest.java     | 209 -------
 .../apache/syncope/sra/TLSRouteProviderTest.java   | 148 +++++
 .../apache/syncope/sra/ZookeeperTestingServer.java |  40 +-
 .../BodyPropertyAddingGatewayFilterFactory.java    |   2 +-
 .../BodyPropertyMatchingRoutePredicateFactory.java |   2 +-
 sra/src/test/resources/application-tls.properties  |  31 +
 sra/src/test/resources/client_pavel.p12            | Bin 0 -> 5557 bytes
 sra/src/test/resources/keyStore.p12                | Bin 0 -> 4173 bytes
 sra/src/test/resources/trustStore.jks              | Bin 0 -> 1484 bytes
 40 files changed, 1551 insertions(+), 444 deletions(-)

diff --git 
a/client/am/console/src/main/java/org/apache/syncope/client/console/panels/GatewayRoutePredicatePanel.java
 
b/client/am/console/src/main/java/org/apache/syncope/client/console/panels/GatewayRoutePredicatePanel.java
index 2dc4b58..df95baa 100644
--- 
a/client/am/console/src/main/java/org/apache/syncope/client/console/panels/GatewayRoutePredicatePanel.java
+++ 
b/client/am/console/src/main/java/org/apache/syncope/client/console/panels/GatewayRoutePredicatePanel.java
@@ -30,7 +30,7 @@ import 
org.apache.syncope.client.ui.commons.markup.html.form.AjaxCheckBoxPanel;
 import 
org.apache.syncope.client.ui.commons.markup.html.form.AjaxDropDownChoicePanel;
 import 
org.apache.syncope.client.ui.commons.markup.html.form.AjaxTextFieldPanel;
 import org.apache.syncope.common.lib.types.GatewayRoutePredicate;
-import org.apache.syncope.common.lib.types.PredicateCond;
+import org.apache.syncope.common.lib.types.GatewayRoutePredicateCond;
 import org.apache.syncope.common.lib.types.PredicateFactory;
 import org.apache.wicket.ajax.AjaxRequestTarget;
 import org.apache.wicket.extensions.ajax.markup.html.IndicatingAjaxButton;
@@ -89,9 +89,9 @@ public class GatewayRoutePredicatePanel extends Panel {
                         new AjaxTextFieldPanel("args", "args", new 
PropertyModel<>(predicate, "args"));
                 item.add(args.hideLabel());
 
-                AjaxDropDownChoicePanel<PredicateCond> cond =
+                AjaxDropDownChoicePanel<GatewayRoutePredicateCond> cond =
                         new AjaxDropDownChoicePanel<>("cond", "cond", new 
PropertyModel<>(predicate, "cond"));
-                cond.setChoices(List.of(PredicateCond.values()));
+                cond.setChoices(List.of(GatewayRoutePredicateCond.values()));
                 item.add(cond.hideLabel());
 
                 ActionsPanel<Serializable> actions = new 
ActionsPanel<>("actions", null);
diff --git 
a/client/am/console/src/main/java/org/apache/syncope/client/console/panels/GatewayRouteWizardBuilder.java
 
b/client/am/console/src/main/java/org/apache/syncope/client/console/panels/GatewayRouteWizardBuilder.java
index f043bc7..e4dce5e 100644
--- 
a/client/am/console/src/main/java/org/apache/syncope/client/console/panels/GatewayRouteWizardBuilder.java
+++ 
b/client/am/console/src/main/java/org/apache/syncope/client/console/panels/GatewayRouteWizardBuilder.java
@@ -20,15 +20,11 @@ package org.apache.syncope.client.console.panels;
 
 import java.io.Serializable;
 import java.net.URI;
-import java.util.List;
-
 import org.apache.syncope.client.console.rest.GatewayRouteRestClient;
 import org.apache.syncope.client.console.wizards.BaseAjaxWizardBuilder;
-import 
org.apache.syncope.client.ui.commons.markup.html.form.AjaxDropDownChoicePanel;
 import 
org.apache.syncope.client.ui.commons.markup.html.form.AjaxSpinnerFieldPanel;
 import 
org.apache.syncope.client.ui.commons.markup.html.form.AjaxTextFieldPanel;
 import org.apache.syncope.common.lib.to.GatewayRouteTO;
-import org.apache.syncope.common.lib.types.GatewayRouteStatus;
 import org.apache.wicket.PageReference;
 import org.apache.wicket.extensions.wizard.WizardModel;
 import org.apache.wicket.extensions.wizard.WizardStep;
@@ -100,10 +96,6 @@ public class GatewayRouteWizardBuilder extends 
BaseAjaxWizardBuilder<GatewayRout
             target.addRequiredLabel().setEnabled(true);
             target.getField().add(new UrlValidator(new String[] { "http", 
"https" }));
             add(target);
-
-            add(new AjaxDropDownChoicePanel<>(
-                    "status", "status", new PropertyModel<>(route, "status")).
-                    setChoices(List.of((Serializable[]) 
GatewayRouteStatus.values())));
         }
     }
 
diff --git 
a/client/am/console/src/main/resources/org/apache/syncope/client/console/panels/GatewayRouteWizardBuilder$Profile.html
 
b/client/am/console/src/main/resources/org/apache/syncope/client/console/panels/GatewayRouteWizardBuilder$Profile.html
index 382d1fb..85e6968 100644
--- 
a/client/am/console/src/main/resources/org/apache/syncope/client/console/panels/GatewayRouteWizardBuilder$Profile.html
+++ 
b/client/am/console/src/main/resources/org/apache/syncope/client/console/panels/GatewayRouteWizardBuilder$Profile.html
@@ -21,6 +21,5 @@ under the License.
     <div class="form-group"><span wicket:id="name">[name]</span></div>
     <div class="form-group"><span wicket:id="order">[order]</span></div>
     <div class="form-group"><span wicket:id="target">[target]</span></div>
-    <div class="form-group"><span wicket:id="status">[status]</span></div>
   </wicket:panel>
 </html>
diff --git 
a/common/am/lib/src/main/java/org/apache/syncope/common/lib/to/GatewayRouteTO.java
 
b/common/am/lib/src/main/java/org/apache/syncope/common/lib/to/GatewayRouteTO.java
index fe8d987..8249c87 100644
--- 
a/common/am/lib/src/main/java/org/apache/syncope/common/lib/to/GatewayRouteTO.java
+++ 
b/common/am/lib/src/main/java/org/apache/syncope/common/lib/to/GatewayRouteTO.java
@@ -26,7 +26,6 @@ import org.apache.commons.lang3.builder.EqualsBuilder;
 import org.apache.commons.lang3.builder.HashCodeBuilder;
 import org.apache.syncope.common.lib.types.GatewayRouteFilter;
 import org.apache.syncope.common.lib.types.GatewayRoutePredicate;
-import org.apache.syncope.common.lib.types.GatewayRouteStatus;
 
 public class GatewayRouteTO implements NamedEntityTO {
 
@@ -44,8 +43,6 @@ public class GatewayRouteTO implements NamedEntityTO {
 
     private final List<GatewayRoutePredicate> predicates = new ArrayList<>();
 
-    private GatewayRouteStatus status;
-
     @Override
     public String getKey() {
         return key;
@@ -91,14 +88,6 @@ public class GatewayRouteTO implements NamedEntityTO {
         return predicates;
     }
 
-    public GatewayRouteStatus getStatus() {
-        return status;
-    }
-
-    public void setStatus(final GatewayRouteStatus status) {
-        this.status = status;
-    }
-
     @Override
     public int hashCode() {
         return new HashCodeBuilder().
@@ -107,7 +96,6 @@ public class GatewayRouteTO implements NamedEntityTO {
                 append(target).
                 append(filters).
                 append(predicates).
-                append(status).
                 build();
     }
 
@@ -129,7 +117,6 @@ public class GatewayRouteTO implements NamedEntityTO {
                 append(target, other.target).
                 append(filters, other.filters).
                 append(predicates, other.predicates).
-                append(status, other.status).
                 build();
     }
 }
diff --git 
a/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/FilterFactory.java
 
b/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/FilterFactory.java
index 5d71698..e7d1403 100644
--- 
a/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/FilterFactory.java
+++ 
b/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/FilterFactory.java
@@ -42,6 +42,9 @@ public enum FilterFactory {
     STRIP_PREFIX,
     REQUEST_HEADER_TO_REQUEST_URI,
     SET_REQUEST_SIZE,
+    LINK_REWRITE,
+    CLIENT_CERTS_TO_REQUEST_HEADER,
+    QUERY_PARAM_TO_REQUEST_HEADER,
     CUSTOM
 
 }
diff --git 
a/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/GatewayRoutePredicate.java
 
b/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/GatewayRoutePredicate.java
index 2c0c3c7..2a19a5f 100644
--- 
a/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/GatewayRoutePredicate.java
+++ 
b/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/GatewayRoutePredicate.java
@@ -35,7 +35,7 @@ public class GatewayRoutePredicate implements BaseBean {
             return this;
         }
 
-        public Builder cond(final PredicateCond cond) {
+        public Builder cond(final GatewayRoutePredicateCond cond) {
             instance.setCond(cond);
             return this;
         }
@@ -57,7 +57,7 @@ public class GatewayRoutePredicate implements BaseBean {
 
     private boolean negate;
 
-    private PredicateCond cond;
+    private GatewayRoutePredicateCond cond;
 
     private PredicateFactory factory;
 
@@ -71,11 +71,11 @@ public class GatewayRoutePredicate implements BaseBean {
         this.negate = negate;
     }
 
-    public PredicateCond getCond() {
+    public GatewayRoutePredicateCond getCond() {
         return cond;
     }
 
-    public void setCond(final PredicateCond cond) {
+    public void setCond(final GatewayRoutePredicateCond cond) {
         this.cond = cond;
     }
 
diff --git 
a/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/PredicateCond.java
 
b/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/GatewayRoutePredicateCond.java
similarity index 95%
rename from 
common/am/lib/src/main/java/org/apache/syncope/common/lib/types/PredicateCond.java
rename to 
common/am/lib/src/main/java/org/apache/syncope/common/lib/types/GatewayRoutePredicateCond.java
index c62ff78..843659d 100644
--- 
a/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/PredicateCond.java
+++ 
b/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/GatewayRoutePredicateCond.java
@@ -18,7 +18,7 @@
  */
 package org.apache.syncope.common.lib.types;
 
-public enum PredicateCond {
+public enum GatewayRoutePredicateCond {
     AND,
     OR
 
diff --git 
a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/GatewayRoute.java
 
b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/GatewayRoute.java
index 4aef3f7..cf04b4e 100644
--- 
a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/GatewayRoute.java
+++ 
b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/GatewayRoute.java
@@ -22,7 +22,6 @@ import java.net.URI;
 import java.util.List;
 import org.apache.syncope.common.lib.types.GatewayRouteFilter;
 import org.apache.syncope.common.lib.types.GatewayRoutePredicate;
-import org.apache.syncope.common.lib.types.GatewayRouteStatus;
 
 public interface GatewayRoute extends Entity {
 
@@ -45,8 +44,4 @@ public interface GatewayRoute extends Entity {
     List<GatewayRoutePredicate> getPredicates();
 
     void setPredicates(List<GatewayRoutePredicate> predicates);
-
-    GatewayRouteStatus getStatus();
-
-    void setStatus(GatewayRouteStatus status);
 }
diff --git 
a/core/persistence-jpa-json/src/test/resources/domains/MasterContent.xml 
b/core/persistence-jpa-json/src/test/resources/domains/MasterContent.xml
index b0053e4..ca04393 100644
--- a/core/persistence-jpa-json/src/test/resources/domains/MasterContent.xml
+++ b/core/persistence-jpa-json/src/test/resources/domains/MasterContent.xml
@@ -2378,8 +2378,7 @@ $$ }&#10;
 
   <SecurityQuestion id="887028ea-66fc-41e7-b397-620d7ea6dfbb" content="What's 
your mother's maiden name?"/>
 
-  <GatewayRoute id="ec7bada2-3dd6-460c-8441-65521d005ffa" name="basic1" 
target="http://httpbin.org:80"; status="PUBLISHED"
-                
predicates="[{&quot;cond&quot;:null,&quot;factory&quot;:&quot;METHOD&quot;,&quot;args&quot;:&quot;GET&quot;}]"/>
+  <GatewayRoute id="ec7bada2-3dd6-460c-8441-65521d005ffa" name="basic1" 
target="http://httpbin.org:80"; 
predicates="[{&quot;cond&quot;:null,&quot;factory&quot;:&quot;METHOD&quot;,&quot;args&quot;:&quot;GET&quot;}]"/>
 
   <SyncopeLogger logType="AUDIT" 
logName="syncope.audit.[LOGIC]:[SyncopeLogic]:[]:[isSelfRegAllowed]:[SUCCESS]" 
logLevel="DEBUG"/>
 
diff --git 
a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPAGatewayRoute.java
 
b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPAGatewayRoute.java
index 9b938fb..9970ff3 100644
--- 
a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPAGatewayRoute.java
+++ 
b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPAGatewayRoute.java
@@ -24,14 +24,11 @@ import java.util.Optional;
 
 import javax.persistence.Column;
 import javax.persistence.Entity;
-import javax.persistence.EnumType;
-import javax.persistence.Enumerated;
 import javax.persistence.Lob;
 import javax.persistence.Table;
 import javax.validation.constraints.NotNull;
 import org.apache.syncope.common.lib.types.GatewayRouteFilter;
 import org.apache.syncope.common.lib.types.GatewayRoutePredicate;
-import org.apache.syncope.common.lib.types.GatewayRouteStatus;
 import org.apache.syncope.core.persistence.api.entity.GatewayRoute;
 import 
org.apache.syncope.core.persistence.jpa.validation.entity.GatewayRouteCheck;
 import org.apache.syncope.core.provisioning.api.serialization.POJOHelper;
@@ -59,10 +56,6 @@ public class JPAGatewayRoute extends 
AbstractGeneratedKeyEntity implements Gatew
     @Lob
     private String filters;
 
-    @NotNull
-    @Enumerated(EnumType.STRING)
-    private GatewayRouteStatus status;
-
     @Override
     public String getName() {
         return name;
@@ -116,14 +109,4 @@ public class JPAGatewayRoute extends 
AbstractGeneratedKeyEntity implements Gatew
     public void setPredicates(final List<GatewayRoutePredicate> predicates) {
         this.predicates = POJOHelper.serialize(predicates);
     }
-
-    @Override
-    public GatewayRouteStatus getStatus() {
-        return status;
-    }
-
-    @Override
-    public void setStatus(final GatewayRouteStatus status) {
-        this.status = status;
-    }
 }
diff --git 
a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/GatewayRouteTest.java
 
b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/GatewayRouteTest.java
index abdf50d..36b7b09 100644
--- 
a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/GatewayRouteTest.java
+++ 
b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/GatewayRouteTest.java
@@ -29,7 +29,6 @@ import javax.ws.rs.HttpMethod;
 import org.apache.syncope.common.lib.types.FilterFactory;
 import org.apache.syncope.common.lib.types.GatewayRouteFilter;
 import org.apache.syncope.common.lib.types.GatewayRoutePredicate;
-import org.apache.syncope.common.lib.types.GatewayRouteStatus;
 import org.apache.syncope.common.lib.types.PredicateFactory;
 import org.apache.syncope.core.persistence.api.dao.GatewayRouteDAO;
 import org.apache.syncope.core.persistence.api.entity.GatewayRoute;
@@ -48,7 +47,6 @@ public class GatewayRouteTest extends AbstractTest {
     public void find() {
         GatewayRoute route = 
routeDAO.find("ec7bada2-3dd6-460c-8441-65521d005ffa");
         assertNotNull(route);
-        assertEquals(GatewayRouteStatus.PUBLISHED, route.getStatus());
         assertEquals(1, route.getPredicates().size());
 
         route = routeDAO.find(UUID.randomUUID().toString());
@@ -71,7 +69,6 @@ public class GatewayRouteTest extends AbstractTest {
                 
factory(PredicateFactory.METHOD).args(HttpMethod.GET).build()));
         route.setFilters(List.of(new GatewayRouteFilter.Builder().
                 factory(FilterFactory.ADD_REQUEST_HEADER).args("X-Request-Foo, 
Bar").build()));
-        route.setStatus(GatewayRouteStatus.DRAFT);
 
         int beforeCount = routeDAO.findAll().size();
 
diff --git a/core/persistence-jpa/src/test/resources/domains/MasterContent.xml 
b/core/persistence-jpa/src/test/resources/domains/MasterContent.xml
index 890cf97..34a8718 100644
--- a/core/persistence-jpa/src/test/resources/domains/MasterContent.xml
+++ b/core/persistence-jpa/src/test/resources/domains/MasterContent.xml
@@ -2465,7 +2465,7 @@ $$ }&#10;
   
   <SecurityQuestion id="887028ea-66fc-41e7-b397-620d7ea6dfbb" content="What's 
your mother's maiden name?"/>
 
-  <GatewayRoute id="ec7bada2-3dd6-460c-8441-65521d005ffa" name="basic1" 
target="http://httpbin.org:80"; status="PUBLISHED"                
predicates="[{&quot;cond&quot;:null,&quot;factory&quot;:&quot;METHOD&quot;,&quot;args&quot;:&quot;GET&quot;}]"/>
+  <GatewayRoute id="ec7bada2-3dd6-460c-8441-65521d005ffa" name="basic1" 
target="http://httpbin.org:80"; 
predicates="[{&quot;cond&quot;:null,&quot;factory&quot;:&quot;METHOD&quot;,&quot;args&quot;:&quot;GET&quot;}]"/>
 
   <SyncopeLogger logType="AUDIT" 
logName="syncope.audit.[LOGIC]:[SyncopeLogic]:[]:[isSelfRegAllowed]:[SUCCESS]" 
logLevel="DEBUG"/>
 
diff --git 
a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/GatewayRouteDataBinderImpl.java
 
b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/GatewayRouteDataBinderImpl.java
index 0259510..a086cfb 100644
--- 
a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/GatewayRouteDataBinderImpl.java
+++ 
b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/GatewayRouteDataBinderImpl.java
@@ -22,7 +22,6 @@ import org.apache.commons.lang3.StringUtils;
 import org.apache.syncope.common.lib.SyncopeClientException;
 import org.apache.syncope.common.lib.to.GatewayRouteTO;
 import org.apache.syncope.common.lib.types.ClientExceptionType;
-import org.apache.syncope.common.lib.types.GatewayRouteStatus;
 import org.apache.syncope.core.persistence.api.entity.GatewayRoute;
 import org.apache.syncope.core.provisioning.api.data.GatewayRouteDataBinder;
 import org.springframework.stereotype.Component;
@@ -48,7 +47,6 @@ public class GatewayRouteDataBinderImpl implements 
GatewayRouteDataBinder {
         route.setTarget(routeTO.getTarget());
         route.setFilters(routeTO.getFilters());
         route.setPredicates(routeTO.getPredicates());
-        route.setStatus(routeTO.getStatus() == null ? GatewayRouteStatus.DRAFT 
: routeTO.getStatus());
     }
 
     @Override
@@ -60,7 +58,6 @@ public class GatewayRouteDataBinderImpl implements 
GatewayRouteDataBinder {
         routeTO.setTarget(route.getTarget());
         routeTO.getFilters().addAll(route.getFilters());
         routeTO.getPredicates().addAll(route.getPredicates());
-        routeTO.setStatus(route.getStatus());
 
         return routeTO;
     }
diff --git 
a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/GatewayRouteITCase.java
 
b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/GatewayRouteITCase.java
index 4994066..76ac2be 100644
--- 
a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/GatewayRouteITCase.java
+++ 
b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/GatewayRouteITCase.java
@@ -34,7 +34,6 @@ import 
org.apache.syncope.common.lib.types.ClientExceptionType;
 import org.apache.syncope.common.lib.types.FilterFactory;
 import org.apache.syncope.common.lib.types.GatewayRouteFilter;
 import org.apache.syncope.common.lib.types.GatewayRoutePredicate;
-import org.apache.syncope.common.lib.types.GatewayRouteStatus;
 import org.apache.syncope.common.lib.types.PredicateFactory;
 import org.apache.syncope.common.rest.api.service.GatewayRouteService;
 import org.apache.syncope.fit.AbstractITCase;
@@ -46,7 +45,6 @@ public class GatewayRouteITCase extends AbstractITCase {
     public void read() {
         GatewayRouteTO route = 
gatewayRouteService.read("ec7bada2-3dd6-460c-8441-65521d005ffa");
         assertNotNull(route);
-        assertEquals(GatewayRouteStatus.PUBLISHED, route.getStatus());
         assertEquals(1, route.getPredicates().size());
 
         try {
@@ -73,7 +71,6 @@ public class GatewayRouteITCase extends AbstractITCase {
                 factory(PredicateFactory.METHOD).args(HttpMethod.GET).build());
         route.getFilters().add(new GatewayRouteFilter.Builder().
                 factory(FilterFactory.ADD_REQUEST_HEADER).args("X-Request-Foo, 
Bar").build());
-        route.setStatus(GatewayRouteStatus.DRAFT);
 
         int beforeCount = gatewayRouteService.list().size();
 
@@ -86,11 +83,6 @@ public class GatewayRouteITCase extends AbstractITCase {
         int afterCount = gatewayRouteService.list().size();
         assertEquals(afterCount, beforeCount + 1);
 
-        route.setStatus(GatewayRouteStatus.STAGING);
-        gatewayRouteService.update(route);
-        route = gatewayRouteService.read(route.getKey());
-        assertEquals(GatewayRouteStatus.STAGING, route.getStatus());
-
         gatewayRouteService.delete(route.getKey());
 
         try {
diff --git a/pom.xml b/pom.xml
index 413a1f6..3104a45 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1993,6 +1993,12 @@ under the License.
         <version>8.19</version>
       </dependency>
 
+      <dependency>
+        <groupId>org.jsoup</groupId>
+        <artifactId>jsoup</artifactId>
+        <version>1.13.1</version>
+      </dependency>
+
       <!-- TEST -->
       <dependency>
         <groupId>org.apache.curator</groupId>
diff --git a/sra/pom.xml b/sra/pom.xml
index b98503d..53275d4 100644
--- a/sra/pom.xml
+++ b/sra/pom.xml
@@ -77,6 +77,11 @@ under the License.
     </dependency>
 
     <dependency>
+      <groupId>org.jsoup</groupId>
+      <artifactId>jsoup</artifactId>
+    </dependency>
+
+    <dependency>
       <groupId>org.apache.syncope.common.keymaster</groupId>
       <artifactId>syncope-common-keymaster-client-api</artifactId>
       <version>${project.version}</version>
@@ -180,6 +185,18 @@ under the License.
       <testResource>
         <directory>${basedir}/src/test/resources</directory>
         <filtering>true</filtering>
+        <excludes>
+          <exclude>*.p12</exclude>
+          <exclude>*.jks</exclude>
+        </excludes>
+      </testResource>
+      <testResource>
+        <directory>${basedir}/src/test/resources</directory>
+        <filtering>false</filtering>
+        <includes>
+          <include>*.p12</include>
+          <include>*.jks</include>
+        </includes>
       </testResource>
     </testResources>
   </build>
diff --git 
a/sra/src/main/java/org/apache/syncope/sra/ApplicationContextUtils.java 
b/sra/src/main/java/org/apache/syncope/sra/ApplicationContextUtils.java
new file mode 100644
index 0000000..e042b26
--- /dev/null
+++ b/sra/src/main/java/org/apache/syncope/sra/ApplicationContextUtils.java
@@ -0,0 +1,57 @@
+/*
+ * 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.syncope.sra;
+
+import org.springframework.beans.factory.config.BeanDefinition;
+import org.springframework.beans.factory.support.AbstractBeanDefinition;
+import org.springframework.beans.factory.support.BeanDefinitionRegistry;
+import org.springframework.beans.factory.support.RootBeanDefinition;
+import org.springframework.context.ApplicationListener;
+import org.springframework.context.ConfigurableApplicationContext;
+
+public final class ApplicationContextUtils {
+
+    public static <T> T getOrCreateBean(
+            final ConfigurableApplicationContext ctx,
+            final String actualClazz,
+            final Class<T> type) throws ClassNotFoundException {
+
+        T bean;
+        if (ctx.getBeanFactory().containsSingleton(actualClazz)) {
+            bean = type.cast(ctx.getBeanFactory().getSingleton(actualClazz));
+        } else {
+            if (ApplicationListener.class.isAssignableFrom(type)) {
+                RootBeanDefinition bd = new RootBeanDefinition(
+                        Class.forName(actualClazz), 
AbstractBeanDefinition.AUTOWIRE_BY_TYPE, false);
+                bd.setScope(BeanDefinition.SCOPE_SINGLETON);
+                ((BeanDefinitionRegistry) 
ctx.getBeanFactory()).registerBeanDefinition(actualClazz, bd);
+                bean = ctx.getBean(type);
+            } else {
+                bean = type.cast(ctx.getBeanFactory().
+                        createBean(Class.forName(actualClazz), 
AbstractBeanDefinition.AUTOWIRE_BY_TYPE, false));
+                ctx.getBeanFactory().registerSingleton(actualClazz, bean);
+            }
+        }
+        return bean;
+    }
+
+    private ApplicationContextUtils() {
+        // private constructor for static utility class
+    }
+}
diff --git a/sra/src/main/java/org/apache/syncope/sra/RouteProvider.java 
b/sra/src/main/java/org/apache/syncope/sra/RouteProvider.java
index fb6c37c..00c1ac8 100644
--- a/sra/src/main/java/org/apache/syncope/sra/RouteProvider.java
+++ b/sra/src/main/java/org/apache/syncope/sra/RouteProvider.java
@@ -19,8 +19,10 @@
 package org.apache.syncope.sra;
 
 import java.time.ZonedDateTime;
+import java.util.Arrays;
 import java.util.List;
 import java.util.Objects;
+import java.util.function.Consumer;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 import org.apache.commons.lang3.BooleanUtils;
@@ -33,13 +35,16 @@ import 
org.apache.syncope.common.keymaster.client.api.model.NetworkService;
 import org.apache.syncope.common.lib.to.GatewayRouteTO;
 import org.apache.syncope.common.lib.types.GatewayRouteFilter;
 import org.apache.syncope.common.lib.types.GatewayRoutePredicate;
-import org.apache.syncope.common.lib.types.GatewayRouteStatus;
 import org.apache.syncope.common.rest.api.service.GatewayRouteService;
+import org.apache.syncope.sra.filters.ClientCertsToRequestHeaderFilterFactory;
+import org.apache.syncope.sra.filters.CustomGatewayFilterFactory;
+import org.apache.syncope.sra.filters.LinkRewriteGatewayFilterFactory;
+import org.apache.syncope.sra.filters.QueryParamToRequestHeaderFilterFactory;
+import org.apache.syncope.sra.predicates.CustomRoutePredicateFactory;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
-import org.springframework.beans.factory.support.AbstractBeanDefinition;
 import org.springframework.cloud.gateway.filter.GatewayFilter;
 import org.springframework.cloud.gateway.filter.OrderedGatewayFilter;
 import 
org.springframework.cloud.gateway.filter.factory.AddRequestHeaderGatewayFilterFactory;
@@ -110,7 +115,7 @@ public class RouteProvider {
     private SyncopeClient client;
 
     @SuppressWarnings("unchecked")
-    private GatewayFilter toFilter(final String routeId, final 
GatewayRouteFilter gwfilter)
+    private GatewayFilter toFilter(final GatewayRouteTO route, final 
GatewayRouteFilter gwfilter)
             throws ClassNotFoundException {
 
         GatewayFilter filter;
@@ -140,7 +145,7 @@ public class RouteProvider {
             case HYSTRIX:
                 String[] hystrixArgs = gwfilter.getArgs().split(",");
                 filter = ctx.getBean(HystrixGatewayFilterFactory.class).
-                        apply(routeId, c -> {
+                        apply(route.getKey(), c -> {
                             if (StringUtils.isNotBlank(hystrixArgs[0])) {
                                 c.setName(hystrixArgs[0].trim());
                             }
@@ -285,17 +290,43 @@ public class RouteProvider {
                         apply(c -> 
c.setMaxSize(DataSize.ofBytes(Long.valueOf(gwfilter.getArgs().trim()))));
                 break;
 
+            case LINK_REWRITE:
+                filter = ApplicationContextUtils.getOrCreateBean(
+                        ctx,
+                        LinkRewriteGatewayFilterFactory.class.getName(),
+                        LinkRewriteGatewayFilterFactory.class).
+                        apply(c -> c.setData(route.getTarget().toASCIIString() 
+ "," + gwfilter.getArgs().trim()));
+                break;
+
+            case CLIENT_CERTS_TO_REQUEST_HEADER:
+                String header = StringUtils.isBlank(gwfilter.getArgs()) ? 
"X-Client-Certificate" : gwfilter.getArgs();
+                filter = ApplicationContextUtils.getOrCreateBean(
+                        ctx,
+                        
ClientCertsToRequestHeaderFilterFactory.class.getName(),
+                        ClientCertsToRequestHeaderFilterFactory.class).
+                        apply(c -> c.setName(header.trim()));
+                break;
+
+            case QUERY_PARAM_TO_REQUEST_HEADER:
+                filter = ApplicationContextUtils.getOrCreateBean(
+                        ctx,
+                        QueryParamToRequestHeaderFilterFactory.class.getName(),
+                        QueryParamToRequestHeaderFilterFactory.class).
+                        apply(c -> c.setName(gwfilter.getArgs().trim()));
+                break;
+
             case CUSTOM:
                 String[] customArgs = gwfilter.getArgs().split(";");
-                CustomGatewayFilterFactory factory;
-                if (ctx.getBeanFactory().containsSingleton(customArgs[0])) {
-                    factory = (CustomGatewayFilterFactory) 
ctx.getBeanFactory().getSingleton(customArgs[0]);
-                } else {
-                    factory = (CustomGatewayFilterFactory) 
ctx.getBeanFactory().
-                            createBean(Class.forName(customArgs[0]), 
AbstractBeanDefinition.AUTOWIRE_BY_TYPE, false);
-                    ctx.getBeanFactory().registerSingleton(customArgs[0], 
factory);
-                }
-                filter = factory.apply(c -> c.setData(customArgs[1]));
+                Consumer<CustomGatewayFilterFactory.Config> customConsumer = 
customArgs.length > 1
+                        ? c -> c.setData(customArgs[1])
+                        : c -> c.setData(null);
+                CustomGatewayFilterFactory factory = 
ApplicationContextUtils.getOrCreateBean(
+                        ctx,
+                        customArgs[0],
+                        CustomGatewayFilterFactory.class);
+                filter = factory.getOrder().
+                        map(order -> (GatewayFilter) new 
OrderedGatewayFilter(factory.apply(customConsumer), order)).
+                        orElseGet(() -> factory.apply(customConsumer));
                 break;
 
             default:
@@ -339,14 +370,16 @@ public class RouteProvider {
                 break;
 
             case HEADER:
+                String[] headerArgs = gwpredicate.getArgs().split(",");
                 predicate = ctx.getBean(HeaderRoutePredicateFactory.class).
-                        applyAsync(c -> 
c.setHeader(gwpredicate.getArgs().trim()));
+                        applyAsync(c -> c.setHeader(headerArgs[0].trim()).
+                        setRegexp(headerArgs[1].trim()));
                 break;
 
             case HOST:
                 String[] hostArgs = gwpredicate.getArgs().split(",");
                 predicate = ctx.getBean(HostRoutePredicateFactory.class).
-                        applyAsync(c -> c.setPatterns(List.of(hostArgs)));
+                        applyAsync(c -> 
c.setPatterns(Arrays.asList(hostArgs)));
                 break;
 
             case METHOD:
@@ -359,33 +392,32 @@ public class RouteProvider {
             case PATH:
                 String[] pathArgs = gwpredicate.getArgs().split(",");
                 predicate = ctx.getBean(PathRoutePredicateFactory.class).
-                        applyAsync(c -> c.setPatterns(List.of(pathArgs)));
+                        applyAsync(c -> 
c.setPatterns(Arrays.asList(pathArgs)));
                 break;
 
             case QUERY:
                 String[] queryArgs = gwpredicate.getArgs().split(",");
+                Consumer<QueryRoutePredicateFactory.Config> queryConsumer =
+                        queryArgs.length > 1
+                                ? c -> 
c.setParam(queryArgs[0].trim()).setRegexp(queryArgs[1].trim())
+                                : c -> c.setParam(queryArgs[0].trim());
                 predicate = ctx.getBean(QueryRoutePredicateFactory.class).
-                        applyAsync(c -> c.setParam(queryArgs[0].trim()).
-                        setRegexp(queryArgs[1].trim()));
+                        applyAsync(queryConsumer);
                 break;
 
             case REMOTE_ADDR:
                 String[] remoteAddrArgs = gwpredicate.getArgs().split(",");
                 predicate = ctx.getBean(RemoteAddrRoutePredicateFactory.class).
-                        applyAsync(c -> c.setSources(List.of(remoteAddrArgs)));
+                        applyAsync(c -> 
c.setSources(Arrays.asList(remoteAddrArgs)));
                 break;
 
             case CUSTOM:
                 String[] customArgs = gwpredicate.getArgs().split(";");
-                CustomRoutePredicateFactory factory;
-                if (ctx.getBeanFactory().containsSingleton(customArgs[0])) {
-                    factory = (CustomRoutePredicateFactory) 
ctx.getBeanFactory().getSingleton(customArgs[0]);
-                } else {
-                    factory = (CustomRoutePredicateFactory) 
ctx.getBeanFactory().
-                            createBean(Class.forName(customArgs[0]), 
AbstractBeanDefinition.AUTOWIRE_BY_TYPE, false);
-                    ctx.getBeanFactory().registerSingleton(customArgs[0], 
factory);
-                }
-                predicate = factory.applyAsync(c -> c.setData(customArgs[1]));
+                predicate = ApplicationContextUtils.getOrCreateBean(
+                        ctx,
+                        customArgs[0],
+                        CustomRoutePredicateFactory.class).
+                        applyAsync(c -> c.setData(customArgs[1]));
                 break;
 
             default:
@@ -393,13 +425,10 @@ public class RouteProvider {
         }
 
         if (predicate == null) {
-            throw new IllegalArgumentException("Could not translate " + 
gwpredicate);
+            throw new IllegalArgumentException("Could not translate predicate 
" + gwpredicate);
         }
 
-        if (negate) {
-            predicate.negate();
-        }
-        return predicate;
+        return negate ? predicate.negate() : predicate;
     }
 
     private Route.AsyncBuilder toRoute(final GatewayRouteTO gwroute) {
@@ -438,7 +467,7 @@ public class RouteProvider {
             builder.filters(gwroute.getFilters().stream().
                     map(gwfilter -> {
                         try {
-                            return toFilter(gwroute.getKey(), gwfilter);
+                            return toFilter(gwroute, gwfilter);
                         } catch (Exception e) {
                             LOG.error("Could not translate {}, skipping", 
gwfilter, e);
                             return null;
@@ -467,7 +496,6 @@ public class RouteProvider {
         }
 
         return client.getService(GatewayRouteService.class).list().stream().
-                filter(gwroute -> gwroute.getStatus() == 
GatewayRouteStatus.PUBLISHED).
                 map(this::toRoute).
                 collect(Collectors.toList());
     }
diff --git 
a/sra/src/main/java/org/apache/syncope/sra/SyncopeSRAApplication.java 
b/sra/src/main/java/org/apache/syncope/sra/SecurityConfig.java
similarity index 63%
copy from sra/src/main/java/org/apache/syncope/sra/SyncopeSRAApplication.java
copy to sra/src/main/java/org/apache/syncope/sra/SecurityConfig.java
index 3f56eb4..b11a7ac 100644
--- a/sra/src/main/java/org/apache/syncope/sra/SyncopeSRAApplication.java
+++ b/sra/src/main/java/org/apache/syncope/sra/SecurityConfig.java
@@ -19,20 +19,11 @@
 package org.apache.syncope.sra;
 
 import java.util.Objects;
-import org.apache.syncope.common.keymaster.client.api.model.NetworkService;
-import org.apache.syncope.common.keymaster.client.api.startstop.KeymasterStart;
-import org.apache.syncope.common.keymaster.client.api.startstop.KeymasterStop;
 import org.apache.syncope.common.lib.types.IdRepoEntitlement;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.SpringApplication;
 import 
org.springframework.boot.actuate.autoconfigure.security.reactive.EndpointRequest;
-import org.springframework.boot.autoconfigure.SpringBootApplication;
-import org.springframework.cloud.gateway.route.Route;
-import org.springframework.cloud.gateway.route.RouteLocator;
-import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
-import org.springframework.context.EnvironmentAware;
 import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.PropertySource;
+import org.springframework.context.annotation.Configuration;
 import org.springframework.core.env.Environment;
 import 
org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
 import org.springframework.security.config.web.server.ServerHttpSecurity;
@@ -42,43 +33,14 @@ import 
org.springframework.security.core.userdetails.UserDetails;
 import org.springframework.security.web.server.SecurityWebFilterChain;
 import 
org.springframework.security.web.server.util.matcher.NegatedServerWebExchangeMatcher;
 import 
org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
-import reactor.core.publisher.Flux;
 
-@PropertySource("classpath:sra.properties")
-@PropertySource(value = "file:${conf.directory}/sra.properties", 
ignoreResourceNotFound = true)
 @EnableWebFluxSecurity
-@SpringBootApplication
-public class SyncopeSRAApplication implements EnvironmentAware {
-
-    public static void main(final String[] args) {
-        SpringApplication.run(SyncopeSRAApplication.class, args);
-    }
+@Configuration
+public class SecurityConfig {
 
     @Autowired
-    private RouteProvider provider;
-
     private Environment env;
 
-    @Override
-    public void setEnvironment(final Environment env) {
-        this.env = env;
-    }
-
-    @Bean
-    public KeymasterStart keymasterStart() {
-        return new KeymasterStart(NetworkService.Type.SRA);
-    }
-
-    @Bean
-    public KeymasterStop keymasterStop() {
-        return new KeymasterStop(NetworkService.Type.SRA);
-    }
-
-    @Bean
-    public RouteLocator routes(final RouteLocatorBuilder builder) {
-        return () -> 
Flux.fromIterable(provider.fetch()).map(Route.AbstractBuilder::build);
-    }
-
     @Bean
     public SecurityWebFilterChain actuatorSecurityFilterChain(final 
ServerHttpSecurity http) {
         ServerWebExchangeMatcher actuatorMatcher = 
EndpointRequest.toAnyEndpoint();
diff --git 
a/sra/src/main/java/org/apache/syncope/sra/SyncopeSRAApplication.java 
b/sra/src/main/java/org/apache/syncope/sra/SyncopeSRAApplication.java
index 3f56eb4..10785b1 100644
--- a/sra/src/main/java/org/apache/syncope/sra/SyncopeSRAApplication.java
+++ b/sra/src/main/java/org/apache/syncope/sra/SyncopeSRAApplication.java
@@ -18,37 +18,23 @@
  */
 package org.apache.syncope.sra;
 
-import java.util.Objects;
 import org.apache.syncope.common.keymaster.client.api.model.NetworkService;
 import org.apache.syncope.common.keymaster.client.api.startstop.KeymasterStart;
 import org.apache.syncope.common.keymaster.client.api.startstop.KeymasterStop;
-import org.apache.syncope.common.lib.types.IdRepoEntitlement;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.SpringApplication;
-import 
org.springframework.boot.actuate.autoconfigure.security.reactive.EndpointRequest;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
 import org.springframework.cloud.gateway.route.Route;
 import org.springframework.cloud.gateway.route.RouteLocator;
 import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
-import org.springframework.context.EnvironmentAware;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.PropertySource;
-import org.springframework.core.env.Environment;
-import 
org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
-import org.springframework.security.config.web.server.ServerHttpSecurity;
-import 
org.springframework.security.core.userdetails.MapReactiveUserDetailsService;
-import org.springframework.security.core.userdetails.User;
-import org.springframework.security.core.userdetails.UserDetails;
-import org.springframework.security.web.server.SecurityWebFilterChain;
-import 
org.springframework.security.web.server.util.matcher.NegatedServerWebExchangeMatcher;
-import 
org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
 import reactor.core.publisher.Flux;
 
 @PropertySource("classpath:sra.properties")
 @PropertySource(value = "file:${conf.directory}/sra.properties", 
ignoreResourceNotFound = true)
-@EnableWebFluxSecurity
 @SpringBootApplication
-public class SyncopeSRAApplication implements EnvironmentAware {
+public class SyncopeSRAApplication {
 
     public static void main(final String[] args) {
         SpringApplication.run(SyncopeSRAApplication.class, args);
@@ -57,11 +43,9 @@ public class SyncopeSRAApplication implements 
EnvironmentAware {
     @Autowired
     private RouteProvider provider;
 
-    private Environment env;
-
-    @Override
-    public void setEnvironment(final Environment env) {
-        this.env = env;
+    @Bean
+    public RouteLocator routes(final RouteLocatorBuilder builder) {
+        return () -> 
Flux.fromIterable(provider.fetch()).map(Route.AbstractBuilder::build);
     }
 
     @Bean
@@ -73,29 +57,4 @@ public class SyncopeSRAApplication implements 
EnvironmentAware {
     public KeymasterStop keymasterStop() {
         return new KeymasterStop(NetworkService.Type.SRA);
     }
-
-    @Bean
-    public RouteLocator routes(final RouteLocatorBuilder builder) {
-        return () -> 
Flux.fromIterable(provider.fetch()).map(Route.AbstractBuilder::build);
-    }
-
-    @Bean
-    public SecurityWebFilterChain actuatorSecurityFilterChain(final 
ServerHttpSecurity http) {
-        ServerWebExchangeMatcher actuatorMatcher = 
EndpointRequest.toAnyEndpoint();
-        return http.securityMatcher(actuatorMatcher).
-                authorizeExchange().anyExchange().authenticated().
-                and().httpBasic().
-                and().csrf().requireCsrfProtectionMatcher(new 
NegatedServerWebExchangeMatcher(actuatorMatcher)).
-                and().build();
-    }
-
-    @Bean
-    public MapReactiveUserDetailsService userDetailsService() {
-        UserDetails user = User.builder().
-                
username(Objects.requireNonNull(env.getProperty("anonymousUser"))).
-                password("{noop}" + env.getProperty("anonymousKey")).
-                roles(IdRepoEntitlement.ANONYMOUS).
-                build();
-        return new MapReactiveUserDetailsService(user);
-    }
 }
diff --git 
a/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/GatewayRouteStatus.java
 b/sra/src/main/java/org/apache/syncope/sra/filters/AddRefererFilterFactory.java
similarity index 58%
rename from 
common/am/lib/src/main/java/org/apache/syncope/common/lib/types/GatewayRouteStatus.java
rename to 
sra/src/main/java/org/apache/syncope/sra/filters/AddRefererFilterFactory.java
index 5155a1d..c8fa789 100644
--- 
a/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/GatewayRouteStatus.java
+++ 
b/sra/src/main/java/org/apache/syncope/sra/filters/AddRefererFilterFactory.java
@@ -16,12 +16,18 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.syncope.common.lib.types;
+package org.apache.syncope.sra.filters;
 
-public enum GatewayRouteStatus {
-    DRAFT,
-    STAGING,
-    PUBLISHED,
-    DEPRECATED
+import org.springframework.cloud.gateway.filter.GatewayFilter;
+import org.springframework.http.HttpHeaders;
 
+public class AddRefererFilterFactory extends CustomGatewayFilterFactory {
+
+    @Override
+    public GatewayFilter apply(final Config config) {
+        return (exchange, chain) -> chain.filter(exchange.mutate().request(
+                exchange.getRequest().mutate().headers(headers -> headers.add(
+                HttpHeaders.REFERER, 
exchange.getRequest().getURI().toASCIIString())).build()).
+                build());
+    }
 }
diff --git 
a/sra/src/main/java/org/apache/syncope/sra/filters/ClientCertsToRequestHeaderFilterFactory.java
 
b/sra/src/main/java/org/apache/syncope/sra/filters/ClientCertsToRequestHeaderFilterFactory.java
new file mode 100644
index 0000000..259535b
--- /dev/null
+++ 
b/sra/src/main/java/org/apache/syncope/sra/filters/ClientCertsToRequestHeaderFilterFactory.java
@@ -0,0 +1,74 @@
+/*
+ * 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.syncope.sra.filters;
+
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.List;
+import org.apache.commons.lang3.ArrayUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.cloud.gateway.filter.GatewayFilter;
+import 
org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
+import 
org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory.NameConfig;
+import org.springframework.http.server.reactive.ServerHttpRequest;
+
+public class ClientCertsToRequestHeaderFilterFactory extends 
AbstractGatewayFilterFactory<NameConfig> {
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(ClientCertsToRequestHeaderFilterFactory.class);
+
+    public ClientCertsToRequestHeaderFilterFactory() {
+        super(NameConfig.class);
+    }
+
+    @Override
+    public GatewayFilter apply(final NameConfig config) {
+        return (exchange, chain) -> {
+            ServerHttpRequest originalRequest = exchange.getRequest();
+
+            ServerHttpRequest mutatedRequest;
+            if (originalRequest.getSslInfo() != null
+                    && 
ArrayUtils.isNotEmpty(originalRequest.getSslInfo().getPeerCertificates())) {
+
+                LOG.debug("Client certificates found in original request: {}",
+                        
originalRequest.getSslInfo().getPeerCertificates().length);
+
+                List<String> certs = new ArrayList<>();
+                for (X509Certificate cert : 
originalRequest.getSslInfo().getPeerCertificates()) {
+                    try {
+                        
certs.add(Base64.getEncoder().encodeToString(cert.getEncoded()));
+                    } catch (CertificateEncodingException e) {
+                        LOG.error("Could not encode one of client 
certificates", e);
+                    }
+                }
+
+                mutatedRequest = originalRequest.mutate().
+                        headers(headers -> headers.addAll(config.getName(), 
certs)).
+                        sslInfo(null).
+                        build();
+            } else {
+                mutatedRequest = originalRequest;
+            }
+
+            return 
chain.filter(exchange.mutate().request(mutatedRequest).build());
+        };
+    }
+}
diff --git 
a/sra/src/main/java/org/apache/syncope/sra/CustomGatewayFilterFactory.java 
b/sra/src/main/java/org/apache/syncope/sra/filters/CustomGatewayFilterFactory.java
similarity index 89%
rename from 
sra/src/main/java/org/apache/syncope/sra/CustomGatewayFilterFactory.java
rename to 
sra/src/main/java/org/apache/syncope/sra/filters/CustomGatewayFilterFactory.java
index 7ac2cd8..d15bc41 100644
--- a/sra/src/main/java/org/apache/syncope/sra/CustomGatewayFilterFactory.java
+++ 
b/sra/src/main/java/org/apache/syncope/sra/filters/CustomGatewayFilterFactory.java
@@ -16,8 +16,9 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.syncope.sra;
+package org.apache.syncope.sra.filters;
 
+import java.util.Optional;
 import org.springframework.cloud.gateway.filter.GatewayFilter;
 import 
org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
 
@@ -27,7 +28,7 @@ import 
org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFac
 public abstract class CustomGatewayFilterFactory
         extends 
AbstractGatewayFilterFactory<CustomGatewayFilterFactory.Config> {
 
-    public static class Config {
+    public static final class Config {
 
         private String data;
 
@@ -44,6 +45,10 @@ public abstract class CustomGatewayFilterFactory
         super(CustomGatewayFilterFactory.Config.class);
     }
 
+    public Optional<Integer> getOrder() {
+        return Optional.empty();
+    }
+
     @Override
     public abstract GatewayFilter apply(Config config);
 }
diff --git 
a/sra/src/main/java/org/apache/syncope/sra/filters/LinkRewriteGatewayFilterFactory.java
 
b/sra/src/main/java/org/apache/syncope/sra/filters/LinkRewriteGatewayFilterFactory.java
new file mode 100644
index 0000000..8120b0b
--- /dev/null
+++ 
b/sra/src/main/java/org/apache/syncope/sra/filters/LinkRewriteGatewayFilterFactory.java
@@ -0,0 +1,88 @@
+/*
+ * 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.syncope.sra.filters;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import org.apache.commons.lang3.BooleanUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Document;
+import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
+import org.springframework.web.server.ServerWebExchange;
+
+public class LinkRewriteGatewayFilterFactory extends 
ModifyResponseGatewayFilterFactory {
+
+    @Override
+    protected boolean skipCond(final ServerHttpResponseDecorator decorator) {
+        return decorator.getHeaders().getContentType() == null
+                || 
!StringUtils.containsIgnoreCase(decorator.getHeaders().getContentType().toString(),
 "html");
+    }
+
+    private Charset getCharset(final ServerHttpResponseDecorator decorator) {
+        return decorator.getHeaders().getContentType() != null
+                && decorator.getHeaders().getContentType().getCharset() != null
+                ? decorator.getHeaders().getContentType().getCharset()
+                : StandardCharsets.UTF_8;
+    }
+
+    private void replace(final Document doc, final String element, final 
String attr, final String prefix) {
+        doc.select(element).forEach(link -> {
+            String attrValue = link.attributes().get(attr);
+            if (attrValue.startsWith("/") && !attrValue.startsWith("//")) {
+                link.attr(attr, attrValue.replace(attrValue, prefix + 
attrValue));
+            }
+        });
+    }
+
+    @Override
+    protected byte[] modifyResponse(
+            final InputStream responseBody,
+            final Config config,
+            final ServerHttpResponseDecorator decorator,
+            final ServerWebExchange exchange)
+            throws IOException {
+
+        String[] keyValue = config.getData().split(",");
+
+        String oldBase = StringUtils.appendIfMissing(keyValue[0], "/");
+        String newBase = StringUtils.appendIfMissing(keyValue[1], "/");
+        String newBaseAsPrefix = StringUtils.removeEnd(keyValue[1], "/");
+
+        boolean rewriterRootAttrs = true;
+        if (keyValue.length == 3) {
+            rewriterRootAttrs = BooleanUtils.toBoolean(keyValue[2]);
+        }
+
+        Document doc = Jsoup.parse(
+                responseBody, getCharset(decorator).name(), 
exchange.getRequest().getURI().toASCIIString());
+
+        if (rewriterRootAttrs) {
+            replace(doc, "a", "href", newBaseAsPrefix);
+            replace(doc, "link", "href", newBaseAsPrefix);
+            replace(doc, "img", "src", newBaseAsPrefix);
+            replace(doc, "script", "src", newBaseAsPrefix);
+            replace(doc, "object", "data", newBaseAsPrefix);
+        }
+
+        return doc.toString().replace(oldBase, newBase).getBytes();
+    }
+}
diff --git 
a/sra/src/main/java/org/apache/syncope/sra/filters/ModifyResponseGatewayFilterFactory.java
 
b/sra/src/main/java/org/apache/syncope/sra/filters/ModifyResponseGatewayFilterFactory.java
new file mode 100644
index 0000000..36f3d00
--- /dev/null
+++ 
b/sra/src/main/java/org/apache/syncope/sra/filters/ModifyResponseGatewayFilterFactory.java
@@ -0,0 +1,155 @@
+/*
+ * 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.syncope.sra.filters;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.zip.GZIPInputStream;
+import java.util.zip.GZIPOutputStream;
+import org.reactivestreams.Publisher;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.cloud.gateway.filter.GatewayFilter;
+import org.springframework.cloud.gateway.filter.GatewayFilterChain;
+import org.springframework.cloud.gateway.filter.NettyWriteResponseFilter;
+import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
+import org.springframework.core.Ordered;
+import org.springframework.core.io.buffer.DataBuffer;
+import org.springframework.core.io.buffer.DataBufferUtils;
+import org.springframework.core.io.buffer.PooledDataBuffer;
+import org.springframework.http.server.reactive.ServerHttpResponse;
+import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
+import org.springframework.web.server.ServerWebExchange;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+/**
+ * Inspired by {@link 
org.springframework.cloud.gateway.filter.factory.rewrite.ModifyResponseBodyGatewayFilterFactory}.
+ */
+public abstract class ModifyResponseGatewayFilterFactory extends 
CustomGatewayFilterFactory {
+
+    protected static final Logger LOG = 
LoggerFactory.getLogger(ModifyResponseGatewayFilterFactory.class);
+
+    @Override
+    public GatewayFilter apply(final Config config) {
+        return new InternalModifyResponseGatewayFilter(config);
+    }
+
+    protected abstract byte[] modifyResponse(
+            InputStream responseBody,
+            Config config,
+            ServerHttpResponseDecorator decorator,
+            ServerWebExchange exchange)
+            throws IOException;
+
+    protected boolean skipCond(final ServerHttpResponseDecorator decorator) {
+        LOG.debug("Decorator: {}", decorator);
+        return false;
+    }
+
+    protected class InternalModifyResponseGatewayFilter implements 
GatewayFilter, Ordered {
+
+        private final Config config;
+
+        public InternalModifyResponseGatewayFilter(final Config config) {
+            this.config = config;
+        }
+
+        @Override
+        public Mono<Void> filter(final ServerWebExchange exchange, final 
GatewayFilterChain chain) {
+            return 
chain.filter(exchange.mutate().response(decorate(exchange)).build());
+        }
+
+        @SuppressWarnings("squid:S3776")
+        private ServerHttpResponse decorate(final ServerWebExchange exchange) {
+            return new ServerHttpResponseDecorator(exchange.getResponse()) {
+
+                @SuppressWarnings("squid:S3358")
+                @Override
+                public Mono<Void> writeWith(final Publisher<? extends 
DataBuffer> body) {
+                    return skipCond(this)
+                            ? super.writeWith(body)
+                            : super.writeWith(Flux.from(body).
+                                    collectList().
+                                    filter(list -> !list.isEmpty()).
+                                    map(list -> 
list.get(0).factory().join(list)).
+                                    doOnDiscard(PooledDataBuffer.class, 
DataBufferUtils::release).
+                                    map(dataBuffer -> {
+                                        if (dataBuffer.readableByteCount() > 
0) {
+                                            LOG.trace("Retaining body in 
exchange attribute");
+                                            exchange.getAttributes().put(
+                                                    
ServerWebExchangeUtils.CACHED_REQUEST_BODY_ATTR, dataBuffer);
+                                        }
+
+                                        boolean inputCompressed = false;
+                                        if (dataBuffer.readableByteCount() >= 
2) {
+                                            byte[] first2 = new byte[2];
+                                            dataBuffer.read(first2, 0, 2);
+                                            dataBuffer.readPosition(0);
+
+                                            inputCompressed = ((first2[0] == 
(byte) (GZIPInputStream.GZIP_MAGIC))
+                                                    && (first2[1] == (byte) 
(GZIPInputStream.GZIP_MAGIC >> 8)));
+                                        }
+
+                                        boolean outputCompressed = false;
+                                        byte[] output;
+                                        try (InputStream is = inputCompressed
+                                                ? new 
GZIPInputStream(dataBuffer.asInputStream())
+                                                : dataBuffer.asInputStream()) {
+
+                                            outputCompressed = is instanceof 
GZIPInputStream;
+
+                                            output = modifyResponse(is, 
config, this, exchange);
+                                        } catch (IOException e) {
+                                            LOG.error("While modifying 
response", e);
+
+                                            output = new 
byte[dataBuffer.readableByteCount()];
+                                            dataBuffer.read(output);
+                                        }
+
+                                        if (outputCompressed) {
+                                            try (ByteArrayOutputStream baos = 
new ByteArrayOutputStream(output.length);
+                                                    GZIPOutputStream gzipos = 
new GZIPOutputStream(baos)) {
+
+                                                gzipos.write(output);
+                                                gzipos.finish();
+                                                output = baos.toByteArray();
+                                            } catch (IOException e) {
+                                                LOG.error("While GZIP-encoding 
output", e);
+                                            }
+                                        }
+
+                                        return 
exchange.getResponse().bufferFactory().wrap(output);
+                                    }));
+                }
+
+                @Override
+                public Mono<Void> writeAndFlushWith(final Publisher<? extends 
Publisher<? extends DataBuffer>> body) {
+                    return writeWith(Flux.from(body).flatMapSequential(p -> 
p));
+                }
+            };
+        }
+
+        @Override
+        public int getOrder() {
+            return NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER - 1;
+        }
+    }
+}
diff --git 
a/sra/src/main/java/org/apache/syncope/sra/filters/QueryParamToRequestHeaderFilterFactory.java
 
b/sra/src/main/java/org/apache/syncope/sra/filters/QueryParamToRequestHeaderFilterFactory.java
new file mode 100644
index 0000000..85975f3
--- /dev/null
+++ 
b/sra/src/main/java/org/apache/syncope/sra/filters/QueryParamToRequestHeaderFilterFactory.java
@@ -0,0 +1,56 @@
+/*
+ * 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.syncope.sra.filters;
+
+import java.net.URI;
+import org.springframework.cloud.gateway.filter.GatewayFilter;
+import 
org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
+import 
org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory.NameConfig;
+import org.springframework.http.server.reactive.ServerHttpRequest;
+import org.springframework.web.util.UriComponentsBuilder;
+
+public class QueryParamToRequestHeaderFilterFactory extends 
AbstractGatewayFilterFactory<NameConfig> {
+
+    public QueryParamToRequestHeaderFilterFactory() {
+        super(NameConfig.class);
+    }
+
+    @Override
+    public GatewayFilter apply(final NameConfig config) {
+        return (exchange, chain) -> {
+            ServerHttpRequest originalRequest = exchange.getRequest();
+
+            ServerHttpRequest mutatedRequest;
+            if (originalRequest.getQueryParams().isEmpty()) {
+                mutatedRequest = originalRequest;
+            } else {
+                URI newUri = 
UriComponentsBuilder.fromUri(originalRequest.getURI()).
+                        replaceQueryParam(config.getName()).build().toUri();
+
+                mutatedRequest = exchange.getRequest().mutate().
+                        uri(newUri).
+                        headers(headers -> headers.addAll(
+                        config.getName(), 
originalRequest.getQueryParams().get(config.getName()))).
+                        build();
+            }
+
+            return 
chain.filter(exchange.mutate().request(mutatedRequest).build());
+        };
+    }
+}
diff --git 
a/sra/src/main/java/org/apache/syncope/sra/CustomRoutePredicateFactory.java 
b/sra/src/main/java/org/apache/syncope/sra/predicates/CustomRoutePredicateFactory.java
similarity index 97%
rename from 
sra/src/main/java/org/apache/syncope/sra/CustomRoutePredicateFactory.java
rename to 
sra/src/main/java/org/apache/syncope/sra/predicates/CustomRoutePredicateFactory.java
index 22d9299..992eab8 100644
--- a/sra/src/main/java/org/apache/syncope/sra/CustomRoutePredicateFactory.java
+++ 
b/sra/src/main/java/org/apache/syncope/sra/predicates/CustomRoutePredicateFactory.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.syncope.sra;
+package org.apache.syncope.sra.predicates;
 
 import java.util.function.Predicate;
 import org.springframework.cloud.gateway.handler.AsyncPredicate;
diff --git a/sra/src/test/java/org/apache/syncope/sra/AbstractTest.java 
b/sra/src/test/java/org/apache/syncope/sra/AbstractTest.java
new file mode 100644
index 0000000..c6a3ee2
--- /dev/null
+++ b/sra/src/test/java/org/apache/syncope/sra/AbstractTest.java
@@ -0,0 +1,65 @@
+/*
+ * 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.syncope.sra;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import java.io.IOException;
+import java.net.Socket;
+import java.util.Base64;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.cloud.contract.wiremock.AutoConfigureWireMock;
+import org.springframework.test.context.ContextConfiguration;
+
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+@ContextConfiguration(initializers = ZookeeperTestingServer.class)
+@AutoConfigureWireMock(port = 0)
+public abstract class AbstractTest {
+
+    protected static final ObjectMapper MAPPER = new 
ObjectMapper().registerModule(new JavaTimeModule());
+
+    public static boolean available(int port) {
+        try (Socket ignored = new Socket("localhost", port)) {
+            return false;
+        } catch (IOException ignored) {
+            return true;
+        }
+    }
+
+    @Autowired
+    protected RouteRefresher routeRefresher;
+
+    @Value("${local.server.port}")
+    protected int gatewayPort;
+
+    @Value("${wiremock.server.port}")
+    protected int wiremockPort;
+
+    @Value("${anonymousUser}")
+    private String anonymousUser;
+
+    @Value("${anonymousKey}")
+    private String anonymousKey;
+
+    protected String basicAuthHeader() {
+        return "Basic " + Base64.getEncoder().encodeToString((anonymousUser + 
":" + anonymousKey).getBytes());
+    }
+}
diff --git a/sra/src/test/java/org/apache/syncope/sra/ActuatorTest.java 
b/sra/src/test/java/org/apache/syncope/sra/ActuatorTest.java
new file mode 100644
index 0000000..72a8432
--- /dev/null
+++ b/sra/src/test/java/org/apache/syncope/sra/ActuatorTest.java
@@ -0,0 +1,64 @@
+/*
+ * 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.syncope.sra;
+
+import javax.net.ssl.SSLException;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.actuate.endpoint.http.ActuatorMediaType;
+import org.springframework.http.HttpHeaders;
+import org.springframework.test.web.reactive.server.WebTestClient;
+
+public class ActuatorTest extends AbstractTest {
+
+    @Autowired
+    private WebTestClient webClient;
+
+    @Test
+    public void health() throws SSLException {
+        webClient.get().uri("/actuator/health").
+                exchange().expectStatus().isUnauthorized();
+
+        webClient.get().uri("/actuator/health").
+                header(HttpHeaders.AUTHORIZATION, basicAuthHeader()).
+                exchange().
+                expectStatus().isOk().
+                expectHeader().valueEquals(HttpHeaders.CONTENT_TYPE, 
ActuatorMediaType.V3_JSON);
+    }
+
+    @Test
+    public void routes() throws SSLException {
+        webClient.get().uri("/actuator/gateway/routes").
+                exchange().expectStatus().isUnauthorized();
+
+        webClient.get().uri("/actuator/gateway/routes").
+                header(HttpHeaders.AUTHORIZATION, basicAuthHeader()).
+                exchange().expectStatus().isOk();
+    }
+
+    @Test
+    public void requests() throws SSLException {
+        webClient.get().uri("/actuator/metrics/gateway.requests").
+                exchange().expectStatus().isUnauthorized();
+
+        webClient.get().uri("/actuator/metrics/gateway.requests").
+                header(HttpHeaders.AUTHORIZATION, basicAuthHeader()).
+                exchange().expectStatus().isNotFound();
+    }
+}
diff --git a/sra/src/test/java/org/apache/syncope/sra/RouteProviderTest.java 
b/sra/src/test/java/org/apache/syncope/sra/RouteProviderTest.java
new file mode 100644
index 0000000..4c778ee
--- /dev/null
+++ b/sra/src/test/java/org/apache/syncope/sra/RouteProviderTest.java
@@ -0,0 +1,647 @@
+/*
+ * 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.syncope.sra;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.get;
+import static com.github.tomakehurst.wiremock.client.WireMock.post;
+import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
+import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import java.io.IOException;
+import java.net.URI;
+import java.time.ZonedDateTime;
+import org.apache.syncope.common.lib.to.GatewayRouteTO;
+import org.apache.syncope.common.lib.types.FilterFactory;
+import org.apache.syncope.common.lib.types.GatewayRouteFilter;
+import org.apache.syncope.common.lib.types.GatewayRoutePredicate;
+import org.apache.syncope.common.lib.types.GatewayRoutePredicateCond;
+import org.apache.syncope.common.lib.types.PredicateFactory;
+import org.apache.syncope.sra.filters.BodyPropertyAddingGatewayFilterFactory;
+import 
org.apache.syncope.sra.predicates.BodyPropertyMatchingRoutePredicateFactory;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.reactive.server.WebTestClient;
+import org.springframework.web.reactive.function.BodyInserters;
+
+public class RouteProviderTest extends AbstractTest {
+
+    @Autowired
+    private WebTestClient webClient;
+
+    @BeforeEach
+    public void clearRoutes() {
+        SyncopeCoreTestingServer.ROUTES.clear();
+    }
+
+    @Test
+    public void root() {
+        webClient.get().exchange().expectStatus().isNotFound();
+    }
+
+    @Test
+    public void addResponseHeader() {
+        // 1. no mapping for URL
+        
webClient.get().uri("/addResponseHeader").exchange().expectStatus().isNotFound();
+
+        // 2. stub for proxied URL
+        stubFor(get(urlEqualTo("/addResponseHeader")).willReturn(aResponse()));
+
+        // 3. create route configuration
+        GatewayRouteTO route = new GatewayRouteTO();
+        route.setKey("addResponseHeader");
+        route.setTarget(URI.create("http://localhost:"; + wiremockPort));
+        route.getPredicates().add(new GatewayRoutePredicate.Builder().
+                factory(PredicateFactory.METHOD).args("GET").build());
+        route.getPredicates().add(new GatewayRoutePredicate.Builder().
+                
factory(PredicateFactory.PATH).args("/addResponseHeader").cond(GatewayRoutePredicateCond.AND).build());
+        route.getFilters().add(new GatewayRouteFilter.Builder().
+                
factory(FilterFactory.ADD_RESPONSE_HEADER).args("Hello,World").build());
+
+        SyncopeCoreTestingServer.ROUTES.put(route.getKey(), route);
+        routeRefresher.refresh();
+
+        // 4. now mapping works for URL
+        webClient.get().uri("/addResponseHeader").exchange().
+                expectStatus().isOk().
+                expectHeader().valueEquals("Hello", "World");
+
+        // 5. update route configuration
+        route.getFilters().clear();
+        route.getFilters().add(new GatewayRouteFilter.Builder().
+                
factory(FilterFactory.ADD_RESPONSE_HEADER).args("Hello,WorldZ").build());
+
+        routeRefresher.refresh();
+
+        // 6. mapping for URL is updated too
+        webClient.get().uri("/addResponseHeader").exchange().
+                expectStatus().isOk().
+                expectHeader().valueEquals("Hello", "WorldZ");
+
+        // 7. update route configuration again
+        route.getFilters().clear();
+
+        routeRefresher.refresh();
+
+        // 8. mapping for URL is updated again
+        webClient.get().uri("/addResponseHeader").exchange().
+                expectStatus().isOk().
+                expectHeader().doesNotExist("Hello");
+    }
+
+    @Test
+    public void addRequestHeader() {
+        
webClient.get().uri("/requestHeader").exchange().expectStatus().isNotFound();
+
+        stubFor(get(urlEqualTo("/requestHeader")).withHeader("Hello", 
equalTo("World")).willReturn(aResponse()));
+
+        GatewayRouteTO route = new GatewayRouteTO();
+        route.setKey("requestHeader");
+        route.setTarget(URI.create("http://localhost:"; + wiremockPort));
+        route.getPredicates().add(new GatewayRoutePredicate.Builder().
+                
factory(PredicateFactory.REMOTE_ADDR).args("localhost").build());
+        route.getFilters().add(new GatewayRouteFilter.Builder().
+                
factory(FilterFactory.ADD_REQUEST_HEADER).args("Hello,World").build());
+
+        SyncopeCoreTestingServer.ROUTES.put(route.getKey(), route);
+        routeRefresher.refresh();
+
+        webClient.get().uri("/requestHeader").exchange().expectStatus().isOk();
+
+        route.getFilters().clear();
+        route.getFilters().add(new GatewayRouteFilter.Builder().
+                
factory(FilterFactory.ADD_REQUEST_HEADER).args("Hello,Mondo").build());
+
+        SyncopeCoreTestingServer.ROUTES.put(route.getKey(), route);
+        routeRefresher.refresh();
+
+        
webClient.get().uri("/requestHeader").exchange().expectStatus().isNotFound();
+
+        route.getFilters().clear();
+        route.getFilters().add(new GatewayRouteFilter.Builder().
+                
factory(FilterFactory.REMOVE_REQUEST_HEADER).args("Hello").build());
+
+        SyncopeCoreTestingServer.ROUTES.put(route.getKey(), route);
+        routeRefresher.refresh();
+
+        webClient.get().uri("/requestHeader").header("Hello", 
"World").exchange().expectStatus().isNotFound();
+
+        route.getFilters().clear();
+        route.getFilters().add(new GatewayRouteFilter.Builder().
+                factory(FilterFactory.SET_REQUEST_HEADER).args("Hello, 
World").build());
+
+        SyncopeCoreTestingServer.ROUTES.put(route.getKey(), route);
+        routeRefresher.refresh();
+
+        webClient.get().uri("/requestHeader").header("Hello", 
"Mondo").exchange().expectStatus().isOk();
+    }
+
+    @Test
+    public void hystrix() {
+        webClient.get().uri("/fallback").exchange().
+                expectStatus().isOk().
+                expectBody().
+                consumeWith(response -> 
assertThat(response.getResponseBody()).isEqualTo("fallback".getBytes()));
+
+        stubFor(get(urlEqualTo("/delay/3")).
+                willReturn(aResponse().
+                        withBody("no fallback").
+                        withFixedDelay(3000)));
+
+        GatewayRouteTO route = new GatewayRouteTO();
+        route.setKey("hystrix");
+        route.setTarget(URI.create("http://localhost:"; + wiremockPort));
+        route.getPredicates().add(new GatewayRoutePredicate.Builder().
+                factory(PredicateFactory.HOST).args("*.hystrix.com").build());
+        route.getFilters().add(new GatewayRouteFilter.Builder().
+                
factory(FilterFactory.HYSTRIX).args("fallbackcmd,forward:/fallback").build());
+
+        SyncopeCoreTestingServer.ROUTES.put(route.getKey(), route);
+        routeRefresher.refresh();
+
+        webClient.get().uri("/delay/3").
+                header(HttpHeaders.HOST, "www.hystrix.com").
+                exchange().
+                expectStatus().isOk().
+                expectBody().
+                consumeWith(response -> 
assertThat(response.getResponseBody()).isEqualTo("fallback".getBytes()));
+    }
+
+    @Test
+    public void requestHeaderToRequestUri() {
+        
webClient.get().uri("/requestHeaderToRequestUri").exchange().expectStatus().isNotFound();
+
+        
stubFor(get(urlEqualTo("/requestHeaderToRequestUri")).willReturn(aResponse()));
+
+        GatewayRouteTO route = new GatewayRouteTO();
+        route.setKey("requestHeaderToRequestUri");
+        route.setTarget(URI.create("http://localhost:"; + wiremockPort));
+        route.getFilters().add(new GatewayRouteFilter.Builder().
+                
factory(FilterFactory.REQUEST_HEADER_TO_REQUEST_URI).args("NewUri").build());
+
+        SyncopeCoreTestingServer.ROUTES.put(route.getKey(), route);
+        routeRefresher.refresh();
+
+        webClient.get().uri("/requestHeaderToRequestUri").
+                header("NewUri", "http://localhost:"; + wiremockPort + 
"/requestHeaderToRequestUri").
+                exchange().expectStatus().isOk();
+    }
+
+    @Test
+    public void responseHeader() {
+        
webClient.get().uri("/responseHeader").exchange().expectStatus().isNotFound();
+
+        
stubFor(get(urlEqualTo("/responseHeader")).willReturn(aResponse().withHeader("Hello",
 "World")));
+
+        GatewayRouteTO route = new GatewayRouteTO();
+        route.setKey("responseHeader");
+        route.setTarget(URI.create("http://localhost:"; + wiremockPort));
+        route.getFilters().add(new GatewayRouteFilter.Builder().
+                
factory(FilterFactory.REMOVE_RESPONSE_HEADER).args("Hello").build());
+
+        SyncopeCoreTestingServer.ROUTES.put(route.getKey(), route);
+        routeRefresher.refresh();
+
+        webClient.get().uri("/responseHeader").exchange().
+                expectStatus().isOk().
+                expectHeader().doesNotExist("Hello");
+
+        route.getFilters().clear();
+
+        SyncopeCoreTestingServer.ROUTES.put(route.getKey(), route);
+        routeRefresher.refresh();
+
+        webClient.get().uri("/responseHeader").exchange().
+                expectStatus().isOk().
+                expectHeader().valueEquals("Hello", "World");
+
+        route.getFilters().clear();
+        route.getFilters().add(new GatewayRouteFilter.Builder().
+                
factory(FilterFactory.REWRITE_RESPONSE_HEADER).args("Hello,World,Mondo").build());
+        SyncopeCoreTestingServer.ROUTES.put(route.getKey(), route);
+        routeRefresher.refresh();
+
+        webClient.get().uri("/responseHeader").exchange().
+                expectStatus().isOk().
+                expectHeader().valueEquals("Hello", "Mondo");
+
+        route.getFilters().clear();
+        route.getFilters().add(new GatewayRouteFilter.Builder().
+                
factory(FilterFactory.SET_RESPONSE_HEADER).args("Hello,Mondo").build());
+        SyncopeCoreTestingServer.ROUTES.put(route.getKey(), route);
+        routeRefresher.refresh();
+
+        webClient.get().uri("/responseHeader").exchange().
+                expectStatus().isOk().
+                expectHeader().valueEquals("Hello", "Mondo");
+    }
+
+    @Test
+    public void addRequestParameter() {
+        
webClient.get().uri("/addRequestParameter?Hello=World").exchange().expectStatus().isNotFound();
+
+        
stubFor(get(urlEqualTo("/addRequestParameter?Hello=World")).withQueryParam("Hello",
 equalTo("World")).
+                willReturn(aResponse()));
+
+        GatewayRouteTO route = new GatewayRouteTO();
+        route.setKey("addRequestParameter");
+        route.setTarget(URI.create("http://localhost:"; + wiremockPort));
+        route.getFilters().add(new GatewayRouteFilter.Builder().
+                
factory(FilterFactory.ADD_REQUEST_PARAMETER).args("Hello,World").build());
+
+        SyncopeCoreTestingServer.ROUTES.put(route.getKey(), route);
+        routeRefresher.refresh();
+
+        
webClient.get().uri("/addRequestParameter").exchange().expectStatus().isOk();
+
+        route.getFilters().clear();
+        route.getFilters().add(new GatewayRouteFilter.Builder().
+                
factory(FilterFactory.ADD_REQUEST_PARAMETER).args("Hello,Mondo").build());
+
+        SyncopeCoreTestingServer.ROUTES.put(route.getKey(), route);
+        routeRefresher.refresh();
+
+        
webClient.get().uri("/addRequestParameter").exchange().expectStatus().isNotFound();
+    }
+
+    @Test
+    public void rewritePath() {
+        webClient.get().uri("/rewrite").exchange().expectStatus().isNotFound();
+
+        stubFor(get(urlEqualTo("/rewrite")).willReturn(aResponse()));
+
+        GatewayRouteTO route = new GatewayRouteTO();
+        route.setKey("rewrite");
+        route.setTarget(URI.create("http://localhost:"; + wiremockPort));
+        route.getFilters().add(new GatewayRouteFilter.Builder().
+                
factory(FilterFactory.REWRITE_PATH).args("/remove/(?<segment>.*), 
/${segment}").build());
+        route.getFilters().add(new GatewayRouteFilter.Builder().
+                factory(FilterFactory.SECURE_HEADERS).build());
+
+        SyncopeCoreTestingServer.ROUTES.put(route.getKey(), route);
+        routeRefresher.refresh();
+
+        webClient.get().uri("/remove/rewrite").exchange().
+                expectStatus().isOk().
+                expectHeader().valueEquals("X-XSS-Protection", "1 ; 
mode=block");
+
+        route.getFilters().clear();
+
+        SyncopeCoreTestingServer.ROUTES.put(route.getKey(), route);
+        routeRefresher.refresh();
+
+        webClient.get().uri("/remove/rewrite").exchange().
+                expectStatus().isNotFound();
+
+        route.getFilters().clear();
+
+        SyncopeCoreTestingServer.ROUTES.put(route.getKey(), route);
+        routeRefresher.refresh();
+
+        webClient.get().uri("/rewrite").exchange().
+                expectStatus().isOk().
+                expectHeader().doesNotExist("X-XSS-Protection");
+
+        route.getFilters().clear();
+        route.getPredicates().add(new GatewayRoutePredicate.Builder().
+                
factory(PredicateFactory.PATH).args("/remove/{segment}").build());
+        route.getFilters().add(new GatewayRouteFilter.Builder().
+                factory(FilterFactory.SET_PATH).args("/{segment}").build());
+
+        SyncopeCoreTestingServer.ROUTES.put(route.getKey(), route);
+        routeRefresher.refresh();
+
+        webClient.get().uri("/remove/rewrite").exchange().
+                expectStatus().isOk();
+
+        route.getFilters().clear();
+        route.getFilters().add(new GatewayRouteFilter.Builder().
+                factory(FilterFactory.STRIP_PREFIX).args("1").build());
+
+        SyncopeCoreTestingServer.ROUTES.put(route.getKey(), route);
+        routeRefresher.refresh();
+
+        
webClient.get().uri("/remove/rewrite").exchange().expectStatus().isOk();
+    }
+
+    @Test
+    public void redirect() {
+        
webClient.get().uri("/redirect").exchange().expectStatus().isNotFound();
+
+        stubFor(get(urlEqualTo("/redirect")).willReturn(aResponse()));
+
+        GatewayRouteTO route = new GatewayRouteTO();
+        route.setKey("redirect");
+        route.setTarget(URI.create("http://localhost:"; + wiremockPort));
+        route.getFilters().add(new GatewayRouteFilter.Builder().
+                factory(FilterFactory.REDIRECT).args("307,http://127.0.0.1:"; + 
wiremockPort).build());
+
+        SyncopeCoreTestingServer.ROUTES.put(route.getKey(), route);
+        routeRefresher.refresh();
+
+        
webClient.get().uri("/redirect").exchange().expectStatus().isTemporaryRedirect();
+
+        route.getFilters().clear();
+
+        SyncopeCoreTestingServer.ROUTES.put(route.getKey(), route);
+        routeRefresher.refresh();
+
+        webClient.get().uri("/redirect").exchange().expectStatus().isOk();
+
+        route.getFilters().clear();
+        route.getFilters().add(new 
GatewayRouteFilter.Builder().factory(FilterFactory.SET_STATUS).args("404").build());
+
+        SyncopeCoreTestingServer.ROUTES.put(route.getKey(), route);
+        routeRefresher.refresh();
+
+        
webClient.get().uri("/redirect").exchange().expectStatus().isNotFound();
+    }
+
+    @Test
+    public void datetime() {
+        
webClient.get().uri("/prefix/datetime").exchange().expectStatus().isNotFound();
+
+        stubFor(get(urlEqualTo("/prefix/datetime")).willReturn(aResponse()));
+
+        GatewayRouteTO route = new GatewayRouteTO();
+        route.setKey("datetime");
+        route.setTarget(URI.create("http://localhost:"; + wiremockPort));
+        route.getPredicates().add(new GatewayRoutePredicate.Builder().
+                
factory(PredicateFactory.AFTER).args(ZonedDateTime.now().minusYears(1).toString()).build());
+        route.getPredicates().add(new GatewayRoutePredicate.Builder().
+                
factory(PredicateFactory.BEFORE).args(ZonedDateTime.now().plusYears(1).toString()).
+                cond(GatewayRoutePredicateCond.AND).build());
+        route.getPredicates().add(new GatewayRoutePredicate.Builder().
+                
factory(PredicateFactory.BETWEEN).args(ZonedDateTime.now().minusYears(1).toString()
 + ","
+                + ZonedDateTime.now().plusYears(1).toString()).
+                cond(GatewayRoutePredicateCond.AND).build());
+        route.getFilters().add(new GatewayRouteFilter.Builder().
+                factory(FilterFactory.PREFIX_PATH).args("/prefix").build());
+
+        SyncopeCoreTestingServer.ROUTES.put(route.getKey(), route);
+        routeRefresher.refresh();
+
+        webClient.get().uri("/datetime").exchange().
+                expectStatus().isOk();
+
+        route.getPredicates().clear();
+        route.getPredicates().add(new GatewayRoutePredicate.Builder().
+                
factory(PredicateFactory.AFTER).args(ZonedDateTime.now().plusYears(1).toString()).build());
+        route.getPredicates().add(new GatewayRoutePredicate.Builder().
+                
factory(PredicateFactory.BEFORE).args(ZonedDateTime.now().minusYears(1).toString()).
+                cond(GatewayRoutePredicateCond.OR).build());
+        route.getPredicates().add(new GatewayRoutePredicate.Builder().
+                
factory(PredicateFactory.BETWEEN).args(ZonedDateTime.now().plusYears(1).toString()
 + ","
+                + 
ZonedDateTime.now().minusYears(1).toString()).cond(GatewayRoutePredicateCond.OR).build());
+
+        SyncopeCoreTestingServer.ROUTES.put(route.getKey(), route);
+        routeRefresher.refresh();
+
+        
webClient.get().uri("/datetime").exchange().expectStatus().isNotFound();
+
+        route.getPredicates().clear();
+        route.getPredicates().add(new GatewayRoutePredicate.Builder().
+                
factory(PredicateFactory.BEFORE).negate().args(ZonedDateTime.now().minusYears(1).toString()).build());
+
+        SyncopeCoreTestingServer.ROUTES.put(route.getKey(), route);
+        routeRefresher.refresh();
+
+        webClient.get().uri("/datetime").exchange().expectStatus().isOk();
+    }
+
+    @Test
+    public void header() {
+        webClient.get().uri("/header").exchange().expectStatus().isNotFound();
+
+        stubFor(get(urlEqualTo("/header")).willReturn(aResponse()));
+
+        GatewayRouteTO route = new GatewayRouteTO();
+        route.setKey("header");
+        route.setTarget(URI.create("http://localhost:"; + wiremockPort));
+        route.getPredicates().add(new GatewayRoutePredicate.Builder().
+                factory(PredicateFactory.COOKIE).args("Hello,World").build());
+        route.getPredicates().add(new GatewayRoutePredicate.Builder().
+                
factory(PredicateFactory.HOST).args("host").cond(GatewayRoutePredicateCond.AND).build());
+        route.getPredicates().add(new GatewayRoutePredicate.Builder().
+                
factory(PredicateFactory.HEADER).args("Hello,World").cond(GatewayRoutePredicateCond.AND).build());
+        route.getFilters().add(new 
GatewayRouteFilter.Builder().factory(FilterFactory.PRESERVE_HOST_HEADER).build());
+
+        SyncopeCoreTestingServer.ROUTES.put(route.getKey(), route);
+        routeRefresher.refresh();
+
+        webClient.get().uri("/header").cookie("Hello", "World").header("Host", 
"host").header("Hello", "World").
+                exchange().expectStatus().isOk();
+
+        webClient.get().uri("/header").cookie("Hello", "Mondo").header("Host", 
"host").header("Hello", "World").
+                exchange().expectStatus().isNotFound();
+
+        webClient.get().uri("/header").cookie("Hello", "World").header("Host", 
"anotherHost").header("Hello", "World").
+                exchange().expectStatus().isNotFound();
+
+        webClient.get().uri("/header").cookie("Hello", "World").header("Host", 
"host").header("Hello", "Mondo").
+                exchange().expectStatus().isNotFound();
+    }
+
+    @Test
+    public void query() {
+        stubFor(get(urlEqualTo("/query?name=value")).willReturn(aResponse()));
+
+        GatewayRouteTO route = new GatewayRouteTO();
+        route.setKey("query");
+        route.setTarget(URI.create("http://localhost:"; + wiremockPort));
+        route.getPredicates().add(new GatewayRoutePredicate.Builder().
+                factory(PredicateFactory.QUERY).args("name,value").build());
+        route.getFilters().add(new GatewayRouteFilter.Builder().
+                factory(FilterFactory.SAVE_SESSION).build());
+        route.getFilters().add(new GatewayRouteFilter.Builder().
+                factory(FilterFactory.SET_REQUEST_SIZE).args("5000").build());
+        route.getFilters().add(new GatewayRouteFilter.Builder().
+                factory(FilterFactory.RETRY).args("3").build());
+
+        SyncopeCoreTestingServer.ROUTES.put(route.getKey(), route);
+        routeRefresher.refresh();
+
+        
webClient.get().uri("/query?name=value").exchange().expectStatus().isOk();
+
+        route.getPredicates().clear();
+        route.getPredicates().add(new GatewayRoutePredicate.Builder().
+                
factory(PredicateFactory.QUERY).args("name,anotherValue").build());
+
+        SyncopeCoreTestingServer.ROUTES.put(route.getKey(), route);
+        routeRefresher.refresh();
+
+        
webClient.get().uri("/query?name=value").exchange().expectStatus().isNotFound();
+    }
+
+    @Test
+    public void path() {
+        stubFor(get(urlEqualTo("/pathMatcher/1")).willReturn(aResponse()));
+        stubFor(get(urlEqualTo("/pathMatcher/2")).willReturn(aResponse()));
+        stubFor(get(urlEqualTo("/pathMatcher/2/3")).willReturn(aResponse()));
+
+        GatewayRouteTO route = new GatewayRouteTO();
+        route.setKey("pathMatcher");
+        route.setTarget(URI.create("http://localhost:"; + wiremockPort));
+        route.getPredicates().add(new GatewayRoutePredicate.Builder().
+                
factory(PredicateFactory.PATH).args("/pathMatcher/**").build());
+
+        SyncopeCoreTestingServer.ROUTES.put(route.getKey(), route);
+        routeRefresher.refresh();
+
+        webClient.get().uri("/pathMatcher/1").exchange().expectStatus().isOk();
+        webClient.get().uri("/pathMatcher/2").exchange().expectStatus().isOk();
+        
webClient.get().uri("/pathMatcher/2/3").exchange().expectStatus().isOk();
+        
webClient.get().uri("/pathMatcher/4").exchange().expectStatus().isNotFound();
+    }
+
+    @Test
+    public void linkRewrite() {
+        stubFor(get(urlEqualTo("/linkRewrite")).willReturn(aResponse().
+                withHeader(HttpHeaders.CONTENT_TYPE, 
MediaType.TEXT_HTML_VALUE).
+                withBody("<html><head></head><body><a 
href=\"/absolute\">absolute link</a></body></html>")));
+
+        GatewayRouteTO route = new GatewayRouteTO();
+        route.setKey("linkRewrite");
+        route.setTarget(URI.create("http://localhost:"; + wiremockPort));
+        route.getFilters().add(new 
GatewayRouteFilter.Builder().factory(FilterFactory.LINK_REWRITE).
+                args("http://localhost:"; + gatewayPort).build());
+
+        SyncopeCoreTestingServer.ROUTES.put(route.getKey(), route);
+        routeRefresher.refresh();
+
+        webClient.get().uri("/linkRewrite").exchange().
+                expectStatus().isOk().
+                expectBody().consumeWith(exchange -> {
+                    assertTrue(new String(exchange.getResponseBody()).
+                            contains("<a href=\"http://localhost:"; + 
gatewayPort + "/absolute\">"));
+                });
+
+        route.getFilters().clear();
+        route.getFilters().add(new 
GatewayRouteFilter.Builder().factory(FilterFactory.LINK_REWRITE).
+                args("http://localhost:"; + gatewayPort + ",true").build());
+
+        SyncopeCoreTestingServer.ROUTES.put(route.getKey(), route);
+        routeRefresher.refresh();
+
+        webClient.get().uri("/linkRewrite").exchange().
+                expectStatus().isOk().
+                expectBody().consumeWith(exchange -> {
+                    assertTrue(new String(exchange.getResponseBody()).
+                            contains("<a href=\"http://localhost:"; + 
gatewayPort + "/absolute\">"));
+                });
+    }
+
+    @Test
+    public void clientCertToRequestHeader() {
+        stubFor(get(urlEqualTo("/clientCert")).willReturn(aResponse().
+                withHeader(HttpHeaders.CONTENT_TYPE, 
MediaType.TEXT_HTML_VALUE)));
+
+        GatewayRouteTO route = new GatewayRouteTO();
+        route.setKey("clientCert");
+        route.setTarget(URI.create("http://localhost:"; + wiremockPort));
+        route.getFilters().add(new GatewayRouteFilter.Builder().
+                factory(FilterFactory.CLIENT_CERTS_TO_REQUEST_HEADER).build());
+
+        SyncopeCoreTestingServer.ROUTES.put(route.getKey(), route);
+        routeRefresher.refresh();
+
+        webClient.get().uri("/clientCert").exchange().
+                expectStatus().isOk().
+                expectHeader().doesNotExist("X-Client-Certificate");
+    }
+
+    @Test
+    public void queryParamToRequestHeader() {
+        stubFor(get(urlEqualTo("/queryParamToRequestHeader")).
+                withHeader("Hello", equalTo("World")).willReturn(aResponse()));
+
+        
stubFor(get(urlEqualTo("/queryParamToRequestHeader?Header=Test&Header=Test1")).
+                withHeader("Hello", equalTo("World")).willReturn(aResponse()));
+
+        GatewayRouteTO route = new GatewayRouteTO();
+        route.setKey("queryParamToRequestHeader");
+        route.setTarget(URI.create("http://localhost:"; + wiremockPort));
+        route.getFilters().add(new GatewayRouteFilter.Builder().
+                
factory(FilterFactory.QUERY_PARAM_TO_REQUEST_HEADER).args("Hello").build());
+
+        SyncopeCoreTestingServer.ROUTES.put(route.getKey(), route);
+        routeRefresher.refresh();
+
+        webClient.get().uri("/queryParamToRequestHeader").exchange().
+                expectStatus().isNotFound();
+
+        
webClient.get().uri("/queryParamToRequestHeader?Hello=World").exchange().
+                expectStatus().isOk();
+
+        
webClient.get().uri("/queryParamToRequestHeader?Header=Test&Hello=World&Header=Test1").exchange().
+                expectStatus().isOk();
+    }
+
+    @Test
+    public void custom() {
+        stubFor(post(urlEqualTo("/custom")).
+                willReturn(aResponse().
+                        withHeader(HttpHeaders.CONTENT_TYPE, 
MediaType.APPLICATION_JSON_VALUE).
+                        withBody("{\"data\": \"data\"}")));
+
+        GatewayRouteTO routeTO = new GatewayRouteTO();
+        routeTO.setKey("custom");
+        routeTO.setTarget(URI.create("http://localhost:"; + wiremockPort));
+        routeTO.getPredicates().add(new GatewayRoutePredicate.Builder().
+                factory(PredicateFactory.CUSTOM).
+                args(BodyPropertyMatchingRoutePredicateFactory.class.getName() 
+ ";cool").build());
+        routeTO.getFilters().add(new GatewayRouteFilter.Builder().
+                
factory(FilterFactory.ADD_RESPONSE_HEADER).args("Custom,matched").build());
+        routeTO.getFilters().add(new GatewayRouteFilter.Builder().
+                factory(FilterFactory.CUSTOM).
+                args(BodyPropertyAddingGatewayFilterFactory.class.getName() + 
";customized=true").build());
+
+        SyncopeCoreTestingServer.ROUTES.put(routeTO.getKey(), routeTO);
+        routeRefresher.refresh();
+
+        webClient.post().uri("/custom").
+                
body(BodyInserters.fromValue(MAPPER.createObjectNode().put("other", true))).
+                exchange().
+                expectStatus().isNotFound();
+
+        webClient.post().uri("/custom").
+                
body(BodyInserters.fromValue(MAPPER.createObjectNode().put("cool", true))).
+                exchange().
+                expectStatus().isOk().
+                expectHeader().valueEquals("Custom", "matched").
+                expectBody().
+                consumeWith(response -> {
+                    try {
+                        JsonNode body = 
MAPPER.readTree(response.getResponseBody());
+                        assertTrue(body.has("customized"));
+                        assertTrue(body.get("customized").asBoolean());
+                    } catch (IOException e) {
+                        fail(e.getMessage(), e);
+                    }
+                });
+    }
+}
diff --git 
a/sra/src/test/java/org/apache/syncope/sra/SyncopeCoreTestingServer.java 
b/sra/src/test/java/org/apache/syncope/sra/SyncopeCoreTestingServer.java
index 0d740e4..a90217b 100644
--- a/sra/src/test/java/org/apache/syncope/sra/SyncopeCoreTestingServer.java
+++ b/sra/src/test/java/org/apache/syncope/sra/SyncopeCoreTestingServer.java
@@ -53,21 +53,23 @@ public class SyncopeCoreTestingServer implements 
ApplicationListener<ContextRefr
 
     @Override
     public void onApplicationEvent(final ContextRefreshedEvent event) {
-        // 1. start (mocked) Core as embedded CXF
-        JAXRSServerFactoryBean sf = new JAXRSServerFactoryBean();
-        sf.setAddress(ADDRESS);
-        sf.setResourceClasses(GatewayRouteService.class);
-        sf.setResourceProvider(
-                GatewayRouteService.class,
-                new SingletonResourceProvider(new StubGatewayRouteService(), 
true));
-        sf.setProviders(List.of(new JacksonJsonProvider()));
-        sf.create();
-
-        // 2. register Core in Keymaster
-        NetworkService core = new NetworkService();
-        core.setType(NetworkService.Type.CORE);
-        core.setAddress(ADDRESS);
-        serviceOps.register(core);
+        if (AbstractTest.available(9080)) {
+            // 1. start (mocked) Core as embedded CXF
+            JAXRSServerFactoryBean sf = new JAXRSServerFactoryBean();
+            sf.setAddress(ADDRESS);
+            sf.setResourceClasses(GatewayRouteService.class);
+            sf.setResourceProvider(
+                    GatewayRouteService.class,
+                    new SingletonResourceProvider(new 
StubGatewayRouteService(), true));
+            sf.setProviders(List.of(new JacksonJsonProvider()));
+            sf.create();
+
+            // 2. register Core in Keymaster
+            NetworkService core = new NetworkService();
+            core.setType(NetworkService.Type.CORE);
+            core.setAddress(ADDRESS);
+            serviceOps.register(core);
+        }
     }
 
     public class StubGatewayRouteService implements GatewayRouteService {
diff --git a/sra/src/test/java/org/apache/syncope/sra/SyncopeSRATest.java 
b/sra/src/test/java/org/apache/syncope/sra/SyncopeSRATest.java
deleted file mode 100644
index e5caef8..0000000
--- a/sra/src/test/java/org/apache/syncope/sra/SyncopeSRATest.java
+++ /dev/null
@@ -1,209 +0,0 @@
-/*
- * 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.syncope.sra;
-
-import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
-import static com.github.tomakehurst.wiremock.client.WireMock.get;
-import static com.github.tomakehurst.wiremock.client.WireMock.post;
-import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
-import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
-import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.junit.jupiter.api.Assertions.fail;
-
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import java.io.IOException;
-import java.net.URI;
-import org.apache.syncope.common.lib.to.GatewayRouteTO;
-import org.apache.syncope.common.lib.types.FilterFactory;
-import org.apache.syncope.common.lib.types.GatewayRouteFilter;
-import org.apache.syncope.common.lib.types.GatewayRoutePredicate;
-import org.apache.syncope.common.lib.types.GatewayRouteStatus;
-import org.apache.syncope.common.lib.types.PredicateCond;
-import org.apache.syncope.common.lib.types.PredicateFactory;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.cloud.contract.wiremock.AutoConfigureWireMock;
-import org.springframework.http.HttpHeaders;
-import org.springframework.http.MediaType;
-import org.springframework.test.context.ContextConfiguration;
-import org.springframework.test.web.reactive.server.WebTestClient;
-import org.springframework.web.reactive.function.BodyInserters;
-
-@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
-@ContextConfiguration(initializers = ZookeeperTestingServer.class)
-@AutoConfigureWireMock(port = 0)
-public class SyncopeSRATest {
-
-    private static final ObjectMapper MAPPER = new ObjectMapper();
-
-    @Autowired
-    private WebTestClient webClient;
-
-    @Autowired
-    private RouteRefresher routeRefresher;
-
-    @Value("${wiremock.server.port}")
-    private int wiremockPort;
-
-    @BeforeEach
-    public void clearRoutes() {
-        SyncopeCoreTestingServer.ROUTES.clear();
-    }
-
-    @Test
-    public void root() {
-        webClient.get().exchange().expectStatus().isNotFound();
-    }
-
-    @Test
-    public void getAddResponseHeader() {
-        // 1. no mapping for URL
-        
webClient.get().uri("/getAddResponseHeader").exchange().expectStatus().isNotFound();
-
-        // 2. stub for proxied URL
-        
stubFor(get(urlEqualTo("/getAddResponseHeader")).willReturn(aResponse()));
-
-        // 3. create route configuration
-        GatewayRouteTO routeTO = new GatewayRouteTO();
-        routeTO.setKey("getAddResponseHeader");
-        routeTO.setStatus(GatewayRouteStatus.PUBLISHED);
-        routeTO.setTarget(URI.create("http://localhost:"; + wiremockPort));
-        routeTO.getPredicates().add(new GatewayRoutePredicate.Builder().
-                factory(PredicateFactory.METHOD).args("GET").build());
-        routeTO.getPredicates().add(new GatewayRoutePredicate.Builder().
-                
factory(PredicateFactory.PATH).args("/getAddResponseHeader").cond(PredicateCond.AND).build());
-        routeTO.getFilters().add(new GatewayRouteFilter.Builder().
-                
factory(FilterFactory.ADD_RESPONSE_HEADER).args("Hello,World").build());
-
-        SyncopeCoreTestingServer.ROUTES.put(routeTO.getKey(), routeTO);
-
-        routeRefresher.refresh();
-
-        // 4. now mapping works for URL
-        webClient.get().uri("/getAddResponseHeader").exchange().
-                expectStatus().isOk().
-                expectHeader().valueEquals("Hello", "World");
-
-        // 5. update route configuration
-        routeTO.getFilters().clear();
-        routeTO.getFilters().add(new GatewayRouteFilter.Builder().
-                
factory(FilterFactory.ADD_RESPONSE_HEADER).args("Hello,WorldZ").build());
-
-        routeRefresher.refresh();
-
-        // 6. mapping for URL is updated too
-        webClient.get().uri("/getAddResponseHeader").exchange().
-                expectStatus().isOk().
-                expectHeader().valueEquals("Hello", "WorldZ");
-
-        // 7. update route configuration again
-        routeTO.getFilters().clear();
-
-        routeRefresher.refresh();
-
-        // 8. mapping for URL is updated again
-        webClient.get().uri("/getAddResponseHeader").exchange().
-                expectStatus().isOk().
-                expectHeader().doesNotExist("Hello");
-    }
-
-    @Test
-    public void hystrix() {
-        webClient.get().uri("/fallback").exchange().
-                expectStatus().isOk().
-                expectBody().
-                consumeWith(response -> 
assertThat(response.getResponseBody()).isEqualTo("fallback".getBytes()));
-
-        stubFor(get(urlEqualTo("/delay/3")).
-                willReturn(aResponse().
-                        withBody("no fallback").
-                        withFixedDelay(3000)));
-
-        GatewayRouteTO routeTO = new GatewayRouteTO();
-        routeTO.setKey("hystrix");
-        routeTO.setStatus(GatewayRouteStatus.PUBLISHED);
-        routeTO.setTarget(URI.create("http://localhost:"; + wiremockPort));
-        routeTO.getPredicates().add(new GatewayRoutePredicate.Builder().
-                factory(PredicateFactory.HOST).args("*.hystrix.com").build());
-        routeTO.getFilters().add(new GatewayRouteFilter.Builder().
-                
factory(FilterFactory.HYSTRIX).args("fallbackcmd,forward:/fallback").build());
-
-        SyncopeCoreTestingServer.ROUTES.put(routeTO.getKey(), routeTO);
-
-        routeRefresher.refresh();
-
-        webClient.get().uri("/delay/3").
-                header(HttpHeaders.HOST, "www.hystrix.com").
-                exchange().
-                expectStatus().isOk().
-                expectBody().
-                consumeWith(response -> 
assertThat(response.getResponseBody()).isEqualTo("fallback".getBytes()));
-    }
-
-    @Test
-    public void custom() {
-        stubFor(post(urlEqualTo("/custom")).
-                willReturn(aResponse().
-                        withHeader(HttpHeaders.CONTENT_TYPE, 
MediaType.APPLICATION_JSON_VALUE).
-                        withBody("{\"data\": \"data\"}")));
-
-        GatewayRouteTO routeTO = new GatewayRouteTO();
-        routeTO.setKey("custom");
-        routeTO.setStatus(GatewayRouteStatus.PUBLISHED);
-        routeTO.setTarget(URI.create("http://localhost:"; + wiremockPort));
-        routeTO.getPredicates().add(new GatewayRoutePredicate.Builder().
-                factory(PredicateFactory.CUSTOM).
-                args(BodyPropertyMatchingRoutePredicateFactory.class.getName() 
+ ";cool").build());
-        routeTO.getFilters().add(new GatewayRouteFilter.Builder().
-                
factory(FilterFactory.ADD_RESPONSE_HEADER).args("Custom,matched").build());
-        routeTO.getFilters().add(new GatewayRouteFilter.Builder().
-                factory(FilterFactory.CUSTOM).
-                args(BodyPropertyAddingGatewayFilterFactory.class.getName() + 
";customized=true").build());
-
-        SyncopeCoreTestingServer.ROUTES.put(routeTO.getKey(), routeTO);
-
-        routeRefresher.refresh();
-
-        webClient.post().uri("/custom").
-                
body(BodyInserters.fromValue(MAPPER.createObjectNode().put("other", true))).
-                exchange().
-                expectStatus().isNotFound();
-
-        webClient.post().uri("/custom").
-                
body(BodyInserters.fromValue(MAPPER.createObjectNode().put("cool", true))).
-                exchange().
-                expectStatus().isOk().
-                expectHeader().valueEquals("Custom", "matched").
-                expectBody().
-                consumeWith(response -> {
-                    try {
-                        JsonNode body = 
MAPPER.readTree(response.getResponseBody());
-                        assertTrue(body.has("customized"));
-                        assertTrue(body.get("customized").asBoolean());
-                    } catch (IOException e) {
-                        fail(e.getMessage(), e);
-                    }
-                });
-    }
-}
diff --git a/sra/src/test/java/org/apache/syncope/sra/TLSRouteProviderTest.java 
b/sra/src/test/java/org/apache/syncope/sra/TLSRouteProviderTest.java
new file mode 100644
index 0000000..8401abe
--- /dev/null
+++ b/sra/src/test/java/org/apache/syncope/sra/TLSRouteProviderTest.java
@@ -0,0 +1,148 @@
+/*
+ * 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.syncope.sra;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.get;
+import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.verify;
+
+import com.github.tomakehurst.wiremock.matching.AnythingPattern;
+import io.netty.handler.ssl.SslContext;
+import io.netty.handler.ssl.SslContextBuilder;
+import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
+import java.io.IOException;
+import java.net.URI;
+import java.security.KeyManagementException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.CertificateException;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLException;
+import org.apache.syncope.common.lib.to.GatewayRouteTO;
+import org.apache.syncope.common.lib.types.FilterFactory;
+import org.apache.syncope.common.lib.types.GatewayRouteFilter;
+import org.apache.syncope.common.lib.types.GatewayRoutePredicate;
+import org.apache.syncope.common.lib.types.GatewayRoutePredicateCond;
+import org.apache.syncope.common.lib.types.PredicateFactory;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.http.client.reactive.ClientHttpConnector;
+import org.springframework.http.client.reactive.ReactorClientHttpConnector;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.web.reactive.server.WebTestClient;
+import reactor.netty.http.client.HttpClient;
+
+@ActiveProfiles({ "tls" })
+public class TLSRouteProviderTest extends AbstractTest {
+
+    private WebTestClient webClient() throws SSLException {
+        SslContext sslContext = SslContextBuilder.forClient().
+                trustManager(InsecureTrustManagerFactory.INSTANCE).
+                build();
+        return webClient(sslContext);
+    }
+
+    private WebTestClient webClient(final SslContext sslContext) throws 
SSLException {
+        HttpClient httpClient = HttpClient.create().
+                secure(sslContextSpec -> 
sslContextSpec.sslContext(sslContext));
+        ClientHttpConnector connector = new 
ReactorClientHttpConnector(httpClient);
+        return 
WebTestClient.bindToServer(connector).baseUrl("https://localhost:"; + 
gatewayPort).build();
+    }
+
+    @BeforeEach
+    public void clearRoutes() {
+        SyncopeCoreTestingServer.ROUTES.clear();
+    }
+
+    @Test
+    public void root() throws SSLException {
+        webClient().get().exchange().expectStatus().isNotFound();
+    }
+
+    @Test
+    public void clientAuth() throws SSLException, KeyStoreException, 
IOException, NoSuchAlgorithmException,
+            CertificateException, UnrecoverableKeyException, 
KeyManagementException {
+
+        KeyStore store = KeyStore.getInstance("PKCS12");
+        store.load(getClass().getResourceAsStream("/client_pavel.p12"), 
"password".toCharArray());
+
+        KeyManagerFactory kmf = 
KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+        kmf.init(store, "password".toCharArray());
+
+        SslContext sslContext = SslContextBuilder.forClient().
+                trustManager(InsecureTrustManagerFactory.INSTANCE).
+                keyManager(kmf).
+                build();
+        WebTestClient webClient = webClient(sslContext);
+
+        stubFor(get(urlEqualTo("/getWithClientAuth")).willReturn(aResponse()));
+
+        GatewayRouteTO route = new GatewayRouteTO();
+        route.setKey("getWithClientAuth");
+        route.setTarget(URI.create("http://localhost:"; + wiremockPort));
+        route.getPredicates().add(new GatewayRoutePredicate.Builder().
+                factory(PredicateFactory.METHOD).args("GET").build());
+        route.getPredicates().add(new GatewayRoutePredicate.Builder().
+                
factory(PredicateFactory.PATH).args("/getWithClientAuth").cond(GatewayRoutePredicateCond.AND).build());
+        route.getFilters().add(new GatewayRouteFilter.Builder().
+                factory(FilterFactory.CLIENT_CERTS_TO_REQUEST_HEADER).build());
+
+        SyncopeCoreTestingServer.ROUTES.put(route.getKey(), route);
+        routeRefresher.refresh();
+
+        
webClient.get().uri("/getWithClientAuth").exchange().expectStatus().isOk();
+
+        verify(getRequestedFor(urlEqualTo("/getWithClientAuth")).
+                withHeader("X-Client-Certificate", new AnythingPattern()));
+    }
+
+    @Test
+    public void withoutClientCert() throws SSLException, KeyStoreException, 
IOException, NoSuchAlgorithmException,
+            CertificateException, UnrecoverableKeyException, 
KeyManagementException {
+
+        SslContext sslContext = SslContextBuilder.forClient().
+                trustManager(InsecureTrustManagerFactory.INSTANCE).
+                build();
+        WebTestClient webClient = webClient(sslContext);
+
+        stubFor(get(urlEqualTo("/withoutClientCert")).willReturn(aResponse()));
+
+        GatewayRouteTO route = new GatewayRouteTO();
+        route.setKey("withoutClientCert");
+        route.setTarget(URI.create("http://localhost:"; + wiremockPort));
+        route.getPredicates().add(new GatewayRoutePredicate.Builder().
+                factory(PredicateFactory.METHOD).args("GET").build());
+        route.getPredicates().add(new GatewayRoutePredicate.Builder().
+                
factory(PredicateFactory.PATH).args("/withoutClientCert").cond(GatewayRoutePredicateCond.AND).build());
+        route.getFilters().add(new GatewayRouteFilter.Builder().
+                factory(FilterFactory.CLIENT_CERTS_TO_REQUEST_HEADER).build());
+
+        SyncopeCoreTestingServer.ROUTES.put(route.getKey(), route);
+        routeRefresher.refresh();
+
+        webClient.get().uri("/withoutClientCert").exchange().
+                expectStatus().isOk().
+                expectHeader().doesNotExist("X-Client-Certificate");
+    }
+}
diff --git 
a/sra/src/test/java/org/apache/syncope/sra/ZookeeperTestingServer.java 
b/sra/src/test/java/org/apache/syncope/sra/ZookeeperTestingServer.java
index 930d724..b9c8762 100644
--- a/sra/src/test/java/org/apache/syncope/sra/ZookeeperTestingServer.java
+++ b/sra/src/test/java/org/apache/syncope/sra/ZookeeperTestingServer.java
@@ -53,29 +53,31 @@ public class ZookeeperTestingServer implements 
ApplicationContextInitializer<Con
             throw new IllegalStateException("Could not load 
/keymaster.properties", e);
         }
 
-        Configuration.setConfiguration(new Configuration() {
+        if (AbstractTest.available(port.get())) {
+            Configuration.setConfiguration(new Configuration() {
 
-            private final AppConfigurationEntry[] entries = {
-                new AppConfigurationEntry(
-                DigestLoginModule.class.getName(),
-                AppConfigurationEntry.LoginModuleControlFlag.REQUIRED,
-                Map.of("user_" + username.get(), password.get()))
-            };
+                private final AppConfigurationEntry[] entries = {
+                    new AppConfigurationEntry(
+                    DigestLoginModule.class.getName(),
+                    AppConfigurationEntry.LoginModuleControlFlag.REQUIRED,
+                    Map.of("user_" + username.get(), password.get()))
+                };
 
-            @Override
-            public AppConfigurationEntry[] getAppConfigurationEntry(final 
String name) {
-                return entries;
-            }
-        });
+                @Override
+                public AppConfigurationEntry[] getAppConfigurationEntry(final 
String name) {
+                    return entries;
+                }
+            });
 
-        Map<String, Object> customProperties = new HashMap<>();
-        customProperties.put("authProvider.1", 
SASLAuthenticationProvider.class.getName());
-        InstanceSpec spec = new InstanceSpec(null, port.get(), -1, -1, true, 
1, -1, -1, customProperties);
+            Map<String, Object> customProperties = new HashMap<>();
+            customProperties.put("authProvider.1", 
SASLAuthenticationProvider.class.getName());
+            InstanceSpec spec = new InstanceSpec(null, port.get(), -1, -1, 
true, 1, -1, -1, customProperties);
 
-        try {
-            new TestingServer(spec, true);
-        } catch (Exception e) {
-            fail(e);
+            try {
+                new TestingServer(spec, true);
+            } catch (Exception e) {
+                fail(e);
+            }
         }
     }
 }
diff --git 
a/sra/src/test/java/org/apache/syncope/sra/BodyPropertyAddingGatewayFilterFactory.java
 
b/sra/src/test/java/org/apache/syncope/sra/filters/BodyPropertyAddingGatewayFilterFactory.java
similarity index 99%
rename from 
sra/src/test/java/org/apache/syncope/sra/BodyPropertyAddingGatewayFilterFactory.java
rename to 
sra/src/test/java/org/apache/syncope/sra/filters/BodyPropertyAddingGatewayFilterFactory.java
index d849771..853c92b 100644
--- 
a/sra/src/test/java/org/apache/syncope/sra/BodyPropertyAddingGatewayFilterFactory.java
+++ 
b/sra/src/test/java/org/apache/syncope/sra/filters/BodyPropertyAddingGatewayFilterFactory.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.syncope.sra;
+package org.apache.syncope.sra.filters;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.node.ObjectNode;
diff --git 
a/sra/src/test/java/org/apache/syncope/sra/BodyPropertyMatchingRoutePredicateFactory.java
 
b/sra/src/test/java/org/apache/syncope/sra/predicates/BodyPropertyMatchingRoutePredicateFactory.java
similarity index 98%
rename from 
sra/src/test/java/org/apache/syncope/sra/BodyPropertyMatchingRoutePredicateFactory.java
rename to 
sra/src/test/java/org/apache/syncope/sra/predicates/BodyPropertyMatchingRoutePredicateFactory.java
index 9be1ab8..cdbd3fd 100644
--- 
a/sra/src/test/java/org/apache/syncope/sra/BodyPropertyMatchingRoutePredicateFactory.java
+++ 
b/sra/src/test/java/org/apache/syncope/sra/predicates/BodyPropertyMatchingRoutePredicateFactory.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.syncope.sra;
+package org.apache.syncope.sra.predicates;
 
 import com.fasterxml.jackson.databind.JsonNode;
 import java.util.List;
diff --git a/sra/src/test/resources/application-tls.properties 
b/sra/src/test/resources/application-tls.properties
new file mode 100644
index 0000000..5aa6f20
--- /dev/null
+++ b/sra/src/test/resources/application-tls.properties
@@ -0,0 +1,31 @@
+# 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.
+server.ssl.enabled=true
+
+server.ssl.key-store-type=PKCS12
+server.ssl.key-store=classpath:keyStore.p12
+server.ssl.key-store-password=password
+
+server.ssl.trust-store=classpath:trustStore.jks
+server.ssl.trust-store-password=password
+server.ssl.trust-store-type=JKS
+
+server.ssl.client-auth=want
+
+server.port=8443
+security.require-ssl=true
+service.discovery.address=http://localhost:8443
diff --git a/sra/src/test/resources/client_pavel.p12 
b/sra/src/test/resources/client_pavel.p12
new file mode 100644
index 0000000..b7958f2
Binary files /dev/null and b/sra/src/test/resources/client_pavel.p12 differ
diff --git a/sra/src/test/resources/keyStore.p12 
b/sra/src/test/resources/keyStore.p12
new file mode 100644
index 0000000..85505b9
Binary files /dev/null and b/sra/src/test/resources/keyStore.p12 differ
diff --git a/sra/src/test/resources/trustStore.jks 
b/sra/src/test/resources/trustStore.jks
new file mode 100644
index 0000000..6000a06
Binary files /dev/null and b/sra/src/test/resources/trustStore.jks differ

Reply via email to