From 20bda01e37087b103c036cad152e651356e91145 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Dominik=20Franti=C5=A1ek=20Bu=C4=8D=C3=ADk?=
 <bucik@ics.muni.cz>
Date: Mon, 8 Aug 2022 14:58:58 +0200
Subject: [PATCH] refactor: Refactored how brokers are structured and
 configured

Needs new configuration

BRAKING CHANGE: Update configuration according to application.yml,
dropped profiles
---
 pom.xml                                       |  41 +-
 .../java/cz/muni/ics/ga4gh/Application.java   |   1 +
 .../cz/muni/ics/ga4gh/ApplicationContext.java |  21 +-
 .../muni/ics/ga4gh/adapters/PerunAdapter.java |  39 --
 .../ga4gh/adapters/PerunAdapterMethods.java   |  23 -
 .../ga4gh/adapters/impl/PerunAdapterImpl.java |  72 ---
 .../ga4gh/adapters/impl/PerunAdapterLdap.java | 199 --------
 .../ga4gh/adapters/impl/PerunAdapterRpc.java  | 283 -----------
 .../java/cz/muni/ics/ga4gh/aop/LogTimes.java  |  11 -
 .../cz/muni/ics/ga4gh/base/AdapterBeans.java  |  68 +++
 .../keystore => base}/JWKSetKeyStore.java     |  11 +-
 .../java/cz/muni/ics/ga4gh/base/Utils.java    | 332 ++++++++++++
 .../ics/ga4gh/base/adapters/PerunAdapter.java |  52 ++
 .../base/adapters/PerunAdapterMethods.java    |  28 +
 .../adapters/PerunAdapterMethodsLdap.java     |   2 +-
 .../adapters/PerunAdapterMethodsRpc.java      |   9 +-
 .../adapters/PerunRpcAdapterMapper.java}      |  29 +-
 .../base/adapters/impl/PerunAdapterImpl.java  | 128 +++++
 .../base/adapters/impl/PerunAdapterLdap.java  | 350 +++++++++++++
 .../base/adapters/impl/PerunAdapterRpc.java   | 480 ++++++++++++++++++
 .../connectors/PerunConnectorLdap.java        |  79 ++-
 .../connectors/PerunConnectorRpc.java         | 145 +++---
 .../ga4gh/{ => base}/enums/MemberStatus.java  |   2 +-
 .../exceptions/ConfigurationException.java    |  26 +
 .../InconvertibleValueException.java          |   2 +-
 .../InvalidRequestParametersException.java    |  26 +
 .../exceptions/MissingFieldException.java     |   2 +-
 .../PerunAdapterOperationException.java       |  26 +
 .../exceptions/UserNotFoundException.java     |  26 +
 .../exceptions/UserNotUniqueException.java    |   4 +-
 .../ga4gh/{ => base}/model/Affiliation.java   |   7 +-
 .../{ => base}/model/AttributeMapping.java    |  10 +-
 .../base/model/BasicAuthCredentials.java      |  28 +
 .../model/ClaimRepositoryHeader.java}         |  18 +-
 .../muni/ics/ga4gh/base/model/ExtSource.java  |  28 +
 .../model/Ga4ghClaimRepository.java           |  10 +-
 .../ics/ga4gh/base/model/Ga4ghPassport.java   |  34 ++
 .../ga4gh/base/model/Ga4ghPassportVisa.java   | 119 +++++
 .../ga4gh/base/model/Ga4ghPassportVisaV1.java |  31 ++
 .../ics/ga4gh/{ => base}/model/Group.java     |  18 +-
 .../ics/ga4gh/{ => base}/model/Member.java    |  14 +-
 .../{ => base}/model/PerunAttributeValue.java |  19 +-
 .../ga4gh/{ => base}/model/UserExtSource.java |  22 +-
 .../AttributeMappingProperties.java           |  38 ++
 .../base/properties/BasicAuthProperties.java  |  47 ++
 .../properties/BrokerInstanceProperties.java  |  89 ++++
 .../properties/Ga4ghBrokersProperties.java    |  75 +++
 .../Ga4ghClaimRepositoryProperties.java       |  32 ++
 .../properties/PerunAdapterProperties.java    |  45 ++
 .../PerunLdapConnectorProperties.java         |  86 ++++
 .../PerunRpcConnectorProperties.java          |  79 +++
 .../muni/ics/ga4gh/config/AdapterConfig.java  |  19 -
 .../ics/ga4gh/config/AttributesConfig.java    |  20 -
 .../ics/ga4gh/config/BasicAuthConfig.java     |  19 -
 .../muni/ics/ga4gh/config/BrokerConfig.java   |  37 --
 .../cz/muni/ics/ga4gh/config/Ga4ghConfig.java |  20 -
 .../cz/muni/ics/ga4gh/config/LdapConfig.java  |  33 --
 .../cz/muni/ics/ga4gh/config/RpcConfig.java   |  25 -
 .../controllers/Ga4ghBrokerController.java    |  35 --
 .../ics/ga4gh/facade/Ga4ghBrokerFacade.java   |   8 +-
 .../facade/impl/Ga4GhBrokerFacadeImpl.java    |  33 +-
 .../cz/muni/ics/ga4gh/model/ExtSource.java    |  40 --
 .../ics/ga4gh/model/Ga4ghPassportVisa.java    |  59 ---
 .../java/cz/muni/ics/ga4gh/model/Repo.java    |  27 -
 .../ics/ga4gh/service/Ga4ghBrokerService.java |   9 +-
 .../service/Ga4ghPassportBrokerBeans.java     |  56 ++
 .../JWTSigningAndValidationService.java       |  19 +-
 .../service/PassportAssemblyContext.java      |  35 ++
 .../impl/Ga4GhBrokerBrokerServiceImpl.java    |  94 ++--
 .../JWTSigningAndValidationServiceImpl.java   | 134 ++---
 .../service/impl/VisaAssemblyParameters.java  |  37 ++
 .../impl/brokers/BbmriGa4ghBroker.java        | 331 ++++++++----
 .../impl/brokers/ElixirGa4ghBroker.java       | 326 ++++++++----
 .../service/impl/brokers/Ga4ghBroker.java     | 277 ++++++----
 .../brokers/LifescienceRiGa4ghBroker.java     | 330 ++++++++++++
 .../impl/brokers/PerunGa4ghBroker.java        | 154 ++++++
 .../java/cz/muni/ics/ga4gh/utils/Utils.java   | 218 --------
 .../web/controllers/ExceptionTranslator.java  |  29 ++
 .../controllers/Ga4ghBrokerController.java    |  39 ++
 .../controllers/JwkSetPublishingEndpoint.java |  19 +-
 .../security/WebSecurityConfigurer.java       |  30 +-
 src/main/resources/application-bbmri.yml      |  77 ---
 src/main/resources/application-elixir.yml     |  92 ----
 src/main/resources/application.yml            | 134 ++++-
 84 files changed, 4107 insertions(+), 2074 deletions(-)
 delete mode 100644 src/main/java/cz/muni/ics/ga4gh/adapters/PerunAdapter.java
 delete mode 100644 src/main/java/cz/muni/ics/ga4gh/adapters/PerunAdapterMethods.java
 delete mode 100644 src/main/java/cz/muni/ics/ga4gh/adapters/impl/PerunAdapterImpl.java
 delete mode 100644 src/main/java/cz/muni/ics/ga4gh/adapters/impl/PerunAdapterLdap.java
 delete mode 100644 src/main/java/cz/muni/ics/ga4gh/adapters/impl/PerunAdapterRpc.java
 delete mode 100644 src/main/java/cz/muni/ics/ga4gh/aop/LogTimes.java
 create mode 100644 src/main/java/cz/muni/ics/ga4gh/base/AdapterBeans.java
 rename src/main/java/cz/muni/ics/ga4gh/{jose/keystore => base}/JWKSetKeyStore.java (98%)
 create mode 100644 src/main/java/cz/muni/ics/ga4gh/base/Utils.java
 create mode 100644 src/main/java/cz/muni/ics/ga4gh/base/adapters/PerunAdapter.java
 create mode 100644 src/main/java/cz/muni/ics/ga4gh/base/adapters/PerunAdapterMethods.java
 rename src/main/java/cz/muni/ics/ga4gh/{ => base}/adapters/PerunAdapterMethodsLdap.java (52%)
 rename src/main/java/cz/muni/ics/ga4gh/{ => base}/adapters/PerunAdapterMethodsRpc.java (54%)
 rename src/main/java/cz/muni/ics/ga4gh/{mappers/RpcMapper.java => base/adapters/PerunRpcAdapterMapper.java} (91%)
 create mode 100644 src/main/java/cz/muni/ics/ga4gh/base/adapters/impl/PerunAdapterImpl.java
 create mode 100644 src/main/java/cz/muni/ics/ga4gh/base/adapters/impl/PerunAdapterLdap.java
 create mode 100644 src/main/java/cz/muni/ics/ga4gh/base/adapters/impl/PerunAdapterRpc.java
 rename src/main/java/cz/muni/ics/ga4gh/{ => base}/connectors/PerunConnectorLdap.java (66%)
 rename src/main/java/cz/muni/ics/ga4gh/{ => base}/connectors/PerunConnectorRpc.java (56%)
 rename src/main/java/cz/muni/ics/ga4gh/{ => base}/enums/MemberStatus.java (94%)
 create mode 100644 src/main/java/cz/muni/ics/ga4gh/base/exceptions/ConfigurationException.java
 rename src/main/java/cz/muni/ics/ga4gh/{ => base}/exceptions/InconvertibleValueException.java (92%)
 create mode 100644 src/main/java/cz/muni/ics/ga4gh/base/exceptions/InvalidRequestParametersException.java
 rename src/main/java/cz/muni/ics/ga4gh/{ => base}/exceptions/MissingFieldException.java (92%)
 create mode 100644 src/main/java/cz/muni/ics/ga4gh/base/exceptions/PerunAdapterOperationException.java
 create mode 100644 src/main/java/cz/muni/ics/ga4gh/base/exceptions/UserNotFoundException.java
 rename src/main/java/cz/muni/ics/ga4gh/{ => base}/exceptions/UserNotUniqueException.java (82%)
 rename src/main/java/cz/muni/ics/ga4gh/{ => base}/model/Affiliation.java (63%)
 rename src/main/java/cz/muni/ics/ga4gh/{ => base}/model/AttributeMapping.java (66%)
 create mode 100644 src/main/java/cz/muni/ics/ga4gh/base/model/BasicAuthCredentials.java
 rename src/main/java/cz/muni/ics/ga4gh/{model/RepoHeader.java => base/model/ClaimRepositoryHeader.java} (59%)
 create mode 100644 src/main/java/cz/muni/ics/ga4gh/base/model/ExtSource.java
 rename src/main/java/cz/muni/ics/ga4gh/{ => base}/model/Ga4ghClaimRepository.java (60%)
 create mode 100644 src/main/java/cz/muni/ics/ga4gh/base/model/Ga4ghPassport.java
 create mode 100644 src/main/java/cz/muni/ics/ga4gh/base/model/Ga4ghPassportVisa.java
 create mode 100644 src/main/java/cz/muni/ics/ga4gh/base/model/Ga4ghPassportVisaV1.java
 rename src/main/java/cz/muni/ics/ga4gh/{ => base}/model/Group.java (75%)
 rename src/main/java/cz/muni/ics/ga4gh/{ => base}/model/Member.java (56%)
 rename src/main/java/cz/muni/ics/ga4gh/{ => base}/model/PerunAttributeValue.java (98%)
 rename src/main/java/cz/muni/ics/ga4gh/{ => base}/model/UserExtSource.java (77%)
 create mode 100644 src/main/java/cz/muni/ics/ga4gh/base/properties/AttributeMappingProperties.java
 create mode 100644 src/main/java/cz/muni/ics/ga4gh/base/properties/BasicAuthProperties.java
 create mode 100644 src/main/java/cz/muni/ics/ga4gh/base/properties/BrokerInstanceProperties.java
 create mode 100644 src/main/java/cz/muni/ics/ga4gh/base/properties/Ga4ghBrokersProperties.java
 create mode 100644 src/main/java/cz/muni/ics/ga4gh/base/properties/Ga4ghClaimRepositoryProperties.java
 create mode 100644 src/main/java/cz/muni/ics/ga4gh/base/properties/PerunAdapterProperties.java
 create mode 100644 src/main/java/cz/muni/ics/ga4gh/base/properties/PerunLdapConnectorProperties.java
 create mode 100644 src/main/java/cz/muni/ics/ga4gh/base/properties/PerunRpcConnectorProperties.java
 delete mode 100644 src/main/java/cz/muni/ics/ga4gh/config/AdapterConfig.java
 delete mode 100644 src/main/java/cz/muni/ics/ga4gh/config/AttributesConfig.java
 delete mode 100644 src/main/java/cz/muni/ics/ga4gh/config/BasicAuthConfig.java
 delete mode 100644 src/main/java/cz/muni/ics/ga4gh/config/BrokerConfig.java
 delete mode 100644 src/main/java/cz/muni/ics/ga4gh/config/Ga4ghConfig.java
 delete mode 100644 src/main/java/cz/muni/ics/ga4gh/config/LdapConfig.java
 delete mode 100644 src/main/java/cz/muni/ics/ga4gh/config/RpcConfig.java
 delete mode 100644 src/main/java/cz/muni/ics/ga4gh/controllers/Ga4ghBrokerController.java
 delete mode 100644 src/main/java/cz/muni/ics/ga4gh/model/ExtSource.java
 delete mode 100644 src/main/java/cz/muni/ics/ga4gh/model/Ga4ghPassportVisa.java
 delete mode 100644 src/main/java/cz/muni/ics/ga4gh/model/Repo.java
 create mode 100644 src/main/java/cz/muni/ics/ga4gh/service/Ga4ghPassportBrokerBeans.java
 create mode 100644 src/main/java/cz/muni/ics/ga4gh/service/PassportAssemblyContext.java
 create mode 100644 src/main/java/cz/muni/ics/ga4gh/service/impl/VisaAssemblyParameters.java
 create mode 100644 src/main/java/cz/muni/ics/ga4gh/service/impl/brokers/LifescienceRiGa4ghBroker.java
 create mode 100644 src/main/java/cz/muni/ics/ga4gh/service/impl/brokers/PerunGa4ghBroker.java
 delete mode 100644 src/main/java/cz/muni/ics/ga4gh/utils/Utils.java
 create mode 100644 src/main/java/cz/muni/ics/ga4gh/web/controllers/ExceptionTranslator.java
 create mode 100644 src/main/java/cz/muni/ics/ga4gh/web/controllers/Ga4ghBrokerController.java
 rename src/main/java/cz/muni/ics/ga4gh/{ => web}/controllers/JwkSetPublishingEndpoint.java (77%)
 rename src/main/java/cz/muni/ics/ga4gh/{ => web}/security/WebSecurityConfigurer.java (51%)
 delete mode 100644 src/main/resources/application-bbmri.yml
 delete mode 100644 src/main/resources/application-elixir.yml

diff --git a/pom.xml b/pom.xml
index 4544eee..f0c12fb 100644
--- a/pom.xml
+++ b/pom.xml
@@ -24,15 +24,15 @@
         <maven.compiler.source>${java.version}</maven.compiler.source>
         <maven.compiler.target>${java.version}</maven.compiler.target>
 
-        <version.springfox>3.0.0</version.springfox>
-        <version.swagger>3.0.0</version.swagger>
-        <version.apache-directory-api>2.1.0</version.apache-directory-api>
-        <version.nimbus-jose-jwt>9.23</version.nimbus-jose-jwt>
-        <version.guava>31.1-jre</version.guava>
-        <version.bouncy-castle>1.70</version.bouncy-castle>
+        <springfox.version>3.0.0</springfox.version>
+        <apache-directory-api.version>2.1.0</apache-directory-api.version>
+        <nimbus-jose-jwt.version>9.23</nimbus-jose-jwt.version>
+        <guava.version>31.1-jre</guava.version>
+        <bouncy-castle.version>1.70</bouncy-castle.version>
     </properties>
 
     <dependencies>
+        <!-- SPRING -->
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-web</artifactId>
@@ -55,51 +55,68 @@
             <artifactId>spring-boot-starter-tomcat</artifactId>
             <scope>provided</scope>
         </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-validation</artifactId>
+        </dependency>
 
+        <!-- LOMBOK -->
         <dependency>
             <groupId>org.projectlombok</groupId>
             <artifactId>lombok</artifactId>
             <optional>true</optional>
         </dependency>
 
+        <!-- SPRINGFOX -->
         <dependency>
             <groupId>io.springfox</groupId>
             <artifactId>springfox-boot-starter</artifactId>
-            <version>${version.springfox}</version>
+            <version>${springfox.version}</version>
         </dependency>
         <dependency>
             <groupId>io.springfox</groupId>
             <artifactId>springfox-swagger-ui</artifactId>
-            <version>${version.swagger}</version>
+            <version>${springfox.version}</version>
         </dependency>
 
+        <!-- APACHE HTTPCLIENT -->
         <dependency>
             <groupId>org.apache.httpcomponents</groupId>
             <artifactId>httpclient</artifactId>
         </dependency>
+
+        <!-- APACHE DIRECTORY -->
         <dependency>
             <groupId>org.apache.directory.api</groupId>
             <artifactId>api-all</artifactId>
-            <version>${version.apache-directory-api}</version>
+            <version>${apache-directory-api.version}</version>
         </dependency>
 
+        <!-- BOUNCYCASTLE -->
         <dependency>
             <groupId>org.bouncycastle</groupId>
             <artifactId>bcpkix-jdk15on</artifactId>
-            <version>${version.bouncy-castle}</version>
+            <version>${bouncy-castle.version}</version>
             <scope>compile</scope>
         </dependency>
         <dependency>
             <groupId>org.bouncycastle</groupId>
             <artifactId>bcprov-jdk15on</artifactId>
-            <version>${version.bouncy-castle}</version>
+            <version>${bouncy-castle.version}</version>
             <scope>compile</scope>
         </dependency>
 
+        <!-- GUAVA -->
         <dependency>
             <groupId>com.google.guava</groupId>
             <artifactId>guava</artifactId>
-            <version>${version.guava}</version>
+            <version>${guava.version}</version>
+        </dependency>
+
+        <!-- HIBERNATE VALIDATOR -->
+        <dependency>
+            <groupId>org.hibernate.validator</groupId>
+            <artifactId>hibernate-validator</artifactId>
         </dependency>
     </dependencies>
 
diff --git a/src/main/java/cz/muni/ics/ga4gh/Application.java b/src/main/java/cz/muni/ics/ga4gh/Application.java
index efc5f3c..b729527 100644
--- a/src/main/java/cz/muni/ics/ga4gh/Application.java
+++ b/src/main/java/cz/muni/ics/ga4gh/Application.java
@@ -10,4 +10,5 @@ public class Application extends SpringBootServletInitializer {
     public static void main(String[] args) {
         SpringApplication.run(Application.class, args);
     }
+
 }
diff --git a/src/main/java/cz/muni/ics/ga4gh/ApplicationContext.java b/src/main/java/cz/muni/ics/ga4gh/ApplicationContext.java
index 13e6043..be93e28 100644
--- a/src/main/java/cz/muni/ics/ga4gh/ApplicationContext.java
+++ b/src/main/java/cz/muni/ics/ga4gh/ApplicationContext.java
@@ -1,28 +1,21 @@
 package cz.muni.ics.ga4gh;
 
-import cz.muni.ics.ga4gh.config.BrokerConfig;
+import cz.muni.ics.ga4gh.base.properties.Ga4ghBrokersProperties;
+import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
 import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.core.io.FileUrlResource;
 import org.springframework.core.io.Resource;
 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
 import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.stereotype.Component;
 import springfox.documentation.builders.PathSelectors;
 import springfox.documentation.builders.RequestHandlerSelectors;
 import springfox.documentation.spi.DocumentationType;
 import springfox.documentation.spring.web.plugins.Docket;
 
-import java.net.MalformedURLException;
-
-@Configuration
+@Component
+@ConfigurationPropertiesScan
 public class ApplicationContext {
 
-    private final String pathToJwkFile;
-
-    public ApplicationContext(BrokerConfig config) {
-        this.pathToJwkFile = config.getPathToJwkFile();
-    }
-
     @Bean
     public Docket api() {
         return new Docket(DocumentationType.SWAGGER_2)
@@ -38,7 +31,7 @@ public class ApplicationContext {
     }
 
     @Bean
-    public Resource jwks() throws MalformedURLException {
-        return new FileUrlResource(pathToJwkFile);
+    public Resource jwkFileResource(Ga4ghBrokersProperties ga4ghBrokersProperties) {
+        return ga4ghBrokersProperties.getJwkKeystoreFile();
     }
 }
diff --git a/src/main/java/cz/muni/ics/ga4gh/adapters/PerunAdapter.java b/src/main/java/cz/muni/ics/ga4gh/adapters/PerunAdapter.java
deleted file mode 100644
index 7358f9a..0000000
--- a/src/main/java/cz/muni/ics/ga4gh/adapters/PerunAdapter.java
+++ /dev/null
@@ -1,39 +0,0 @@
-package cz.muni.ics.ga4gh.adapters;
-
-import cz.muni.ics.ga4gh.adapters.impl.PerunAdapterLdap;
-import cz.muni.ics.ga4gh.adapters.impl.PerunAdapterRpc;
-import cz.muni.ics.ga4gh.config.AdapterConfig;
-import lombok.Getter;
-import lombok.Setter;
-
-import java.util.Objects;
-
-@Getter
-@Setter
-public abstract class PerunAdapter implements PerunAdapterMethods {
-
-    private final String RPC = "rpc";
-
-    private PerunAdapterMethods adapterPrimary;
-    private PerunAdapterMethods adapterFallback;
-
-    private PerunAdapterMethodsRpc adapterRpc;
-    private PerunAdapterMethodsLdap adapterLdap;
-
-    private boolean callFallback;
-
-    public PerunAdapter(AdapterConfig config, PerunAdapterRpc adapterRpc, PerunAdapterLdap adapterLdap) {
-        if (config.getAdapterPrimary() != null && config.getAdapterPrimary().equalsIgnoreCase(RPC)) {
-            this.adapterPrimary = adapterRpc;
-            this.adapterFallback = adapterLdap;
-        } else {
-            this.adapterPrimary = adapterLdap;
-            this.adapterFallback = adapterRpc;
-        }
-
-        this.adapterRpc = adapterRpc;
-        this.adapterLdap = adapterLdap;
-
-        this.callFallback = Objects.requireNonNullElse(config.getCallFallback(), false);
-    }
-}
diff --git a/src/main/java/cz/muni/ics/ga4gh/adapters/PerunAdapterMethods.java b/src/main/java/cz/muni/ics/ga4gh/adapters/PerunAdapterMethods.java
deleted file mode 100644
index 85485da..0000000
--- a/src/main/java/cz/muni/ics/ga4gh/adapters/PerunAdapterMethods.java
+++ /dev/null
@@ -1,23 +0,0 @@
-package cz.muni.ics.ga4gh.adapters;
-
-import cz.muni.ics.ga4gh.model.Affiliation;
-import cz.muni.ics.ga4gh.model.AttributeMapping;
-
-import java.util.List;
-import java.util.Set;
-
-public interface PerunAdapterMethods {
-
-    /**
-     * Fetch user based on his principal (extLogin and extSource) from Perun
-     *
-     * @return PerunUser with id of found user
-     */
-    Long getPreauthenticatedUserId(String extLogin, String extSourceName);
-
-    boolean isUserInGroup(Long userId, Long groupId);
-
-    List<Affiliation> getGroupAffiliations(Long userId, String groupAffiliationsAttr);
-
-    Set<Long> getUserIdsByAttributeValue(AttributeMapping attrName, String attrValue);
-}
diff --git a/src/main/java/cz/muni/ics/ga4gh/adapters/impl/PerunAdapterImpl.java b/src/main/java/cz/muni/ics/ga4gh/adapters/impl/PerunAdapterImpl.java
deleted file mode 100644
index fd5553b..0000000
--- a/src/main/java/cz/muni/ics/ga4gh/adapters/impl/PerunAdapterImpl.java
+++ /dev/null
@@ -1,72 +0,0 @@
-package cz.muni.ics.ga4gh.adapters.impl;
-
-import cz.muni.ics.ga4gh.adapters.PerunAdapter;
-import cz.muni.ics.ga4gh.config.AdapterConfig;
-import cz.muni.ics.ga4gh.model.Affiliation;
-import cz.muni.ics.ga4gh.model.AttributeMapping;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Component;
-
-import java.util.List;
-import java.util.Set;
-
-@Component
-public class PerunAdapterImpl extends PerunAdapter {
-
-    @Autowired
-    public PerunAdapterImpl(AdapterConfig config, PerunAdapterRpc adapterRpc, PerunAdapterLdap adapterLdap) {
-        super(config, adapterRpc, adapterLdap);
-    }
-
-    @Override
-    public Long getPreauthenticatedUserId(String extLogin, String extSourceName) {
-        try {
-            return this.getAdapterPrimary().getPreauthenticatedUserId(extLogin, extSourceName);
-        } catch (UnsupportedOperationException e) {
-            if (this.isCallFallback()) {
-                return this.getAdapterFallback().getPreauthenticatedUserId(extLogin, extSourceName);
-            } else {
-                throw e;
-            }
-        }
-    }
-
-    @Override
-    public boolean isUserInGroup(Long userId, Long groupId) {
-        try {
-            return this.getAdapterPrimary().isUserInGroup(userId, groupId);
-        } catch (UnsupportedOperationException e) {
-            if (this.isCallFallback()) {
-                return this.getAdapterFallback().isUserInGroup(userId, groupId);
-            } else {
-                throw e;
-            }
-        }
-    }
-
-    @Override
-    public List<Affiliation> getGroupAffiliations(Long userId, String groupAffiliationsAttr) {
-        try {
-            return this.getAdapterPrimary().getGroupAffiliations(userId, groupAffiliationsAttr);
-        } catch (UnsupportedOperationException e) {
-            if (this.isCallFallback()) {
-                return this.getAdapterFallback().getGroupAffiliations(userId, groupAffiliationsAttr);
-            } else {
-                throw e;
-            }
-        }
-    }
-
-    @Override
-    public Set<Long> getUserIdsByAttributeValue(AttributeMapping attrName, String attrValue) {
-        try {
-            return this.getAdapterPrimary().getUserIdsByAttributeValue(attrName, attrValue);
-        } catch (UnsupportedOperationException e) {
-            if (this.isCallFallback()) {
-                return this.getAdapterFallback().getUserIdsByAttributeValue(attrName, attrValue);
-            } else {
-                throw e;
-            }
-        }
-    }
-}
diff --git a/src/main/java/cz/muni/ics/ga4gh/adapters/impl/PerunAdapterLdap.java b/src/main/java/cz/muni/ics/ga4gh/adapters/impl/PerunAdapterLdap.java
deleted file mode 100644
index 2c68d7e..0000000
--- a/src/main/java/cz/muni/ics/ga4gh/adapters/impl/PerunAdapterLdap.java
+++ /dev/null
@@ -1,199 +0,0 @@
-package cz.muni.ics.ga4gh.adapters.impl;
-
-import cz.muni.ics.ga4gh.adapters.PerunAdapterMethods;
-import cz.muni.ics.ga4gh.adapters.PerunAdapterMethodsLdap;
-import cz.muni.ics.ga4gh.config.AttributesConfig;
-import cz.muni.ics.ga4gh.connectors.PerunConnectorLdap;
-import cz.muni.ics.ga4gh.model.Affiliation;
-import cz.muni.ics.ga4gh.model.AttributeMapping;
-import org.apache.directory.api.ldap.model.entry.Attribute;
-import org.apache.directory.api.ldap.model.entry.Entry;
-import org.apache.directory.api.ldap.model.message.SearchScope;
-import org.apache.directory.ldap.client.api.search.FilterBuilder;
-import org.apache.directory.ldap.client.template.EntryMapper;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Component;
-import org.springframework.util.StringUtils;
-
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.stream.Collectors;
-
-import static org.apache.directory.ldap.client.api.search.FilterBuilder.and;
-import static org.apache.directory.ldap.client.api.search.FilterBuilder.equal;
-import static org.apache.directory.ldap.client.api.search.FilterBuilder.or;
-
-@Component
-public class PerunAdapterLdap implements PerunAdapterMethods, PerunAdapterMethodsLdap {
-
-    public static final String OBJECT_CLASS = "objectClass";
-    public static final String OU_PEOPLE = "ou=People";
-
-    public static final String PERUN_USER_ID = "perunUserId";
-    public static final String MEMBER_OF = "memberOf";
-
-    public static final String PERUN_GROUP = "perunGroup";
-    public static final String PERUN_USER = "perunUser";
-    public static final String PERUN_GROUP_ID = "perunGroupId";
-    public static final String UNIQUE_MEMBER = "uniqueMember";
-
-    public static final String PERUN_VO_ID = "perunVoId";
-    public static final String EDU_PERSON_PRINCIPAL_NAMES = "eduPersonPrincipalNames";
-
-    private final PerunConnectorLdap connectorLdap;
-    private final Map<String, AttributeMapping> attributeMappings;
-
-    @Autowired
-    PerunAdapterLdap(PerunConnectorLdap connectorLdap, AttributesConfig attributesConfig) {
-        this.connectorLdap = connectorLdap;
-        this.attributeMappings = attributesConfig.getAttributeMappings();
-    }
-
-    @Override
-    public Long getPreauthenticatedUserId(String extLogin, String extSourceName) {
-        FilterBuilder filter = and(
-                equal(OBJECT_CLASS, PERUN_USER), equal(EDU_PERSON_PRINCIPAL_NAMES, extLogin)
-        );
-
-        return getPerunUserId(filter);
-    }
-
-    @Override
-    public boolean isUserInGroup(Long userId, Long groupId) {
-        String uniqueMemberValue = PERUN_USER_ID + '=' + userId + ',' + OU_PEOPLE + ',' + connectorLdap.getBaseDN();
-
-        FilterBuilder filter = and(
-                equal(OBJECT_CLASS, PERUN_GROUP),
-                equal(PERUN_GROUP_ID, String.valueOf(groupId)),
-                equal(UNIQUE_MEMBER, uniqueMemberValue)
-        );
-
-        EntryMapper<Long> mapper = e -> Long.parseLong(e.get(PERUN_GROUP_ID).getString());
-        String[] attributes = new String[] { PERUN_GROUP_ID };
-        List<Long> ids = connectorLdap.search(null, filter, SearchScope.SUBTREE, attributes, mapper);
-
-        return ids.stream().filter(groupId::equals).count() == 1L;
-    }
-
-    @Override
-    public List<Affiliation> getGroupAffiliations(Long userId, String groupAffiliationsAttr) {
-        Set<Long> userGroupIds = getGroupIdsWhereUserIsMember(userId, null);
-        if (userGroupIds.isEmpty()) {
-            return new ArrayList<>();
-        }
-
-        FilterBuilder[] groupIdFilters = new FilterBuilder[userGroupIds.size()];
-        int i = 0;
-
-        for (Long id: userGroupIds) {
-            groupIdFilters[i++] = equal(PERUN_GROUP_ID, String.valueOf(id));
-        }
-
-        AttributeMapping affiliationsMapping = attributeMappings.get(groupAffiliationsAttr);
-        FilterBuilder filterBuilder = and(equal(OBJECT_CLASS, PERUN_GROUP), or(groupIdFilters));
-        String[] attributes = new String[] { affiliationsMapping.getLdapName() };
-
-        EntryMapper<Set<Affiliation>> mapper = e -> {
-            Set<Affiliation> affiliations = new HashSet<>();
-            if (!checkHasAttributes(e, attributes)) {
-                return affiliations;
-            }
-
-            Attribute a = e.get(affiliationsMapping.getLdapName());
-            long linuxTime = System.currentTimeMillis() / 1000L;
-            a.iterator().forEachRemaining(v -> affiliations.add(new Affiliation(null, v.getString(), linuxTime)));
-
-            return affiliations;
-        };
-
-        List<Set<Affiliation>> affiliationSets = connectorLdap.search(null, filterBuilder, SearchScope.SUBTREE, attributes, mapper);
-
-        return affiliationSets.stream().flatMap(Set::stream).distinct().collect(Collectors.toList());
-    }
-
-    @Override
-    public Set<Long> getUserIdsByAttributeValue(AttributeMapping attrName, String attrValue) {
-        if (!StringUtils.hasText(attrName.getLdapName())) {
-            return new HashSet<>();
-        }
-
-        FilterBuilder filter = and(
-                equal(OBJECT_CLASS, PERUN_USER),
-                equal(attrName.getLdapName(), attrValue)
-        );
-
-        SearchScope scope = SearchScope.ONELEVEL;
-        String[] attributes = new String[]{PERUN_USER_ID};
-        EntryMapper<Long> mapper = e -> Long.parseLong(e.get(PERUN_USER_ID).getString());
-
-        List<Long> result = connectorLdap.search(OU_PEOPLE, filter, scope, attributes, mapper);
-
-        return Set.copyOf(result);
-    }
-
-    private Set<Long> getGroupIdsWhereUserIsMember(Long userId, Long voId) {
-        String dnPrefix = getDnPrefixForUserId(userId);
-        String[] attributes = new String[] { MEMBER_OF };
-
-        EntryMapper<Set<Long>> mapper = e -> {
-            Set<Long> ids = new HashSet<>();
-            if (checkHasAttributes(e, attributes)) {
-                Attribute a = e.get(MEMBER_OF);
-                a.iterator().forEachRemaining(id -> {
-                    String fullVal = id.getString();
-                    String[] parts = fullVal.split(",", 3);
-
-                    String groupId = parts[0];
-                    groupId = groupId.replace(PERUN_GROUP_ID + '=', "");
-
-                    String voIdStr = parts[1];
-                    voIdStr = voIdStr.replace(PERUN_VO_ID + '=', "");
-
-                    if (voId == null || voId.equals(Long.parseLong(voIdStr))) {
-                        ids.add(Long.parseLong(groupId));
-                    }
-                });
-            }
-
-            return ids;
-        };
-
-        Set<Long> res = connectorLdap.lookup(dnPrefix, attributes, mapper);
-        if (res == null) {
-            res = new HashSet<>();
-        }
-
-        return res;
-    }
-
-    private String getDnPrefixForUserId(Long userId) {
-        return PERUN_USER_ID + '=' + userId + ',' + OU_PEOPLE;
-    }
-
-    private boolean checkHasAttributes(Entry e, String[] attributes) {
-        if (e == null) {
-            return false;
-        } else if (attributes == null) {
-            return true;
-        }
-
-        for (String attr: attributes) {
-            if (e.get(attr) == null) {
-                return false;
-            }
-        }
-
-        return true;
-    }
-
-    private Long getPerunUserId(FilterBuilder filter) {
-        SearchScope scope = SearchScope.ONELEVEL;
-        String[] attributes = new String[]{PERUN_USER_ID};
-        EntryMapper<Long> mapper = e -> Long.parseLong(e.get(PERUN_USER_ID).getString());
-
-        return connectorLdap.searchFirst(OU_PEOPLE, filter, scope, attributes, mapper);
-    }
-}
diff --git a/src/main/java/cz/muni/ics/ga4gh/adapters/impl/PerunAdapterRpc.java b/src/main/java/cz/muni/ics/ga4gh/adapters/impl/PerunAdapterRpc.java
deleted file mode 100644
index 7747579..0000000
--- a/src/main/java/cz/muni/ics/ga4gh/adapters/impl/PerunAdapterRpc.java
+++ /dev/null
@@ -1,283 +0,0 @@
-package cz.muni.ics.ga4gh.adapters.impl;
-
-import com.fasterxml.jackson.databind.JsonNode;
-import cz.muni.ics.ga4gh.adapters.PerunAdapterMethods;
-import cz.muni.ics.ga4gh.adapters.PerunAdapterMethodsRpc;
-import cz.muni.ics.ga4gh.config.AttributesConfig;
-import cz.muni.ics.ga4gh.config.RpcConfig;
-import cz.muni.ics.ga4gh.connectors.PerunConnectorRpc;
-import cz.muni.ics.ga4gh.mappers.RpcMapper;
-import cz.muni.ics.ga4gh.model.Affiliation;
-import cz.muni.ics.ga4gh.model.AttributeMapping;
-import cz.muni.ics.ga4gh.model.Group;
-import cz.muni.ics.ga4gh.model.Member;
-import cz.muni.ics.ga4gh.model.PerunAttributeValue;
-import cz.muni.ics.ga4gh.model.UserExtSource;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Component;
-import org.springframework.util.StringUtils;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.stream.Collectors;
-
-import static cz.muni.ics.ga4gh.enums.MemberStatus.VALID;
-
-@Component
-@Slf4j
-public class PerunAdapterRpc implements PerunAdapterMethods, PerunAdapterMethodsRpc {
-
-    public static final String EXT_LOGIN = "extLogin";
-    public static final String EXT_SOURCE_NAME = "extSourceName";
-    public static final String USER_EXT_SOURCE = "userExtSource";
-    public static final String ATTR_NAMES = "attrNames";
-    public static final String VALUE_CREATED_AT = "valueCreatedAt";
-
-    public static final String ID = "id";
-    public static final String VO = "vo";
-    public static final String USER = "user";
-    public static final String MEMBER = "member";
-    public static final String GROUP = "group";
-
-    public static final String ATTRIBUTE_NAME = "attributeName";
-    public static final String ATTRIBUTE_VALUE = "attributeValue";
-
-    public static final String ATTRIBUTES_MANAGER = "attributesManager";
-    public static final String GROUPS_MANAGER = "groupsManager";
-    public static final String MEMBERS_MANAGER = "membersManager";
-    public static final String USERS_MANAGER = "usersManager";
-
-    public static final String GET_USER_BY_EXT_SOURCE_NAME_AND_EXT_LOGIN = "getUserByExtSourceNameAndExtLogin";
-    public static final String GET_USER_EXT_SOURCES = "getUserExtSources";
-    public static final String GET_GROUP_BY_ID = "getGroupById";
-    public static final String GET_MEMBER_BY_USER = "getMemberByUser";
-    public static final String GET_MEMBERS_BY_USER = "getMembersByUser";
-    public static final String GET_MEMBER_GROUPS = "getMemberGroups";
-    public static final String IS_GROUP_MEMBER = "isGroupMember";
-    public static final String GET_ATTRIBUTE = "getAttribute";
-    public static final String GET_ATTRIBUTES = "getAttributes";
-    public static final String GET_USERS_BY_ATTRIBUTE_VALUE = "getUsersByAttributeValue";
-
-    private final PerunConnectorRpc connectorRpc;
-    private final Map<String, AttributeMapping> attributeMappings;
-
-    @Autowired
-    PerunAdapterRpc(PerunConnectorRpc connectorRpc, AttributesConfig attributesConfig) {
-        this.connectorRpc = connectorRpc;
-        this.attributeMappings = attributesConfig.getAttributeMappings();
-    }
-
-    @Override
-    public Long getPreauthenticatedUserId(String extLogin, String extSourceName) {
-        if (!connectorRpc.isEnabled()) {
-            return null;
-        }
-
-        Map<String, Object> map = new LinkedHashMap<>();
-        map.put(EXT_LOGIN, extLogin);
-        map.put(EXT_SOURCE_NAME, extSourceName);
-
-        JsonNode response = connectorRpc.post(USERS_MANAGER, GET_USER_BY_EXT_SOURCE_NAME_AND_EXT_LOGIN, map);
-
-        return response.get(ID) == null ? null : response.get(ID).asLong();
-    }
-
-    @Override
-    public boolean isUserInGroup(Long userId, Long groupId) {
-        if (!connectorRpc.isEnabled()) {
-            return false;
-        }
-
-        Map<String, Object> groupParams = new LinkedHashMap<>();
-        groupParams.put(ID, groupId);
-        JsonNode groupResponse = connectorRpc.post(GROUPS_MANAGER, GET_GROUP_BY_ID, groupParams);
-        Group group = RpcMapper.mapGroup(groupResponse);
-
-        Map<String, Object> memberParams = new LinkedHashMap<>();
-        memberParams.put(VO, group.getVoId());
-        memberParams.put(USER, userId);
-        JsonNode memberResponse = connectorRpc.post(MEMBERS_MANAGER, GET_MEMBER_BY_USER, memberParams);
-        Member member = RpcMapper.mapMember(memberResponse);
-
-        Map<String, Object> isGroupMemberParams = new LinkedHashMap<>();
-        isGroupMemberParams.put(GROUP, groupId);
-        isGroupMemberParams.put(MEMBER, member.getId());
-        JsonNode res = connectorRpc.post(GROUPS_MANAGER, IS_GROUP_MEMBER, isGroupMemberParams);
-
-        return res.asBoolean(false);
-    }
-
-    @Override
-    public List<Affiliation> getGroupAffiliations(Long userId, String groupAffiliationsAttr) {
-        if (!connectorRpc.isEnabled()) {
-            return new ArrayList<>();
-        }
-
-        List<Affiliation> affiliations = new ArrayList<>();
-        List<Member> userMembers = getMembersByUser(userId);
-
-        for (Member member : userMembers) {
-            if (VALID.equals(member.getStatus())) {
-                List<Group> memberGroups = getMemberGroups(member.getId());
-                for (Group group : memberGroups) {
-                    PerunAttributeValue attrValue = this.getGroupAttributeValue(group, groupAffiliationsAttr);
-                    if (attrValue != null && attrValue.valueAsList() != null) {
-                        long linuxTime = System.currentTimeMillis() / 1000L;
-
-                        for (String value : attrValue.valueAsList()) {
-                            Affiliation affiliation = new Affiliation(null, value, linuxTime);
-                            log.debug("found {} on group {}", value, group.getName());
-                            affiliations.add(affiliation);
-                        }
-                    }
-                }
-            }
-        }
-
-        return affiliations;
-    }
-
-    @Override
-    public String getUserAttributeCreatedAt(Long userId, String attrName) {
-        if (!connectorRpc.isEnabled() || attributeMappings.get(attrName) == null) {
-            return null;
-        }
-
-        Map<String, Object> map = new LinkedHashMap<>();
-        map.put(USER, userId);
-        map.put(ATTRIBUTE_NAME, attributeMappings.get(attrName).getRpcName());
-
-        JsonNode res = connectorRpc.post(ATTRIBUTES_MANAGER, GET_ATTRIBUTE, map);
-
-        if (res == null || !res.hasNonNull(VALUE_CREATED_AT)) {
-            return null;
-        }
-
-        return res.get(VALUE_CREATED_AT).asText();
-    }
-
-    @Override
-    public List<Affiliation> getUserExtSourcesAffiliations(Long userId, String affiliationsAttr, String orgUrlAttr) {
-        if (!connectorRpc.isEnabled()) {
-            return new ArrayList<>();
-        }
-
-        List<UserExtSource> userExtSources = getUserExtSources(userId);
-        List<Affiliation> affiliations = new ArrayList<>();
-
-        Map<String, AttributeMapping> attrMappings = new HashMap<>();
-        attrMappings.put(affiliationsAttr, attributeMappings.get(affiliationsAttr));
-        attrMappings.put(orgUrlAttr, attributeMappings.get(orgUrlAttr));
-
-        for (UserExtSource ues : userExtSources) {
-            if ("cz.metacentrum.perun.core.impl.ExtSourceIdp".equals(ues.getExtSource().getType())) {
-                Map<String, PerunAttributeValue> uesAttrValues = getUserExtSourceAttributeValues(ues.getId(), attrMappings);
-
-                long asserted = ues.getLastAccess().getTime() / 1000L;
-
-                String orgUrl = uesAttrValues.get(affiliationsAttr).valueAsString();
-                String affs = uesAttrValues.get(orgUrlAttr).valueAsString();
-
-                if (affs != null) {
-                    for (String aff : affs.split(";")) {
-                        String source = ( (orgUrl != null) ? orgUrl : ues.getExtSource().getName() );
-                        Affiliation affiliation = new Affiliation(source, aff, asserted);
-                        log.debug("found {} from IdP {} with orgURL {} asserted at {}", aff, ues.getExtSource().getName(),
-                                orgUrl, asserted);
-                        affiliations.add(affiliation);
-                    }
-                }
-            }
-        }
-
-        return affiliations;
-    }
-
-    @Override
-    public Set<Long> getUserIdsByAttributeValue(AttributeMapping attrName, String attrValue) {
-        if (!connectorRpc.isEnabled()) {
-            return new HashSet<>();
-        }
-
-        Set<Long> result = new HashSet<>();
-
-        if (!StringUtils.hasText(attrName.getRpcName())) {
-            return result;
-        }
-
-        Map<String, Object> map = new LinkedHashMap<>();
-        map.put(ATTRIBUTE_NAME, attrName.getRpcName());
-        map.put(ATTRIBUTE_VALUE, attrValue);
-
-        JsonNode res = connectorRpc.post(USERS_MANAGER, GET_USERS_BY_ATTRIBUTE_VALUE, map);
-
-        if (res != null) {
-            for (int i = 0; i < res.size(); i++) {
-                result.add(res.get(i).get(ID).asLong());
-            }
-        }
-
-        return result;
-    }
-
-    private List<Member> getMembersByUser(Long userId) {
-        if (!this.connectorRpc.isEnabled()) {
-            return new ArrayList<>();
-        }
-
-        Map<String, Object> params = new LinkedHashMap<>();
-        params.put(USER, userId);
-        JsonNode jsonNode = connectorRpc.post(MEMBERS_MANAGER, GET_MEMBERS_BY_USER, params);
-
-        return RpcMapper.mapMembers(jsonNode);
-    }
-
-    private List<Group> getMemberGroups(Long memberId) {
-        if (!this.connectorRpc.isEnabled()) {
-            return new ArrayList<>();
-        }
-
-        Map<String, Object> map = new LinkedHashMap<>();
-        map.put(MEMBER, memberId);
-
-        JsonNode response = connectorRpc.post(GROUPS_MANAGER, GET_MEMBER_GROUPS, map);
-        return RpcMapper.mapGroups(response);
-    }
-
-    private PerunAttributeValue getGroupAttributeValue(Group group, String attrToFetch) {
-        if (attributeMappings.get(attrToFetch) == null) {
-            return null;
-        }
-
-        Map<String, Object> map = new LinkedHashMap<>();
-        map.put(GROUP, group.getId());
-        map.put(ATTRIBUTE_NAME, attributeMappings.get(attrToFetch).getRpcName());
-        JsonNode res = connectorRpc.post(ATTRIBUTES_MANAGER, GET_ATTRIBUTE, map);
-
-        return RpcMapper.mapAttributeValue(res);
-    }
-
-    private List<UserExtSource> getUserExtSources(Long userId) {
-        Map<String, Object> map = new LinkedHashMap<>();
-        map.put(USER, userId);
-
-        JsonNode response = connectorRpc.post(USERS_MANAGER, GET_USER_EXT_SOURCES, map);
-        return RpcMapper.mapUserExtSources(response);
-    }
-
-    private Map<String, PerunAttributeValue> getUserExtSourceAttributeValues(Long uesId, Map<String, AttributeMapping> attrMappings) {
-        Map<String, Object> map = new LinkedHashMap<>();
-        map.put(USER_EXT_SOURCE, uesId);
-        map.put(ATTR_NAMES, attrMappings.values().stream().map(AttributeMapping::getRpcName).collect(Collectors.toList()));
-
-        JsonNode response = connectorRpc.post(ATTRIBUTES_MANAGER, GET_ATTRIBUTES, map);
-
-        return RpcMapper.mapAttributes(response, attrMappings);
-    }
-}
diff --git a/src/main/java/cz/muni/ics/ga4gh/aop/LogTimes.java b/src/main/java/cz/muni/ics/ga4gh/aop/LogTimes.java
deleted file mode 100644
index 0e2e60b..0000000
--- a/src/main/java/cz/muni/ics/ga4gh/aop/LogTimes.java
+++ /dev/null
@@ -1,11 +0,0 @@
-package cz.muni.ics.ga4gh.aop;
-
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-
-@Target(ElementType.METHOD)
-@Retention(RetentionPolicy.RUNTIME)
-public @interface LogTimes {
-}
diff --git a/src/main/java/cz/muni/ics/ga4gh/base/AdapterBeans.java b/src/main/java/cz/muni/ics/ga4gh/base/AdapterBeans.java
new file mode 100644
index 0000000..631d00f
--- /dev/null
+++ b/src/main/java/cz/muni/ics/ga4gh/base/AdapterBeans.java
@@ -0,0 +1,68 @@
+package cz.muni.ics.ga4gh.base;
+
+import cz.muni.ics.ga4gh.base.adapters.PerunAdapter;
+import cz.muni.ics.ga4gh.base.adapters.impl.PerunAdapterImpl;
+import cz.muni.ics.ga4gh.base.adapters.impl.PerunAdapterLdap;
+import cz.muni.ics.ga4gh.base.adapters.impl.PerunAdapterRpc;
+import cz.muni.ics.ga4gh.base.connectors.PerunConnectorLdap;
+import cz.muni.ics.ga4gh.base.connectors.PerunConnectorRpc;
+import cz.muni.ics.ga4gh.base.exceptions.ConfigurationException;
+import cz.muni.ics.ga4gh.base.properties.AttributeMappingProperties;
+import cz.muni.ics.ga4gh.base.properties.PerunAdapterProperties;
+import cz.muni.ics.ga4gh.base.properties.PerunLdapConnectorProperties;
+import cz.muni.ics.ga4gh.base.properties.PerunRpcConnectorProperties;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.stereotype.Component;
+
+@Component
+public class AdapterBeans {
+
+    @Bean
+    public PerunAdapterRpc perunAdapterRpc(PerunConnectorRpc connectorRpc,
+                                           AttributeMappingProperties attributeMappingProperties)
+    {
+        return new PerunAdapterRpc(connectorRpc, attributeMappingProperties);
+    }
+
+    @Bean
+    public PerunConnectorRpc perunConnectorRpc(PerunRpcConnectorProperties rpcConnectorProperties) {
+        return new PerunConnectorRpc(rpcConnectorProperties);
+    }
+
+    @Bean
+    @ConditionalOnBean(PerunLdapConnectorProperties.class)
+    public PerunConnectorLdap perunConnectorLdap(PerunLdapConnectorProperties ldapConnectorProperties)
+    {
+        return new PerunConnectorLdap(ldapConnectorProperties);
+    }
+
+    @Bean
+    @ConditionalOnBean(PerunConnectorLdap.class)
+    public PerunAdapterLdap perunAdapterLdap(PerunConnectorLdap connectorLdap,
+                                             AttributeMappingProperties attributeMappingProperties)
+    {
+        return new PerunAdapterLdap(connectorLdap, attributeMappingProperties);
+    }
+
+    @Bean
+    @ConditionalOnBean(PerunAdapterLdap.class)
+    public PerunAdapter perunAdapterLdapRpc(PerunAdapterProperties adapterProperties,
+                                            PerunAdapterRpc adapterRpc,
+                                            PerunAdapterLdap adapterLdap)
+        throws ConfigurationException
+    {
+        return new PerunAdapterImpl(adapterProperties, adapterRpc, adapterLdap);
+    }
+
+    @Bean
+    @ConditionalOnMissingBean(PerunAdapterLdap.class)
+    public PerunAdapter perunAdapterRpcOnly(PerunAdapterProperties adapterProperties,
+                                            PerunAdapterRpc adapterRpc)
+        throws ConfigurationException
+    {
+        return new PerunAdapterImpl(adapterProperties, adapterRpc);
+    }
+
+}
diff --git a/src/main/java/cz/muni/ics/ga4gh/jose/keystore/JWKSetKeyStore.java b/src/main/java/cz/muni/ics/ga4gh/base/JWKSetKeyStore.java
similarity index 98%
rename from src/main/java/cz/muni/ics/ga4gh/jose/keystore/JWKSetKeyStore.java
rename to src/main/java/cz/muni/ics/ga4gh/base/JWKSetKeyStore.java
index 0c03f88..5655804 100644
--- a/src/main/java/cz/muni/ics/ga4gh/jose/keystore/JWKSetKeyStore.java
+++ b/src/main/java/cz/muni/ics/ga4gh/base/JWKSetKeyStore.java
@@ -1,12 +1,7 @@
-package cz.muni.ics.ga4gh.jose.keystore;
+package cz.muni.ics.ga4gh.base;
 
 import com.nimbusds.jose.jwk.JWK;
 import com.nimbusds.jose.jwk.JWKSet;
-import lombok.Getter;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.core.io.Resource;
-import org.springframework.stereotype.Component;
-
 import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.InputStreamReader;
@@ -14,6 +9,10 @@ import java.nio.charset.StandardCharsets;
 import java.text.ParseException;
 import java.util.List;
 import java.util.stream.Collectors;
+import lombok.Getter;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.core.io.Resource;
+import org.springframework.stereotype.Component;
 
 @Component
 @Getter
diff --git a/src/main/java/cz/muni/ics/ga4gh/base/Utils.java b/src/main/java/cz/muni/ics/ga4gh/base/Utils.java
new file mode 100644
index 0000000..535c86c
--- /dev/null
+++ b/src/main/java/cz/muni/ics/ga4gh/base/Utils.java
@@ -0,0 +1,332 @@
+package cz.muni.ics.ga4gh.base;
+
+import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.ASSERTED;
+import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.BY;
+import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.CONDITIONS;
+import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.SOURCE;
+import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.TYPE;
+import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.VALUE;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.nimbusds.jose.JOSEObjectType;
+import com.nimbusds.jose.JWSHeader;
+import com.nimbusds.jose.crypto.RSASSAVerifier;
+import com.nimbusds.jose.jwk.JWK;
+import com.nimbusds.jose.jwk.JWKMatcher;
+import com.nimbusds.jose.jwk.JWKSelector;
+import com.nimbusds.jose.jwk.RSAKey;
+import com.nimbusds.jose.jwk.source.RemoteJWKSet;
+import com.nimbusds.jose.proc.SecurityContext;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.JWTParser;
+import com.nimbusds.jwt.SignedJWT;
+import cz.muni.ics.ga4gh.base.model.ClaimRepositoryHeader;
+import cz.muni.ics.ga4gh.base.model.Ga4ghClaimRepository;
+import cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa;
+import cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisaV1;
+import cz.muni.ics.ga4gh.base.properties.BrokerInstanceProperties;
+import cz.muni.ics.ga4gh.base.properties.Ga4ghClaimRepositoryProperties;
+import cz.muni.ics.ga4gh.service.JWTSigningAndValidationService;
+import cz.muni.ics.ga4gh.service.impl.VisaAssemblyParameters;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.text.ParseException;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.client.ClientHttpRequestInterceptor;
+import org.springframework.http.client.InterceptingClientHttpRequestFactory;
+import org.springframework.util.StringUtils;
+import org.springframework.web.client.RestTemplate;
+
+@Slf4j
+public class Utils {
+
+    public static void initializeClaimRepositories(BrokerInstanceProperties brokerProperties,
+                                                   List<Ga4ghClaimRepository> claimRepositories,
+                                                   Map<URI, RemoteJWKSet<SecurityContext>> remoteJwkSets,
+                                                   Map<URI, String> signers)
+    {
+        for (Ga4ghClaimRepositoryProperties ga4ghClaimRepositoryProperties : brokerProperties.getPassportRepositories()) {
+            initializeClaimRepository(ga4ghClaimRepositoryProperties, claimRepositories);
+            initializeSigner(signers, remoteJwkSets, ga4ghClaimRepositoryProperties.getName(), ga4ghClaimRepositoryProperties.getJwks());
+        }
+    }
+
+    public static Ga4ghPassportVisa createVisa(VisaAssemblyParameters parameters)
+    {
+        long now = System.currentTimeMillis() / 1000L;
+
+        if (parameters.getAsserted() > now) {
+            log.warn("Visa asserted in future, it will be ignored!");
+            log.debug("Visa information: {}", parameters);
+            return null;
+        }
+
+        if (parameters.getExpires() <= now) {
+            log.warn("Visa is expired, it will be ignored!");
+            log.debug("Visa information: {}", parameters);
+            return null;
+        }
+
+        Ga4ghPassportVisaV1 ga4ghVisaV1 = new Ga4ghPassportVisaV1();
+        ga4ghVisaV1.setAsserted(parameters.getAsserted());
+        ga4ghVisaV1.setBy(parameters.getBy());
+        ga4ghVisaV1.setValue(parameters.getValue());
+        ga4ghVisaV1.setType(parameters.getType());
+        ga4ghVisaV1.setSource(parameters.getSource());
+        ga4ghVisaV1.setConditions(parameters.getConditions());
+
+        Ga4ghPassportVisa visa = new Ga4ghPassportVisa();
+        visa.setKid(parameters.getJwtService().getSignerKeyId());
+        visa.setTyp(JOSEObjectType.JWT);
+        visa.setJku(parameters.getJku());
+
+        visa.setIss(parameters.getIssuer());
+        visa.setIat(new Date());
+        visa.setExp(new Date(parameters.getExpires() * 1000L));
+        visa.setSub(parameters.getSub());
+        visa.setJti(UUID.randomUUID().toString());
+        visa.setGa4ghVisaV1(ga4ghVisaV1);
+
+        visa.setVerified(true);
+        visa.setSigner(parameters.getSigner());
+        visa.generateSignedJwt(parameters.getJwtService());
+
+        return visa;
+    }
+
+    public static Ga4ghPassportVisa parseVisa(String jwtString) {
+        try {
+            SignedJWT signedJWT = (SignedJWT) JWTParser.parse(jwtString);
+            return parseVisa(signedJWT);
+        } catch (Exception ex) {
+            log.error("Visa '{}' cannot be parsed", jwtString, ex);
+        }
+        return null;
+    }
+
+    public static Ga4ghPassportVisa parseVisa(SignedJWT jwt)
+        throws ParseException, JsonProcessingException
+    {
+        JWSHeader header = jwt.getHeader();
+        JWTClaimsSet payloadClaimSet = jwt.getJWTClaimsSet();
+
+        Ga4ghPassportVisa visa = new Ga4ghPassportVisa();
+
+        JsonNode visaPayload = new ObjectMapper().readValue(jwt.getPayload().toString(), JsonNode.class);
+        JsonNode visaV1 = visaPayload.path(Ga4ghPassportVisaV1.GA4GH_VISA_V1);
+        visa.setVerified(checkVisaHeader(header) && checkVisaValue(visaV1));
+
+        if (!visa.isVerified()) {
+            log.debug("Visa '{}' (payload '{}') did not pass verification", jwt, visaPayload);
+            return visa;
+        }
+
+        visa.setKid(header.getKeyID());
+        visa.setJku(header.getJWKURL());
+        visa.setTyp(header.getType());
+
+        visa.setSub(payloadClaimSet.getSubject());
+        visa.setIss(payloadClaimSet.getIssuer());
+        visa.setIat(payloadClaimSet.getIssueTime());
+        visa.setExp(payloadClaimSet.getExpirationTime());
+        visa.setJti(payloadClaimSet.getJWTID());
+        visa.setGa4ghVisaV1(parseVisaV1(visaV1));
+        visa.setLinkedIdentity(Utils.constructLinkedIdentity(visa.getSub(), visa.getIss()));
+        visa.setJwt(jwt);
+
+        return visa;
+    }
+
+    private static boolean checkVisaExpiration(Ga4ghPassportVisa visa) {
+        long exp = visa.getExp().getTime();
+        boolean expirationValid = exp > Instant.now().getEpochSecond();
+        if (!expirationValid) {
+            log.warn("Visa did not pass expiration validation. Visa expired on '{}'.", isoDateTime(exp));
+        }
+        return expirationValid;
+    }
+
+    public static Ga4ghPassportVisaV1 parseVisaV1(JsonNode visaV1Json) {
+        Ga4ghPassportVisaV1 visaV1 = new Ga4ghPassportVisaV1();
+        visaV1.setAsserted(visaV1Json.path(ASSERTED).longValue());
+        visaV1.setSource(visaV1Json.path(SOURCE).textValue());
+        visaV1.setType(visaV1Json.path(TYPE).textValue());
+        visaV1.setValue(visaV1Json.path(VALUE).textValue());
+        if (visaV1Json.hasNonNull(BY)) {
+            visaV1.setBy(visaV1Json.path(BY).textValue());
+        }
+        if (visaV1Json.hasNonNull(CONDITIONS)) {
+            visaV1.setConditions(visaV1Json.get(CONDITIONS));
+        }
+        return visaV1;
+    }
+
+    private static boolean checkVisaHeader(JWSHeader visaHeader) {
+        boolean valid = visaHeader.getJWKURL() != null
+            && visaHeader.getType() != null
+            && StringUtils.hasText(visaHeader.getKeyID());
+        if (!valid) {
+            log.debug("Visa header did not pass verification");
+        }
+        return valid;
+    }
+
+    private static boolean checkVisaValue(JsonNode visaV1) {
+        if (visaV1.isMissingNode() || visaV1.isNull() || visaV1.isEmpty()) {
+            log.warn("Visa value ({}) is not present. Visa did not pass value verification.",
+                Ga4ghPassportVisaV1.GA4GH_VISA_V1);
+            return false;
+        }
+        boolean keysValid = checkKeyPresence(visaV1, TYPE)
+            && checkKeyPresence(visaV1, Ga4ghPassportVisa.ASSERTED)
+            && checkKeyPresence(visaV1, Ga4ghPassportVisa.VALUE)
+            && checkKeyPresence(visaV1, Ga4ghPassportVisa.SOURCE);
+        if (!keysValid) {
+            log.debug("Visa value did not pass verification of required keys presence.");
+        }
+        return keysValid;
+    }
+
+    private static boolean checkKeyPresence(JsonNode jsonNode, String key) {
+        if (jsonNode.path(key).isMissingNode()) {
+            log.warn("Key '{}' is missing in the Visa.", key);
+            return false;
+        }
+        return true;
+    }
+
+    public static void verifyVisa(Ga4ghPassportVisa visa,
+                                  Map<URI, String> signers,
+                                  Map<URI, RemoteJWKSet<SecurityContext>> remoteJwkSets)
+    {
+        SignedJWT jwt = visa.getJwt();
+        try {
+            boolean expirationVerified = checkVisaExpiration(visa);
+            if (!expirationVerified) {
+                log.warn("Visa is expired, Visa verification failed");
+                visa.setVerified(false);
+                return;
+            }
+
+            URI jku = visa.getJku();
+            if (jku == null) {
+                log.warn("JKU is missing in Visa, verification did not pass ");
+                visa.setVerified(false);
+                return;
+            }
+
+            String signer = signers.getOrDefault(jku, null);
+            if (signer == null) {
+                log.warn("No signer found for JKU '{}, Visa verification failed'", jku);
+                visa.setVerified(false);
+                return;
+            } else {
+                visa.setSigner(signer);
+            }
+
+            RemoteJWKSet<SecurityContext> remoteJWKSet = remoteJwkSets.getOrDefault(jku, null);
+            if (remoteJWKSet == null) {
+                log.error("Trusted key sets does not contain JKU '{}', Visa verification failed", jku);
+                visa.setVerified(false);
+                return;
+            }
+
+            List<JWK> keys = remoteJWKSet.get(new JWKSelector(
+                new JWKMatcher.Builder().keyID(jwt.getHeader().getKeyID()).build()), null);
+            RSASSAVerifier verifier = new RSASSAVerifier(((RSAKey) keys.get(0)).toRSAPublicKey());
+            boolean signatureVerified = jwt.verify(verifier);
+            if (!signatureVerified) {
+                log.warn("Visa signature verification failed, Visa verification failed");
+                visa.setVerified(false);
+                return;
+            }
+            visa.setVerified(true);
+        } catch (Exception ex) {
+            log.error("Visa '{}' cannot be verified", jwt, ex);
+        }
+    }
+
+    public static long getOneYearExpires(long asserted) {
+        return getExpires(asserted, 1L);
+    }
+
+    public static long getExpires(long asserted, long addYears) {
+        return Instant.ofEpochSecond(asserted).atZone(ZoneId.systemDefault()).plusYears(addYears).toEpochSecond();
+    }
+
+    private static void initializeSigner(Map<URI, String> signers,
+                                         Map<URI, RemoteJWKSet<SecurityContext>> remoteJwkSets,
+                                         String name,
+                                         String jwks)
+    {
+        try {
+            URL jku = new URL(jwks);
+            remoteJwkSets.put(jku.toURI(), new RemoteJWKSet<>(jku));
+            signers.put(jku.toURI(), name);
+
+            log.info("JWKS Signer '{}' added with keys '{}'", name, jwks);
+        } catch (MalformedURLException | URISyntaxException e) {
+            log.error("cannot add to RemoteJWKSet map: '{}' -> '{}'", name, jwks, e);
+        }
+    }
+
+    private static void initializeClaimRepository(
+        Ga4ghClaimRepositoryProperties ga4ghClaimRepositoryProperties, List<Ga4ghClaimRepository> claimRepositories) {
+        String name = ga4ghClaimRepositoryProperties.getName();
+        String actionURL = ga4ghClaimRepositoryProperties.getUrl();
+        List<ClaimRepositoryHeader> headers = ga4ghClaimRepositoryProperties.getHeaders();
+
+        if (actionURL == null || headers.isEmpty()) {
+            log.error("claim repository '{}' not defined with url|auth_header|auth_value",
+                ga4ghClaimRepositoryProperties);
+            return;
+        }
+
+        RestTemplate restTemplate = new RestTemplate();
+        restTemplate.setRequestFactory(
+                new InterceptingClientHttpRequestFactory(restTemplate.getRequestFactory(), getClientHttpRequestInterceptors(headers))
+        );
+
+        claimRepositories.add(new Ga4ghClaimRepository(name, actionURL, restTemplate));
+        log.info("GA4GH Claims Repository '{}' configured at '{}'", name, actionURL);
+    }
+
+    private static String isoDate(long linuxTime) {
+        return isoFormat(linuxTime, DateTimeFormatter.ISO_LOCAL_DATE);
+    }
+
+    private static String isoDateTime(long linuxTime) {
+        return isoFormat(linuxTime, DateTimeFormatter.ISO_DATE_TIME);
+    }
+
+    private static String isoFormat(long linuxTime, DateTimeFormatter formatter) {
+        ZonedDateTime zdt = ZonedDateTime.ofInstant(Instant.ofEpochSecond(linuxTime), ZoneId.systemDefault());
+        return formatter.format(zdt);
+    }
+
+    private static List<ClientHttpRequestInterceptor> getClientHttpRequestInterceptors(List<ClaimRepositoryHeader> headers) {
+        return new ArrayList<>(headers);
+    }
+
+    public static String constructLinkedIdentity(String sub, String iss) {
+        return URLEncoder.encode(sub, StandardCharsets.UTF_8)
+            + ','
+            + URLEncoder.encode(iss, StandardCharsets.UTF_8);
+    }
+
+}
diff --git a/src/main/java/cz/muni/ics/ga4gh/base/adapters/PerunAdapter.java b/src/main/java/cz/muni/ics/ga4gh/base/adapters/PerunAdapter.java
new file mode 100644
index 0000000..f270e8c
--- /dev/null
+++ b/src/main/java/cz/muni/ics/ga4gh/base/adapters/PerunAdapter.java
@@ -0,0 +1,52 @@
+package cz.muni.ics.ga4gh.base.adapters;
+
+import cz.muni.ics.ga4gh.base.adapters.impl.PerunAdapterLdap;
+import cz.muni.ics.ga4gh.base.adapters.impl.PerunAdapterRpc;
+import cz.muni.ics.ga4gh.base.exceptions.ConfigurationException;
+import cz.muni.ics.ga4gh.base.properties.PerunAdapterProperties;
+import java.util.ArrayList;
+import java.util.List;
+import lombok.Getter;
+
+@Getter
+public abstract class PerunAdapter implements PerunAdapterMethods {
+
+    public static final String RPC = "RPC";
+    public static final String LDAP = "LDAP";
+
+    private final List<PerunAdapterMethods> adaptersChain = new ArrayList<>();
+    private final PerunAdapterMethodsRpc adapterRpc;
+    private final PerunAdapterMethodsLdap adapterLdap;
+    private final boolean callFallback;
+
+    public PerunAdapter(PerunAdapterProperties config,
+                        PerunAdapterRpc adapterRpc,
+                        PerunAdapterLdap adapterLdap)
+        throws ConfigurationException
+    {
+        if (adapterRpc == null) {
+            throw new ConfigurationException("No Perun RPC adapter configured");
+        }
+
+        if (RPC.equalsIgnoreCase(config.getAdapterPrimary())) {
+            this.adaptersChain.add(adapterRpc);
+            if (adapterLdap != null) {
+                this.adaptersChain.add(adapterLdap);
+            }
+            this.adaptersChain.add(adapterRpc);
+            this.adaptersChain.add(adapterLdap);
+        } else if (LDAP.equalsIgnoreCase(config.getAdapterPrimary())) {
+            if (adapterLdap == null) {
+                throw new ConfigurationException("LDAP adapter specified as primary, but not defined");
+            }
+            this.adaptersChain.add(adapterLdap);
+        } else {
+            throw new ConfigurationException("Unrecognized primary adapter set");
+        }
+
+        this.adapterRpc = adapterRpc;
+        this.adapterLdap = adapterLdap;
+        this.callFallback = config.isCallFallback();
+    }
+
+}
diff --git a/src/main/java/cz/muni/ics/ga4gh/base/adapters/PerunAdapterMethods.java b/src/main/java/cz/muni/ics/ga4gh/base/adapters/PerunAdapterMethods.java
new file mode 100644
index 0000000..042727b
--- /dev/null
+++ b/src/main/java/cz/muni/ics/ga4gh/base/adapters/PerunAdapterMethods.java
@@ -0,0 +1,28 @@
+package cz.muni.ics.ga4gh.base.adapters;
+
+import cz.muni.ics.ga4gh.base.model.Affiliation;
+import java.util.List;
+import java.util.Set;
+
+public interface PerunAdapterMethods {
+
+    /**
+     * Fetch user based on his principal (extLogin and extSource) from Perun
+     *
+     * @return PerunUser with id of found user
+     */
+    Long getPerunUserId(String extLogin, String extSourceName);
+
+    boolean isUserInGroup(Long userId, Long groupId);
+
+    List<Affiliation> getGroupAffiliations(Long userId, String groupAffiliationsAttr);
+
+    List<Affiliation> getGroupAffiliations(Long userId, Long voId, String groupAffiliationsAttr);
+
+    Set<Long> getUserIdsByAttributeValue(String attrName, String attrValue);
+
+    String getUserSub(Long userId, String subAttribute);
+
+    boolean isUserInVo(Long userId, Long voId);
+
+}
diff --git a/src/main/java/cz/muni/ics/ga4gh/adapters/PerunAdapterMethodsLdap.java b/src/main/java/cz/muni/ics/ga4gh/base/adapters/PerunAdapterMethodsLdap.java
similarity index 52%
rename from src/main/java/cz/muni/ics/ga4gh/adapters/PerunAdapterMethodsLdap.java
rename to src/main/java/cz/muni/ics/ga4gh/base/adapters/PerunAdapterMethodsLdap.java
index 70c67cd..d589f81 100644
--- a/src/main/java/cz/muni/ics/ga4gh/adapters/PerunAdapterMethodsLdap.java
+++ b/src/main/java/cz/muni/ics/ga4gh/base/adapters/PerunAdapterMethodsLdap.java
@@ -1,4 +1,4 @@
-package cz.muni.ics.ga4gh.adapters;
+package cz.muni.ics.ga4gh.base.adapters;
 
 public interface PerunAdapterMethodsLdap {
 }
diff --git a/src/main/java/cz/muni/ics/ga4gh/adapters/PerunAdapterMethodsRpc.java b/src/main/java/cz/muni/ics/ga4gh/base/adapters/PerunAdapterMethodsRpc.java
similarity index 54%
rename from src/main/java/cz/muni/ics/ga4gh/adapters/PerunAdapterMethodsRpc.java
rename to src/main/java/cz/muni/ics/ga4gh/base/adapters/PerunAdapterMethodsRpc.java
index f2c4d5e..195a5d6 100644
--- a/src/main/java/cz/muni/ics/ga4gh/adapters/PerunAdapterMethodsRpc.java
+++ b/src/main/java/cz/muni/ics/ga4gh/base/adapters/PerunAdapterMethodsRpc.java
@@ -1,7 +1,7 @@
-package cz.muni.ics.ga4gh.adapters;
-
-import cz.muni.ics.ga4gh.model.Affiliation;
+package cz.muni.ics.ga4gh.base.adapters;
 
+import cz.muni.ics.ga4gh.base.model.Affiliation;
+import cz.muni.ics.ga4gh.base.model.UserExtSource;
 import java.util.List;
 
 public interface PerunAdapterMethodsRpc {
@@ -9,4 +9,7 @@ public interface PerunAdapterMethodsRpc {
     String getUserAttributeCreatedAt(Long userId, String attrName);
 
     List<Affiliation> getUserExtSourcesAffiliations(Long userId, String affiliationsAttr, String orgUrlAttr);
+
+    List<UserExtSource> getIdpUserExtSources(Long perunUserId);
+
 }
diff --git a/src/main/java/cz/muni/ics/ga4gh/mappers/RpcMapper.java b/src/main/java/cz/muni/ics/ga4gh/base/adapters/PerunRpcAdapterMapper.java
similarity index 91%
rename from src/main/java/cz/muni/ics/ga4gh/mappers/RpcMapper.java
rename to src/main/java/cz/muni/ics/ga4gh/base/adapters/PerunRpcAdapterMapper.java
index 37254d6..f09da28 100644
--- a/src/main/java/cz/muni/ics/ga4gh/mappers/RpcMapper.java
+++ b/src/main/java/cz/muni/ics/ga4gh/base/adapters/PerunRpcAdapterMapper.java
@@ -1,28 +1,27 @@
-package cz.muni.ics.ga4gh.mappers;
+package cz.muni.ics.ga4gh.base.adapters;
 
 import com.fasterxml.jackson.databind.JsonNode;
+import cz.muni.ics.ga4gh.base.enums.MemberStatus;
+import cz.muni.ics.ga4gh.base.exceptions.MissingFieldException;
+import cz.muni.ics.ga4gh.base.model.AttributeMapping;
+import cz.muni.ics.ga4gh.base.model.ExtSource;
+import cz.muni.ics.ga4gh.base.model.Group;
+import cz.muni.ics.ga4gh.base.model.Member;
+import cz.muni.ics.ga4gh.base.model.PerunAttributeValue;
+import cz.muni.ics.ga4gh.base.model.UserExtSource;
 import java.sql.Timestamp;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
-import cz.muni.ics.ga4gh.enums.MemberStatus;
-import cz.muni.ics.ga4gh.exceptions.MissingFieldException;
-import cz.muni.ics.ga4gh.model.AttributeMapping;
-import cz.muni.ics.ga4gh.model.ExtSource;
-import cz.muni.ics.ga4gh.model.Group;
-import cz.muni.ics.ga4gh.model.Member;
-import cz.muni.ics.ga4gh.model.PerunAttributeValue;
-import cz.muni.ics.ga4gh.model.UserExtSource;
-
 
 /**
  * This class is mapping JsonNodes to object models.
  *
  * @author Dominik Frantisek Bucik <bucik@ics.muni.cz>
  */
-public class RpcMapper {
+public class PerunRpcAdapterMapper {
 
     public static final String ID = "id";
     public static final String UUID = "uuid";
@@ -77,7 +76,7 @@ public class RpcMapper {
         List<Group> result = new ArrayList<>();
         for (int i = 0; i < jsonArray.size(); i++) {
             JsonNode groupNode = jsonArray.get(i);
-            Group mappedGroup = RpcMapper.mapGroup(groupNode);
+            Group mappedGroup = PerunRpcAdapterMapper.mapGroup(groupNode);
             result.add(mappedGroup);
         }
 
@@ -117,7 +116,7 @@ public class RpcMapper {
         List<Member> members = new ArrayList<>();
         for (int i = 0; i < jsonArray.size(); i++) {
             JsonNode memberNode = jsonArray.get(i);
-            Member mappedMember = RpcMapper.mapMember(memberNode);
+            Member mappedMember = PerunRpcAdapterMapper.mapMember(memberNode);
             members.add(mappedMember);
         }
 
@@ -155,7 +154,7 @@ public class RpcMapper {
 
         Long id = getRequiredFieldAsLong(json, ID);
         String login = getRequiredFieldAsString(json, LOGIN);
-        ExtSource extSource = RpcMapper.mapExtSource(getRequiredFieldAsJsonNode(json, EXT_SOURCE));
+        ExtSource extSource = PerunRpcAdapterMapper.mapExtSource(getRequiredFieldAsJsonNode(json, EXT_SOURCE));
         int loa = getRequiredFieldAsInt(json, LOA);
         boolean persistent = getRequiredFieldAsBoolean(json, PERSISTENT);
         Timestamp lastAccess = Timestamp.valueOf(getRequiredFieldAsString(json, LAST_ACCESS));
@@ -178,7 +177,7 @@ public class RpcMapper {
 
         for (int i = 0; i < jsonArray.size(); i++) {
             JsonNode userExtSource = jsonArray.get(i);
-            UserExtSource mappedUes = RpcMapper.mapUserExtSource(userExtSource);
+            UserExtSource mappedUes = PerunRpcAdapterMapper.mapUserExtSource(userExtSource);
             userExtSources.add(mappedUes);
         }
 
diff --git a/src/main/java/cz/muni/ics/ga4gh/base/adapters/impl/PerunAdapterImpl.java b/src/main/java/cz/muni/ics/ga4gh/base/adapters/impl/PerunAdapterImpl.java
new file mode 100644
index 0000000..e018c08
--- /dev/null
+++ b/src/main/java/cz/muni/ics/ga4gh/base/adapters/impl/PerunAdapterImpl.java
@@ -0,0 +1,128 @@
+package cz.muni.ics.ga4gh.base.adapters.impl;
+
+import cz.muni.ics.ga4gh.base.adapters.PerunAdapter;
+import cz.muni.ics.ga4gh.base.adapters.PerunAdapterMethods;
+import cz.muni.ics.ga4gh.base.exceptions.ConfigurationException;
+import cz.muni.ics.ga4gh.base.exceptions.PerunAdapterOperationException;
+import cz.muni.ics.ga4gh.base.model.Affiliation;
+import cz.muni.ics.ga4gh.base.properties.PerunAdapterProperties;
+import java.util.List;
+import java.util.Set;
+
+public class PerunAdapterImpl extends PerunAdapter {
+
+
+    public PerunAdapterImpl(PerunAdapterProperties config,
+                            PerunAdapterRpc adapterRpc,
+                            PerunAdapterLdap adapterLdap)
+        throws ConfigurationException
+    {
+        super(config, adapterRpc, adapterLdap);
+    }
+
+    public PerunAdapterImpl(PerunAdapterProperties config,
+                            PerunAdapterRpc adapterRpc)
+        throws ConfigurationException
+    {
+        super(config, adapterRpc, null);
+    }
+
+    @Override
+    public Long getPerunUserId(String extLogin, String extSourceName) {
+        try {
+            for (PerunAdapterMethods adapter: getAdaptersChain()) {
+                return adapter.getPerunUserId(extLogin, extSourceName);
+            }
+        } catch (UnsupportedOperationException e) {
+            if (!this.isCallFallback()) {
+                throw e;
+            }
+        }
+        throw new PerunAdapterOperationException("No adapter able to perform call");
+    }
+
+    @Override
+    public boolean isUserInGroup(Long userId, Long groupId) {
+        try {
+            for (PerunAdapterMethods adapter: getAdaptersChain()) {
+                return adapter.isUserInGroup(userId, groupId);
+            }
+        } catch (UnsupportedOperationException e) {
+            if (!this.isCallFallback()) {
+                throw e;
+            }
+        }
+        throw new PerunAdapterOperationException("No adapter able to perform call");
+    }
+
+    @Override
+    public List<Affiliation> getGroupAffiliations(Long userId, String groupAffiliationsAttr) {
+        try {
+            for (PerunAdapterMethods adapter: getAdaptersChain()) {
+                return adapter.getGroupAffiliations(userId, groupAffiliationsAttr);
+            }
+        } catch (UnsupportedOperationException e) {
+            if (!this.isCallFallback()) {
+                throw e;
+            }
+        }
+        throw new PerunAdapterOperationException("No adapter able to perform call");
+    }
+
+    @Override
+    public List<Affiliation> getGroupAffiliations(Long userId, Long voId, String groupAffiliationsAttr) {
+        try {
+            for (PerunAdapterMethods adapter: getAdaptersChain()) {
+                return adapter.getGroupAffiliations(userId, voId, groupAffiliationsAttr);
+            }
+        } catch (UnsupportedOperationException e) {
+            if (!this.isCallFallback()) {
+                throw e;
+            }
+        }
+        throw new PerunAdapterOperationException("No adapter able to perform call");
+    }
+
+    @Override
+    public Set<Long> getUserIdsByAttributeValue(String attrName, String attrValue) {
+        try {
+            for (PerunAdapterMethods adapter: getAdaptersChain()) {
+                return adapter.getUserIdsByAttributeValue(attrName, attrValue);
+            }
+        } catch (UnsupportedOperationException e) {
+            if (!this.isCallFallback()) {
+                throw e;
+            }
+        }
+        throw new PerunAdapterOperationException("No adapter able to perform call");
+    }
+
+    @Override
+    public String getUserSub(Long userId, String subAttribute) {
+        try {
+            for (PerunAdapterMethods adapter: getAdaptersChain()) {
+                return adapter.getUserSub(userId, subAttribute);
+            }
+        } catch (UnsupportedOperationException e) {
+            if (!this.isCallFallback()) {
+                throw e;
+            }
+        }
+        throw new PerunAdapterOperationException("No adapter able to perform call");
+    }
+
+    @Override
+    public boolean isUserInVo(Long userId, Long voId) {
+        try {
+            for (PerunAdapterMethods adapter: getAdaptersChain()) {
+                return adapter.isUserInVo(userId, voId);
+            }
+        } catch (UnsupportedOperationException e) {
+            if (!this.isCallFallback()) {
+                throw e;
+            }
+        }
+        throw new PerunAdapterOperationException("No adapter able to perform call");
+    }
+
+}
diff --git a/src/main/java/cz/muni/ics/ga4gh/base/adapters/impl/PerunAdapterLdap.java b/src/main/java/cz/muni/ics/ga4gh/base/adapters/impl/PerunAdapterLdap.java
new file mode 100644
index 0000000..a896427
--- /dev/null
+++ b/src/main/java/cz/muni/ics/ga4gh/base/adapters/impl/PerunAdapterLdap.java
@@ -0,0 +1,350 @@
+package cz.muni.ics.ga4gh.base.adapters.impl;
+
+import static org.apache.directory.ldap.client.api.search.FilterBuilder.and;
+import static org.apache.directory.ldap.client.api.search.FilterBuilder.equal;
+import static org.apache.directory.ldap.client.api.search.FilterBuilder.or;
+
+import cz.muni.ics.ga4gh.base.adapters.PerunAdapterMethods;
+import cz.muni.ics.ga4gh.base.adapters.PerunAdapterMethodsLdap;
+import cz.muni.ics.ga4gh.base.connectors.PerunConnectorLdap;
+import cz.muni.ics.ga4gh.base.model.Affiliation;
+import cz.muni.ics.ga4gh.base.model.AttributeMapping;
+import cz.muni.ics.ga4gh.base.properties.AttributeMappingProperties;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.directory.api.ldap.model.entry.Attribute;
+import org.apache.directory.api.ldap.model.entry.Entry;
+import org.apache.directory.api.ldap.model.message.SearchScope;
+import org.apache.directory.ldap.client.api.search.FilterBuilder;
+import org.apache.directory.ldap.client.template.EntryMapper;
+import org.springframework.util.StringUtils;
+
+@Slf4j
+public class PerunAdapterLdap implements PerunAdapterMethods, PerunAdapterMethodsLdap {
+
+    public static final String OBJECT_CLASS = "objectClass";
+    public static final String OU_PEOPLE = "ou=People";
+
+    public static final String PERUN_USER_ID = "perunUserId";
+    public static final String MEMBER_OF = "memberOf";
+
+    public static final String PERUN_GROUP = "perunGroup";
+    public static final String PERUN_USER = "perunUser";
+    public static final String PERUN_VO = "perunVo";
+    public static final String PERUN_GROUP_ID = "perunGroupId";
+    public static final String UNIQUE_MEMBER = "uniqueMember";
+
+    public static final String PERUN_VO_ID = "perunVoId";
+    public static final String EDU_PERSON_PRINCIPAL_NAMES = "eduPersonPrincipalNames";
+
+    private final PerunConnectorLdap connectorLdap;
+    private final Map<String, AttributeMapping> attributeMappings;
+
+    public PerunAdapterLdap(PerunConnectorLdap connectorLdap,
+                            AttributeMappingProperties attributeMappingProperties)
+    {
+        this.connectorLdap = connectorLdap;
+        this.attributeMappings = attributeMappingProperties.getAttributeMappings();
+    }
+
+    @Override
+    public Long getPerunUserId(String extLogin, String extSourceName) {
+        if (!StringUtils.hasText(extLogin)) {
+            throw new IllegalArgumentException("Empty extLogin passed");
+        }
+        // PARAM EXT_SOURCE_NAME IS NOT USED, THUS NOT VALIDATED
+
+        FilterBuilder filter = and(
+                equal(OBJECT_CLASS, PERUN_USER), equal(EDU_PERSON_PRINCIPAL_NAMES, extLogin)
+        );
+
+        return getPerunUserId(filter);
+    }
+
+    @Override
+    public boolean isUserInGroup(Long userId, Long groupId) {
+        if (userId == null) {
+            throw new IllegalArgumentException("Null user ID passed");
+        } else if (groupId == null) {
+            throw new IllegalArgumentException("Null group ID passed");
+        }
+
+        String uniqueMemberValue = getUniqueMemberValue(userId);
+
+        FilterBuilder filter = and(
+                equal(OBJECT_CLASS, PERUN_GROUP),
+                equal(PERUN_GROUP_ID, String.valueOf(groupId)),
+                equal(UNIQUE_MEMBER, uniqueMemberValue)
+        );
+
+        EntryMapper<Long> mapper = e -> Long.parseLong(e.get(PERUN_GROUP_ID).getString());
+        String[] attributes = new String[] { PERUN_GROUP_ID };
+        List<Long> ids = connectorLdap.search(null, filter, SearchScope.SUBTREE, attributes, mapper);
+
+        return ids.stream().filter(groupId::equals).count() == 1L;
+    }
+
+    @Override
+    public List<Affiliation> getGroupAffiliations(Long userId, String groupAffiliationsAttr) {
+        if (userId == null) {
+            throw new IllegalArgumentException("Null user ID passed");
+        } else if (!StringUtils.hasText(groupAffiliationsAttr)) {
+            throw new IllegalArgumentException("Empty group affiliations attribute name passed");
+        }
+
+        Set<Long> userGroupIds = getGroupIdsWhereUserIsMember(userId, null);
+        if (userGroupIds.isEmpty()) {
+            return new ArrayList<>();
+        }
+
+        FilterBuilder[] groupIdFilters = new FilterBuilder[userGroupIds.size()];
+        int i = 0;
+
+        for (Long id: userGroupIds) {
+            groupIdFilters[i++] = equal(PERUN_GROUP_ID, String.valueOf(id));
+        }
+
+        AttributeMapping affiliationsMapping = attributeMappings.get(groupAffiliationsAttr);
+        FilterBuilder filterBuilder = and(equal(OBJECT_CLASS, PERUN_GROUP), or(groupIdFilters));
+        String[] attributes = new String[] { affiliationsMapping.getLdapName() };
+
+        EntryMapper<Set<Affiliation>> mapper = e -> {
+            Set<Affiliation> affiliations = new HashSet<>();
+            if (!checkHasAttributes(e, attributes)) {
+                return affiliations;
+            }
+
+            Attribute a = e.get(affiliationsMapping.getLdapName());
+            long linuxTime = System.currentTimeMillis() / 1000L;
+            a.iterator().forEachRemaining(v -> affiliations.add(
+                new Affiliation(null, v.getString(), linuxTime)));
+
+            return affiliations;
+        };
+
+        List<Set<Affiliation>> affiliationSets = connectorLdap.search(
+            null, filterBuilder, SearchScope.SUBTREE, attributes, mapper);
+
+        return affiliationSets.stream().flatMap(Set::stream).distinct().collect(Collectors.toList());
+    }
+
+    @Override
+    public List<Affiliation> getGroupAffiliations(Long userId, Long voId,
+                                                  String groupAffiliationsAttr)
+    {
+        if (userId == null) {
+            throw new IllegalArgumentException("Null user ID passed");
+        } else if (voId == null) {
+            throw new IllegalArgumentException("Null vo ID passed");
+        } else if (!StringUtils.hasText(groupAffiliationsAttr)) {
+            throw new IllegalArgumentException("Empty group affiliations attribute name passed");
+        }
+
+        Set<Long> userGroupIds = getGroupIdsWhereUserIsMember(userId, voId);
+        if (userGroupIds.isEmpty()) {
+            return new ArrayList<>();
+        }
+
+        FilterBuilder[] groupIdFilters = new FilterBuilder[userGroupIds.size()];
+        int i = 0;
+
+        for (Long id: userGroupIds) {
+            groupIdFilters[i++] = equal(PERUN_GROUP_ID, String.valueOf(id));
+        }
+
+        AttributeMapping affiliationsMapping = attributeMappings.get(groupAffiliationsAttr);
+        FilterBuilder filterBuilder = and(equal(OBJECT_CLASS, PERUN_GROUP), or(groupIdFilters));
+        String[] attributes = new String[] { affiliationsMapping.getLdapName() };
+
+        EntryMapper<Set<Affiliation>> mapper = e -> {
+            Set<Affiliation> affiliations = new HashSet<>();
+            if (!checkHasAttributes(e, attributes)) {
+                return affiliations;
+            }
+
+            Attribute a = e.get(affiliationsMapping.getLdapName());
+            long linuxTime = System.currentTimeMillis() / 1000L;
+            a.iterator().forEachRemaining(v -> affiliations.add(
+                new Affiliation(null, v.getString(), linuxTime)));
+
+            return affiliations;
+        };
+
+        List<Set<Affiliation>> affiliationSets = connectorLdap.search(
+            null, filterBuilder, SearchScope.SUBTREE, attributes, mapper);
+
+        return affiliationSets.stream().flatMap(Set::stream).distinct().collect(Collectors.toList());
+    }
+
+    @Override
+    public Set<Long> getUserIdsByAttributeValue(String attrName, String attrValue) {
+        if (!StringUtils.hasText(attrName)) {
+            throw new IllegalArgumentException("Empty attribute name passed");
+        } else if (!StringUtils.hasText(attrValue)) {
+            throw new IllegalArgumentException("Empty attribute value passed");
+        }
+
+        AttributeMapping attributeMapping = attributeMappings.getOrDefault(attrName, null);
+        if (attributeMapping == null) {
+            log.error("No LDAP mapping found for attribute '{}'", attrName);
+            return new HashSet<>();
+        }
+
+        FilterBuilder filter = and(
+                equal(OBJECT_CLASS, PERUN_USER),
+                equal(attributeMapping.getLdapName(), attrValue)
+        );
+
+        SearchScope scope = SearchScope.ONELEVEL;
+        String[] attributes = new String[]{ PERUN_USER_ID };
+        EntryMapper<Long> mapper = e -> Long.parseLong(e.get(PERUN_USER_ID).getString());
+
+        List<Long> result = connectorLdap.search(OU_PEOPLE, filter, scope, attributes, mapper);
+
+        return Set.copyOf(result);
+    }
+
+    @Override
+    public String getUserSub(Long userId, String subAttribute) {
+        if (userId == null) {
+            throw new IllegalArgumentException("Null user ID passed");
+        } else if (!StringUtils.hasText(subAttribute)) {
+            throw new IllegalArgumentException("Empty attribute name passed");
+        }
+        AttributeMapping attributeMapping = getAttributeMapping(subAttribute);
+        if (attributeMapping == null) {
+            return null;
+        }
+
+        FilterBuilder filter = and(
+            equal(OBJECT_CLASS, PERUN_USER),
+            equal(PERUN_USER_ID, String.valueOf(userId))
+        );
+
+        SearchScope scope = SearchScope.SUBTREE;
+        String[] attributes = new String[]{ attributeMapping.getLdapName() };
+        EntryMapper<String> mapper = e -> e.get(attributeMapping.getLdapName()).getString();
+
+        return connectorLdap.searchFirst(OU_PEOPLE, filter, scope, attributes, mapper);
+    }
+
+    @Override
+    public boolean isUserInVo(Long userId, Long voId) {
+        if (userId == null) {
+            throw new IllegalArgumentException("Null user ID passed");
+        } else if (voId == null) {
+            throw new IllegalArgumentException("Null vo ID passed");
+        }
+
+        String uniqueMemberValue = getUniqueMemberValue(userId);
+
+        FilterBuilder filter = and(
+            equal(OBJECT_CLASS, PERUN_VO),
+            equal(PERUN_VO_ID, String.valueOf(voId)),
+            equal(UNIQUE_MEMBER, uniqueMemberValue)
+        );
+
+        EntryMapper<Long> mapper = e -> Long.parseLong(e.get(PERUN_VO_ID).getString());
+        String[] attributes = new String[] { PERUN_VO_ID };
+        List<Long> ids = connectorLdap.search(null, filter, SearchScope.SUBTREE, attributes, mapper);
+
+        return ids.stream().filter(voId::equals).count() == 1L;
+    }
+
+    private Set<Long> getGroupIdsWhereUserIsMember(Long userId, Long voId) {
+        if (userId == null) {
+            throw new IllegalArgumentException("Null user ID passed");
+        }
+
+        String dnPrefix = getDnPrefixForUserId(userId);
+        String[] attributes = new String[] { MEMBER_OF };
+
+        EntryMapper<Set<Long>> mapper = e -> {
+            Set<Long> ids = new HashSet<>();
+            if (checkHasAttributes(e, attributes)) {
+                Attribute a = e.get(MEMBER_OF);
+                a.iterator().forEachRemaining(id -> {
+                    String fullVal = id.getString();
+                    String[] parts = fullVal.split(",", 3);
+
+                    String groupId = parts[0];
+                    groupId = groupId.replace(PERUN_GROUP_ID + '=', "");
+
+                    String voIdStr = parts[1];
+                    voIdStr = voIdStr.replace(PERUN_VO_ID + '=', "");
+
+                    if (voId == null || voId.equals(Long.parseLong(voIdStr))) {
+                        ids.add(Long.parseLong(groupId));
+                    }
+                });
+            }
+
+            return ids;
+        };
+
+        Set<Long> res = connectorLdap.lookup(dnPrefix, attributes, mapper);
+        if (res == null) {
+            res = new HashSet<>();
+        }
+
+        return res;
+    }
+
+    private String getDnPrefixForUserId(Long userId) {
+        if (userId == null) {
+            throw new IllegalArgumentException("Null user ID passed");
+        }
+        return PERUN_USER_ID + '=' + userId + ',' + OU_PEOPLE;
+    }
+
+    private boolean checkHasAttributes(Entry e, String[] attributes) {
+        if (e == null) {
+            return false;
+        } else if (attributes == null) {
+            return true;
+        }
+
+        for (String attr: attributes) {
+            if (e.get(attr) == null) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    private Long getPerunUserId(FilterBuilder filter) {
+        SearchScope scope = SearchScope.ONELEVEL;
+        String[] attributes = new String[]{PERUN_USER_ID};
+        EntryMapper<Long> mapper = e -> Long.parseLong(e.get(PERUN_USER_ID).getString());
+
+        return connectorLdap.searchFirst(OU_PEOPLE, filter, scope, attributes, mapper);
+    }
+
+    private AttributeMapping getAttributeMapping(String attribute) {
+        if (!StringUtils.hasText(attribute)) {
+            throw new IllegalArgumentException("Empty attribute name passed");
+        }
+
+        AttributeMapping mapping = attributeMappings.getOrDefault(attribute, null);
+        if (mapping == null) {
+            log.warn("No attribute mapping found for attribute '{}'", attribute);
+            return null;
+        } else if (!StringUtils.hasText(mapping.getLdapName())) {
+            log.warn("No LDAP name found in mapping for attribute '{}'", attribute);
+            return null;
+        } else {
+            return mapping;
+        }
+    }
+
+    private String getUniqueMemberValue(Long userId) {
+        return PERUN_USER_ID + '=' + userId + ',' + OU_PEOPLE + ',' + connectorLdap.getBaseDN();
+    }
+
+}
diff --git a/src/main/java/cz/muni/ics/ga4gh/base/adapters/impl/PerunAdapterRpc.java b/src/main/java/cz/muni/ics/ga4gh/base/adapters/impl/PerunAdapterRpc.java
new file mode 100644
index 0000000..4afec68
--- /dev/null
+++ b/src/main/java/cz/muni/ics/ga4gh/base/adapters/impl/PerunAdapterRpc.java
@@ -0,0 +1,480 @@
+package cz.muni.ics.ga4gh.base.adapters.impl;
+
+import static cz.muni.ics.ga4gh.base.enums.MemberStatus.VALID;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import cz.muni.ics.ga4gh.base.adapters.PerunAdapterMethods;
+import cz.muni.ics.ga4gh.base.adapters.PerunAdapterMethodsRpc;
+import cz.muni.ics.ga4gh.base.adapters.PerunRpcAdapterMapper;
+import cz.muni.ics.ga4gh.base.connectors.PerunConnectorRpc;
+import cz.muni.ics.ga4gh.base.model.Affiliation;
+import cz.muni.ics.ga4gh.base.model.AttributeMapping;
+import cz.muni.ics.ga4gh.base.model.Group;
+import cz.muni.ics.ga4gh.base.model.Member;
+import cz.muni.ics.ga4gh.base.model.PerunAttributeValue;
+import cz.muni.ics.ga4gh.base.model.UserExtSource;
+import cz.muni.ics.ga4gh.base.properties.AttributeMappingProperties;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.util.StringUtils;
+
+@Slf4j
+public class PerunAdapterRpc implements PerunAdapterMethods, PerunAdapterMethodsRpc {
+
+    public static final String EXT_SOURCE_IDP = "cz.metacentrum.perun.core.impl.ExtSourceIdp";
+    public static final String EXT_LOGIN = "extLogin";
+    public static final String EXT_SOURCE_NAME = "extSourceName";
+    public static final String USER_EXT_SOURCE = "userExtSource";
+    public static final String ATTR_NAMES = "attrNames";
+    public static final String VALUE_CREATED_AT = "valueCreatedAt";
+
+    public static final String ID = "id";
+    public static final String VO = "vo";
+    public static final String USER = "user";
+    public static final String MEMBER = "member";
+    public static final String GROUP = "group";
+
+    public static final String ATTRIBUTE_NAME = "attributeName";
+    public static final String ATTRIBUTE_VALUE = "attributeValue";
+
+    public static final String ATTRIBUTES_MANAGER = "attributesManager";
+    public static final String GROUPS_MANAGER = "groupsManager";
+    public static final String MEMBERS_MANAGER = "membersManager";
+    public static final String USERS_MANAGER = "usersManager";
+
+    public static final String GET_USER_BY_EXT_SOURCE_NAME_AND_EXT_LOGIN = "getUserByExtSourceNameAndExtLogin";
+    public static final String GET_USER_EXT_SOURCES = "getUserExtSources";
+    public static final String GET_GROUP_BY_ID = "getGroupById";
+    public static final String GET_MEMBER_BY_USER = "getMemberByUser";
+    public static final String GET_MEMBERS_BY_USER = "getMembersByUser";
+    public static final String GET_MEMBER_GROUPS = "getMemberGroups";
+    public static final String IS_GROUP_MEMBER = "isGroupMember";
+    public static final String GET_ATTRIBUTE = "getAttribute";
+    public static final String GET_ATTRIBUTES = "getAttributes";
+    public static final String GET_USERS_BY_ATTRIBUTE_VALUE = "getUsersByAttributeValue";
+
+    private final PerunConnectorRpc connectorRpc;
+    private final Map<String, AttributeMapping> attributeMappings;
+
+    public PerunAdapterRpc(PerunConnectorRpc connectorRpc,
+                    AttributeMappingProperties attributeMappingProperties)
+    {
+        this.connectorRpc = connectorRpc;
+        this.attributeMappings = attributeMappingProperties.getAttributeMappings();
+    }
+
+    @Override
+    public Long getPerunUserId(String extLogin, String extSourceName) {
+        if (!StringUtils.hasText(extLogin)) {
+            throw new IllegalArgumentException("Empty extLogin passed");
+        } else if (!StringUtils.hasText(extSourceName)) {
+            throw new IllegalArgumentException("Empty extSourceName passed");
+        }
+
+        if (!connectorRpc.isEnabled()) {
+            return null;
+        }
+
+        Map<String, Object> map = new LinkedHashMap<>();
+        map.put(EXT_LOGIN, extLogin);
+        map.put(EXT_SOURCE_NAME, extSourceName);
+
+        JsonNode response = connectorRpc.post(USERS_MANAGER, GET_USER_BY_EXT_SOURCE_NAME_AND_EXT_LOGIN, map);
+
+        return response.get(ID) == null ? null : response.get(ID).asLong();
+    }
+
+    @Override
+    public boolean isUserInGroup(Long userId, Long groupId) {
+        if (userId == null) {
+            throw new IllegalArgumentException("Null user ID passed");
+        } else if (groupId == null) {
+            throw new IllegalArgumentException("Null group ID passed");
+        }
+
+        if (!connectorRpc.isEnabled()) {
+            return false;
+        }
+
+        Map<String, Object> groupParams = new LinkedHashMap<>();
+        groupParams.put(ID, groupId);
+        JsonNode groupResponse = connectorRpc.post(GROUPS_MANAGER, GET_GROUP_BY_ID, groupParams);
+        Group group = PerunRpcAdapterMapper.mapGroup(groupResponse);
+
+        Map<String, Object> memberParams = new LinkedHashMap<>();
+        memberParams.put(VO, group.getVoId());
+        memberParams.put(USER, userId);
+        JsonNode memberResponse = connectorRpc.post(MEMBERS_MANAGER, GET_MEMBER_BY_USER, memberParams);
+        Member member = PerunRpcAdapterMapper.mapMember(memberResponse);
+
+        Map<String, Object> isGroupMemberParams = new LinkedHashMap<>();
+        isGroupMemberParams.put(GROUP, groupId);
+        isGroupMemberParams.put(MEMBER, member.getId());
+        JsonNode res = connectorRpc.post(GROUPS_MANAGER, IS_GROUP_MEMBER, isGroupMemberParams);
+
+        return res.asBoolean(false);
+    }
+
+    @Override
+    public List<Affiliation> getGroupAffiliations(Long userId, String groupAffiliationsAttr) {
+        if (userId == null) {
+            throw new IllegalArgumentException("Null user ID passed");
+        } else if (!StringUtils.hasText(groupAffiliationsAttr)) {
+            throw new IllegalArgumentException("Empty group affiliations attribute name passed");
+        }
+        if (!connectorRpc.isEnabled()) {
+            return new ArrayList<>();
+        }
+
+        List<Affiliation> affiliations = new ArrayList<>();
+        List<Member> userMembers = getMembersByUser(userId);
+
+        for (Member member : userMembers) {
+            if (!VALID.equals(member.getStatus())) {
+                continue;
+            }
+            List<Group> memberGroups = getMemberGroups(member.getId());
+            for (Group group : memberGroups) {
+                PerunAttributeValue attrValue = getGroupAttributeValue(group, groupAffiliationsAttr);
+                if (attrValue != null && attrValue.valueAsList() != null) {
+                    long linuxTime = System.currentTimeMillis() / 1000L;
+
+                    for (String value : attrValue.valueAsList()) {
+                        Affiliation affiliation = new Affiliation(null, value, linuxTime);
+                        log.debug("found {} on group {}", value, group.getName());
+                        affiliations.add(affiliation);
+                    }
+                }
+            }
+        }
+
+        return affiliations;
+    }
+
+    @Override
+    public List<Affiliation> getGroupAffiliations(Long userId, Long voId,
+                                                  String groupAffiliationsAttr)
+    {
+        if (userId == null) {
+            throw new IllegalArgumentException("Null user ID passed");
+        } else if (voId == null) {
+            throw new IllegalArgumentException("Null vo ID passed");
+        } else if (!StringUtils.hasText(groupAffiliationsAttr)) {
+            throw new IllegalArgumentException("Empty group affiliations attribute name passed");
+        }
+        if (!connectorRpc.isEnabled()) {
+            return new ArrayList<>();
+        }
+
+        List<Affiliation> affiliations = new ArrayList<>();
+        List<Member> userMembers = getMembersByUser(userId);
+
+        for (Member member : userMembers) {
+            if (!VALID.equals(member.getStatus())) {
+                continue;
+            } else if (!Objects.equals(voId, member.getVoId())) {
+                continue;
+            }
+            List<Group> memberGroups = getMemberGroups(member.getId());
+            for (Group group : memberGroups) {
+                PerunAttributeValue attrValue = getGroupAttributeValue(group, groupAffiliationsAttr);
+                if (attrValue != null && attrValue.valueAsList() != null) {
+                    long linuxTime = System.currentTimeMillis() / 1000L;
+
+                    for (String value : attrValue.valueAsList()) {
+                        Affiliation affiliation = new Affiliation(null, value, linuxTime);
+                        log.debug("found {} on group {}", value, group.getName());
+                        affiliations.add(affiliation);
+                    }
+                }
+            }
+        }
+
+        return affiliations;
+    }
+
+    @Override
+    public String getUserAttributeCreatedAt(Long userId, String attrName) {
+        if (userId == null) {
+            throw new IllegalArgumentException("Null user ID passed");
+        } else if (!StringUtils.hasText(attrName)) {
+            throw new IllegalArgumentException("Empty attribute name passed");
+        }
+        if (!connectorRpc.isEnabled()) {
+            return null;
+        }
+
+        AttributeMapping mapping = getAttributeMapping(attrName);
+        if (mapping == null) {
+            return null;
+        }
+
+        Map<String, Object> map = new LinkedHashMap<>();
+        map.put(USER, userId);
+        map.put(ATTRIBUTE_NAME, mapping.getRpcName());
+
+        JsonNode res = connectorRpc.post(ATTRIBUTES_MANAGER, GET_ATTRIBUTE, map);
+        if (res == null || !res.hasNonNull(VALUE_CREATED_AT)) {
+            return null;
+        }
+        return res.get(VALUE_CREATED_AT).asText();
+    }
+
+    @Override
+    public List<Affiliation> getUserExtSourcesAffiliations(Long userId,
+                                                           String affiliationsAttr,
+                                                           String orgUrlAttr)
+    {
+        if (userId == null) {
+            throw new IllegalArgumentException("Null user ID passed");
+        } else if (!StringUtils.hasText(affiliationsAttr)) {
+            throw new IllegalArgumentException("Empty affiliations attr name passed");
+        } else if (!StringUtils.hasText(orgUrlAttr)) {
+            throw new IllegalArgumentException("Empty organizationURL attr name passed");
+        }
+
+        if (!connectorRpc.isEnabled()) {
+            return new ArrayList<>();
+        }
+
+        List<UserExtSource> userExtSources = getUserExtSources(userId);
+        if (userExtSources.isEmpty()) {
+            return new ArrayList<>();
+        }
+
+        List<Affiliation> affiliations = new ArrayList<>();
+        for (UserExtSource ues : userExtSources) {
+            if (!EXT_SOURCE_IDP.equals(ues.getExtSource().getType())) {
+                continue;
+            }
+            Map<String, PerunAttributeValue> uesAttrValues = getUserExtSourceAttributeValues(
+                ues.getId(), Arrays.asList(affiliationsAttr, orgUrlAttr));
+
+            long asserted = ues.getLastAccess().getTime() / 1000L;
+
+            String affs = uesAttrValues.get(affiliationsAttr).valueAsString();
+            String orgUrl = uesAttrValues.get(orgUrlAttr).valueAsString();
+
+            if (affs != null) {
+                for (String aff : affs.split(";")) {
+                    String source = ( (orgUrl != null) ? orgUrl : ues.getExtSource().getName() );
+                    Affiliation affiliation = new Affiliation(source, aff, asserted);
+                    log.debug("found {} from IdP {} with orgURL {} asserted at {}", aff, ues.getExtSource().getName(),
+                            orgUrl, asserted);
+                    affiliations.add(affiliation);
+                }
+            }
+        }
+
+        return affiliations;
+    }
+
+    @Override
+    public Set<Long> getUserIdsByAttributeValue(String attrName, String attrValue) {
+        if (!StringUtils.hasText(attrName)) {
+            throw new IllegalArgumentException("Empty attribute name passed");
+        } else if (!StringUtils.hasText(attrValue)) {
+            throw new IllegalArgumentException("Empty attribute value passed");
+        }
+
+        if (!connectorRpc.isEnabled()) {
+            return new HashSet<>();
+        }
+
+        AttributeMapping mapping = getAttributeMapping(attrName);
+        if (mapping == null) {
+            return new HashSet<>();
+        }
+
+        Set<Long> result = new HashSet<>();
+        Map<String, Object> map = new LinkedHashMap<>();
+        map.put(ATTRIBUTE_NAME, mapping.getRpcName());
+        map.put(ATTRIBUTE_VALUE, attrValue);
+
+        JsonNode res = connectorRpc.post(USERS_MANAGER, GET_USERS_BY_ATTRIBUTE_VALUE, map);
+        if (res != null) {
+            for (int i = 0; i < res.size(); i++) {
+                result.add(res.get(i).get(ID).asLong());
+            }
+        }
+        return result;
+    }
+
+    @Override
+    public String getUserSub(Long userId, String subAttribute) {
+        if (userId == null) {
+            throw new IllegalArgumentException("Null user ID passed");
+        } else if (!StringUtils.hasText(subAttribute)) {
+            throw new IllegalArgumentException("Empty sub attribute name passed");
+        }
+        if (!connectorRpc.isEnabled()) {
+            return null;
+        }
+
+        AttributeMapping mapping = getAttributeMapping(subAttribute);
+        if (mapping == null) {
+            return null;
+        }
+
+        Map<String, Object> map = new LinkedHashMap<>();
+        map.put(USER, userId);
+        map.put(ATTRIBUTE_NAME, mapping.getRpcName());
+        JsonNode res = connectorRpc.post(ATTRIBUTES_MANAGER, GET_ATTRIBUTE, map);
+
+        PerunAttributeValue value = PerunRpcAdapterMapper.mapAttributeValue(res);
+        if (value != null) {
+            return value.getValue().textValue();
+        } else {
+            return null;
+        }
+    }
+    @Override
+    public boolean isUserInVo(Long userId, Long voId) {
+        if (userId == null) {
+            throw new IllegalArgumentException("Null user ID passed");
+        } else if (voId == null) {
+            throw new IllegalArgumentException("Null vo ID passed");
+        }
+
+        if (!connectorRpc.isEnabled()) {
+            return false;
+        }
+
+        Map<String, Object> memberParams = new LinkedHashMap<>();
+        memberParams.put(VO, voId);
+        memberParams.put(USER, userId);
+        JsonNode memberResponse = connectorRpc.post(MEMBERS_MANAGER, GET_MEMBER_BY_USER, memberParams);
+        Member member = PerunRpcAdapterMapper.mapMember(memberResponse);
+
+        return member != null && member.getStatus() == VALID;
+    }
+
+    @Override
+    public List<UserExtSource> getIdpUserExtSources(Long userId) {
+        if (userId == null) {
+            throw new IllegalArgumentException("Null user ID passed");
+        }
+
+        List<UserExtSource> userExtSources = getUserExtSources(userId);
+        if (userExtSources.isEmpty()) {
+            return new ArrayList<>();
+        }
+        userExtSources = userExtSources.stream()
+            .filter(ues -> EXT_SOURCE_IDP.equals(ues.getExtSource().getType()))
+            .collect(Collectors.toList());
+        return userExtSources;
+    }
+
+    private List<Member> getMembersByUser(Long userId) {
+        if (!this.connectorRpc.isEnabled()) {
+            return new ArrayList<>();
+        }
+
+        Map<String, Object> params = new LinkedHashMap<>();
+        params.put(USER, userId);
+        JsonNode jsonNode = connectorRpc.post(MEMBERS_MANAGER, GET_MEMBERS_BY_USER, params);
+
+        return PerunRpcAdapterMapper.mapMembers(jsonNode);
+    }
+
+    private List<Group> getMemberGroups(Long memberId) {
+        if (memberId == null) {
+            throw new IllegalArgumentException("Null member ID passed");
+        }
+        if (!this.connectorRpc.isEnabled()) {
+            return new ArrayList<>();
+        }
+
+        Map<String, Object> map = new LinkedHashMap<>();
+        map.put(MEMBER, memberId);
+
+        JsonNode response = connectorRpc.post(GROUPS_MANAGER, GET_MEMBER_GROUPS, map);
+        return PerunRpcAdapterMapper.mapGroups(response);
+    }
+
+    private PerunAttributeValue getGroupAttributeValue(Group group, String attrToFetch) {
+        if (group == null || group.getId() == null) {
+            throw new IllegalArgumentException("Null group or group with null ID passed");
+        } else if (!StringUtils.hasText(attrToFetch)) {
+            throw new IllegalArgumentException("Empty attribute name passed");
+        }
+        AttributeMapping attributeMapping = getAttributeMapping(attrToFetch);
+        if (attributeMapping == null) {
+            return null;
+        }
+
+        Map<String, Object> map = new LinkedHashMap<>();
+        map.put(GROUP, group.getId());
+        map.put(ATTRIBUTE_NAME, attributeMapping.getRpcName());
+        JsonNode res = connectorRpc.post(ATTRIBUTES_MANAGER, GET_ATTRIBUTE, map);
+
+        return PerunRpcAdapterMapper.mapAttributeValue(res);
+    }
+
+    private List<UserExtSource> getUserExtSources(Long userId) {
+        if (userId == null) {
+            throw new IllegalArgumentException("Null user ID passed");
+        }
+        Map<String, Object> map = new LinkedHashMap<>();
+        map.put(USER, userId);
+
+        JsonNode response = connectorRpc.post(USERS_MANAGER, GET_USER_EXT_SOURCES, map);
+        return PerunRpcAdapterMapper.mapUserExtSources(response);
+    }
+
+    private Map<String, PerunAttributeValue> getUserExtSourceAttributeValues(Long uesId,
+                                                                             List<String> attributeNames)
+    {
+        if (uesId == null) {
+            throw new IllegalArgumentException("Null user ext source ID passed");
+        } else if (attributeNames == null || attributeNames.isEmpty()) {
+            throw new IllegalArgumentException("Null or empty list of attribute names passed");
+        }
+
+        Map<String, Object> map = new LinkedHashMap<>();
+
+        Map<String, AttributeMapping> mappings = new HashMap<>();
+        for (String attrName: attributeNames) {
+            AttributeMapping mapping = getAttributeMapping(attrName);
+            if (mapping != null) {
+                mappings.put(attrName, mapping);
+            }
+        }
+
+        map.put(USER_EXT_SOURCE, uesId);
+        map.put(ATTR_NAMES, mappings.values().stream()
+            .map(AttributeMapping::getRpcName)
+            .collect(Collectors.toList()));
+
+        JsonNode response = connectorRpc.post(ATTRIBUTES_MANAGER, GET_ATTRIBUTES, map);
+
+        return PerunRpcAdapterMapper.mapAttributes(response, mappings);
+    }
+
+    private AttributeMapping getAttributeMapping(String attribute) {
+        if (!StringUtils.hasText(attribute)) {
+            throw new IllegalArgumentException("Empty attribute name passed");
+        }
+
+        AttributeMapping mapping = attributeMappings.getOrDefault(attribute, null);
+        if (mapping == null) {
+            log.warn("No attribute mapping found for attribute '{}'", attribute);
+            return null;
+        } else if (!StringUtils.hasText(mapping.getRpcName())) {
+            log.warn("No RPC name found in mapping for attribute '{}'", attribute);
+            return null;
+        } else {
+            return mapping;
+        }
+    }
+
+}
diff --git a/src/main/java/cz/muni/ics/ga4gh/connectors/PerunConnectorLdap.java b/src/main/java/cz/muni/ics/ga4gh/base/connectors/PerunConnectorLdap.java
similarity index 66%
rename from src/main/java/cz/muni/ics/ga4gh/connectors/PerunConnectorLdap.java
rename to src/main/java/cz/muni/ics/ga4gh/base/connectors/PerunConnectorLdap.java
index 5f4c66f..fbebb58 100644
--- a/src/main/java/cz/muni/ics/ga4gh/connectors/PerunConnectorLdap.java
+++ b/src/main/java/cz/muni/ics/ga4gh/base/connectors/PerunConnectorLdap.java
@@ -1,7 +1,7 @@
-package cz.muni.ics.ga4gh.connectors;
+package cz.muni.ics.ga4gh.base.connectors;
 
-import cz.muni.ics.ga4gh.aop.LogTimes;
-import cz.muni.ics.ga4gh.config.LdapConfig;
+import cz.muni.ics.ga4gh.base.properties.PerunLdapConnectorProperties;
+import java.util.List;
 import lombok.Getter;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
@@ -17,69 +17,58 @@ import org.apache.directory.ldap.client.template.EntryMapper;
 import org.apache.directory.ldap.client.template.LdapConnectionTemplate;
 import org.springframework.beans.factory.DisposableBean;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Repository;
 import org.springframework.util.StringUtils;
 
-import java.util.List;
-import java.util.Objects;
-
-@Repository
 @Slf4j
-@Getter
 public class PerunConnectorLdap implements DisposableBean {
 
+    @Getter
     private final String baseDN;
     private final LdapConnectionPool pool;
     private final LdapConnectionTemplate ldap;
 
     @Autowired
-    public PerunConnectorLdap(LdapConfig config) {
-        if (config.getHost() == null || config.getHost().trim().isEmpty()) {
+    public PerunConnectorLdap(PerunLdapConnectorProperties perunLdapConnectorProperties) {
+        if (!StringUtils.hasText(perunLdapConnectorProperties.getHost())) {
             throw new IllegalArgumentException("Host cannot be null or empty");
-        } else if (config.getBaseDn() == null || config.getBaseDn().trim().isEmpty()) {
+        } else if (!StringUtils.hasText(perunLdapConnectorProperties.getBaseDn())) {
             throw new IllegalArgumentException("baseDN cannot be null or empty");
         }
 
-        boolean useTLS = Objects.requireNonNullElse(config.getUseTls(), false);
-        boolean useSSL = Objects.requireNonNullElse(config.getUseSsl(), false);
-        boolean allowUntrustedSsl = Objects.requireNonNullElse(config.getAllowUntrustedSsl(), false);
-        long timeoutSecs = Objects.requireNonNullElse(config.getTimeoutSecs(), 5L);
+        this.baseDN = perunLdapConnectorProperties.getBaseDn();
 
-
-        this.baseDN = config.getBaseDn();
-
-        LdapConnectionConfig ldapConnectionConfig = getLdapConnectionConfig(config.getHost(), config.getPort(), useTLS, useSSL, allowUntrustedSsl);
-        if (config.getUser() != null && !config.getUser().isEmpty()) {
-            log.debug("setting ldap user to {}", config.getUser());
-            ldapConnectionConfig.setName(config.getUser());
+        LdapConnectionConfig ldapConnectionConfig = getLdapConnectionConfig(
+            perunLdapConnectorProperties);
+        if (StringUtils.hasText(perunLdapConnectorProperties.getUser())) {
+            log.debug("Setting ldap user to '{}'", perunLdapConnectorProperties.getUser());
+            ldapConnectionConfig.setName(perunLdapConnectorProperties.getUser());
         }
-        if (config.getPassword() != null && !config.getPassword().isEmpty()) {
-            log.debug("setting ldap password");
-            ldapConnectionConfig.setCredentials(config.getPassword());
+
+        if (StringUtils.hasText(perunLdapConnectorProperties.getPassword())) {
+            log.debug("Setting ldap password");
+            ldapConnectionConfig.setCredentials(perunLdapConnectorProperties.getPassword());
         }
+
         DefaultLdapConnectionFactory factory = new DefaultLdapConnectionFactory(ldapConnectionConfig);
-        factory.setTimeOut(timeoutSecs * 1000L);
+        factory.setTimeOut(perunLdapConnectorProperties.getTimeoutSecs() * 1000L);
 
         GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
         poolConfig.setTestOnBorrow(true);
 
-        pool = new LdapConnectionPool(new DefaultPoolableLdapConnectionFactory(factory), poolConfig);
-        ldap = new LdapConnectionTemplate(pool);
+        this.pool = new LdapConnectionPool(new DefaultPoolableLdapConnectionFactory(factory), poolConfig);
+        this.ldap = new LdapConnectionTemplate(pool);
         log.debug("initialized LDAP connector");
     }
 
-    public String getBaseDN() {
-        return baseDN;
-    }
-
-    private LdapConnectionConfig getLdapConnectionConfig(String host, int port, boolean useTLS, boolean useSSL,
-                                           boolean allowUntrustedSsl) {
+    private LdapConnectionConfig getLdapConnectionConfig(
+        PerunLdapConnectorProperties perunLdapConnectorProperties)
+    {
         LdapConnectionConfig config = new LdapConnectionConfig();
-        config.setLdapHost(host);
-        config.setLdapPort(port);
-        config.setUseSsl(useSSL);
-        config.setUseTls(useTLS);
-        if (allowUntrustedSsl) {
+        config.setLdapHost(perunLdapConnectorProperties.getHost());
+        config.setLdapPort(perunLdapConnectorProperties.getPort());
+        config.setUseSsl(perunLdapConnectorProperties.isUseSsl());
+        config.setUseTls(perunLdapConnectorProperties.isUseTls());
+        if (perunLdapConnectorProperties.isAllowUntrustedSsl()) {
             config.setTrustManagers(new NoVerificationTrustManager());
         }
 
@@ -98,12 +87,11 @@ public class PerunConnectorLdap implements DisposableBean {
      * @param dnPrefix Prefix to be added to the base DN. (i.e. ou=People) !DO NOT END WITH A COMMA!
      * @param filter Filter for entries
      * @param scope Search scope
-     * @param attributes Attributes to be fetch for entry
+     * @param attributes Attributes to be fetched for entry
      * @param entryMapper Mapper of entries to the target class T
      * @param <T> Class that the result should be mapped to.
      * @return Found entry mapped to target class
      */
-    @LogTimes
     public <T> T searchFirst(String dnPrefix, FilterBuilder filter, SearchScope scope, String[] attributes,
                              EntryMapper<T> entryMapper)
     {
@@ -114,12 +102,11 @@ public class PerunConnectorLdap implements DisposableBean {
     /**
      * Perform lookup for the entry that satisfies criteria.
      * @param dnPrefix Prefix to be added to the base DN. (i.e. ou=People) !DO NOT END WITH A COMMA!
-     * @param attributes Attributes to be fetch for entry
+     * @param attributes Attributes to be fetched for entry
      * @param entryMapper Mapper of entries to the target class T
      * @param <T> Class that the result should be mapped to.
      * @return Found entry mapped to target class
      */
-    @LogTimes
     public <T> T lookup(String dnPrefix, String[] attributes, EntryMapper<T> entryMapper) {
         Dn fullDn = getFullDn(dnPrefix);
         return ldap.lookup(fullDn, attributes, entryMapper);
@@ -130,12 +117,11 @@ public class PerunConnectorLdap implements DisposableBean {
      * @param dnPrefix Prefix to be added to the base DN. (i.e. ou=People) !DO NOT END WITH A COMMA!
      * @param filter Filter for entries
      * @param scope Search scope
-     * @param attributes Attributes to be fetch for entry
+     * @param attributes Attributes to be fetched for entry
      * @param entryMapper Mapper of entries to the target class T
      * @param <T> Class that the result should be mapped to.
      * @return List of found entries mapped to target class
      */
-    @LogTimes
     public <T> List<T> search(String dnPrefix, FilterBuilder filter, SearchScope scope, String[] attributes,
                               EntryMapper<T> entryMapper)
     {
@@ -151,4 +137,5 @@ public class PerunConnectorLdap implements DisposableBean {
 
         return ldap.newDn(dn);
     }
+
 }
diff --git a/src/main/java/cz/muni/ics/ga4gh/connectors/PerunConnectorRpc.java b/src/main/java/cz/muni/ics/ga4gh/base/connectors/PerunConnectorRpc.java
similarity index 56%
rename from src/main/java/cz/muni/ics/ga4gh/connectors/PerunConnectorRpc.java
rename to src/main/java/cz/muni/ics/ga4gh/base/connectors/PerunConnectorRpc.java
index ecacbf7..f5d9772 100644
--- a/src/main/java/cz/muni/ics/ga4gh/connectors/PerunConnectorRpc.java
+++ b/src/main/java/cz/muni/ics/ga4gh/base/connectors/PerunConnectorRpc.java
@@ -1,10 +1,15 @@
-package cz.muni.ics.ga4gh.connectors;
+package cz.muni.ics.ga4gh.base.connectors;
 
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.node.JsonNodeFactory;
-import cz.muni.ics.ga4gh.aop.LogTimes;
-import cz.muni.ics.ga4gh.config.RpcConfig;
+import cz.muni.ics.ga4gh.base.properties.PerunRpcConnectorProperties;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
 import lombok.Getter;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.http.HeaderElement;
@@ -21,96 +26,67 @@ import org.springframework.http.MediaType;
 import org.springframework.http.client.ClientHttpRequestInterceptor;
 import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
 import org.springframework.http.client.InterceptingClientHttpRequestFactory;
-import org.springframework.http.client.support.BasicAuthorizationInterceptor;
-import org.springframework.stereotype.Repository;
+import org.springframework.http.client.support.BasicAuthenticationInterceptor;
 import org.springframework.util.StringUtils;
+import org.springframework.validation.annotation.Validated;
 import org.springframework.web.client.HttpClientErrorException;
 import org.springframework.web.client.RestTemplate;
 
-import javax.annotation.PostConstruct;
-import java.io.IOException;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-
-@Repository
-@Slf4j
 @Getter
+@Slf4j
+@Validated
 public class PerunConnectorRpc {
 
-    public static final String ATTRIBUTES_MANAGER = "attributesManager";
-    public static final String FACILITIES_MANAGER = "facilitiesManager";
-    public static final String GROUPS_MANAGER = "groupsManager";
-    public static final String MEMBERS_MANAGER = "membersManager";
-    public static final String REGISTRAR_MANAGER = "registrarManager";
-    public static final String SEARCHER = "searcher";
-    public static final String USERS_MANAGER = "usersManager";
-    public static final String VOS_MANAGER = "vosManager";
-    public static final String RESOURCES_MANAGER = "resourcesManager";
-
-    private String perunUrl;
-    private String perunUser;
-    private String perunPassword;
-    private boolean isEnabled;
-    private String serializer;
-    private RestTemplate restTemplate;
+    @NotBlank
+    private final String url;
 
-    @Autowired
-    public PerunConnectorRpc(RpcConfig config) {
-        this.setPerunUrl(config.getUrl());
-        this.setPerunUser(config.getUsername());
-        this.setPerunPassword(config.getPassword());
-        this.setEnabled(config.getEnabled());
-        this.setSerializer(config.getSerializer());
-    }
+    private final boolean enabled;
 
-    public void setPerunUrl(String perunUrl) {
-        if (!StringUtils.hasText(perunUrl)) {
-            throw new IllegalArgumentException("Perun URL cannot be null or empty");
-        } else if (perunUrl.endsWith("/")) {
-            perunUrl = perunUrl.substring(0, perunUrl.length() - 1);
-        }
+    @NotBlank
+    private final String serializer;
 
-        this.perunUrl = perunUrl;
-    }
+    @NotNull
+    private RestTemplate restTemplate;
 
-    public void setPerunUser(String perunUser) {
-        if (!StringUtils.hasText(perunUser)) {
-            throw new IllegalArgumentException("Perun USER cannot be null or empty");
-        }
+    private final long connectionTimeout;
+    private final long connectionRequestTimeout;
+    private final long requestTimeout;
 
-        this.perunUser = perunUser;
+    @Autowired
+    public PerunConnectorRpc(PerunRpcConnectorProperties rpcProperties) {
+        this.url = setUrl(rpcProperties.getUrl());
+        this.enabled = rpcProperties.isEnabled();
+        this.serializer = setSerializer(rpcProperties.getSerializer());
+        this.connectionTimeout = rpcProperties.getConnectionTimeout();
+        this.connectionRequestTimeout = rpcProperties.getConnectionRequestTimeout();
+        this.requestTimeout = rpcProperties.getRequestTimeout();
+        initRestTemplate(rpcProperties);
     }
 
-    public void setPerunPassword(String perunPassword) {
-        if (!StringUtils.hasText(perunPassword)) {
-            throw new IllegalArgumentException("Perun PASSWORD cannot be null or empty");
+    private String setUrl(String url) {
+        if (!StringUtils.hasText(url)) {
+            throw new IllegalArgumentException("Perun URL cannot be null or empty");
+        } else if (url.endsWith("/")) {
+            url = url.substring(0, url.length() - 1);
         }
 
-        this.perunPassword = perunPassword;
+        return url;
     }
 
-    public void setEnabled(Boolean enabled) {
-        this.isEnabled = Objects.requireNonNullElse(enabled, false);
-    }
-
-    public void setSerializer(String serializer) {
+    private String setSerializer(String serializer) {
         if (!StringUtils.hasText(serializer)) {
-            this.serializer = "json";
+            serializer = "json";
         }
-
-        this.serializer = serializer;
+        return serializer;
     }
 
-    @PostConstruct
-    public void postInit() {
+    public void initRestTemplate(PerunRpcConnectorProperties rpcProperties) {
         restTemplate = new RestTemplate();
         //HTTP connection pooling, see https://howtodoinjava.com/spring-restful/resttemplate-httpclient-java-config/
         RequestConfig requestConfig = RequestConfig.custom()
-                .setConnectionRequestTimeout(30000) // The timeout when requesting a connection from the connection manager
-                .setConnectTimeout(30000) // Determines the timeout in milliseconds until a connection is established
-                .setSocketTimeout(60000) // The timeout for waiting for data
+                .setConnectionRequestTimeout(rpcProperties.getConnectionRequestTimeout()) // The timeout when requesting a connection from the connection manager
+                .setConnectTimeout(rpcProperties.getConnectionTimeout()) // Determines the timeout in milliseconds until a connection is established
+                .setSocketTimeout(rpcProperties.getRequestTimeout()) // The timeout for waiting for data
                 .build();
 
         PoolingHttpClientConnectionManager poolingConnectionManager = new PoolingHttpClientConnectionManager();
@@ -118,8 +94,8 @@ public class PerunConnectorRpc {
         poolingConnectionManager.setDefaultMaxPerRoute(18);
 
         ConnectionKeepAliveStrategy connectionKeepAliveStrategy = (response, context) -> {
-            HeaderElementIterator it = new BasicHeaderElementIterator(response.headerIterator(HTTP.CONN_KEEP_ALIVE));
-
+            HeaderElementIterator it = new BasicHeaderElementIterator(
+                response.headerIterator(HTTP.CONN_KEEP_ALIVE));
             while (it.hasNext()) {
                 HeaderElement he = it.nextElement();
                 String param = he.getName();
@@ -142,8 +118,11 @@ public class PerunConnectorRpc {
         HttpComponentsClientHttpRequestFactory poolingRequestFactory = new HttpComponentsClientHttpRequestFactory();
         poolingRequestFactory.setHttpClient(httpClient);
         //basic authentication
-        List<ClientHttpRequestInterceptor> interceptors =
-                Collections.singletonList(new BasicAuthorizationInterceptor(perunUser, perunPassword));
+        List<ClientHttpRequestInterceptor> interceptors = Collections.singletonList(
+            new BasicAuthenticationInterceptor(
+                rpcProperties.getUsername(),
+                rpcProperties.getPassword()
+            ));
         InterceptingClientHttpRequestFactory authenticatingRequestFactory = new InterceptingClientHttpRequestFactory(poolingRequestFactory, interceptors);
         restTemplate.setRequestFactory(authenticatingRequestFactory);
     }
@@ -155,26 +134,29 @@ public class PerunConnectorRpc {
      * @param map Map of parameters to be passed as request body
      * @return Response from Perun
      */
-    @LogTimes
     public JsonNode post(String manager, String method, Map<String, Object> map) {
-        if (!this.isEnabled) {
+        if (!this.enabled) {
             return JsonNodeFactory.instance.nullNode();
         }
 
-        String actionUrl = perunUrl + '/' + serializer + '/' + manager + '/' + method;
+        String actionUrl = url + '/' + serializer + '/' + manager + '/' + method;
         //make the call
         try {
-            log.debug("calling {} with {}", actionUrl, map);
-
+            log.debug("Calling Perun - URL '{}' with parameters '{}'", actionUrl, map);
             return restTemplate.postForObject(actionUrl, map, JsonNode.class);
         } catch (HttpClientErrorException ex) {
-            MediaType contentType = ex.getResponseHeaders().getContentType();
+            MediaType contentType = null;
+            if (ex.getResponseHeaders() != null) {
+                contentType = ex.getResponseHeaders().getContentType();
+            }
             String body = ex.getResponseBodyAsString();
-            log.error("HTTP ERROR " + ex.getRawStatusCode() + " URL " + actionUrl + " Content-Type: " + contentType);
+            log.error("HTTP ERROR when calling Perun RPC - {}, {}, {}",
+                ex.getRawStatusCode(), contentType, actionUrl);
 
-            if ("json".equals(contentType.getSubtype())) {
+            if (contentType != null && "json".equals(contentType.getSubtype())) {
                 try {
-                    log.error(new ObjectMapper().readValue(body, JsonNode.class).path("message").asText());
+                    log.error(new ObjectMapper().readValue(body, JsonNode.class)
+                        .path("message").asText());
                 } catch (IOException e) {
                     log.error("cannot parse error message from JSON", e);
                 }
@@ -185,4 +167,5 @@ public class PerunConnectorRpc {
             throw new RuntimeException("cannot connect to Perun RPC", ex);
         }
     }
+
 }
diff --git a/src/main/java/cz/muni/ics/ga4gh/enums/MemberStatus.java b/src/main/java/cz/muni/ics/ga4gh/base/enums/MemberStatus.java
similarity index 94%
rename from src/main/java/cz/muni/ics/ga4gh/enums/MemberStatus.java
rename to src/main/java/cz/muni/ics/ga4gh/base/enums/MemberStatus.java
index 9794780..738bb8f 100644
--- a/src/main/java/cz/muni/ics/ga4gh/enums/MemberStatus.java
+++ b/src/main/java/cz/muni/ics/ga4gh/base/enums/MemberStatus.java
@@ -1,4 +1,4 @@
-package cz.muni.ics.ga4gh.enums;
+package cz.muni.ics.ga4gh.base.enums;
 
 public enum MemberStatus {
 
diff --git a/src/main/java/cz/muni/ics/ga4gh/base/exceptions/ConfigurationException.java b/src/main/java/cz/muni/ics/ga4gh/base/exceptions/ConfigurationException.java
new file mode 100644
index 0000000..dee3be1
--- /dev/null
+++ b/src/main/java/cz/muni/ics/ga4gh/base/exceptions/ConfigurationException.java
@@ -0,0 +1,26 @@
+package cz.muni.ics.ga4gh.base.exceptions;
+
+public class ConfigurationException extends Exception {
+
+    public ConfigurationException() {
+        super();
+    }
+
+    public ConfigurationException(String message) {
+        super(message);
+    }
+
+    public ConfigurationException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public ConfigurationException(Throwable cause) {
+        super(cause);
+    }
+
+    protected ConfigurationException(String message, Throwable cause, boolean enableSuppression,
+                                     boolean writableStackTrace) {
+        super(message, cause, enableSuppression, writableStackTrace);
+    }
+
+}
diff --git a/src/main/java/cz/muni/ics/ga4gh/exceptions/InconvertibleValueException.java b/src/main/java/cz/muni/ics/ga4gh/base/exceptions/InconvertibleValueException.java
similarity index 92%
rename from src/main/java/cz/muni/ics/ga4gh/exceptions/InconvertibleValueException.java
rename to src/main/java/cz/muni/ics/ga4gh/base/exceptions/InconvertibleValueException.java
index 5816702..33b6c76 100644
--- a/src/main/java/cz/muni/ics/ga4gh/exceptions/InconvertibleValueException.java
+++ b/src/main/java/cz/muni/ics/ga4gh/base/exceptions/InconvertibleValueException.java
@@ -1,4 +1,4 @@
-package cz.muni.ics.ga4gh.exceptions;
+package cz.muni.ics.ga4gh.base.exceptions;
 
 public class InconvertibleValueException extends RuntimeException {
 
diff --git a/src/main/java/cz/muni/ics/ga4gh/base/exceptions/InvalidRequestParametersException.java b/src/main/java/cz/muni/ics/ga4gh/base/exceptions/InvalidRequestParametersException.java
new file mode 100644
index 0000000..fa562e6
--- /dev/null
+++ b/src/main/java/cz/muni/ics/ga4gh/base/exceptions/InvalidRequestParametersException.java
@@ -0,0 +1,26 @@
+package cz.muni.ics.ga4gh.base.exceptions;
+
+public class InvalidRequestParametersException extends Exception {
+
+    public InvalidRequestParametersException() {
+        super();
+    }
+
+    public InvalidRequestParametersException(String message) {
+        super(message);
+    }
+
+    public InvalidRequestParametersException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public InvalidRequestParametersException(Throwable cause) {
+        super(cause);
+    }
+
+    protected InvalidRequestParametersException(String message, Throwable cause, boolean enableSuppression,
+                                                boolean writableStackTrace) {
+        super(message, cause, enableSuppression, writableStackTrace);
+    }
+
+}
diff --git a/src/main/java/cz/muni/ics/ga4gh/exceptions/MissingFieldException.java b/src/main/java/cz/muni/ics/ga4gh/base/exceptions/MissingFieldException.java
similarity index 92%
rename from src/main/java/cz/muni/ics/ga4gh/exceptions/MissingFieldException.java
rename to src/main/java/cz/muni/ics/ga4gh/base/exceptions/MissingFieldException.java
index a889a9b..d2e07ea 100644
--- a/src/main/java/cz/muni/ics/ga4gh/exceptions/MissingFieldException.java
+++ b/src/main/java/cz/muni/ics/ga4gh/base/exceptions/MissingFieldException.java
@@ -1,4 +1,4 @@
-package cz.muni.ics.ga4gh.exceptions;
+package cz.muni.ics.ga4gh.base.exceptions;
 
 public class MissingFieldException extends RuntimeException {
 
diff --git a/src/main/java/cz/muni/ics/ga4gh/base/exceptions/PerunAdapterOperationException.java b/src/main/java/cz/muni/ics/ga4gh/base/exceptions/PerunAdapterOperationException.java
new file mode 100644
index 0000000..ea469d2
--- /dev/null
+++ b/src/main/java/cz/muni/ics/ga4gh/base/exceptions/PerunAdapterOperationException.java
@@ -0,0 +1,26 @@
+package cz.muni.ics.ga4gh.base.exceptions;
+
+public class PerunAdapterOperationException extends RuntimeException {
+
+    public PerunAdapterOperationException() {
+        super();
+    }
+
+    public PerunAdapterOperationException(String message) {
+        super(message);
+    }
+
+    public PerunAdapterOperationException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public PerunAdapterOperationException(Throwable cause) {
+        super(cause);
+    }
+
+    protected PerunAdapterOperationException(String message, Throwable cause,
+                                             boolean enableSuppression,
+                                             boolean writableStackTrace) {
+        super(message, cause, enableSuppression, writableStackTrace);
+    }
+}
diff --git a/src/main/java/cz/muni/ics/ga4gh/base/exceptions/UserNotFoundException.java b/src/main/java/cz/muni/ics/ga4gh/base/exceptions/UserNotFoundException.java
new file mode 100644
index 0000000..84df696
--- /dev/null
+++ b/src/main/java/cz/muni/ics/ga4gh/base/exceptions/UserNotFoundException.java
@@ -0,0 +1,26 @@
+package cz.muni.ics.ga4gh.base.exceptions;
+
+public class UserNotFoundException extends Exception {
+
+    public UserNotFoundException() {
+        super();
+    }
+
+    public UserNotFoundException(String message) {
+        super(message);
+    }
+
+    public UserNotFoundException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public UserNotFoundException(Throwable cause) {
+        super(cause);
+    }
+
+    protected UserNotFoundException(String message, Throwable cause, boolean enableSuppression,
+                                    boolean writableStackTrace) {
+        super(message, cause, enableSuppression, writableStackTrace);
+    }
+
+}
diff --git a/src/main/java/cz/muni/ics/ga4gh/exceptions/UserNotUniqueException.java b/src/main/java/cz/muni/ics/ga4gh/base/exceptions/UserNotUniqueException.java
similarity index 82%
rename from src/main/java/cz/muni/ics/ga4gh/exceptions/UserNotUniqueException.java
rename to src/main/java/cz/muni/ics/ga4gh/base/exceptions/UserNotUniqueException.java
index dc3eee0..5eb3222 100644
--- a/src/main/java/cz/muni/ics/ga4gh/exceptions/UserNotUniqueException.java
+++ b/src/main/java/cz/muni/ics/ga4gh/base/exceptions/UserNotUniqueException.java
@@ -1,6 +1,6 @@
-package cz.muni.ics.ga4gh.exceptions;
+package cz.muni.ics.ga4gh.base.exceptions;
 
-public class UserNotUniqueException extends RuntimeException {
+public class UserNotUniqueException extends Exception {
 
     public UserNotUniqueException() {
         super();
diff --git a/src/main/java/cz/muni/ics/ga4gh/model/Affiliation.java b/src/main/java/cz/muni/ics/ga4gh/base/model/Affiliation.java
similarity index 63%
rename from src/main/java/cz/muni/ics/ga4gh/model/Affiliation.java
rename to src/main/java/cz/muni/ics/ga4gh/base/model/Affiliation.java
index 767a472..ed21e07 100644
--- a/src/main/java/cz/muni/ics/ga4gh/model/Affiliation.java
+++ b/src/main/java/cz/muni/ics/ga4gh/base/model/Affiliation.java
@@ -1,19 +1,24 @@
-package cz.muni.ics.ga4gh.model;
+package cz.muni.ics.ga4gh.base.model;
 
+import javax.validation.constraints.NotBlank;
 import lombok.AllArgsConstructor;
 import lombok.EqualsAndHashCode;
 import lombok.Getter;
 import lombok.ToString;
+import org.springframework.validation.annotation.Validated;
 
 @Getter
 @ToString
 @EqualsAndHashCode
 @AllArgsConstructor
+@Validated
 public class Affiliation {
 
     private final String source;
 
+    @NotBlank
     private final String value;
 
     private final long asserted;
+
 }
diff --git a/src/main/java/cz/muni/ics/ga4gh/model/AttributeMapping.java b/src/main/java/cz/muni/ics/ga4gh/base/model/AttributeMapping.java
similarity index 66%
rename from src/main/java/cz/muni/ics/ga4gh/model/AttributeMapping.java
rename to src/main/java/cz/muni/ics/ga4gh/base/model/AttributeMapping.java
index c90f81f..1c190f7 100644
--- a/src/main/java/cz/muni/ics/ga4gh/model/AttributeMapping.java
+++ b/src/main/java/cz/muni/ics/ga4gh/base/model/AttributeMapping.java
@@ -1,20 +1,22 @@
-package cz.muni.ics.ga4gh.model;
+package cz.muni.ics.ga4gh.base.model;
 
+import javax.validation.constraints.NotBlank;
 import lombok.AllArgsConstructor;
 import lombok.EqualsAndHashCode;
 import lombok.Getter;
-import lombok.NoArgsConstructor;
 import lombok.Setter;
 import lombok.ToString;
+import org.springframework.validation.annotation.Validated;
 
 @Getter
 @Setter
-@NoArgsConstructor
-@AllArgsConstructor
 @ToString
 @EqualsAndHashCode
+@AllArgsConstructor
+@Validated
 public class AttributeMapping {
 
+    @NotBlank
     private String internalName;
 
     private String rpcName;
diff --git a/src/main/java/cz/muni/ics/ga4gh/base/model/BasicAuthCredentials.java b/src/main/java/cz/muni/ics/ga4gh/base/model/BasicAuthCredentials.java
new file mode 100644
index 0000000..8525212
--- /dev/null
+++ b/src/main/java/cz/muni/ics/ga4gh/base/model/BasicAuthCredentials.java
@@ -0,0 +1,28 @@
+package cz.muni.ics.ga4gh.base.model;
+
+import javax.validation.constraints.NotBlank;
+import lombok.AllArgsConstructor;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.Setter;
+import org.springframework.validation.annotation.Validated;
+
+@Getter
+@Setter
+@EqualsAndHashCode
+@AllArgsConstructor
+@Validated
+public class BasicAuthCredentials {
+    @NotBlank
+    private String username;
+    @NotBlank
+    private String password;
+
+    @Override
+    public String toString() {
+        return "BasicAuthCredentials{" +
+            "username='" + username + '\'' +
+            ", password='PROTECTED_STRING'" +
+            '}';
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/cz/muni/ics/ga4gh/model/RepoHeader.java b/src/main/java/cz/muni/ics/ga4gh/base/model/ClaimRepositoryHeader.java
similarity index 59%
rename from src/main/java/cz/muni/ics/ga4gh/model/RepoHeader.java
rename to src/main/java/cz/muni/ics/ga4gh/base/model/ClaimRepositoryHeader.java
index d8932c1..e36f878 100644
--- a/src/main/java/cz/muni/ics/ga4gh/model/RepoHeader.java
+++ b/src/main/java/cz/muni/ics/ga4gh/base/model/ClaimRepositoryHeader.java
@@ -1,5 +1,7 @@
-package cz.muni.ics.ga4gh.model;
+package cz.muni.ics.ga4gh.base.model;
 
+import java.io.IOException;
+import javax.validation.constraints.NotBlank;
 import lombok.AllArgsConstructor;
 import lombok.Getter;
 import lombok.NoArgsConstructor;
@@ -9,22 +11,26 @@ import org.springframework.http.client.ClientHttpRequestExecution;
 import org.springframework.http.client.ClientHttpRequestInterceptor;
 import org.springframework.http.client.ClientHttpResponse;
 
-import java.io.IOException;
-
 @Getter
 @Setter
 @AllArgsConstructor
 @NoArgsConstructor
-public class RepoHeader implements ClientHttpRequestInterceptor {
+public class ClaimRepositoryHeader implements ClientHttpRequestInterceptor {
 
+    @NotBlank
     private String header;
 
+    @NotBlank
     private String value;
 
     @Override
-    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
+    public ClientHttpResponse intercept(HttpRequest request,
+                                        byte[] body,
+                                        ClientHttpRequestExecution execution)
+        throws IOException
+    {
         request.getHeaders().add(header, value);
-
         return execution.execute(request, body);
     }
+
 }
diff --git a/src/main/java/cz/muni/ics/ga4gh/base/model/ExtSource.java b/src/main/java/cz/muni/ics/ga4gh/base/model/ExtSource.java
new file mode 100644
index 0000000..d37340f
--- /dev/null
+++ b/src/main/java/cz/muni/ics/ga4gh/base/model/ExtSource.java
@@ -0,0 +1,28 @@
+package cz.muni.ics.ga4gh.base.model;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import lombok.AllArgsConstructor;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.ToString;
+import org.springframework.validation.annotation.Validated;
+
+@Getter
+@Setter
+@ToString
+@EqualsAndHashCode
+@AllArgsConstructor
+@Validated
+public class ExtSource {
+
+    @NotNull
+    private Long id;
+
+    @NotBlank
+    private String name;
+
+    @NotBlank
+    private String type;
+}
diff --git a/src/main/java/cz/muni/ics/ga4gh/model/Ga4ghClaimRepository.java b/src/main/java/cz/muni/ics/ga4gh/base/model/Ga4ghClaimRepository.java
similarity index 60%
rename from src/main/java/cz/muni/ics/ga4gh/model/Ga4ghClaimRepository.java
rename to src/main/java/cz/muni/ics/ga4gh/base/model/Ga4ghClaimRepository.java
index f9c7bd9..2e22dfb 100644
--- a/src/main/java/cz/muni/ics/ga4gh/model/Ga4ghClaimRepository.java
+++ b/src/main/java/cz/muni/ics/ga4gh/base/model/Ga4ghClaimRepository.java
@@ -1,20 +1,28 @@
-package cz.muni.ics.ga4gh.model;
+package cz.muni.ics.ga4gh.base.model;
 
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
 import lombok.AllArgsConstructor;
 import lombok.EqualsAndHashCode;
 import lombok.Getter;
 import lombok.ToString;
+import org.springframework.validation.annotation.Validated;
 import org.springframework.web.client.RestTemplate;
 
 @Getter
 @ToString
 @EqualsAndHashCode
 @AllArgsConstructor
+@Validated
 public class Ga4ghClaimRepository {
 
+    @NotBlank
     private final String name;
 
+    @NotBlank
     private final String actionURL;
 
+    @NotNull
     private final RestTemplate restTemplate;
+
 }
diff --git a/src/main/java/cz/muni/ics/ga4gh/base/model/Ga4ghPassport.java b/src/main/java/cz/muni/ics/ga4gh/base/model/Ga4ghPassport.java
new file mode 100644
index 0000000..5e7a5d9
--- /dev/null
+++ b/src/main/java/cz/muni/ics/ga4gh/base/model/Ga4ghPassport.java
@@ -0,0 +1,34 @@
+package cz.muni.ics.ga4gh.base.model;
+
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.JsonNodeFactory;
+import java.util.ArrayList;
+import java.util.List;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.ToString;
+
+@Getter
+@ToString
+@EqualsAndHashCode
+public class Ga4ghPassport {
+
+    private final List<Ga4ghPassportVisa> visas = new ArrayList<>();
+
+    public void addVisas(List<Ga4ghPassportVisa> visas) {
+        if (visas == null || visas.isEmpty()) {
+            return;
+        }
+        this.visas.addAll(visas);
+    }
+
+    public ArrayNode toJsonObject() {
+        ArrayNode passport = JsonNodeFactory.instance.arrayNode();
+        if (!visas.isEmpty()) {
+            for (Ga4ghPassportVisa visa: visas) {
+                passport.add(visa.serialize());
+            }
+        }
+        return passport;
+    }
+}
diff --git a/src/main/java/cz/muni/ics/ga4gh/base/model/Ga4ghPassportVisa.java b/src/main/java/cz/muni/ics/ga4gh/base/model/Ga4ghPassportVisa.java
new file mode 100644
index 0000000..85381ae
--- /dev/null
+++ b/src/main/java/cz/muni/ics/ga4gh/base/model/Ga4ghPassportVisa.java
@@ -0,0 +1,119 @@
+package cz.muni.ics.ga4gh.base.model;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.nimbusds.jose.JOSEObjectType;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.JWSHeader;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.SignedJWT;
+import cz.muni.ics.ga4gh.service.JWTSigningAndValidationService;
+import java.net.URI;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.ToString;
+
+@Getter
+@Setter
+@ToString
+@EqualsAndHashCode
+public class Ga4ghPassportVisa {
+
+    public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+
+    public static final String TYPE_AFFILIATION_AND_ROLE = "AffiliationAndRole";
+    public static final String TYPE_ACCEPTED_TERMS_AND_POLICIES = "AcceptedTermsAndPolicies";
+    public static final String TYPE_RESEARCHER_STATUS = "ResearcherStatus";
+    public static final String TYPE_LINKED_IDENTITIES = "LinkedIdentities";
+    public static final String TYPE_CONTROLLED_ACCESS_GRANTS = "ControlledAccessGrants";
+
+    public static final String BY_SYSTEM = "system";
+    public static final String BY_SO = "so";
+    public static final String BY_PEER = "peer";
+    public static final String BY_SELF = "self";
+
+    public static final String SUB = "sub";
+    public static final String ISS = "iss";
+    public static final String IAT = "iat";
+    public static final String EXP = "exp";
+    public static final String JTI = "jti";
+    public static final String TYPE = "type";
+    public static final String ASSERTED = "asserted";
+    public static final String VALUE = "value";
+    public static final String SOURCE = "source";
+    public static final String BY = "by";
+    public static final String CONDITIONS = "conditions";
+
+    // === VISA HEADER FIELDS ===
+    private String kid;
+
+    private JOSEObjectType typ;
+
+    private URI jku;
+
+    // === VISA PAYLOAD FIELDS ===
+
+    private String iss;
+
+    private String sub;
+
+    private Date iat;
+
+    private Date exp;
+
+    private String jti;
+
+    // value of the visa
+    private Ga4ghPassportVisaV1 ga4ghVisaV1;
+
+    // === CUSTOM FIELDS FOR WORKING WITH VISA ===
+
+    @ToString.Exclude
+    private String signer = null;
+
+    private boolean verified = false;
+
+    private String linkedIdentity;
+    private SignedJWT jwt;
+
+    public void generateSignedJwt(JWTSigningAndValidationService jwtService) {
+        Map<String, Object> passportVisaObject = new HashMap<>();
+        passportVisaObject.put(TYPE, ga4ghVisaV1.getType());
+        passportVisaObject.put(Ga4ghPassportVisa.ASSERTED, ga4ghVisaV1.getAsserted());
+        passportVisaObject.put(Ga4ghPassportVisa.VALUE, ga4ghVisaV1.getValue());
+        passportVisaObject.put(Ga4ghPassportVisa.SOURCE, ga4ghVisaV1.getSource());
+        passportVisaObject.put(Ga4ghPassportVisa.BY, ga4ghVisaV1.getBy());
+
+        if (ga4ghVisaV1.getConditions() != null) {
+            passportVisaObject.put(Ga4ghPassportVisa.CONDITIONS, ga4ghVisaV1.getConditions());
+        }
+
+        JWTClaimsSet jwtClaimsSet = new JWTClaimsSet.Builder()
+            .issuer(iss)
+            .issueTime(iat)
+            .expirationTime(exp)
+            .subject(sub)
+            .jwtID(jti)
+            .claim(Ga4ghPassportVisaV1.GA4GH_VISA_V1, passportVisaObject)
+            .build();
+
+        JWSHeader
+            jwsHeader = new JWSHeader.Builder(JWSAlgorithm.parse(jwtService.getSigningAlgorithm().getName()))
+            .keyID(jwtService.getSignerKeyId())
+            .type(JOSEObjectType.JWT)
+            .jwkURL(jku)
+            .build();
+
+        SignedJWT signedVisaJwt = new SignedJWT(jwsHeader, jwtClaimsSet);
+        jwtService.signJwt(signedVisaJwt);
+        this.jwt = signedVisaJwt;
+    }
+
+    public String serialize() {
+        return jwt.serialize();
+    }
+
+}
diff --git a/src/main/java/cz/muni/ics/ga4gh/base/model/Ga4ghPassportVisaV1.java b/src/main/java/cz/muni/ics/ga4gh/base/model/Ga4ghPassportVisaV1.java
new file mode 100644
index 0000000..082ec4f
--- /dev/null
+++ b/src/main/java/cz/muni/ics/ga4gh/base/model/Ga4ghPassportVisaV1.java
@@ -0,0 +1,31 @@
+package cz.muni.ics.ga4gh.base.model;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.ToString;
+
+@Getter
+@Setter
+@ToString
+@EqualsAndHashCode
+public class Ga4ghPassportVisaV1 {
+    public static final String GA4GH_VISA_V1 = "ga4gh_visa_v1";
+
+    // === mandatory ===
+    private long asserted;
+
+    private String source;
+
+    private String type;
+
+    private String value;
+
+
+    // === optional ===
+    private String by;
+
+    private JsonNode conditions;
+
+}
diff --git a/src/main/java/cz/muni/ics/ga4gh/model/Group.java b/src/main/java/cz/muni/ics/ga4gh/base/model/Group.java
similarity index 75%
rename from src/main/java/cz/muni/ics/ga4gh/model/Group.java
rename to src/main/java/cz/muni/ics/ga4gh/base/model/Group.java
index 49aea56..637fe1b 100644
--- a/src/main/java/cz/muni/ics/ga4gh/model/Group.java
+++ b/src/main/java/cz/muni/ics/ga4gh/base/model/Group.java
@@ -1,26 +1,31 @@
-package cz.muni.ics.ga4gh.model;
+package cz.muni.ics.ga4gh.base.model;
 
 import com.fasterxml.jackson.databind.JsonNode;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import lombok.AllArgsConstructor;
 import lombok.EqualsAndHashCode;
 import lombok.Getter;
-import lombok.NoArgsConstructor;
 import lombok.Setter;
 import lombok.ToString;
-
-import java.util.LinkedHashMap;
-import java.util.Map;
+import org.springframework.validation.annotation.Validated;
 
 @Getter
 @Setter
 @ToString
 @EqualsAndHashCode
-@NoArgsConstructor
+@AllArgsConstructor
+@Validated
 public class Group {
 
+    @NotNull
     private Long id;
 
     private Long parentGroupId;
 
+    @NotBlank
     private String name;
 
     private String description;
@@ -29,6 +34,7 @@ public class Group {
 
     private String uuid;
 
+    @NotNull
     private Long voId;
 
     private Map<String, JsonNode> attributes = new LinkedHashMap<>();
diff --git a/src/main/java/cz/muni/ics/ga4gh/model/Member.java b/src/main/java/cz/muni/ics/ga4gh/base/model/Member.java
similarity index 56%
rename from src/main/java/cz/muni/ics/ga4gh/model/Member.java
rename to src/main/java/cz/muni/ics/ga4gh/base/model/Member.java
index ca73483..5f0b1ce 100644
--- a/src/main/java/cz/muni/ics/ga4gh/model/Member.java
+++ b/src/main/java/cz/muni/ics/ga4gh/base/model/Member.java
@@ -1,26 +1,32 @@
-package cz.muni.ics.ga4gh.model;
+package cz.muni.ics.ga4gh.base.model;
 
-import cz.muni.ics.ga4gh.enums.MemberStatus;
+import cz.muni.ics.ga4gh.base.enums.MemberStatus;
+import javax.validation.constraints.NotNull;
 import lombok.AllArgsConstructor;
 import lombok.EqualsAndHashCode;
 import lombok.Getter;
-import lombok.NoArgsConstructor;
 import lombok.Setter;
 import lombok.ToString;
+import org.springframework.validation.annotation.Validated;
 
 @Getter
 @Setter
 @ToString
 @EqualsAndHashCode
-@NoArgsConstructor
 @AllArgsConstructor
+@Validated
 public class Member {
 
+    @NotNull
     private Long id;
 
+    @NotNull
     private Long userId;
 
+    @NotNull
     private Long voId;
 
+    @NotNull
     private MemberStatus status;
+
 }
diff --git a/src/main/java/cz/muni/ics/ga4gh/model/PerunAttributeValue.java b/src/main/java/cz/muni/ics/ga4gh/base/model/PerunAttributeValue.java
similarity index 98%
rename from src/main/java/cz/muni/ics/ga4gh/model/PerunAttributeValue.java
rename to src/main/java/cz/muni/ics/ga4gh/base/model/PerunAttributeValue.java
index 541b63a..1bb71ec 100644
--- a/src/main/java/cz/muni/ics/ga4gh/model/PerunAttributeValue.java
+++ b/src/main/java/cz/muni/ics/ga4gh/base/model/PerunAttributeValue.java
@@ -1,4 +1,4 @@
-package cz.muni.ics.ga4gh.model;
+package cz.muni.ics.ga4gh.base.model;
 
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.node.ArrayNode;
@@ -8,7 +8,12 @@ import com.fasterxml.jackson.databind.node.NullNode;
 import com.fasterxml.jackson.databind.node.NumericNode;
 import com.fasterxml.jackson.databind.node.ObjectNode;
 import com.fasterxml.jackson.databind.node.TextNode;
-import cz.muni.ics.ga4gh.exceptions.InconvertibleValueException;
+import cz.muni.ics.ga4gh.base.exceptions.InconvertibleValueException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
 import lombok.AllArgsConstructor;
 import lombok.EqualsAndHashCode;
 import lombok.Getter;
@@ -16,17 +21,11 @@ import lombok.NoArgsConstructor;
 import lombok.ToString;
 import org.springframework.util.StringUtils;
 
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-
 @Getter
+@ToString
+@EqualsAndHashCode
 @NoArgsConstructor
 @AllArgsConstructor
-@EqualsAndHashCode
-@ToString
 public class PerunAttributeValue {
 
     public final static String STRING_TYPE = "java.lang.String";
diff --git a/src/main/java/cz/muni/ics/ga4gh/model/UserExtSource.java b/src/main/java/cz/muni/ics/ga4gh/base/model/UserExtSource.java
similarity index 77%
rename from src/main/java/cz/muni/ics/ga4gh/model/UserExtSource.java
rename to src/main/java/cz/muni/ics/ga4gh/base/model/UserExtSource.java
index 19fcda5..282bd31 100644
--- a/src/main/java/cz/muni/ics/ga4gh/model/UserExtSource.java
+++ b/src/main/java/cz/muni/ics/ga4gh/base/model/UserExtSource.java
@@ -1,33 +1,39 @@
-package cz.muni.ics.ga4gh.model;
+package cz.muni.ics.ga4gh.base.model;
 
+import java.sql.Timestamp;
+import javax.validation.constraints.Min;
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
 import lombok.AllArgsConstructor;
 import lombok.EqualsAndHashCode;
 import lombok.Getter;
-import lombok.NoArgsConstructor;
 import lombok.Setter;
 import lombok.ToString;
 import org.springframework.util.StringUtils;
-
-import java.sql.Timestamp;
+import org.springframework.validation.annotation.Validated;
 
 @Getter
 @Setter
-@NoArgsConstructor
-@AllArgsConstructor
-@EqualsAndHashCode
 @ToString
+@EqualsAndHashCode
+@AllArgsConstructor
+@Validated
 public class UserExtSource {
 
+    @NotNull
     private Long id;
 
     private ExtSource extSource;
 
+    @NotBlank
     private String login;
 
-    private int loa = 0;
+    @Min(0)
+    private int loa;
 
     private boolean persistent;
 
+    @NotNull
     private Timestamp lastAccess;
 
     public void setExtSource(ExtSource extSource) {
diff --git a/src/main/java/cz/muni/ics/ga4gh/base/properties/AttributeMappingProperties.java b/src/main/java/cz/muni/ics/ga4gh/base/properties/AttributeMappingProperties.java
new file mode 100644
index 0000000..b761ff9
--- /dev/null
+++ b/src/main/java/cz/muni/ics/ga4gh/base/properties/AttributeMappingProperties.java
@@ -0,0 +1,38 @@
+package cz.muni.ics.ga4gh.base.properties;
+
+import cz.muni.ics.ga4gh.base.model.AttributeMapping;
+import java.util.HashMap;
+import java.util.Map;
+import javax.annotation.PostConstruct;
+import javax.validation.constraints.NotEmpty;
+import lombok.Getter;
+import lombok.ToString;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.ConstructorBinding;
+import org.springframework.validation.annotation.Validated;
+
+@Getter
+@ToString
+@Slf4j
+
+@Validated
+@ConstructorBinding
+@ConfigurationProperties(prefix = "attributes")
+public class AttributeMappingProperties {
+
+    @NotEmpty
+    private final Map<String, AttributeMapping> attributeMappings = new HashMap<>();
+
+    public AttributeMappingProperties(@NotEmpty Map<String, AttributeMapping> attributeMappings) {
+
+        this.attributeMappings.putAll(attributeMappings);
+    }
+
+    @PostConstruct
+    public void init() {
+        log.info("Initialized '{}' properties", this.getClass().getSimpleName());
+        log.debug("{}", this);
+    }
+
+}
diff --git a/src/main/java/cz/muni/ics/ga4gh/base/properties/BasicAuthProperties.java b/src/main/java/cz/muni/ics/ga4gh/base/properties/BasicAuthProperties.java
new file mode 100644
index 0000000..ea5b626
--- /dev/null
+++ b/src/main/java/cz/muni/ics/ga4gh/base/properties/BasicAuthProperties.java
@@ -0,0 +1,47 @@
+package cz.muni.ics.ga4gh.base.properties;
+
+import cz.muni.ics.ga4gh.base.exceptions.ConfigurationException;
+import cz.muni.ics.ga4gh.base.model.BasicAuthCredentials;
+import java.util.ArrayList;
+import java.util.List;
+import javax.annotation.PostConstruct;
+import javax.validation.constraints.NotEmpty;
+import lombok.Getter;
+import lombok.ToString;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.ConstructorBinding;
+import org.springframework.util.StringUtils;
+import org.springframework.validation.annotation.Validated;
+
+@Getter
+@ToString
+@Slf4j
+
+@Validated
+@ConstructorBinding
+@ConfigurationProperties(prefix = "basic-auth")
+public class BasicAuthProperties {
+
+    @NotEmpty
+    private final List<BasicAuthCredentials> credentials = new ArrayList<>();
+
+    public BasicAuthProperties(@NotEmpty List<BasicAuthCredentials> credentials)
+        throws ConfigurationException
+    {
+        for (BasicAuthCredentials c: credentials) {
+            if (!StringUtils.hasText(c.getUsername()) || !StringUtils.hasText(c.getPassword())) {
+                throw new ConfigurationException("Invalid basic-auth credentials configured - empty username or password. Check your configuration.");
+            }
+        }
+
+        this.credentials.addAll(credentials);
+    }
+
+    @PostConstruct
+    public void init() {
+        log.info("Initialized '{}' properties", this.getClass().getSimpleName());
+        log.debug("{}", this);
+    }
+
+}
diff --git a/src/main/java/cz/muni/ics/ga4gh/base/properties/BrokerInstanceProperties.java b/src/main/java/cz/muni/ics/ga4gh/base/properties/BrokerInstanceProperties.java
new file mode 100644
index 0000000..ab38098
--- /dev/null
+++ b/src/main/java/cz/muni/ics/ga4gh/base/properties/BrokerInstanceProperties.java
@@ -0,0 +1,89 @@
+package cz.muni.ics.ga4gh.base.properties;
+
+import java.util.ArrayList;
+import java.util.List;
+import javax.annotation.PostConstruct;
+import javax.validation.constraints.NotBlank;
+import lombok.Getter;
+import lombok.ToString;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.context.properties.ConstructorBinding;
+import org.springframework.validation.annotation.Validated;
+
+@Getter
+@ToString
+@Slf4j
+
+@Validated
+@ConstructorBinding
+public class BrokerInstanceProperties {
+
+    @NotBlank
+    private final String name;
+
+    @NotBlank
+    private final String brokerClass;
+
+    @NotBlank
+    private final String identifierAttribute;
+
+    private final Long membershipVoId;
+
+    private final String bonaFideStatusAttr;
+
+    private final String bonaFideStatusRemsAttr;
+
+    private final String groupAffiliationsAttr;
+
+    private final String affiliationsAttr;
+
+    private final String orgUrlAttr;
+
+    private final Long termsAndPoliciesGroupId;
+
+    private final String source;
+
+    private final List<Ga4ghClaimRepositoryProperties> passportRepositories = new ArrayList<>();
+
+    private final List<String> whitelistedLinkedIdentitySources = new ArrayList<>();
+
+    public BrokerInstanceProperties(String name,
+                                    String brokerClass,
+                                    String identifierAttribute,
+                                    Long membershipVoId,
+                                    String bonaFideStatusAttr,
+                                    String bonaFideStatusRemsAttr,
+                                    String groupAffiliationsAttr,
+                                    String affiliationsAttr,
+                                    String orgUrlAttr,
+                                    Long termsAndPoliciesGroupId,
+                                    String source,
+                                    List<String> whitelistedLinkedIdentitySources,
+                                    List<Ga4ghClaimRepositoryProperties> passportRepositories)
+    {
+        this.name = name;
+        this.brokerClass = brokerClass;
+        this.identifierAttribute = identifierAttribute;
+        this.membershipVoId = membershipVoId;
+        this.bonaFideStatusAttr = bonaFideStatusAttr;
+        this.bonaFideStatusRemsAttr = bonaFideStatusRemsAttr;
+        this.groupAffiliationsAttr = groupAffiliationsAttr;
+        this.termsAndPoliciesGroupId = termsAndPoliciesGroupId;
+        this.affiliationsAttr = affiliationsAttr;
+        this.orgUrlAttr = orgUrlAttr;
+        this.source = source;
+        if (whitelistedLinkedIdentitySources != null) {
+            this.whitelistedLinkedIdentitySources.addAll(whitelistedLinkedIdentitySources);
+        }
+        if (passportRepositories != null) {
+            this.passportRepositories.addAll(passportRepositories);
+        }
+    }
+
+    @PostConstruct
+    public void init() {
+        log.info("Initialized '{}' properties", this.getClass().getSimpleName());
+        log.debug("{}", this);
+    }
+
+}
diff --git a/src/main/java/cz/muni/ics/ga4gh/base/properties/Ga4ghBrokersProperties.java b/src/main/java/cz/muni/ics/ga4gh/base/properties/Ga4ghBrokersProperties.java
new file mode 100644
index 0000000..2657f8d
--- /dev/null
+++ b/src/main/java/cz/muni/ics/ga4gh/base/properties/Ga4ghBrokersProperties.java
@@ -0,0 +1,75 @@
+package cz.muni.ics.ga4gh.base.properties;
+
+import cz.muni.ics.ga4gh.base.exceptions.ConfigurationException;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.List;
+import javax.annotation.PostConstruct;
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+import lombok.Getter;
+import lombok.ToString;
+import lombok.extern.slf4j.Slf4j;
+import org.hibernate.validator.constraints.URL;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.ConstructorBinding;
+import org.springframework.core.io.FileUrlResource;
+import org.springframework.validation.annotation.Validated;
+
+@Getter
+@ToString
+@Slf4j
+
+@Validated
+@ConstructorBinding
+@ConfigurationProperties(prefix = "broker")
+public class Ga4ghBrokersProperties {
+
+    @NotEmpty
+    private final List<String> userIdentificationAttributes = new ArrayList<>();
+
+    @NotBlank
+    private final String issuer;
+
+    @NotNull
+    private final URI jku;
+    @NotNull
+    private final FileUrlResource jwkKeystoreFile;
+
+    @NotEmpty
+    private final List<BrokerInstanceProperties> brokersProperties = new ArrayList<>();
+
+    public Ga4ghBrokersProperties(@NotEmpty List<String> userIdentificationAttributes,
+                                  @NotBlank String issuer,
+                                  @URL String jku,
+                                  @NotBlank String pathToJwkFile,
+                                  @NotEmpty List<BrokerInstanceProperties> brokers)
+        throws ConfigurationException, MalformedURLException, URISyntaxException
+    {
+        try {
+            jwkKeystoreFile = new FileUrlResource(pathToJwkFile);
+            if (!this.jwkKeystoreFile.exists()) {
+                throw new Exception("JWK file does not exist");
+            } else if (!this.jwkKeystoreFile.isReadable()) {
+                throw new Exception("JWK file is not readable");
+            }
+            this.jwkKeystoreFile.getFile();
+        } catch (Exception e) {
+            throw new ConfigurationException("Error when loading JWK keystore file: " + e.getMessage());
+        }
+        this.userIdentificationAttributes.addAll(userIdentificationAttributes);
+        this.issuer = issuer;
+        this.jku = new java.net.URL(jku).toURI();
+        this.brokersProperties.addAll(brokers);
+    }
+
+    @PostConstruct
+    public void init() {
+        log.info("Initialized '{}' properties", this.getClass().getSimpleName());
+        log.debug("{}", this);
+    }
+
+}
diff --git a/src/main/java/cz/muni/ics/ga4gh/base/properties/Ga4ghClaimRepositoryProperties.java b/src/main/java/cz/muni/ics/ga4gh/base/properties/Ga4ghClaimRepositoryProperties.java
new file mode 100644
index 0000000..ad50ee8
--- /dev/null
+++ b/src/main/java/cz/muni/ics/ga4gh/base/properties/Ga4ghClaimRepositoryProperties.java
@@ -0,0 +1,32 @@
+package cz.muni.ics.ga4gh.base.properties;
+
+import cz.muni.ics.ga4gh.base.model.ClaimRepositoryHeader;
+import java.util.List;
+import javax.validation.constraints.NotBlank;
+import lombok.AllArgsConstructor;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.ToString;
+import org.springframework.validation.annotation.Validated;
+
+@Getter
+@Setter
+@ToString
+@EqualsAndHashCode
+@AllArgsConstructor
+@Validated
+public class Ga4ghClaimRepositoryProperties {
+
+    @NotBlank
+    private String name;
+
+    @NotBlank
+    private String url;
+
+    @NotBlank
+    private String jwks;
+
+    private List<ClaimRepositoryHeader> headers;
+
+}
diff --git a/src/main/java/cz/muni/ics/ga4gh/base/properties/PerunAdapterProperties.java b/src/main/java/cz/muni/ics/ga4gh/base/properties/PerunAdapterProperties.java
new file mode 100644
index 0000000..8bba7f7
--- /dev/null
+++ b/src/main/java/cz/muni/ics/ga4gh/base/properties/PerunAdapterProperties.java
@@ -0,0 +1,45 @@
+package cz.muni.ics.ga4gh.base.properties;
+
+import cz.muni.ics.ga4gh.base.adapters.PerunAdapter;
+import java.util.Objects;
+import javax.annotation.PostConstruct;
+import javax.validation.constraints.NotBlank;
+import lombok.Getter;
+import lombok.ToString;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.ConstructorBinding;
+import org.springframework.util.StringUtils;
+import org.springframework.validation.annotation.Validated;
+
+@Getter
+@ToString
+@Slf4j
+
+@Validated
+@ConstructorBinding
+@ConfigurationProperties(prefix = "perun.adapter")
+public class PerunAdapterProperties {
+
+    @NotBlank
+    private final String adapterPrimary;
+
+    private final boolean callFallback;
+
+    public PerunAdapterProperties(String primary, Boolean callFallback) {
+        if (StringUtils.hasText(primary)) {
+            this.adapterPrimary = primary;
+        } else {
+            this.adapterPrimary = PerunAdapter.RPC;
+        }
+
+        this.callFallback = Objects.requireNonNullElse(callFallback, true);
+    }
+
+    @PostConstruct
+    public void init() {
+        log.info("Initialized '{}' properties", this.getClass().getSimpleName());
+        log.debug("{}", this);
+    }
+
+}
diff --git a/src/main/java/cz/muni/ics/ga4gh/base/properties/PerunLdapConnectorProperties.java b/src/main/java/cz/muni/ics/ga4gh/base/properties/PerunLdapConnectorProperties.java
new file mode 100644
index 0000000..9edb1ae
--- /dev/null
+++ b/src/main/java/cz/muni/ics/ga4gh/base/properties/PerunLdapConnectorProperties.java
@@ -0,0 +1,86 @@
+package cz.muni.ics.ga4gh.base.properties;
+
+import java.util.Objects;
+import javax.annotation.PostConstruct;
+import javax.validation.constraints.Min;
+import javax.validation.constraints.NotBlank;
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.ConstructorBinding;
+import org.springframework.validation.annotation.Validated;
+
+@Getter
+@Slf4j
+
+@Validated
+@ConstructorBinding
+@ConfigurationProperties(prefix = "perun.connector.ldap")
+@ConditionalOnProperty("perun.connector.ldap.host")
+public class PerunLdapConnectorProperties {
+
+    @NotBlank
+    private final String host;
+
+    private final String user;
+
+    private final String password;
+
+    @NotBlank
+    private final String baseDn;
+
+    private final boolean useTls;
+
+    private final boolean useSsl;
+
+    private final boolean allowUntrustedSsl;
+
+    @Min(1)
+    private final long timeoutSecs;
+
+    private final int port;
+
+    public PerunLdapConnectorProperties(String host,
+                                        String user,
+                                        String password,
+                                        String baseDn,
+                                        Boolean useTls,
+                                        Boolean useSsl,
+                                        Boolean allowUntrustedSsl,
+                                        Long timeoutSecs,
+                                        Integer port)
+    {
+        this.host = host;
+        this.user = user;
+        this.password = password;
+        this.baseDn = baseDn;
+        this.useTls = Objects.requireNonNullElse(useTls, false);
+        this.useSsl = Objects.requireNonNullElse(useSsl, false);
+        this.allowUntrustedSsl = Objects.requireNonNullElse(allowUntrustedSsl, false);
+        this.timeoutSecs = Objects.requireNonNullElse(timeoutSecs, 5L);
+        this.port = Objects.requireNonNullElse(port, 336);
+    }
+
+    @PostConstruct
+    public void init() {
+        log.info("Initialized '{}' properties", this.getClass().getSimpleName());
+        log.debug("{}", this);
+    }
+
+    @Override
+    public String toString() {
+        return "LdapProperties{" +
+            "host='" + host + '\'' +
+            ", user='" + user + '\'' +
+            ", password='PROTECTED_STRING'" +
+            ", baseDn='" + baseDn + '\'' +
+            ", useTls=" + useTls +
+            ", useSsl=" + useSsl +
+            ", allowUntrustedSsl=" + allowUntrustedSsl +
+            ", timeoutSecs=" + timeoutSecs +
+            ", port=" + port +
+            '}';
+    }
+
+}
diff --git a/src/main/java/cz/muni/ics/ga4gh/base/properties/PerunRpcConnectorProperties.java b/src/main/java/cz/muni/ics/ga4gh/base/properties/PerunRpcConnectorProperties.java
new file mode 100644
index 0000000..ae508cc
--- /dev/null
+++ b/src/main/java/cz/muni/ics/ga4gh/base/properties/PerunRpcConnectorProperties.java
@@ -0,0 +1,79 @@
+package cz.muni.ics.ga4gh.base.properties;
+
+import java.util.Objects;
+import javax.annotation.PostConstruct;
+import javax.validation.constraints.Min;
+import javax.validation.constraints.NotBlank;
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.ConstructorBinding;
+import org.springframework.util.StringUtils;
+import org.springframework.validation.annotation.Validated;
+
+@Getter
+@Slf4j
+
+@Validated
+@ConstructorBinding
+@ConfigurationProperties(prefix = "perun.connector.rpc")
+public class PerunRpcConnectorProperties {
+
+    @NotBlank
+    private final String url;
+    @NotBlank
+    private final String username;
+    @NotBlank
+    private final String password;
+    private final String serializer;
+    @Min(1)
+    private final int connectionTimeout;
+    @Min(1)
+    private final int connectionRequestTimeout;
+    @Min(1)
+    private final int requestTimeout;
+    private final boolean enabled;
+
+    public PerunRpcConnectorProperties(String url,
+                                       String username,
+                                       String password,
+                                       String serializer,
+                                       Integer connectionTimeout,
+                                       Integer connectionRequestTimeout,
+                                       Integer requestTimeout,
+                                       Boolean enabled)
+    {
+        if (!StringUtils.hasText(serializer)) {
+            serializer = "jsonlite";
+        }
+        this.url = url;
+        this.username = username;
+        this.password = password;
+        this.serializer = serializer;
+        this.connectionTimeout = Objects.requireNonNullElse(connectionTimeout, 30000);
+        this.connectionRequestTimeout = Objects.requireNonNullElse(connectionRequestTimeout, 30000);
+        this.requestTimeout = Objects.requireNonNullElse(requestTimeout, 60000);
+        this.enabled = Objects.requireNonNullElse(enabled, true);
+    }
+
+    @PostConstruct
+    public void init() {
+        log.info("Initialized '{}' properties", this.getClass().getSimpleName());
+        log.debug("{}", this);
+    }
+
+    @Override
+    public String toString() {
+        return "RpcAdapterProperties{" +
+            "url='" + url + '\'' +
+            ", username='" + username + '\'' +
+            ", password='PROTECTED_STRING'" +
+            ", serializer='" + serializer + '\'' +
+            ", connectionTimeout='" + connectionTimeout + '\'' +
+            ", connectionRequestTimeout='" + connectionRequestTimeout + '\'' +
+            ", requestTimeout='" + requestTimeout + '\'' +
+            ", enabled='" + enabled + '\'' +
+            '}';
+    }
+
+}
diff --git a/src/main/java/cz/muni/ics/ga4gh/config/AdapterConfig.java b/src/main/java/cz/muni/ics/ga4gh/config/AdapterConfig.java
deleted file mode 100644
index 915582c..0000000
--- a/src/main/java/cz/muni/ics/ga4gh/config/AdapterConfig.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package cz.muni.ics.ga4gh.config;
-
-import lombok.Getter;
-import lombok.Setter;
-import org.springframework.boot.context.properties.ConfigurationProperties;
-import org.springframework.boot.context.properties.EnableConfigurationProperties;
-import org.springframework.context.annotation.Configuration;
-
-@Configuration
-@EnableConfigurationProperties
-@ConfigurationProperties(prefix = "adapter")
-@Getter
-@Setter
-public class AdapterConfig {
-
-    private String adapterPrimary;
-
-    private Boolean callFallback;
-}
diff --git a/src/main/java/cz/muni/ics/ga4gh/config/AttributesConfig.java b/src/main/java/cz/muni/ics/ga4gh/config/AttributesConfig.java
deleted file mode 100644
index 469443e..0000000
--- a/src/main/java/cz/muni/ics/ga4gh/config/AttributesConfig.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package cz.muni.ics.ga4gh.config;
-
-import cz.muni.ics.ga4gh.model.AttributeMapping;
-import lombok.Getter;
-import lombok.Setter;
-import org.springframework.boot.context.properties.ConfigurationProperties;
-import org.springframework.boot.context.properties.EnableConfigurationProperties;
-import org.springframework.context.annotation.Configuration;
-
-import java.util.Map;
-
-@Configuration
-@EnableConfigurationProperties
-@ConfigurationProperties(prefix = "attributes")
-@Getter
-@Setter
-public class AttributesConfig {
-
-    private Map<String, AttributeMapping> attributeMappings;
-}
diff --git a/src/main/java/cz/muni/ics/ga4gh/config/BasicAuthConfig.java b/src/main/java/cz/muni/ics/ga4gh/config/BasicAuthConfig.java
deleted file mode 100644
index 5436cdb..0000000
--- a/src/main/java/cz/muni/ics/ga4gh/config/BasicAuthConfig.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package cz.muni.ics.ga4gh.config;
-
-import lombok.Getter;
-import lombok.Setter;
-import org.springframework.boot.context.properties.ConfigurationProperties;
-import org.springframework.boot.context.properties.EnableConfigurationProperties;
-import org.springframework.context.annotation.Configuration;
-
-@Configuration
-@EnableConfigurationProperties
-@ConfigurationProperties(prefix = "basic-auth")
-@Getter
-@Setter
-public class BasicAuthConfig {
-
-    private String username;
-
-    private String password;
-}
diff --git a/src/main/java/cz/muni/ics/ga4gh/config/BrokerConfig.java b/src/main/java/cz/muni/ics/ga4gh/config/BrokerConfig.java
deleted file mode 100644
index ac59aaf..0000000
--- a/src/main/java/cz/muni/ics/ga4gh/config/BrokerConfig.java
+++ /dev/null
@@ -1,37 +0,0 @@
-package cz.muni.ics.ga4gh.config;
-
-import lombok.Getter;
-import lombok.Setter;
-import org.springframework.boot.context.properties.ConfigurationProperties;
-import org.springframework.boot.context.properties.EnableConfigurationProperties;
-import org.springframework.context.annotation.Configuration;
-
-import java.util.List;
-
-@Configuration
-@EnableConfigurationProperties
-@ConfigurationProperties(prefix = "broker")
-@Getter
-@Setter
-public class BrokerConfig {
-
-    private String bonaFideStatusAttr;
-
-    private String bonaFideStatusRemsAttr;
-
-    private String groupAffiliationsAttr;
-
-    private Long termsAndPoliciesGroupId;
-
-    private String affiliationsAttr;
-
-    private String orgUrlAttr;
-
-    private List<String> attributesToSearch;
-
-    private String issuer;
-
-    private String jku;
-
-    private String pathToJwkFile;
-}
diff --git a/src/main/java/cz/muni/ics/ga4gh/config/Ga4ghConfig.java b/src/main/java/cz/muni/ics/ga4gh/config/Ga4ghConfig.java
deleted file mode 100644
index 188a1de..0000000
--- a/src/main/java/cz/muni/ics/ga4gh/config/Ga4ghConfig.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package cz.muni.ics.ga4gh.config;
-
-import cz.muni.ics.ga4gh.model.Repo;
-import lombok.Getter;
-import lombok.Setter;
-import org.springframework.boot.context.properties.ConfigurationProperties;
-import org.springframework.boot.context.properties.EnableConfigurationProperties;
-import org.springframework.context.annotation.Configuration;
-
-import java.util.List;
-
-@Configuration
-@EnableConfigurationProperties
-@ConfigurationProperties(prefix = "ga4gh")
-@Getter
-@Setter
-public class Ga4ghConfig {
-
-    private List<Repo> repos;
-}
diff --git a/src/main/java/cz/muni/ics/ga4gh/config/LdapConfig.java b/src/main/java/cz/muni/ics/ga4gh/config/LdapConfig.java
deleted file mode 100644
index 8a45101..0000000
--- a/src/main/java/cz/muni/ics/ga4gh/config/LdapConfig.java
+++ /dev/null
@@ -1,33 +0,0 @@
-package cz.muni.ics.ga4gh.config;
-
-import lombok.Getter;
-import lombok.Setter;
-import org.springframework.boot.context.properties.ConfigurationProperties;
-import org.springframework.boot.context.properties.EnableConfigurationProperties;
-import org.springframework.context.annotation.Configuration;
-
-@Configuration
-@EnableConfigurationProperties
-@ConfigurationProperties(prefix = "ldap")
-@Getter
-@Setter
-public class LdapConfig {
-
-    private String host;
-
-    private String user;
-
-    private String password;
-
-    private String baseDn;
-
-    private Boolean useTls;
-
-    private Boolean useSsl;
-
-    private Boolean allowUntrustedSsl;
-
-    private Long timeoutSecs;
-
-    private int port;
-}
diff --git a/src/main/java/cz/muni/ics/ga4gh/config/RpcConfig.java b/src/main/java/cz/muni/ics/ga4gh/config/RpcConfig.java
deleted file mode 100644
index 74f41d8..0000000
--- a/src/main/java/cz/muni/ics/ga4gh/config/RpcConfig.java
+++ /dev/null
@@ -1,25 +0,0 @@
-package cz.muni.ics.ga4gh.config;
-
-import lombok.Getter;
-import lombok.Setter;
-import org.springframework.boot.context.properties.ConfigurationProperties;
-import org.springframework.boot.context.properties.EnableConfigurationProperties;
-import org.springframework.context.annotation.Configuration;
-
-@Configuration
-@EnableConfigurationProperties
-@ConfigurationProperties(prefix = "rpc")
-@Getter
-@Setter
-public class RpcConfig {
-
-    private Boolean enabled;
-
-    private String url;
-
-    private String username;
-
-    private String password;
-
-    private String serializer;
-}
diff --git a/src/main/java/cz/muni/ics/ga4gh/controllers/Ga4ghBrokerController.java b/src/main/java/cz/muni/ics/ga4gh/controllers/Ga4ghBrokerController.java
deleted file mode 100644
index c37ecf1..0000000
--- a/src/main/java/cz/muni/ics/ga4gh/controllers/Ga4ghBrokerController.java
+++ /dev/null
@@ -1,35 +0,0 @@
-package cz.muni.ics.ga4gh.controllers;
-
-import com.fasterxml.jackson.databind.node.ArrayNode;
-import cz.muni.ics.ga4gh.facade.Ga4ghBrokerFacade;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.http.MediaType;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PathVariable;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
-
-import javax.servlet.http.HttpServletResponse;
-
-@RestController
-@RequestMapping("/ga4gh")
-public class Ga4ghBrokerController {
-
-    private final Ga4ghBrokerFacade ga4GhBrokerFacade;
-
-    @Autowired
-    public Ga4ghBrokerController(Ga4ghBrokerFacade ga4GhBrokerFacade) {
-        this.ga4GhBrokerFacade = ga4GhBrokerFacade;
-    }
-
-    @GetMapping(value = "/{eppn}", produces = MediaType.APPLICATION_JSON_VALUE)
-    public ArrayNode getGa4ghPassport(@PathVariable String eppn, HttpServletResponse response) {
-        ArrayNode result = ga4GhBrokerFacade.getGa4ghPassport(eppn);
-
-        if (result == null) {
-            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
-        }
-
-        return result;
-    }
-}
diff --git a/src/main/java/cz/muni/ics/ga4gh/facade/Ga4ghBrokerFacade.java b/src/main/java/cz/muni/ics/ga4gh/facade/Ga4ghBrokerFacade.java
index 2c4ebf4..98658ff 100644
--- a/src/main/java/cz/muni/ics/ga4gh/facade/Ga4ghBrokerFacade.java
+++ b/src/main/java/cz/muni/ics/ga4gh/facade/Ga4ghBrokerFacade.java
@@ -1,8 +1,12 @@
 package cz.muni.ics.ga4gh.facade;
 
-import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.JsonNode;
+import cz.muni.ics.ga4gh.base.exceptions.UserNotFoundException;
+import cz.muni.ics.ga4gh.base.exceptions.UserNotUniqueException;
 
 public interface Ga4ghBrokerFacade {
 
-    ArrayNode getGa4ghPassport(String eppn);
+    JsonNode getGa4ghPassport(String userIdentifier)
+        throws UserNotFoundException, UserNotUniqueException;
+
 }
diff --git a/src/main/java/cz/muni/ics/ga4gh/facade/impl/Ga4GhBrokerFacadeImpl.java b/src/main/java/cz/muni/ics/ga4gh/facade/impl/Ga4GhBrokerFacadeImpl.java
index bf3ce54..4308aee 100644
--- a/src/main/java/cz/muni/ics/ga4gh/facade/impl/Ga4GhBrokerFacadeImpl.java
+++ b/src/main/java/cz/muni/ics/ga4gh/facade/impl/Ga4GhBrokerFacadeImpl.java
@@ -1,23 +1,44 @@
 package cz.muni.ics.ga4gh.facade.impl;
 
-import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.JsonNodeFactory;
+import cz.muni.ics.ga4gh.base.exceptions.UserNotFoundException;
+import cz.muni.ics.ga4gh.base.exceptions.UserNotUniqueException;
+import cz.muni.ics.ga4gh.base.model.Ga4ghPassport;
 import cz.muni.ics.ga4gh.facade.Ga4ghBrokerFacade;
 import cz.muni.ics.ga4gh.service.Ga4ghBrokerService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
 
 @Service
 public class Ga4GhBrokerFacadeImpl implements Ga4ghBrokerFacade {
 
-    Ga4ghBrokerService ga4GhBrokerService;
+    private final Ga4ghBrokerService ga4ghBrokerService;
 
     @Autowired
-    public Ga4GhBrokerFacadeImpl(Ga4ghBrokerService ga4GhBrokerService) {
-        this.ga4GhBrokerService = ga4GhBrokerService;
+    public Ga4GhBrokerFacadeImpl(Ga4ghBrokerService ga4ghBrokerService) {
+        this.ga4ghBrokerService = ga4ghBrokerService;
     }
 
     @Override
-    public ArrayNode getGa4ghPassport(String eppn) {
-        return ga4GhBrokerService.getGa4ghPassport(eppn);
+    public JsonNode getGa4ghPassport(String userIdentifier)
+        throws UserNotFoundException, UserNotUniqueException
+    {
+        if (!StringUtils.hasText(userIdentifier)) {
+            throw new IllegalArgumentException("User identifier cannot be empty");
+        }
+
+        Long perunUserId = ga4ghBrokerService.identifyUser(userIdentifier);
+        if (perunUserId == null) {
+            throw new UserNotFoundException("No user found for given identifier");
+        }
+        Ga4ghPassport passport = ga4ghBrokerService.getGa4ghPassport(perunUserId);
+        if (passport == null) {
+            return JsonNodeFactory.instance.nullNode();
+        } else {
+            return passport.toJsonObject();
+        }
     }
+
 }
diff --git a/src/main/java/cz/muni/ics/ga4gh/model/ExtSource.java b/src/main/java/cz/muni/ics/ga4gh/model/ExtSource.java
deleted file mode 100644
index eeb1cd9..0000000
--- a/src/main/java/cz/muni/ics/ga4gh/model/ExtSource.java
+++ /dev/null
@@ -1,40 +0,0 @@
-package cz.muni.ics.ga4gh.model;
-
-import lombok.AllArgsConstructor;
-import lombok.EqualsAndHashCode;
-import lombok.Getter;
-import lombok.NoArgsConstructor;
-import lombok.Setter;
-import lombok.ToString;
-import org.springframework.util.StringUtils;
-
-@Getter
-@NoArgsConstructor
-@AllArgsConstructor
-@EqualsAndHashCode
-@ToString
-public class ExtSource {
-
-    @Setter
-    private Long id;
-
-    private String name;
-
-    private String type;
-
-    public void setName(String name) {
-        if (StringUtils.hasText(name)) {
-            throw new IllegalArgumentException("name cannot be null nor empty");
-        }
-
-        this.name = name;
-    }
-
-    public void setType(String type) {
-        if (!StringUtils.hasText(type)) {
-            throw new IllegalArgumentException("type cannot be null nor empty");
-        }
-
-        this.type = type;
-    }
-}
diff --git a/src/main/java/cz/muni/ics/ga4gh/model/Ga4ghPassportVisa.java b/src/main/java/cz/muni/ics/ga4gh/model/Ga4ghPassportVisa.java
deleted file mode 100644
index 6917574..0000000
--- a/src/main/java/cz/muni/ics/ga4gh/model/Ga4ghPassportVisa.java
+++ /dev/null
@@ -1,59 +0,0 @@
-package cz.muni.ics.ga4gh.model;
-
-import lombok.EqualsAndHashCode;
-import lombok.Getter;
-import lombok.Setter;
-import lombok.ToString;
-
-@Getter
-@Setter
-@ToString
-@EqualsAndHashCode
-public class Ga4ghPassportVisa {
-
-    public static final String GA4GH_VISA_V1 = "ga4gh_visa_v1";
-
-    public static final String TYPE_AFFILIATION_AND_ROLE = "AffiliationAndRole";
-    public static final String TYPE_ACCEPTED_TERMS_AND_POLICIES = "AcceptedTermsAndPolicies";
-    public static final String TYPE_RESEARCHER_STATUS = "ResearcherStatus";
-    public static final String TYPE_LINKED_IDENTITIES = "LinkedIdentities";
-
-    public static final String BY_SYSTEM = "system";
-    public static final String BY_SO = "so";
-    public static final String BY_PEER = "peer";
-    public static final String BY_SELF = "self";
-
-    public static final String SUB = "sub";
-    public static final String EXP = "exp";
-    public static final String ISS = "iss";
-    public static final String TYPE = "type";
-    public static final String ASSERTED = "asserted";
-    public static final String VALUE = "value";
-    public static final String SOURCE = "source";
-    public static final String BY = "by";
-    public static final String CONDITION = "condition";
-
-    private boolean verified = false;
-    private String linkedIdentity;
-    private String sub;
-    private String iss;
-    private String type;
-    private String value;
-
-    @ToString.Exclude
-    private String signer;
-
-    @ToString.Exclude
-    private String jwt;
-
-    @ToString.Exclude
-    private String prettyPayload;
-
-    public Ga4ghPassportVisa(String jwt) {
-        this.jwt = jwt;
-    }
-
-    public String getPrettyString() {
-        return prettyPayload + ", signed by " + signer;
-    }
-}
diff --git a/src/main/java/cz/muni/ics/ga4gh/model/Repo.java b/src/main/java/cz/muni/ics/ga4gh/model/Repo.java
deleted file mode 100644
index 23016ac..0000000
--- a/src/main/java/cz/muni/ics/ga4gh/model/Repo.java
+++ /dev/null
@@ -1,27 +0,0 @@
-package cz.muni.ics.ga4gh.model;
-
-import lombok.AllArgsConstructor;
-import lombok.EqualsAndHashCode;
-import lombok.Getter;
-import lombok.NoArgsConstructor;
-import lombok.Setter;
-import lombok.ToString;
-
-import java.util.List;
-
-@Getter
-@Setter
-@ToString
-@EqualsAndHashCode
-@NoArgsConstructor
-@AllArgsConstructor
-public class Repo {
-
-    private String name;
-
-    private String url;
-
-    private String jwks;
-
-    private List<RepoHeader> headers;
-}
diff --git a/src/main/java/cz/muni/ics/ga4gh/service/Ga4ghBrokerService.java b/src/main/java/cz/muni/ics/ga4gh/service/Ga4ghBrokerService.java
index 824d9fa..a3ebd90 100644
--- a/src/main/java/cz/muni/ics/ga4gh/service/Ga4ghBrokerService.java
+++ b/src/main/java/cz/muni/ics/ga4gh/service/Ga4ghBrokerService.java
@@ -1,8 +1,13 @@
 package cz.muni.ics.ga4gh.service;
 
-import com.fasterxml.jackson.databind.node.ArrayNode;
+import cz.muni.ics.ga4gh.base.exceptions.UserNotFoundException;
+import cz.muni.ics.ga4gh.base.exceptions.UserNotUniqueException;
+import cz.muni.ics.ga4gh.base.model.Ga4ghPassport;
 
 public interface Ga4ghBrokerService {
 
-    ArrayNode getGa4ghPassport(String eppn);
+    Ga4ghPassport getGa4ghPassport(Long userId) throws UserNotFoundException, UserNotUniqueException;
+
+    Long identifyUser(String userIdentifier)  throws UserNotFoundException, UserNotUniqueException;
+
 }
diff --git a/src/main/java/cz/muni/ics/ga4gh/service/Ga4ghPassportBrokerBeans.java b/src/main/java/cz/muni/ics/ga4gh/service/Ga4ghPassportBrokerBeans.java
new file mode 100644
index 0000000..2843160
--- /dev/null
+++ b/src/main/java/cz/muni/ics/ga4gh/service/Ga4ghPassportBrokerBeans.java
@@ -0,0 +1,56 @@
+package cz.muni.ics.ga4gh.service;
+
+import cz.muni.ics.ga4gh.base.adapters.PerunAdapter;
+import cz.muni.ics.ga4gh.base.exceptions.ConfigurationException;
+import cz.muni.ics.ga4gh.base.properties.BrokerInstanceProperties;
+import cz.muni.ics.ga4gh.base.properties.Ga4ghBrokersProperties;
+import cz.muni.ics.ga4gh.service.impl.brokers.BbmriGa4ghBroker;
+import cz.muni.ics.ga4gh.service.impl.brokers.ElixirGa4ghBroker;
+import cz.muni.ics.ga4gh.service.impl.brokers.Ga4ghBroker;
+import cz.muni.ics.ga4gh.service.impl.brokers.LifescienceRiGa4ghBroker;
+import cz.muni.ics.ga4gh.service.impl.brokers.PerunGa4ghBroker;
+import java.util.ArrayList;
+import java.util.List;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.stereotype.Component;
+
+@Component
+public class Ga4ghPassportBrokerBeans {
+
+    @Autowired
+    @Bean
+    public List<Ga4ghBroker> passportBrokers(Ga4ghBrokersProperties ga4ghBrokersProperties,
+                                             PerunAdapter perunAdapter,
+                                             JWTSigningAndValidationService jwtService)
+        throws ConfigurationException
+    {
+        List<Ga4ghBroker> brokers = new ArrayList<>();
+        List<BrokerInstanceProperties> properties = ga4ghBrokersProperties.getBrokersProperties();
+        for (BrokerInstanceProperties instanceProperties: properties) {
+            brokers.add(initializeBroker(instanceProperties, ga4ghBrokersProperties, perunAdapter, jwtService));
+        }
+        return brokers;
+    }
+
+    private Ga4ghBroker initializeBroker(BrokerInstanceProperties instanceProperties,
+                                         Ga4ghBrokersProperties ga4ghBrokersProperties,
+                                         PerunAdapter perunAdapter,
+                                         JWTSigningAndValidationService jwtService)
+        throws ConfigurationException
+    {
+        String implementationClass = instanceProperties.getBrokerClass();
+        switch (implementationClass) {
+            case "BbmriGa4ghBroker":
+                return new BbmriGa4ghBroker(instanceProperties, ga4ghBrokersProperties, perunAdapter, jwtService);
+            case "ElixirGa4ghBroker":
+                return new ElixirGa4ghBroker(instanceProperties, ga4ghBrokersProperties, perunAdapter, jwtService);
+            case "LifescienceRiGa4ghBroker":
+                return new LifescienceRiGa4ghBroker(instanceProperties, ga4ghBrokersProperties, perunAdapter, jwtService);
+            case "PerunGa4ghBroker":
+                return new PerunGa4ghBroker(instanceProperties, ga4ghBrokersProperties, perunAdapter, jwtService);
+            default:
+                throw new ConfigurationException("Invalid broker class name specified");
+        }
+    }
+}
diff --git a/src/main/java/cz/muni/ics/ga4gh/service/JWTSigningAndValidationService.java b/src/main/java/cz/muni/ics/ga4gh/service/JWTSigningAndValidationService.java
index 9748662..16d84e6 100644
--- a/src/main/java/cz/muni/ics/ga4gh/service/JWTSigningAndValidationService.java
+++ b/src/main/java/cz/muni/ics/ga4gh/service/JWTSigningAndValidationService.java
@@ -3,17 +3,16 @@ package cz.muni.ics.ga4gh.service;
 import com.nimbusds.jose.JWSAlgorithm;
 import com.nimbusds.jose.jwk.JWK;
 import com.nimbusds.jwt.SignedJWT;
-
-import java.util.Collection;
 import java.util.Map;
 
 public interface JWTSigningAndValidationService {
 
-    Map<String, JWK> getAllPublicKeys();
 
-    JWSAlgorithm getDefaultSigningAlgorithm();
+    JWSAlgorithm getSigningAlgorithm();
+
+    String getSignerKeyId();
 
-    Collection<JWSAlgorithm> getAllSigningAlgsSupported();
+    Map<String, JWK> getPublicKeys();
 
     /**
      * Called to sign a jwt in place for a client that hasn't registered a preferred signing algorithm.
@@ -24,14 +23,4 @@ public interface JWTSigningAndValidationService {
      */
     void signJwt(SignedJWT jwt);
 
-    /**
-     * Sign a jwt using the selected algorithm. The algorithm is selected using the String parameter values specified
-     * in the JWT spec, section 6. I.E., "HS256" means HMAC with SHA-256 and corresponds to our HmacSigner class.
-     *
-     * @param jwt the jwt to sign
-     * @param alg the name of the algorithm to use, as specified in JWS s.6
-     */
-    void signJwt(SignedJWT jwt, JWSAlgorithm alg);
-
-    String getDefaultSignerKeyId();
 }
diff --git a/src/main/java/cz/muni/ics/ga4gh/service/PassportAssemblyContext.java b/src/main/java/cz/muni/ics/ga4gh/service/PassportAssemblyContext.java
new file mode 100644
index 0000000..808cbfe
--- /dev/null
+++ b/src/main/java/cz/muni/ics/ga4gh/service/PassportAssemblyContext.java
@@ -0,0 +1,35 @@
+package cz.muni.ics.ga4gh.service;
+
+import cz.muni.ics.ga4gh.base.adapters.PerunAdapter;
+import cz.muni.ics.ga4gh.base.model.Affiliation;
+import cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa;
+import java.util.List;
+import java.util.Set;
+import lombok.Builder;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.ToString;
+
+@Getter
+@ToString
+@EqualsAndHashCode
+@Builder
+public class PassportAssemblyContext {
+
+    private long now;
+
+    private PerunAdapter perunAdapter;
+
+    private String subject;
+
+    private long perunUserId;
+
+    private List<Affiliation> identityAffiliations;
+
+    private Set<String> externalLinkedIdentities;
+
+    private List<Ga4ghPassportVisa> externalControlledAccessGrants;
+
+    private List<Ga4ghPassportVisa> resultVisas;
+
+}
diff --git a/src/main/java/cz/muni/ics/ga4gh/service/impl/Ga4GhBrokerBrokerServiceImpl.java b/src/main/java/cz/muni/ics/ga4gh/service/impl/Ga4GhBrokerBrokerServiceImpl.java
index 9999334..8dc05ad 100644
--- a/src/main/java/cz/muni/ics/ga4gh/service/impl/Ga4GhBrokerBrokerServiceImpl.java
+++ b/src/main/java/cz/muni/ics/ga4gh/service/impl/Ga4GhBrokerBrokerServiceImpl.java
@@ -1,61 +1,91 @@
 package cz.muni.ics.ga4gh.service.impl;
 
-import com.fasterxml.jackson.databind.node.ArrayNode;
-import cz.muni.ics.ga4gh.adapters.PerunAdapter;
-import cz.muni.ics.ga4gh.config.AttributesConfig;
-import cz.muni.ics.ga4gh.config.BrokerConfig;
-import cz.muni.ics.ga4gh.exceptions.UserNotUniqueException;
-import cz.muni.ics.ga4gh.model.AttributeMapping;
-import cz.muni.ics.ga4gh.service.impl.brokers.Ga4ghBroker;
+import cz.muni.ics.ga4gh.base.adapters.PerunAdapter;
+import cz.muni.ics.ga4gh.base.exceptions.UserNotFoundException;
+import cz.muni.ics.ga4gh.base.exceptions.UserNotUniqueException;
+import cz.muni.ics.ga4gh.base.model.Ga4ghPassport;
+import cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa;
+import cz.muni.ics.ga4gh.base.properties.Ga4ghBrokersProperties;
 import cz.muni.ics.ga4gh.service.Ga4ghBrokerService;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Service;
-
+import cz.muni.ics.ga4gh.service.impl.brokers.Ga4ghBroker;
+import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
-import java.util.Map;
 import java.util.Set;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
 
 @Service
 @Slf4j
 public class Ga4GhBrokerBrokerServiceImpl implements Ga4ghBrokerService {
 
-    private final Ga4ghBroker broker;
+    private final List<Ga4ghBroker> brokers = new ArrayList<>();
 
     private final PerunAdapter adapter;
 
-    private final List<String> attributesToSearch;
-
-    private final Map<String, AttributeMapping> attributes;
+    private final List<String> userIdentificationAttributes = new ArrayList<>();
 
     @Autowired
-    public Ga4GhBrokerBrokerServiceImpl(Ga4ghBroker broker, PerunAdapter adapter, BrokerConfig brokerConfig, AttributesConfig attributesConfig) {
-        this.broker = broker;
+    public Ga4GhBrokerBrokerServiceImpl(List<Ga4ghBroker> brokers,
+                                        PerunAdapter adapter,
+                                        Ga4ghBrokersProperties brokersProperties)
+    {
+        this.brokers.addAll(brokers);
         this.adapter = adapter;
-        this.attributesToSearch = brokerConfig.getAttributesToSearch();
-        this.attributes = attributesConfig.getAttributeMappings();
+        this.userIdentificationAttributes.addAll(brokersProperties.getUserIdentificationAttributes());
     }
 
     @Override
-    public ArrayNode getGa4ghPassport(String eppn) {
-        Set<Long> userIds = new HashSet<>();
-
-        for (String attrName : attributesToSearch) {
-            if (attributes.get(attrName) != null) {
-                userIds.addAll(adapter.getAdapterPrimary().getUserIdsByAttributeValue(attributes.get(attrName), eppn));
+    public Ga4ghPassport getGa4ghPassport(Long userId) {
+        Ga4ghPassport passport = new Ga4ghPassport();
+        long startTime = System.currentTimeMillis();
+        for (Ga4ghBroker broker: brokers) {
+            long localStartTime = System.currentTimeMillis();
+            List<Ga4ghPassportVisa> visas = broker.constructGa4ghPassportVisas(userId);
+            if (visas != null) {
+                passport.addVisas(visas);
             }
+            long localEndTime = System.currentTimeMillis();
+            log.info("Generating for user '{}' in broker '{}({})' took {}ms", userId, broker.getBrokerName(), broker.getClass().getSimpleName(), localEndTime - localStartTime);
         }
+        long endTime = System.currentTimeMillis();
+        log.info("Generating for user '{}' in total took {}ms", userId, endTime - startTime);
+        return passport;
+    }
 
-        if (userIds.isEmpty()) {
-            log.debug("User {} not found", eppn);
-            return null;
+    @Override
+    public Long identifyUser(String userIdentifier)
+        throws UserNotFoundException, UserNotUniqueException
+    {
+        if (!StringUtils.hasText(userIdentifier)) {
+            throw new IllegalArgumentException("User identifier cannot be empty");
         }
 
-        if (userIds.size() > 1) {
-            throw new UserNotUniqueException("There are more users found by " + eppn + " - " + String.join(", ", userIds.toString()));
+        Set<Long> perunUserIds = new HashSet<>();
+
+        for (String attrName : userIdentificationAttributes) {
+            Set<Long> foundUserIds = adapter.getUserIdsByAttributeValue(attrName, userIdentifier);
+            if (foundUserIds == null || foundUserIds.isEmpty()) {
+                log.debug("No user IDs found for identifier '{}' and attribute '{}'",
+                    userIdentifier, attrName);
+                continue;
+            }
+            perunUserIds.addAll(foundUserIds);
         }
 
-        return broker.constructGa4ghPassportVisa(userIds.iterator().next(), eppn);
+        if (perunUserIds.isEmpty()) {
+            log.debug("User with identifier '{}' not found", userIdentifier);
+            throw new UserNotFoundException("User with identifier '" + userIdentifier + "' not found");
+        }
+
+        if (perunUserIds.size() > 1) {
+            log.warn("Multiple users found with identifier '{}' - user IDS: '{}'", userIdentifier, perunUserIds);
+            throw new UserNotUniqueException("More than one user found for identifier '" +
+                userIdentifier + '\'');
+        }
+        return perunUserIds.iterator().next();
     }
+
 }
diff --git a/src/main/java/cz/muni/ics/ga4gh/service/impl/JWTSigningAndValidationServiceImpl.java b/src/main/java/cz/muni/ics/ga4gh/service/impl/JWTSigningAndValidationServiceImpl.java
index 0fc7b3b..4865df2 100644
--- a/src/main/java/cz/muni/ics/ga4gh/service/impl/JWTSigningAndValidationServiceImpl.java
+++ b/src/main/java/cz/muni/ics/ga4gh/service/impl/JWTSigningAndValidationServiceImpl.java
@@ -2,7 +2,6 @@ package cz.muni.ics.ga4gh.service.impl;
 
 import com.nimbusds.jose.JOSEException;
 import com.nimbusds.jose.JWSAlgorithm;
-import com.nimbusds.jose.JWSProvider;
 import com.nimbusds.jose.JWSSigner;
 import com.nimbusds.jose.JWSVerifier;
 import com.nimbusds.jose.crypto.ECDSASigner;
@@ -16,20 +15,16 @@ import com.nimbusds.jose.jwk.JWK;
 import com.nimbusds.jose.jwk.OctetSequenceKey;
 import com.nimbusds.jose.jwk.RSAKey;
 import com.nimbusds.jwt.SignedJWT;
-import cz.muni.ics.ga4gh.jose.keystore.JWKSetKeyStore;
+import cz.muni.ics.ga4gh.base.JWKSetKeyStore;
 import cz.muni.ics.ga4gh.service.JWTSigningAndValidationService;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.util.StringUtils;
 
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
-import java.util.UUID;
-
 @Slf4j
 @Service
 public class JWTSigningAndValidationServiceImpl implements JWTSigningAndValidationService {
@@ -37,20 +32,9 @@ public class JWTSigningAndValidationServiceImpl implements JWTSigningAndValidati
     private final Map<String, JWSSigner> signers = new HashMap<>();
     private final Map<String, JWSVerifier> verifiers = new HashMap<>();
 
-    private String defaultSignerKeyId;
-    private JWSAlgorithm defaultAlgorithm;
-    private Map<String, JWK> keys = new HashMap<>();
-
-    /**
-     * Build this service based on the keys given. All public keys will be used
-     * to make verifiers, all private keys will be used to make signers.
-     *
-     * @param keys A map of key identifier to key.
-     */
-    public JWTSigningAndValidationServiceImpl(Map<String, JWK> keys) {
-        this.keys = keys;
-        buildSignersAndVerifiers();
-    }
+    private String signerKeyId;
+    private JWSAlgorithm signingAlgorithm;
+    private final Map<String, JWK> keys = new HashMap<>();
 
     /**
      * Build this service based on the given keystore. All keys must have a key
@@ -59,57 +43,47 @@ public class JWTSigningAndValidationServiceImpl implements JWTSigningAndValidati
      * @param keyStore The keystore to load all keys from.
      */
     @Autowired
-    public JWTSigningAndValidationServiceImpl(JWKSetKeyStore keyStore) {
-        if (keyStore!= null && keyStore.getJwkSet() != null) {
-            for (JWK key : keyStore.getKeys()) {
-                if (!StringUtils.isEmpty(key.getKeyID())) {
-                    this.keys.put(key.getKeyID(), key);
-                } else {
-                    String fakeKid = UUID.randomUUID().toString();
-                    this.keys.put(fakeKid, key);
-                }
-            }
+    public JWTSigningAndValidationServiceImpl(JWKSetKeyStore keyStore) throws Exception {
+        loadKeysFromStore(keyStore);
+        initializeSignersAndVerifiers();
+
+        if (keyStore != null) {
+            JWK defaultKey = keyStore.getKeys().get(0);
+            setSignerKeyId(defaultKey.getKeyID());
+            setDefaultSigningAlgorithmName(defaultKey.getAlgorithm().getName());
+        } else {
+            throw new Exception("Failed to initialize keystore");
         }
-
-        buildSignersAndVerifiers();
-
-        this.defaultSignerKeyId = keyStore.getKeys().get(0).getKeyID();
-        setDefaultSigningAlgorithmName(keyStore.getKeys().get(0).getAlgorithm().getName());
     }
 
     @Override
-    public String getDefaultSignerKeyId() {
-        return defaultSignerKeyId;
+    public String getSignerKeyId() {
+        return signerKeyId;
     }
 
-    public void setDefaultSignerKeyId(String defaultSignerId) {
-        this.defaultSignerKeyId = defaultSignerId;
+    public void setSignerKeyId(String defaultSignerId) {
+        this.signerKeyId = defaultSignerId;
     }
 
     @Override
-    public JWSAlgorithm getDefaultSigningAlgorithm() {
-        return defaultAlgorithm;
+    public JWSAlgorithm getSigningAlgorithm() {
+        return signingAlgorithm;
     }
 
     public void setDefaultSigningAlgorithmName(String algName) {
-        defaultAlgorithm = JWSAlgorithm.parse(algName);
-    }
-
-    public String getDefaultSigningAlgorithmName() {
-        if (defaultAlgorithm != null) {
-            return defaultAlgorithm.getName();
-        } else {
-            return null;
-        }
+        signingAlgorithm = JWSAlgorithm.parse(algName);
     }
 
     @Override
     public void signJwt(SignedJWT jwt) {
-        if (getDefaultSignerKeyId() == null) {
-            throw new IllegalStateException("Tried to call default signing with no default signer ID set");
+        if (getSignerKeyId() == null) {
+            throw new IllegalStateException("No signer key ID is set");
         }
 
-        JWSSigner signer = signers.get(getDefaultSignerKeyId());
+        JWSSigner signer = signers.getOrDefault(getSignerKeyId(), null);
+        if (signer == null) {
+            throw new IllegalStateException("No signer found for set signer key ID");
+        }
 
         try {
             jwt.sign(signer);
@@ -119,29 +93,7 @@ public class JWTSigningAndValidationServiceImpl implements JWTSigningAndValidati
     }
 
     @Override
-    public void signJwt(SignedJWT jwt, JWSAlgorithm alg) {
-        JWSSigner signer = null;
-
-        for (JWSSigner s : signers.values()) {
-            if (s.supportedJWSAlgorithms().contains(alg)) {
-                signer = s;
-                break;
-            }
-        }
-
-        if (signer == null) {
-            log.error("No matching algorithm found for alg={}", alg);
-        } else {
-            try {
-                jwt.sign(signer);
-            } catch (JOSEException e) {
-                log.error("Failed to sign JWT, error was: ", e);
-            }
-        }
-    }
-
-    @Override
-    public Map<String, JWK> getAllPublicKeys() {
+    public Map<String, JWK> getPublicKeys() {
         Map<String, JWK> pubKeys = new HashMap<>();
 
         keys.keySet().forEach(keyId -> {
@@ -155,16 +107,20 @@ public class JWTSigningAndValidationServiceImpl implements JWTSigningAndValidati
         return pubKeys;
     }
 
-    @Override
-    public Collection<JWSAlgorithm> getAllSigningAlgsSupported() {
-        Set<JWSAlgorithm> algs = new HashSet<>();
-        signers.values().stream().map(JWSProvider::supportedJWSAlgorithms).forEach(algs::addAll);
-        verifiers.values().stream().map(JWSProvider::supportedJWSAlgorithms).forEach(algs::addAll);
-
-        return algs;
+    private void loadKeysFromStore(JWKSetKeyStore keyStore) {
+        if (keyStore != null && keyStore.getJwkSet() != null) {
+            for (JWK key : keyStore.getKeys()) {
+                if (StringUtils.hasText(key.getKeyID())) {
+                    this.keys.put(key.getKeyID(), key);
+                } else {
+                    String fakeKid = UUID.randomUUID().toString();
+                    this.keys.put(fakeKid, key);
+                }
+            }
+        }
     }
 
-    private void buildSignersAndVerifiers() {
+    private void initializeSignersAndVerifiers() {
         for (Map.Entry<String, JWK> jwkEntry : keys.entrySet()) {
             String id = jwkEntry.getKey();
             JWK jwk = jwkEntry.getValue();
@@ -184,8 +140,8 @@ public class JWTSigningAndValidationServiceImpl implements JWTSigningAndValidati
             }
         }
 
-        if (defaultSignerKeyId == null && keys.size() == 1) {
-            setDefaultSignerKeyId(keys.keySet().iterator().next());
+        if (signerKeyId == null && keys.size() == 1) {
+            setSignerKeyId(keys.keySet().iterator().next());
         }
     }
 
diff --git a/src/main/java/cz/muni/ics/ga4gh/service/impl/VisaAssemblyParameters.java b/src/main/java/cz/muni/ics/ga4gh/service/impl/VisaAssemblyParameters.java
new file mode 100644
index 0000000..9c1a875
--- /dev/null
+++ b/src/main/java/cz/muni/ics/ga4gh/service/impl/VisaAssemblyParameters.java
@@ -0,0 +1,37 @@
+package cz.muni.ics.ga4gh.service.impl;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import cz.muni.ics.ga4gh.service.JWTSigningAndValidationService;
+import java.net.URI;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import lombok.ToString;
+
+@Getter
+@Setter
+@Builder
+@ToString
+@EqualsAndHashCode
+@NoArgsConstructor
+@AllArgsConstructor
+public class VisaAssemblyParameters {
+
+    private String issuer;
+    private URI jku;
+    private String type;
+    private String sub;
+    private Long userId;
+    private String value;
+    private String source;
+    private String by;
+    private long asserted;
+    private long expires;
+    private JsonNode conditions;
+    private String signer;
+    private JWTSigningAndValidationService jwtService;
+
+}
diff --git a/src/main/java/cz/muni/ics/ga4gh/service/impl/brokers/BbmriGa4ghBroker.java b/src/main/java/cz/muni/ics/ga4gh/service/impl/brokers/BbmriGa4ghBroker.java
index f9a89b7..139752d 100644
--- a/src/main/java/cz/muni/ics/ga4gh/service/impl/brokers/BbmriGa4ghBroker.java
+++ b/src/main/java/cz/muni/ics/ga4gh/service/impl/brokers/BbmriGa4ghBroker.java
@@ -1,190 +1,285 @@
 package cz.muni.ics.ga4gh.service.impl.brokers;
 
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.node.ArrayNode;
-import cz.muni.ics.ga4gh.adapters.PerunAdapter;
-import cz.muni.ics.ga4gh.config.BrokerConfig;
-import cz.muni.ics.ga4gh.config.Ga4ghConfig;
-import cz.muni.ics.ga4gh.model.Affiliation;
-import cz.muni.ics.ga4gh.model.Ga4ghClaimRepository;
+import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.BY_PEER;
+import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.BY_SELF;
+import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.BY_SO;
+import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.BY_SYSTEM;
+import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.TYPE_ACCEPTED_TERMS_AND_POLICIES;
+import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.TYPE_AFFILIATION_AND_ROLE;
+import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.TYPE_CONTROLLED_ACCESS_GRANTS;
+import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.TYPE_LINKED_IDENTITIES;
+import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.TYPE_RESEARCHER_STATUS;
+
+import cz.muni.ics.ga4gh.base.Utils;
+import cz.muni.ics.ga4gh.base.adapters.PerunAdapter;
+import cz.muni.ics.ga4gh.base.model.Affiliation;
+import cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa;
+import cz.muni.ics.ga4gh.base.properties.BrokerInstanceProperties;
+import cz.muni.ics.ga4gh.base.properties.Ga4ghBrokersProperties;
 import cz.muni.ics.ga4gh.service.JWTSigningAndValidationService;
-import cz.muni.ics.ga4gh.utils.Utils;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.context.annotation.Profile;
-import org.springframework.stereotype.Service;
-import org.springframework.util.StringUtils;
-
-import java.net.MalformedURLException;
-import java.net.URISyntaxException;
+import cz.muni.ics.ga4gh.service.PassportAssemblyContext;
+import cz.muni.ics.ga4gh.service.impl.VisaAssemblyParameters;
 import java.sql.Timestamp;
-import java.util.Collections;
-import java.util.HashSet;
 import java.util.List;
-import java.util.Objects;
 import java.util.Set;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.util.StringUtils;
 
-import static cz.muni.ics.ga4gh.model.Ga4ghPassportVisa.BY_PEER;
-import static cz.muni.ics.ga4gh.model.Ga4ghPassportVisa.BY_SELF;
-import static cz.muni.ics.ga4gh.model.Ga4ghPassportVisa.BY_SO;
-import static cz.muni.ics.ga4gh.model.Ga4ghPassportVisa.BY_SYSTEM;
-import static cz.muni.ics.ga4gh.model.Ga4ghPassportVisa.TYPE_ACCEPTED_TERMS_AND_POLICIES;
-import static cz.muni.ics.ga4gh.model.Ga4ghPassportVisa.TYPE_AFFILIATION_AND_ROLE;
-import static cz.muni.ics.ga4gh.model.Ga4ghPassportVisa.TYPE_LINKED_IDENTITIES;
-import static cz.muni.ics.ga4gh.model.Ga4ghPassportVisa.TYPE_RESEARCHER_STATUS;
-
-@Service
-@Profile("bbmri")
+@Slf4j
 public class BbmriGa4ghBroker extends Ga4ghBroker {
 
     private static final String BONA_FIDE_URL = "https://doi.org/10.1038/s41431-018-0219-y";
     private static final String BBMRI_ERIC_ORG_URL = "https://www.bbmri-eric.eu/";
-    private static final String BBMRI_ID = "bbmri_id";
     private static final String FACULTY_AT = "faculty@";
 
+    private final String getBbmriIdAttribute;
     private final String bonaFideStatusAttr;
     private final String groupAffiliationsAttr;
     private final Long termsAndPoliciesGroupId;
 
-    @Autowired
-    public BbmriGa4ghBroker(BrokerConfig brokerConfig, PerunAdapter adapter, JWTSigningAndValidationService jwtService, Ga4ghConfig ga4ghConfig) throws URISyntaxException, MalformedURLException {
-        super(adapter, jwtService, ga4ghConfig, brokerConfig);
+    private final Long bbmriVoId;
+
+    public BbmriGa4ghBroker(BrokerInstanceProperties instanceProperties,
+                            Ga4ghBrokersProperties brokersProperties,
+                            PerunAdapter adapter,
+                            JWTSigningAndValidationService jwtService)
+    {
+        super(instanceProperties, brokersProperties, adapter, jwtService);
+
+        this.getBbmriIdAttribute = instanceProperties.getIdentifierAttribute();
+        this.bonaFideStatusAttr = instanceProperties.getBonaFideStatusAttr();
+        this.groupAffiliationsAttr = instanceProperties.getGroupAffiliationsAttr();
+        this.termsAndPoliciesGroupId = instanceProperties.getTermsAndPoliciesGroupId();
+        this.bbmriVoId = instanceProperties.getMembershipVoId();
+    }
+
+    @Override
+    protected String getSubAttribute() {
+        return getBbmriIdAttribute;
+    }
 
-        bonaFideStatusAttr = brokerConfig.getBonaFideStatusAttr();
-        groupAffiliationsAttr = brokerConfig.getGroupAffiliationsAttr();
-        termsAndPoliciesGroupId = Objects.requireNonNullElse(brokerConfig.getTermsAndPoliciesGroupId(), 10432L);
+    @Override
+    protected Long getCommunityVoId() {
+        return bbmriVoId;
     }
 
     @Override
-    protected void addAffiliationAndRoles(long now, ArrayNode passport, List<Affiliation> affiliations, String sub, Long userId)
+    protected void addAffiliationAndRoles(PassportAssemblyContext ctx)
     {
-        //by=system for users with affiliation asserted by their IdP (set in UserExtSource attribute "affiliation")
-        if (affiliations == null) {
+        String type = TYPE_AFFILIATION_AND_ROLE;
+        logAddingVisas(type);
+        if (!isCommunityMember(ctx.getPerunUserId())) {
+            log.debug("User is not member of the BBMRI community, not adding any {} visas", type);
             return;
         }
-
-        for (Affiliation affiliation: affiliations) {
-            //expires 1 year after the last login from the IdP asserting the affiliation
-            long expires = Utils.getOneYearExpires(affiliation.getAsserted());
-            if (expires < now) {
-                continue;
-            }
-
-            JsonNode visa = createPassportVisa(TYPE_AFFILIATION_AND_ROLE, sub, userId, affiliation.getValue(), affiliation.getSource(), BY_SYSTEM, affiliation.getAsserted(), expires, null);
-            if (visa != null) {
-                passport.add(visa);
-            }
+        Affiliation affiliate = new Affiliation(
+            null, "affiliate@bbmri.eu",System.currentTimeMillis() / 1000L);
+        String value = affiliate.getValue();
+        Ga4ghPassportVisa affiliateVisa = createVisa(
+            VisaAssemblyParameters.builder()
+                .type(type)
+                .sub(ctx.getSubject())
+                .userId(ctx.getPerunUserId())
+                .value(value)
+                .source(affiliate.getSource())
+                .by(BY_SYSTEM)
+                .asserted(affiliate.getAsserted())
+                .expires(Utils.getOneYearExpires(affiliate.getAsserted()))
+                .conditions(null)
+                .build()
+        );
+
+        if (affiliateVisa != null) {
+            ctx.getResultVisas().add(affiliateVisa);
+            logAddedVisa(type, value);
         }
     }
 
     @Override
-    protected void addAcceptedTermsAndPolicies(long now, ArrayNode passport, Long userId, String sub) {
-        //by=self for members of the group 10432 "Bona Fide Researchers"
-        boolean userInGroup = adapter.isUserInGroup(userId, termsAndPoliciesGroupId);
+    protected void addAcceptedTermsAndPolicies(PassportAssemblyContext ctx) {
+        String type = TYPE_ACCEPTED_TERMS_AND_POLICIES;
+        logAddingVisas(type);
+        if (termsAndPoliciesGroupId == null) {
+            log.debug("Group ID for accepted terms and policies is not defined, not adding any {} visas", type);
+            return;
+        }
+
+        boolean userInGroup = adapter.isUserInGroup(ctx.getPerunUserId(), termsAndPoliciesGroupId);
         if (!userInGroup) {
+            log.debug("User is not in the group representing terms and policies approval, not adding any {} visas", type);
             return;
         }
 
-        long asserted = now;
-        if (bonaFideStatusAttr != null) {
-            String bonaFideStatusCreatedAt = adapter.getAdapterRpc().getUserAttributeCreatedAt(userId, bonaFideStatusAttr);
-            if (bonaFideStatusCreatedAt != null) {
-                asserted = Timestamp.valueOf(bonaFideStatusCreatedAt).getTime() / 1000L;
-            }
+        long asserted = ctx.getNow();
+        String bonaFideStatusCreatedAt = adapter.getAdapterRpc()
+            .getUserAttributeCreatedAt(ctx.getPerunUserId(), bonaFideStatusAttr);
+        if (bonaFideStatusCreatedAt != null) {
+            asserted = Timestamp.valueOf(bonaFideStatusCreatedAt).getTime() / 1000L;
         }
 
         long expires = Utils.getExpires(asserted, 100L);
-        if (expires < now) {
-            return;
-        }
 
-        JsonNode visa = createPassportVisa(TYPE_ACCEPTED_TERMS_AND_POLICIES, sub, userId, BONA_FIDE_URL, BBMRI_ERIC_ORG_URL, BY_SELF, asserted, expires, null);
+        String value = BONA_FIDE_URL;
+        Ga4ghPassportVisa visa = createVisa(
+            VisaAssemblyParameters.builder()
+                .type(type)
+                .sub(ctx.getSubject())
+                .userId(ctx.getPerunUserId())
+                .value(value)
+                .source(BBMRI_ERIC_ORG_URL)
+                .by(BY_SELF)
+                .asserted(asserted)
+                .expires(expires)
+                .conditions(null)
+                .build()
+        );
+
         if (visa != null) {
-            passport.add(visa);
+            ctx.getResultVisas().add(visa);
+            logAddedVisa(type, value);
         }
     }
 
     @Override
-    protected void addResearcherStatuses(long now, ArrayNode passport, List<Affiliation> affiliations, String sub, Long userId)
-    {
-        addResearcherStatusFromBonaFideAttribute(now, passport, userId, sub);
-        addResearcherStatusFromAffiliation(affiliations, now, passport, sub, userId);
-        addResearcherStatusGroupAffiliations(now, passport, sub, userId);
-    }
-
-    @Override
-    protected void addControlledAccessGrants(long now, ArrayNode passport, String sub, Long userId) {
-        if (claimRepositories.isEmpty()) {
+    protected void addControlledAccessGrants(PassportAssemblyContext ctx) {
+        String type = TYPE_CONTROLLED_ACCESS_GRANTS;
+        logAddingVisas(type);
+        List<Ga4ghPassportVisa> controlledAccessGrants = ctx.getExternalControlledAccessGrants();
+        if (controlledAccessGrants == null || controlledAccessGrants.isEmpty()) {
+            log.debug("No external {} visas available, not adding any {} visas", type, type);
             return;
         }
+        ctx.getResultVisas().addAll(controlledAccessGrants);
+    }
 
-        Set<String> linkedIdentities = new HashSet<>();
-        for (Ga4ghClaimRepository repo: claimRepositories) {
-            callPermissionsJwtAPI(repo, Collections.singletonMap(BBMRI_ID, sub), passport, linkedIdentities);
-        }
-
-        if (linkedIdentities.isEmpty()) {
+    @Override
+    protected void addLinkedIdentities(PassportAssemblyContext ctx) {
+        String type = TYPE_LINKED_IDENTITIES;
+        logAddingVisas(type);
+        Set<String> externalLinkedIdentities = ctx.getExternalLinkedIdentities();
+        if (externalLinkedIdentities == null || externalLinkedIdentities.isEmpty()) {
+            log.debug("No external {} visas available, not adding any {} visas", type, type);
             return;
         }
-
-        for (String linkedIdentity : linkedIdentities) {
-            long expires = Utils.getOneYearExpires(now);
-
-            JsonNode visa = createPassportVisa(TYPE_LINKED_IDENTITIES, sub, userId, linkedIdentity, BBMRI_ERIC_ORG_URL, BY_SYSTEM, now, expires, null);
+        for (String identity: externalLinkedIdentities) {
+            Ga4ghPassportVisa visa = createVisa(
+                VisaAssemblyParameters.builder()
+                    .type(type)
+                    .sub(ctx.getSubject())
+                    .userId(ctx.getPerunUserId())
+                    .value(identity)
+                    .source(BBMRI_ERIC_ORG_URL)
+                    .by(BY_SYSTEM)
+                    .asserted(ctx.getNow())
+                    .expires(Utils.getOneYearExpires(ctx.getNow()))
+                    .conditions(null)
+                    .build()
+            );
             if (visa != null) {
-                passport.add(visa);
+                ctx.getResultVisas().add(visa);
+                logAddedVisa(type, identity);
             }
         }
     }
 
-    private void addResearcherStatusFromBonaFideAttribute(long now, ArrayNode passport, Long userId, String sub)
+    @Override
+    protected void addResearcherStatuses(PassportAssemblyContext ctx)
     {
-        //by=peer for users with attribute elixirBonaFideStatusREMS
-        String bbmriBonaFideStatusCreatedAt = adapter.getAdapterRpc().getUserAttributeCreatedAt(userId, bonaFideStatusAttr);
+        logAddingVisas(TYPE_RESEARCHER_STATUS);
+        // NOT YET DEFINED
+        /* Below is the approach from LS AAI and ELIXIR AAI
+        addResearcherStatusFromBonaFideAttribute(ctx);
+        addResearcherStatusFromAffiliation(ctx);
+        addResearcherStatusGroupAffiliations(ctx);
+        */
+    }
+
+    private void addResearcherStatusFromBonaFideAttribute(PassportAssemblyContext ctx)
+    {
+        String type = TYPE_RESEARCHER_STATUS;
+        log.debug("Adding {} visa (from bona fide status)", type);
+        if (!StringUtils.hasText(bonaFideStatusAttr)) {
+            log.debug("BonaFideStatus attribute is not defined, not adding any {} visas (from bona fide status)", type);
+            return;
+        }
+        String bbmriBonaFideStatusCreatedAt = adapter.getAdapterRpc()
+            .getUserAttributeCreatedAt(ctx.getPerunUserId(), bonaFideStatusAttr);
 
         if (bbmriBonaFideStatusCreatedAt == null) {
+            log.debug("BBMRI broker - rems bona fide status attr not defined, skipping visa");
             return;
         }
 
         long asserted = Timestamp.valueOf(bbmriBonaFideStatusCreatedAt).getTime() / 1000L;
         long expires = Utils.getOneYearExpires(asserted);
 
-        if (expires > now) {
-            JsonNode visa = createPassportVisa(TYPE_RESEARCHER_STATUS, sub, userId, BONA_FIDE_URL, BBMRI_ERIC_ORG_URL, BY_PEER, asserted, expires, null);
+        String value = BONA_FIDE_URL;
+        if (expires > ctx.getNow()) {
+            Ga4ghPassportVisa visa = createVisa(
+                VisaAssemblyParameters.builder()
+                    .type(type)
+                    .sub(ctx.getSubject())
+                    .userId(ctx.getPerunUserId())
+                    .value(value)
+                    .source(BBMRI_ERIC_ORG_URL)
+                    .by(BY_PEER)
+                    .asserted(asserted)
+                    .expires(expires)
+                    .conditions(null)
+                    .build()
+            );
 
             if (visa != null) {
-                passport.add(visa);
+                ctx.getResultVisas().add(visa);
+                logAddedVisa(type, value);
             }
         }
     }
 
-    private void addResearcherStatusFromAffiliation(List<Affiliation> affiliations, long now, ArrayNode passport, String sub, Long userId)
-    {
-        //by=system for users with faculty affiliation asserted by their IdP (set in UserExtSource attribute "affiliation")
-        if (affiliations == null) {
+    private void addResearcherStatusFromAffiliation(PassportAssemblyContext ctx) {
+        String type = TYPE_RESEARCHER_STATUS;
+        log.debug("Adding {} visa (from affiliations)", type);
+        if (ctx.getIdentityAffiliations() == null || ctx.getIdentityAffiliations().isEmpty()) {
+            log.debug("No affiliations available, not adding any {} visas (from affiliations)", type);
             return;
         }
 
-        for (Affiliation affiliation: affiliations) {
+        for (Affiliation affiliation: ctx.getIdentityAffiliations()) {
             if (!StringUtils.startsWithIgnoreCase(affiliation.getValue(), FACULTY_AT)) {
                 continue;
             }
 
-            long expires = Utils.getOneYearExpires(affiliation.getAsserted());
-            if (expires < now) {
-                continue;
-            }
+            String value = BONA_FIDE_URL;
+            Ga4ghPassportVisa visa = createVisa(
+                VisaAssemblyParameters.builder()
+                    .type(type)
+                    .sub(ctx.getSubject())
+                    .userId(ctx.getPerunUserId())
+                    .value(value)
+                    .source(affiliation.getSource())
+                    .by(BY_SYSTEM)
+                    .asserted(affiliation.getAsserted())
+                    .expires(Utils.getOneYearExpires(affiliation.getAsserted()))
+                    .conditions(null)
+                    .build()
+            );
 
-            JsonNode visa = createPassportVisa(TYPE_RESEARCHER_STATUS, sub, userId, BONA_FIDE_URL, affiliation.getSource(), BY_SYSTEM, affiliation.getAsserted(), expires, null);
             if (visa != null) {
-                passport.add(visa);
+                ctx.getResultVisas().add(visa);
+                logAddedVisa(type, value);
             }
         }
     }
 
-    private void addResearcherStatusGroupAffiliations(long now, ArrayNode passport, String sub, Long userId) {
-        //by=so for users with faculty affiliation asserted by membership in a group with groupAffiliations attribute
-        List<Affiliation> groupAffiliations = adapter.getGroupAffiliations(userId, groupAffiliationsAttr);
-        if (groupAffiliations == null) {
+    private void addResearcherStatusGroupAffiliations(PassportAssemblyContext ctx) {
+        String type = TYPE_RESEARCHER_STATUS;
+        log.debug("Adding {} visa (from group affiliations)", type);
+        if (!StringUtils.hasText(groupAffiliationsAttr)) {
+            log.debug("GroupAffiliations attribute is not defined, not adding any {} visas (from group affiliations)", type);
+            return;
+        }
+        List<Affiliation> groupAffiliations = adapter.getGroupAffiliations(ctx.getPerunUserId(), bbmriVoId, groupAffiliationsAttr);
+        if (groupAffiliations == null || groupAffiliations.isEmpty()) {
             return;
         }
 
@@ -193,12 +288,26 @@ public class BbmriGa4ghBroker extends Ga4ghBroker {
                 continue;
             }
 
-            long expires = Utils.getOneYearExpires(now);
+            String value = BONA_FIDE_URL;
+            Ga4ghPassportVisa visa = createVisa(
+                VisaAssemblyParameters.builder()
+                    .type(type)
+                    .sub(ctx.getSubject())
+                    .userId(ctx.getPerunUserId())
+                    .value(value)
+                    .source(BBMRI_ERIC_ORG_URL)
+                    .by(BY_SO)
+                    .asserted(affiliation.getAsserted())
+                    .expires(Utils.getOneYearExpires(affiliation.getAsserted()))
+                    .conditions(null)
+                    .build()
+            );
 
-            JsonNode visa = createPassportVisa(TYPE_RESEARCHER_STATUS, sub, userId, BONA_FIDE_URL, BBMRI_ERIC_ORG_URL, BY_SO, affiliation.getAsserted(), expires, null);
             if (visa != null) {
-                passport.add(visa);
+                ctx.getResultVisas().add(visa);
+                logAddedVisa(type, value);
             }
         }
     }
+
 }
diff --git a/src/main/java/cz/muni/ics/ga4gh/service/impl/brokers/ElixirGa4ghBroker.java b/src/main/java/cz/muni/ics/ga4gh/service/impl/brokers/ElixirGa4ghBroker.java
index cdef905..8e7ef77 100644
--- a/src/main/java/cz/muni/ics/ga4gh/service/impl/brokers/ElixirGa4ghBroker.java
+++ b/src/main/java/cz/muni/ics/ga4gh/service/impl/brokers/ElixirGa4ghBroker.java
@@ -1,144 +1,209 @@
 package cz.muni.ics.ga4gh.service.impl.brokers;
 
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.node.ArrayNode;
-import cz.muni.ics.ga4gh.adapters.PerunAdapter;
-import cz.muni.ics.ga4gh.config.BrokerConfig;
-import cz.muni.ics.ga4gh.config.Ga4ghConfig;
-import cz.muni.ics.ga4gh.model.Affiliation;
-import cz.muni.ics.ga4gh.model.Ga4ghClaimRepository;
+import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.BY_PEER;
+import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.BY_SELF;
+import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.BY_SO;
+import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.BY_SYSTEM;
+import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.TYPE_ACCEPTED_TERMS_AND_POLICIES;
+import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.TYPE_AFFILIATION_AND_ROLE;
+import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.TYPE_CONTROLLED_ACCESS_GRANTS;
+import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.TYPE_LINKED_IDENTITIES;
+import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.TYPE_RESEARCHER_STATUS;
+
+import cz.muni.ics.ga4gh.base.Utils;
+import cz.muni.ics.ga4gh.base.adapters.PerunAdapter;
+import cz.muni.ics.ga4gh.base.model.Affiliation;
+import cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa;
+import cz.muni.ics.ga4gh.base.properties.BrokerInstanceProperties;
+import cz.muni.ics.ga4gh.base.properties.Ga4ghBrokersProperties;
 import cz.muni.ics.ga4gh.service.JWTSigningAndValidationService;
-import cz.muni.ics.ga4gh.utils.Utils;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.context.annotation.Profile;
-import org.springframework.stereotype.Service;
-import org.springframework.util.StringUtils;
-
-import java.net.MalformedURLException;
-import java.net.URISyntaxException;
+import cz.muni.ics.ga4gh.service.PassportAssemblyContext;
+import cz.muni.ics.ga4gh.service.impl.VisaAssemblyParameters;
 import java.sql.Timestamp;
-import java.util.Collections;
-import java.util.HashSet;
 import java.util.List;
-import java.util.Objects;
 import java.util.Set;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.util.StringUtils;
 
-import static cz.muni.ics.ga4gh.model.Ga4ghPassportVisa.BY_PEER;
-import static cz.muni.ics.ga4gh.model.Ga4ghPassportVisa.BY_SELF;
-import static cz.muni.ics.ga4gh.model.Ga4ghPassportVisa.BY_SO;
-import static cz.muni.ics.ga4gh.model.Ga4ghPassportVisa.BY_SYSTEM;
-import static cz.muni.ics.ga4gh.model.Ga4ghPassportVisa.TYPE_ACCEPTED_TERMS_AND_POLICIES;
-import static cz.muni.ics.ga4gh.model.Ga4ghPassportVisa.TYPE_AFFILIATION_AND_ROLE;
-import static cz.muni.ics.ga4gh.model.Ga4ghPassportVisa.TYPE_LINKED_IDENTITIES;
-import static cz.muni.ics.ga4gh.model.Ga4ghPassportVisa.TYPE_RESEARCHER_STATUS;
-
-@Service
-@Profile("elixir")
+@Slf4j
 public class ElixirGa4ghBroker extends Ga4ghBroker {
 
     private static final String BONA_FIDE_URL = "https://doi.org/10.1038/s41431-018-0219-y";
     private static final String ELIXIR_ORG_URL = "https://elixir-europe.org/";
-    private static final String ELIXIR_ID = "elixir_id";
     private static final String FACULTY_AT = "faculty@";
 
+    private final String elixirIdAttribute;
     private final String bonaFideStatusAttr;
     private final String bonaFideStatusREMSAttr;
     private final String groupAffiliationsAttr;
     private final Long termsAndPoliciesGroupId;
 
-    @Autowired
-    public ElixirGa4ghBroker(BrokerConfig brokerConfig, Ga4ghConfig ga4ghConfig, PerunAdapter adapter, JWTSigningAndValidationService jwtService) throws URISyntaxException, MalformedURLException {
-        super(adapter, jwtService, ga4ghConfig, brokerConfig);
+    private final Long elixirVoId;
 
-        bonaFideStatusAttr = brokerConfig.getBonaFideStatusAttr();
-        bonaFideStatusREMSAttr = brokerConfig.getBonaFideStatusRemsAttr();
-        groupAffiliationsAttr = brokerConfig.getGroupAffiliationsAttr();
-        termsAndPoliciesGroupId = Objects.requireNonNullElse(brokerConfig.getTermsAndPoliciesGroupId(), 10432L);
+    public ElixirGa4ghBroker(BrokerInstanceProperties instanceProperties,
+                             Ga4ghBrokersProperties brokersProperties,
+                             PerunAdapter adapter,
+                             JWTSigningAndValidationService jwtService)
+    {
+        super(instanceProperties, brokersProperties, adapter, jwtService);
+
+        this.elixirIdAttribute = instanceProperties.getIdentifierAttribute();
+        this.bonaFideStatusAttr = instanceProperties.getBonaFideStatusAttr();
+        this.bonaFideStatusREMSAttr = instanceProperties.getBonaFideStatusRemsAttr();
+        this.groupAffiliationsAttr = instanceProperties.getGroupAffiliationsAttr();
+        this.termsAndPoliciesGroupId = instanceProperties.getTermsAndPoliciesGroupId();
+        this.elixirVoId = instanceProperties.getMembershipVoId();
     }
 
     @Override
-    protected void addAffiliationAndRoles(long now, ArrayNode passport, List<Affiliation> affiliations, String sub, Long userId) {
-        if (affiliations == null) {
-            return;
-        }
-
-        for (Affiliation affiliation: affiliations) {
-            long expires = Utils.getOneYearExpires(affiliation.getAsserted());
-
-            if (expires < now) {
-                continue;
-            }
+    protected String getSubAttribute() {
+        return elixirIdAttribute;
+    }
 
-            JsonNode visa = createPassportVisa(TYPE_AFFILIATION_AND_ROLE, sub, userId, affiliation.getValue(),
-                    affiliation.getSource(), BY_SYSTEM, affiliation.getAsserted(), expires, null);
+    @Override
+    protected Long getCommunityVoId() {
+        return elixirVoId;
+    }
 
-            if (visa != null) {
-                passport.add(visa);
-            }
+    @Override
+    protected void addAffiliationAndRoles(PassportAssemblyContext ctx)
+    {
+        String type = TYPE_AFFILIATION_AND_ROLE;
+        logAddingVisas(type);
+        if (!isCommunityMember(ctx.getPerunUserId())) {
+            log.debug("User is not member of the ELIXIR community, not adding any {} visas", type);
+            return;
+        }
+        Affiliation affiliate = new Affiliation(null,
+            "affiliate@elixir-europe.org", System.currentTimeMillis() / 1000L);
+        Ga4ghPassportVisa affiliateVisa = createVisa(
+            VisaAssemblyParameters.builder()
+                .type(type)
+                .sub(ctx.getSubject())
+                .userId(ctx.getPerunUserId())
+                .value(affiliate.getValue())
+                .source(affiliate.getSource())
+                .by(BY_SYSTEM)
+                .asserted(affiliate.getAsserted())
+                .expires(Utils.getOneYearExpires(affiliate.getAsserted()))
+                .conditions(null)
+                .build()
+        );
+
+        if (affiliateVisa != null) {
+            ctx.getResultVisas().add(affiliateVisa);
+            logAddedVisa(type, affiliate.getValue());
         }
     }
 
     @Override
-    protected void addAcceptedTermsAndPolicies(long now, ArrayNode passport, Long userId, String sub) {
-        boolean userInGroup = adapter.isUserInGroup(userId, termsAndPoliciesGroupId);
+    protected void addAcceptedTermsAndPolicies(PassportAssemblyContext ctx) {
+        String type = TYPE_ACCEPTED_TERMS_AND_POLICIES;
+        logAddingVisas(type);
+        if (termsAndPoliciesGroupId == null) {
+            log.debug("Group ID for accepted terms and policies is not defined, not adding any {} visas", type);
+            return;
+        }
+
+        boolean userInGroup = adapter.isUserInGroup(ctx.getPerunUserId(), termsAndPoliciesGroupId);
         if (!userInGroup) {
+            log.debug("User is not in the group representing terms and policies approval, not adding any {} visas", type);
             return;
         }
 
-        long asserted = now;
-        if (bonaFideStatusAttr != null) {
-            String bonaFideStatusCreatedAt = adapter.getAdapterRpc().getUserAttributeCreatedAt(userId, bonaFideStatusAttr);
+        long asserted = ctx.getNow();
+        if (StringUtils.hasText(bonaFideStatusAttr)) {
+            String bonaFideStatusCreatedAt = adapter.getAdapterRpc()
+                .getUserAttributeCreatedAt(ctx.getPerunUserId(), bonaFideStatusAttr);
             if (bonaFideStatusCreatedAt != null) {
                 asserted = Timestamp.valueOf(bonaFideStatusCreatedAt).getTime() / 1000L;
             }
         }
 
         long expires = Utils.getExpires(asserted, 100L);
-        if (expires < now) {
-            return;
-        }
 
-        JsonNode visa = createPassportVisa(TYPE_ACCEPTED_TERMS_AND_POLICIES, sub, userId, BONA_FIDE_URL, ELIXIR_ORG_URL, BY_SELF, asserted, expires, null);
+        String value = BONA_FIDE_URL;
+        Ga4ghPassportVisa visa = createVisa(
+            VisaAssemblyParameters.builder()
+                .type(type)
+                .sub(ctx.getSubject())
+                .userId(ctx.getPerunUserId())
+                .value(value)
+                .source(ELIXIR_ORG_URL)
+                .by(BY_SELF)
+                .asserted(asserted)
+                .expires(expires)
+                .conditions(null)
+                .build()
+        );
+
         if (visa != null) {
-            passport.add(visa);
+            ctx.getResultVisas().add(visa);
+            logAddedVisa(type, value);
         }
-
     }
 
     @Override
-    protected void addResearcherStatuses(long now, ArrayNode passport, List<Affiliation> affiliations, String sub, Long userId)
-    {
-        addResearcherStatusFromBonaFideAttribute(now, passport, sub, userId);
-        addResearcherStatusFromAffiliation(affiliations, now, passport, sub, userId);
-        addResearcherStatusGroupAffiliations(now, passport, userId, sub);
+    protected void addControlledAccessGrants(PassportAssemblyContext ctx) {
+        String type = TYPE_CONTROLLED_ACCESS_GRANTS;
+        logAddingVisas(type);
+        List<Ga4ghPassportVisa> controlledAccessGrants = ctx.getExternalControlledAccessGrants();
+        if (controlledAccessGrants == null || controlledAccessGrants.isEmpty()) {
+            log.debug("No external {} visas available, not adding any {} visas", type, type);
+            return;
+        }
+        ctx.getResultVisas().addAll(controlledAccessGrants);
     }
 
     @Override
-    protected void addControlledAccessGrants(long now, ArrayNode passport, String sub, Long userId) {
-        if (claimRepositories.isEmpty()) {
-            return;
-        }
-        Set<String> linkedIdentities = new HashSet<>();
-        for (Ga4ghClaimRepository repo: claimRepositories) {
-            callPermissionsJwtAPI(repo, Collections.singletonMap(ELIXIR_ID, sub), passport, linkedIdentities);
-        }
-        if (linkedIdentities.isEmpty()) {
+    protected void addLinkedIdentities(PassportAssemblyContext ctx) {
+        String type = TYPE_LINKED_IDENTITIES;
+        logAddingVisas(type);
+        Set<String> externalLinkedIdentities = ctx.getExternalLinkedIdentities();
+        if (externalLinkedIdentities == null || externalLinkedIdentities.isEmpty()) {
+            log.debug("No external {} visas available, not adding any {} visas", type, type);
             return;
         }
-        for (String linkedIdentity : linkedIdentities) {
-            long expires = Utils.getOneYearExpires(now);
-            JsonNode visa = createPassportVisa(TYPE_LINKED_IDENTITIES, sub, userId, linkedIdentity,
-                    ELIXIR_ORG_URL, BY_SYSTEM, now, expires, null);
+        for (String identity: externalLinkedIdentities) {
+            Ga4ghPassportVisa visa = createVisa(
+                VisaAssemblyParameters.builder()
+                    .type(type)
+                    .sub(ctx.getSubject())
+                    .userId(ctx.getPerunUserId())
+                    .value(identity)
+                    .source(ELIXIR_ORG_URL)
+                    .by(BY_SYSTEM)
+                    .asserted(ctx.getNow())
+                    .expires(Utils.getOneYearExpires(ctx.getNow()))
+                    .conditions(null)
+                    .build()
+            );
             if (visa != null) {
-                passport.add(visa);
+                ctx.getResultVisas().add(visa);
+                logAddedVisa(type, identity);
             }
         }
     }
 
-    private void addResearcherStatusFromBonaFideAttribute(long now, ArrayNode passport, String sub, Long userId)
+    @Override
+    protected void addResearcherStatuses(PassportAssemblyContext ctx)
     {
-        String elixirBonaFideStatusREMSCreatedAt = adapter.getAdapterRpc().getUserAttributeCreatedAt(userId, bonaFideStatusREMSAttr);
+        logAddingVisas(TYPE_RESEARCHER_STATUS);
+        addResearcherStatusFromRemsBonaFideAttribute(ctx);
+        addResearcherStatusFromAffiliation(ctx);
+        addResearcherStatusFromGroupAffiliations(ctx);
+    }
+
+    private void addResearcherStatusFromRemsBonaFideAttribute(PassportAssemblyContext ctx) {
+        String type = TYPE_RESEARCHER_STATUS;
+        log.debug("Adding {} visa (from REMS bona fide status)", type);
+        if (!StringUtils.hasText(bonaFideStatusREMSAttr)) {
+            log.debug("REMS bonaFideStatus attribute is not defined, not adding any {} visas (from REMS bona fide status)", type);
+            return;
+        }
 
+        String elixirBonaFideStatusREMSCreatedAt = adapter.getAdapterRpc()
+            .getUserAttributeCreatedAt(ctx.getPerunUserId(), bonaFideStatusREMSAttr);
         if (elixirBonaFideStatusREMSCreatedAt == null) {
             return;
         }
@@ -146,42 +211,70 @@ public class ElixirGa4ghBroker extends Ga4ghBroker {
         long asserted = Timestamp.valueOf(elixirBonaFideStatusREMSCreatedAt).getTime() / 1000L;
         long expires = Utils.getOneYearExpires(asserted);
 
-        if (expires < now) {
-            return;
-        }
-
-        JsonNode visa = createPassportVisa(TYPE_RESEARCHER_STATUS, sub, userId, BONA_FIDE_URL, ELIXIR_ORG_URL, BY_PEER, asserted, expires, null);
+        String value = BONA_FIDE_URL;
+        Ga4ghPassportVisa visa = createVisa(
+            VisaAssemblyParameters.builder()
+                .type(type)
+                .sub(ctx.getSubject())
+                .userId(ctx.getPerunUserId())
+                .value(value)
+                .source(ELIXIR_ORG_URL)
+                .by(BY_PEER)
+                .asserted(asserted)
+                .expires(expires)
+                .conditions(null)
+                .build()
+        );
         if (visa != null) {
-            passport.add(visa);
+            ctx.getResultVisas().add(visa);
+            logAddedVisa(type, value);
         }
     }
 
-    private void addResearcherStatusFromAffiliation(List<Affiliation> affiliations, long now, ArrayNode passport, String sub, Long userId)
-    {
-        if (affiliations == null) {
+    private void addResearcherStatusFromAffiliation(PassportAssemblyContext ctx) {
+        String type = TYPE_RESEARCHER_STATUS;
+        log.debug("Adding {} visa (from affiliations)", type);
+        if (ctx.getIdentityAffiliations() == null || ctx.getIdentityAffiliations().isEmpty()) {
+            log.debug("No affiliations available, not adding any {} visas (from affiliations)", type);
             return;
         }
 
-        for (Affiliation affiliation: affiliations) {
+        for (Affiliation affiliation: ctx.getIdentityAffiliations()) {
             if (!StringUtils.startsWithIgnoreCase(affiliation.getValue(), FACULTY_AT)) {
                 continue;
             }
 
-            long expires = Utils.getOneYearExpires(affiliation.getAsserted());
-            if (expires < now) {
-                continue;
-            }
-
-            JsonNode visa = createPassportVisa(TYPE_RESEARCHER_STATUS, sub, userId, BONA_FIDE_URL, affiliation.getSource(), BY_SYSTEM, affiliation.getAsserted(), expires, null);
+            String value = BONA_FIDE_URL;
+            Ga4ghPassportVisa visa = createVisa(
+                VisaAssemblyParameters.builder()
+                    .type(type)
+                    .sub(ctx.getSubject())
+                    .userId(ctx.getPerunUserId())
+                    .value(value)
+                    .source(affiliation.getSource())
+                    .by(BY_SYSTEM)
+                    .asserted(affiliation.getAsserted())
+                    .expires(Utils.getOneYearExpires(affiliation.getAsserted()))
+                    .conditions(null)
+                    .build()
+            );
             if (visa != null) {
-                passport.add(visa);
+                ctx.getResultVisas().add(visa);
+                logAddedVisa(type, value);
             }
         }
     }
 
-    private void addResearcherStatusGroupAffiliations(long now, ArrayNode passport, Long userId, String sub) {
-        List<Affiliation> groupAffiliations = adapter.getGroupAffiliations(userId, groupAffiliationsAttr);
-        if (groupAffiliations == null) {
+    private void addResearcherStatusFromGroupAffiliations(PassportAssemblyContext ctx) {
+        String type = TYPE_RESEARCHER_STATUS;
+        log.debug("Adding {} visa (from group affiliations)", type);
+        if (!StringUtils.hasText(groupAffiliationsAttr)) {
+            log.debug("GroupAffiliations attribute is not defined, not adding any {} visas (from group affiliations)", type);
+            return;
+        }
+        List<Affiliation> groupAffiliations = adapter.getGroupAffiliations(
+            ctx.getPerunUserId(), elixirVoId, groupAffiliationsAttr);
+        if (groupAffiliations == null || groupAffiliations.isEmpty()) {
             return;
         }
 
@@ -190,12 +283,27 @@ public class ElixirGa4ghBroker extends Ga4ghBroker {
                 continue;
             }
 
-            long expires = Utils.getOneYearExpires(now);
-
-            JsonNode visa = createPassportVisa(TYPE_RESEARCHER_STATUS, sub, userId, BONA_FIDE_URL, ELIXIR_ORG_URL, BY_SO, affiliation.getAsserted(), expires, null);
+            long expires = Utils.getOneYearExpires(ctx.getNow());
+
+            String value = BONA_FIDE_URL;
+            Ga4ghPassportVisa visa = createVisa(
+                VisaAssemblyParameters.builder()
+                    .type(type)
+                    .sub(ctx.getSubject())
+                    .userId(ctx.getPerunUserId())
+                    .value(value)
+                    .source(ELIXIR_ORG_URL)
+                    .by(BY_SO)
+                    .asserted(affiliation.getAsserted())
+                    .expires(Utils.getOneYearExpires(affiliation.getAsserted()))
+                    .conditions(null)
+                    .build()
+            );
             if (visa != null) {
-                passport.add(visa);
+                ctx.getResultVisas().add(visa);
+                logAddedVisa(type, value);
             }
         }
     }
+
 }
diff --git a/src/main/java/cz/muni/ics/ga4gh/service/impl/brokers/Ga4ghBroker.java b/src/main/java/cz/muni/ics/ga4gh/service/impl/brokers/Ga4ghBroker.java
index 51e8cc5..df82784 100644
--- a/src/main/java/cz/muni/ics/ga4gh/service/impl/brokers/Ga4ghBroker.java
+++ b/src/main/java/cz/muni/ics/ga4gh/service/impl/brokers/Ga4ghBroker.java
@@ -2,7 +2,6 @@ package cz.muni.ics.ga4gh.service.impl.brokers;
 
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.node.ArrayNode;
 import com.fasterxml.jackson.databind.node.JsonNodeFactory;
 import com.nimbusds.jose.JOSEObjectType;
 import com.nimbusds.jose.JWSAlgorithm;
@@ -11,31 +10,33 @@ import com.nimbusds.jose.jwk.source.RemoteJWKSet;
 import com.nimbusds.jose.proc.SecurityContext;
 import com.nimbusds.jwt.JWTClaimsSet;
 import com.nimbusds.jwt.SignedJWT;
-import cz.muni.ics.ga4gh.adapters.PerunAdapter;
-import cz.muni.ics.ga4gh.config.BrokerConfig;
-import cz.muni.ics.ga4gh.config.Ga4ghConfig;
-import cz.muni.ics.ga4gh.model.Affiliation;
-import cz.muni.ics.ga4gh.model.Ga4ghClaimRepository;
-import cz.muni.ics.ga4gh.model.Ga4ghPassportVisa;
+import cz.muni.ics.ga4gh.base.Utils;
+import cz.muni.ics.ga4gh.base.adapters.PerunAdapter;
+import cz.muni.ics.ga4gh.base.model.Affiliation;
+import cz.muni.ics.ga4gh.base.model.Ga4ghClaimRepository;
+import cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa;
+import cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisaV1;
+import cz.muni.ics.ga4gh.base.properties.BrokerInstanceProperties;
+import cz.muni.ics.ga4gh.base.properties.Ga4ghBrokersProperties;
 import cz.muni.ics.ga4gh.service.JWTSigningAndValidationService;
-import cz.muni.ics.ga4gh.utils.Utils;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.http.MediaType;
-import org.springframework.web.client.HttpClientErrorException;
-
+import cz.muni.ics.ga4gh.service.PassportAssemblyContext;
+import cz.muni.ics.ga4gh.service.impl.VisaAssemblyParameters;
 import java.io.IOException;
-import java.net.MalformedURLException;
 import java.net.URI;
-import java.net.URISyntaxException;
-import java.net.URL;
 import java.time.Instant;
 import java.util.ArrayList;
-import java.util.Date;
+import java.util.Collections;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.UUID;
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.MediaType;
+import org.springframework.util.StringUtils;
+import org.springframework.web.client.HttpClientErrorException;
 
 @Slf4j
 public abstract class Ga4ghBroker {
@@ -43,13 +44,18 @@ public abstract class Ga4ghBroker {
     public static final String GA4GH_CLAIM = "ga4gh_passport_v1";
     public static final String JSON = "json";
 
+    public static final String REPOSITORY_URL_USER_ID = "user_id";
+
     protected final List<Ga4ghClaimRepository> claimRepositories = new ArrayList<>();
     protected final Map<URI, RemoteJWKSet<SecurityContext>> remoteJwkSets = new HashMap<>();
     protected final Map<URI, String> signers = new HashMap<>();
-    protected final ObjectMapper mapper = new ObjectMapper();
 
-    private final String issuer;
-    private final URI jku;
+    protected final String issuer;
+    protected final URI jku;
+
+    @Getter
+    private final String brokerName;
+
     protected PerunAdapter adapter;
 
     protected JWTSigningAndValidationService jwtService;
@@ -58,119 +64,166 @@ public abstract class Ga4ghBroker {
 
     private final String orgUrlAttr;
 
-    public Ga4ghBroker(PerunAdapter adapter, JWTSigningAndValidationService jwtService, Ga4ghConfig config, BrokerConfig brokerConfig) throws URISyntaxException, MalformedURLException {
+    public Ga4ghBroker(BrokerInstanceProperties instanceProperties,
+                       Ga4ghBrokersProperties brokersProperties,
+                       PerunAdapter adapter,
+                       JWTSigningAndValidationService jwtService)
+    {
         this.adapter = adapter;
+        this.affiliationsAttr = instanceProperties.getAffiliationsAttr();
+        this.orgUrlAttr = instanceProperties.getOrgUrlAttr();
+        this.issuer = brokersProperties.getIssuer();
+        this.jku = brokersProperties.getJku();
         this.jwtService = jwtService;
-        this.affiliationsAttr = brokerConfig.getAffiliationsAttr();
-        this.orgUrlAttr = brokerConfig.getOrgUrlAttr();
-        this.issuer = brokerConfig.getIssuer();
-        this.jku = new URL(brokerConfig.getJku()).toURI();
-
-        Utils.parseConfigFile(config, claimRepositories, remoteJwkSets, signers);
+        this.brokerName = instanceProperties.getName();
+        Utils.initializeClaimRepositories(instanceProperties, claimRepositories, remoteJwkSets, signers);
     }
 
-    public ArrayNode constructGa4ghPassportVisa(Long userId, String sub) {
-        List<Affiliation> affiliations = adapter.getAdapterRpc().getUserExtSourcesAffiliations(userId, affiliationsAttr, orgUrlAttr);
+    public List<Ga4ghPassportVisa> constructGa4ghPassportVisas(Long userId) {
+        if (!isCommunityMember(userId)) {
+            return new ArrayList<>();
+        }
+
+        String communityIdentifier = getUserSub(userId);
+
+        List<Affiliation> identityAffiliations = new ArrayList<>();
+        List<Ga4ghPassportVisa> controlledAccessGrantsFromRepositories = new ArrayList<>();
+        Set<String> linkedIdentitiesFromRepositories = new HashSet<>();
+
+        fillIdentityAffiliations(identityAffiliations, userId);
+        callExternalRepositories(communityIdentifier, controlledAccessGrantsFromRepositories, linkedIdentitiesFromRepositories);
 
-        ArrayNode ga4gh_passport_v1 = JsonNodeFactory.instance.arrayNode();
         long now = Instant.now().getEpochSecond();
 
-        addAffiliationAndRoles(now, ga4gh_passport_v1, affiliations, sub, userId);
-        addAcceptedTermsAndPolicies(now, ga4gh_passport_v1, userId, sub);
-        addResearcherStatuses(now, ga4gh_passport_v1, affiliations, sub, userId);
-        addControlledAccessGrants(now, ga4gh_passport_v1, sub, userId);
+        PassportAssemblyContext ctx = PassportAssemblyContext.builder()
+            .resultVisas(new ArrayList<>())
+            .perunAdapter(adapter)
+            .identityAffiliations(identityAffiliations)
+            .externalControlledAccessGrants(controlledAccessGrantsFromRepositories)
+            .externalLinkedIdentities(linkedIdentitiesFromRepositories)
+            .perunUserId(userId)
+            .subject(communityIdentifier)
+            .now(now)
+            .build();
+
+        addAffiliationAndRoles(ctx);
+        addAcceptedTermsAndPolicies(ctx);
+        addResearcherStatuses(ctx);
+        addControlledAccessGrants(ctx);
+        addLinkedIdentities(ctx);
+
+        return ctx.getResultVisas();
+    }
+
+    private void callExternalRepositories(String sub,
+                                          List<Ga4ghPassportVisa> controlledAccessGrantsFromRepositories,
+                                          Set<String> linkedIdentitiesFromRepositories)
+    {
+        for (Ga4ghClaimRepository repository: claimRepositories) {
+            callPermissionsJwtAPI(
+                repository,
+                Collections.singletonMap(REPOSITORY_URL_USER_ID, sub),
+                controlledAccessGrantsFromRepositories,
+                linkedIdentitiesFromRepositories
+            );
+        }
 
-        return ga4gh_passport_v1;
     }
 
+    private void fillIdentityAffiliations(List<Affiliation> affiliationsList, Long userId) {
+        if (affiliationsList == null) {
+            affiliationsList = new ArrayList<>();
+        }
+        if (!StringUtils.hasText(affiliationsAttr) || !StringUtils.hasText(orgUrlAttr)) {
+            log.warn("Affiliations attribute or orgUrl attribute is not defined");
+        } else {
+            List<Affiliation> affiliations = adapter.getAdapterRpc()
+                .getUserExtSourcesAffiliations(userId, affiliationsAttr, orgUrlAttr);
+            if (affiliations != null) {
+                affiliationsList.addAll(affiliations);
+            }
+        }
+    }
 
-    protected abstract void addAffiliationAndRoles(long now, ArrayNode passport, List<Affiliation> affiliations, String sub, Long userId);
+    protected abstract String getSubAttribute();
 
-    protected abstract void addAcceptedTermsAndPolicies(long now, ArrayNode passport, Long userId, String sub);
+    protected abstract Long getCommunityVoId();
 
-    protected abstract void addResearcherStatuses(long now, ArrayNode passport, List<Affiliation> affiliations, String sub, Long userId);
+    protected abstract void addAffiliationAndRoles(PassportAssemblyContext ctx);
 
-    protected abstract void addControlledAccessGrants(long now, ArrayNode passport, String sub, Long userId);
+    protected abstract void addAcceptedTermsAndPolicies(PassportAssemblyContext ctx);
 
-    protected JsonNode createPassportVisa(String type, String sub, Long userId, String value, String source,
-                                          String by, long asserted, long expires, JsonNode condition)
-    {
-        long now = System.currentTimeMillis() / 1000L;
+    protected abstract void addResearcherStatuses(PassportAssemblyContext ctx);
 
-        if (asserted > now) {
-            log.warn("Visa asserted in future, it will be ignored!");
-            log.debug("Visa information: perunUserId={}, sub={}, type={}, value={}, source={}, by={}, asserted={}",
-                    userId, sub, type, value, source, by, Instant.ofEpochSecond(asserted));
+    protected abstract void addControlledAccessGrants(PassportAssemblyContext ctx);
 
-            return null;
-        }
+    protected abstract void addLinkedIdentities(PassportAssemblyContext ctx);
 
-        if (expires <= now) {
-            log.warn("Visa is expired, it will be ignored!");
-            log.debug("Visa information: perunUserId={}, sub={}, type={}, value={}, source={}, by={}, expired={}",
-                    userId, sub, type, value, source, by, Instant.ofEpochSecond(expires));
+    protected String getUserSub(Long userId) {
+        return adapter.getUserSub(userId, getSubAttribute());
+    }
 
-            return null;
+    protected boolean isCommunityMember(Long userId) {
+        Long communityVoId = getCommunityVoId();
+        if (communityVoId == null) {
+            return true;
         }
+        return adapter.isUserInVo(userId, communityVoId);
+    }
 
-        Map<String, Object> passportVisaObject = new HashMap<>();
-        passportVisaObject.put(Ga4ghPassportVisa.TYPE, type);
-        passportVisaObject.put(Ga4ghPassportVisa.ASSERTED, asserted);
-        passportVisaObject.put(Ga4ghPassportVisa.VALUE, value);
-        passportVisaObject.put(Ga4ghPassportVisa.SOURCE, source);
-        passportVisaObject.put(Ga4ghPassportVisa.BY, by);
+    protected void logAddingVisas(String type) {
+        log.info("Adding '{}' visas", type);
+    }
 
-        if (condition != null && !condition.isNull() && !condition.isMissingNode()) {
-            passportVisaObject.put(Ga4ghPassportVisa.CONDITION, condition);
-        }
+    protected void logAddedVisa(String type, String value) {
+        log.debug("Added '{}' visa with value '{}'", type, value);
+    }
 
-        JWSHeader jwsHeader = new JWSHeader.Builder(JWSAlgorithm.parse(jwtService.getDefaultSigningAlgorithm().getName()))
-                .keyID(jwtService.getDefaultSignerKeyId())
-                .type(JOSEObjectType.JWT)
-                .jwkURL(jku)
-                .build();
-
-        JWTClaimsSet jwtClaimsSet = new JWTClaimsSet.Builder()
-                .issuer(issuer)
-                .issueTime(new Date())
-                .expirationTime(new Date(expires * 1000L))
-                .subject(sub)
-                .jwtID(UUID.randomUUID().toString())
-                .claim(Ga4ghPassportVisa.GA4GH_VISA_V1, passportVisaObject)
-                .build();
-
-        SignedJWT myToken = new SignedJWT(jwsHeader, jwtClaimsSet);
-        jwtService.signJwt(myToken);
-
-        return JsonNodeFactory.instance.textNode(myToken.serialize());
+    protected Ga4ghPassportVisa createVisa(VisaAssemblyParameters parameters)
+    {
+        parameters.setIssuer(issuer);
+        parameters.setJku(jku);
+        parameters.setSigner(issuer);
+        parameters.setJwtService(jwtService);
+        return Utils.createVisa(parameters);
     }
 
     protected void callPermissionsJwtAPI(Ga4ghClaimRepository repo,
                                          Map<String, String> uriVariables,
-                                         ArrayNode passport,
-                                         Set<String> linkedIdentities)
+                                         List<Ga4ghPassportVisa> controlledAccessGrantsList,
+                                         Set<String> linkedIdentitiesSet)
     {
-        log.debug("GA4GH: {}", uriVariables);
+        log.debug("Calling claim repository '{}' with parameters '{}'", repo, uriVariables);
         JsonNode response = callHttpJsonAPI(repo, uriVariables);
-        if (response != null) {
-            JsonNode visas = response.path(GA4GH_CLAIM);
-            if (visas.isArray()) {
-                for (JsonNode visaNode : visas) {
-                    if (visaNode.isTextual()) {
-                        Ga4ghPassportVisa visa = Utils.parseAndVerifyVisa(visaNode.asText(), signers, remoteJwkSets, mapper);
-                        if (visa.isVerified()) {
-                            log.debug("Adding a visa to passport: {}", visa);
-                            passport.add(passport.textNode(visa.getJwt()));
-                            linkedIdentities.add(visa.getLinkedIdentity());
-                        } else {
-                            log.warn("Skipping visa: {}", visa);
-                        }
-                    } else {
-                        log.warn("Element of {} is not a String: {}", GA4GH_CLAIM, visaNode);
-                    }
-                }
+        if (response == null) {
+            log.debug("No response returned");
+            return;
+        } else if (!response.hasNonNull(GA4GH_CLAIM)) {
+            log.debug("Response does not contain non null value for key '{}'", GA4GH_CLAIM);
+            return;
+        }
+
+        JsonNode visas = response.path(GA4GH_CLAIM);
+        if (!visas.isArray()) {
+            log.warn("'{}' claim is not an array. Received response '{}'", GA4GH_CLAIM, response);
+        }
+        for (JsonNode visaNode : visas) {
+            if (!visaNode.isTextual()) {
+                log.warn("Element '{}' of '{}' is not a String, skipping value", visaNode, GA4GH_CLAIM);
+                continue;
+            }
+            Ga4ghPassportVisa visa = Utils.parseVisa(visaNode.asText());
+            if (visa == null) {
+                log.debug("Visa '{}' could not be parsed", visaNode);
+                continue;
+            }
+            Utils.verifyVisa(visa, signers, remoteJwkSets);
+            if (visa.isVerified()) {
+                log.debug("Adding a visa to passport: {}", visa);
+                controlledAccessGrantsList.add(visa);
+                linkedIdentitiesSet.add(visa.getLinkedIdentity());
             } else {
-                log.warn("{} is not an array in {}", GA4GH_CLAIM, response);
+                log.warn("Skipping visa: {}", visa);
             }
         }
     }
@@ -182,24 +235,28 @@ public abstract class Ga4ghBroker {
             JsonNode result;
             try {
                 if (log.isDebugEnabled()) {
-                    log.debug("Calling Permissions API at {}", repo.getRestTemplate().getUriTemplateHandler().expand(repo.getActionURL(), uriVariables));
+                    log.debug("Calling Permissions API at {}", repo.getRestTemplate()
+                        .getUriTemplateHandler().expand(repo.getActionURL(), uriVariables));
                 }
 
                 result = repo.getRestTemplate().getForObject(repo.getActionURL(), JsonNode.class, uriVariables);
             } catch (HttpClientErrorException ex) {
-                MediaType contentType = ex.getResponseHeaders().getContentType();
+                MediaType contentType = null;
+                if (ex.getResponseHeaders() != null) {
+                    contentType = ex.getResponseHeaders().getContentType();
+                }
                 String body = ex.getResponseBodyAsString();
 
-                log.error("HTTP ERROR: {}, URL: {}, Content-Type: {}", ex.getRawStatusCode(), repo.getActionURL(), contentType);
+                log.error("HTTP ERROR: {}, URL: {}, Content-Type: {}",
+                    ex.getRawStatusCode(), repo.getActionURL(), contentType);
 
                 if (ex.getRawStatusCode() == 404) {
-                    log.warn("Got status 404 from Permissions endpoint {}, ELIXIR AAI user is not linked to user at Permissions API",
+                    log.warn("Got status 404 from Permissions endpoint {}, user is not linked to user at Permissions API",
                             repo.getActionURL());
-
                     return null;
                 }
 
-                if (JSON.equals(contentType.getSubtype())) {
+                if (contentType != null && JSON.equals(contentType.getSubtype())) {
                     try {
                         log.error(new ObjectMapper().readValue(body, JsonNode.class).path("message").asText());
                     } catch (IOException e) {
@@ -208,16 +265,14 @@ public abstract class Ga4ghBroker {
                 } else {
                     log.error("cannot make REST call, exception: {} message: {}", ex.getClass().getName(), ex.getMessage());
                 }
-
                 return null;
             }
             log.debug("Permissions API response: {}", result);
-
             return result;
         } catch (Exception ex) {
             log.error("Cannot get dataset permissions", ex);
         }
-
         return null;
     }
+
 }
diff --git a/src/main/java/cz/muni/ics/ga4gh/service/impl/brokers/LifescienceRiGa4ghBroker.java b/src/main/java/cz/muni/ics/ga4gh/service/impl/brokers/LifescienceRiGa4ghBroker.java
new file mode 100644
index 0000000..08b30be
--- /dev/null
+++ b/src/main/java/cz/muni/ics/ga4gh/service/impl/brokers/LifescienceRiGa4ghBroker.java
@@ -0,0 +1,330 @@
+package cz.muni.ics.ga4gh.service.impl.brokers;
+
+import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.BY_PEER;
+import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.BY_SELF;
+import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.BY_SO;
+import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.BY_SYSTEM;
+import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.TYPE_ACCEPTED_TERMS_AND_POLICIES;
+import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.TYPE_AFFILIATION_AND_ROLE;
+import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.TYPE_CONTROLLED_ACCESS_GRANTS;
+import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.TYPE_LINKED_IDENTITIES;
+import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.TYPE_RESEARCHER_STATUS;
+
+import cz.muni.ics.ga4gh.base.Utils;
+import cz.muni.ics.ga4gh.base.adapters.PerunAdapter;
+import cz.muni.ics.ga4gh.base.model.Affiliation;
+import cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa;
+import cz.muni.ics.ga4gh.base.properties.BrokerInstanceProperties;
+import cz.muni.ics.ga4gh.base.properties.Ga4ghBrokersProperties;
+import cz.muni.ics.ga4gh.service.JWTSigningAndValidationService;
+import cz.muni.ics.ga4gh.service.PassportAssemblyContext;
+import cz.muni.ics.ga4gh.service.impl.VisaAssemblyParameters;
+import java.sql.Timestamp;
+import java.util.List;
+import java.util.Set;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.util.StringUtils;
+
+@Slf4j
+public class LifescienceRiGa4ghBroker extends Ga4ghBroker {
+
+    private static final String BONA_FIDE_URL = "https://doi.org/10.1038/s41431-018-0219-y";
+    private static final String LS_RI_ORG_URL = "https://lifescience-ri.eu/";
+    private static final String FACULTY_AT = "faculty@";
+
+    private final String elixirIdAttribute;
+    private final String bonaFideStatusAttr;
+    private final String bonaFideStatusREMSAttr;
+    private final String groupAffiliationsAttr;
+    private final Long termsAndPoliciesGroupId;
+
+    private final Long lifescienceRiVoId;
+
+    public LifescienceRiGa4ghBroker(BrokerInstanceProperties instanceProperties,
+                                    Ga4ghBrokersProperties brokersProperties,
+                                    PerunAdapter adapter,
+                                    JWTSigningAndValidationService jwtService)
+    {
+        super(instanceProperties, brokersProperties, adapter, jwtService);
+
+        this.elixirIdAttribute = instanceProperties.getIdentifierAttribute();
+        this.bonaFideStatusAttr = instanceProperties.getBonaFideStatusAttr();
+        this.bonaFideStatusREMSAttr = instanceProperties.getBonaFideStatusRemsAttr();
+        this.groupAffiliationsAttr = instanceProperties.getGroupAffiliationsAttr();
+        this.termsAndPoliciesGroupId = instanceProperties.getTermsAndPoliciesGroupId();
+        this.lifescienceRiVoId = instanceProperties.getMembershipVoId();
+    }
+
+    @Override
+    protected String getSubAttribute() {
+        return elixirIdAttribute;
+    }
+
+    @Override
+    protected Long getCommunityVoId() {
+        return lifescienceRiVoId;
+    }
+
+    @Override
+    protected void addAffiliationAndRoles(PassportAssemblyContext ctx)
+    {
+        String type = TYPE_AFFILIATION_AND_ROLE;
+        logAddingVisas(type);
+        if (!isCommunityMember(ctx.getPerunUserId())) {
+            log.debug("User is not member of the LS community, not adding any {} visas", type);
+            return;
+        }
+
+        long asserted = System.currentTimeMillis() / 1000L;
+
+        Affiliation affiliate = new Affiliation(null, "affiliate@lifescience-ri.eu",asserted);
+        Ga4ghPassportVisa affiliateVisa = createVisa(
+            VisaAssemblyParameters.builder()
+                .type(type)
+                .sub(ctx.getSubject())
+                .userId(ctx.getPerunUserId())
+                .value(affiliate.getValue())
+                .source(affiliate.getSource())
+                .by(BY_SYSTEM)
+                .asserted(affiliate.getAsserted())
+                .expires(Utils.getOneYearExpires(affiliate.getAsserted()))
+                .conditions(null)
+                .build()
+        );
+        if (affiliateVisa != null) {
+            ctx.getResultVisas().add(affiliateVisa);
+            logAddedVisa(type, affiliate.getValue());
+        }
+
+        Affiliation member = new Affiliation(null, "member@lifescience-ri.eu",asserted);
+        Ga4ghPassportVisa memberVisa = createVisa(
+            VisaAssemblyParameters.builder()
+                .type(type)
+                .sub(ctx.getSubject())
+                .userId(ctx.getPerunUserId())
+                .value(member.getValue())
+                .source(affiliate.getSource())
+                .by(BY_SYSTEM)
+                .asserted(affiliate.getAsserted())
+                .expires(Utils.getOneYearExpires(affiliate.getAsserted()))
+                .conditions(null)
+                .build()
+        );
+
+        if (memberVisa != null) {
+            ctx.getResultVisas().add(memberVisa);
+            logAddedVisa(type, member.getValue());
+        }
+    }
+
+    @Override
+    protected void addAcceptedTermsAndPolicies(PassportAssemblyContext ctx) {
+        String type = TYPE_ACCEPTED_TERMS_AND_POLICIES;
+        logAddingVisas(type);
+        if (termsAndPoliciesGroupId == null) {
+            log.debug("Group ID for accepted terms and policies not defined, not adding any {} visas", type);
+            return;
+        }
+
+        boolean userInGroup = adapter.isUserInGroup(ctx.getPerunUserId(), termsAndPoliciesGroupId);
+        if (!userInGroup) {
+            log.debug("User is not in the group representing terms and policies approval, not adding any {} visas", type);
+            return;
+        }
+
+        long asserted = ctx.getNow();
+        if (StringUtils.hasText(bonaFideStatusAttr)) {
+            String bonaFideStatusCreatedAt = adapter.getAdapterRpc()
+                .getUserAttributeCreatedAt(ctx.getPerunUserId(), bonaFideStatusAttr);
+            if (bonaFideStatusCreatedAt != null) {
+                asserted = Timestamp.valueOf(bonaFideStatusCreatedAt).getTime() / 1000L;
+            }
+        }
+        long expires = Utils.getExpires(asserted, 100L);
+
+        String value = BONA_FIDE_URL;
+        Ga4ghPassportVisa visa = createVisa(
+            VisaAssemblyParameters.builder()
+                .type(type)
+                .sub(ctx.getSubject())
+                .userId(ctx.getPerunUserId())
+                .value(value)
+                .source(LS_RI_ORG_URL)
+                .by(BY_SELF)
+                .asserted(asserted)
+                .expires(expires)
+                .conditions(null)
+                .build()
+        );
+
+        if (visa != null) {
+            ctx.getResultVisas().add(visa);
+            logAddedVisa(type, value);
+        }
+    }
+
+    @Override
+    protected void addControlledAccessGrants(PassportAssemblyContext ctx) {
+        String type = TYPE_CONTROLLED_ACCESS_GRANTS;
+        logAddingVisas(type);
+        List<Ga4ghPassportVisa> controlledAccessGrants = ctx.getExternalControlledAccessGrants();
+        if (controlledAccessGrants == null || controlledAccessGrants.isEmpty()) {
+            log.debug("No external {} visas available, not adding any {} visas", type, type);
+            return;
+        }
+        for (Ga4ghPassportVisa acgVisa: controlledAccessGrants) {
+            ctx.getResultVisas().add(acgVisa);
+            logAddedVisa(type, acgVisa.getGa4ghVisaV1().getValue());
+        }
+    }
+
+    @Override
+    protected void addLinkedIdentities(PassportAssemblyContext ctx) {
+        String type = TYPE_LINKED_IDENTITIES;
+        logAddingVisas(type);
+        Set<String> externalLinkedIdentities = ctx.getExternalLinkedIdentities();
+        if (externalLinkedIdentities == null || externalLinkedIdentities.isEmpty()) {
+            log.debug("No external {} visas available, not adding any {} visas", type, type);
+            return;
+        }
+        for (String identity: externalLinkedIdentities) {
+            Ga4ghPassportVisa visa = createVisa(
+                VisaAssemblyParameters.builder()
+                    .type(type)
+                    .sub(ctx.getSubject())
+                    .userId(ctx.getPerunUserId())
+                    .value(identity)
+                    .source(LS_RI_ORG_URL)
+                    .by(BY_SYSTEM)
+                    .asserted(ctx.getNow())
+                    .expires(Utils.getOneYearExpires(ctx.getNow()))
+                    .conditions(null)
+                    .build()
+            );
+            if (visa != null) {
+                ctx.getResultVisas().add(visa);
+                logAddedVisa(type, identity);
+            }
+        }
+    }
+
+    @Override
+    protected void addResearcherStatuses(PassportAssemblyContext ctx)
+    {
+        logAddingVisas(TYPE_RESEARCHER_STATUS);
+        //addResearcherStatusFromRemsBonaFideAttribute(ctx); - rems not defined yet
+        addResearcherStatusFromAffiliation(ctx);
+        addResearcherStatusFromGroupAffiliations(ctx);
+    }
+
+    private void addResearcherStatusFromRemsBonaFideAttribute(PassportAssemblyContext ctx)
+    {
+        String type = TYPE_RESEARCHER_STATUS;
+        log.debug("Adding {} visa (from REMS bona fide status)", type);
+        if (!StringUtils.hasText(bonaFideStatusREMSAttr)) {
+            log.debug("REMS bonaFideStatus attribute is not defined, not adding any {} visas (from REMS bona fide status)", type);
+            return;
+        }
+
+        String bonaFideStatusREMSCreatedAt = adapter.getAdapterRpc()
+            .getUserAttributeCreatedAt(ctx.getPerunUserId(), bonaFideStatusREMSAttr);
+        if (bonaFideStatusREMSCreatedAt == null) {
+            return;
+        }
+
+        long asserted = Timestamp.valueOf(bonaFideStatusREMSCreatedAt).getTime() / 1000L;
+        long expires = Utils.getOneYearExpires(asserted);
+        String value = BONA_FIDE_URL;
+        Ga4ghPassportVisa visa = createVisa(
+            VisaAssemblyParameters.builder()
+                .type(type)
+                .sub(ctx.getSubject())
+                .userId(ctx.getPerunUserId())
+                .value(value)
+                .source(LS_RI_ORG_URL)
+                .by(BY_PEER)
+                .asserted(asserted)
+                .expires(expires)
+                .conditions(null)
+                .build()
+        );
+        if (visa != null) {
+            ctx.getResultVisas().add(visa);
+            logAddedVisa(type, value);
+        }
+    }
+
+    private void addResearcherStatusFromAffiliation(PassportAssemblyContext ctx) {
+        String type = TYPE_RESEARCHER_STATUS;
+        log.debug("Adding {} visa (from affiliations)", type);
+        if (ctx.getIdentityAffiliations() == null || ctx.getIdentityAffiliations().isEmpty()) {
+            log.debug("No affiliations available, not adding any {} visas (from affiliations)", type);
+            return;
+        }
+
+        for (Affiliation affiliation: ctx.getIdentityAffiliations()) {
+            if (!StringUtils.startsWithIgnoreCase(affiliation.getValue(), FACULTY_AT)) {
+                continue;
+            }
+
+            String value = BONA_FIDE_URL;
+            Ga4ghPassportVisa visa = createVisa(
+                VisaAssemblyParameters.builder()
+                    .type(type)
+                    .sub(ctx.getSubject())
+                    .userId(ctx.getPerunUserId())
+                    .value(value)
+                    .source(affiliation.getSource())
+                    .by(BY_SYSTEM)
+                    .asserted(affiliation.getAsserted())
+                    .expires(Utils.getOneYearExpires(affiliation.getAsserted()))
+                    .conditions(null)
+                    .build()
+            );
+            if (visa != null) {
+                ctx.getResultVisas().add(visa);
+                logAddedVisa(type, value);
+            }
+        }
+    }
+
+    private void addResearcherStatusFromGroupAffiliations(PassportAssemblyContext ctx) {
+        String type = TYPE_RESEARCHER_STATUS;
+        log.debug("Adding {} visa (from group affiliations)", type);
+        if (!StringUtils.hasText(groupAffiliationsAttr)) {
+            log.debug("GroupAffiliations attribute is not defined, not adding any {} visas (from group affiliations)", type);
+            return;
+        }
+        List<Affiliation> groupAffiliations = adapter.getGroupAffiliations(
+            ctx.getPerunUserId(), lifescienceRiVoId, groupAffiliationsAttr);
+        if (groupAffiliations == null || groupAffiliations.isEmpty()) {
+            return;
+        }
+
+        for (Affiliation affiliation: groupAffiliations) {
+            if (!StringUtils.startsWithIgnoreCase(affiliation.getValue(), FACULTY_AT)) {
+                continue;
+            }
+
+            String value = BONA_FIDE_URL;
+            Ga4ghPassportVisa visa = createVisa(
+                VisaAssemblyParameters.builder()
+                    .type(type)
+                    .sub(ctx.getSubject())
+                    .userId(ctx.getPerunUserId())
+                    .value(value)
+                    .source(LS_RI_ORG_URL)
+                    .by(BY_SO)
+                    .asserted(affiliation.getAsserted())
+                    .expires(Utils.getOneYearExpires(affiliation.getAsserted()))
+                    .conditions(null)
+                    .build()
+            );
+            if (visa != null) {
+                ctx.getResultVisas().add(visa);
+                logAddedVisa(type, value);
+            }
+        }
+    }
+
+}
diff --git a/src/main/java/cz/muni/ics/ga4gh/service/impl/brokers/PerunGa4ghBroker.java b/src/main/java/cz/muni/ics/ga4gh/service/impl/brokers/PerunGa4ghBroker.java
new file mode 100644
index 0000000..3b5080b
--- /dev/null
+++ b/src/main/java/cz/muni/ics/ga4gh/service/impl/brokers/PerunGa4ghBroker.java
@@ -0,0 +1,154 @@
+package cz.muni.ics.ga4gh.service.impl.brokers;
+
+import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.BY_SYSTEM;
+import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.TYPE_ACCEPTED_TERMS_AND_POLICIES;
+import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.TYPE_AFFILIATION_AND_ROLE;
+import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.TYPE_CONTROLLED_ACCESS_GRANTS;
+import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.TYPE_LINKED_IDENTITIES;
+import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.TYPE_RESEARCHER_STATUS;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import cz.muni.ics.ga4gh.base.Utils;
+import cz.muni.ics.ga4gh.base.adapters.PerunAdapter;
+import cz.muni.ics.ga4gh.base.model.Affiliation;
+import cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa;
+import cz.muni.ics.ga4gh.base.model.UserExtSource;
+import cz.muni.ics.ga4gh.base.properties.BrokerInstanceProperties;
+import cz.muni.ics.ga4gh.base.properties.Ga4ghBrokersProperties;
+import cz.muni.ics.ga4gh.service.JWTSigningAndValidationService;
+import cz.muni.ics.ga4gh.service.PassportAssemblyContext;
+import cz.muni.ics.ga4gh.service.impl.VisaAssemblyParameters;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import javax.validation.constraints.NotBlank;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.validation.annotation.Validated;
+
+@Slf4j
+@Validated
+public class PerunGa4ghBroker extends Ga4ghBroker {
+
+    @NotBlank
+    private final String idAttribute;
+    
+    @NotBlank
+    private final String source;
+
+    private final Set<String> ignoreLastAccessIdps = new HashSet<>();
+
+    public PerunGa4ghBroker(BrokerInstanceProperties instanceProperties,
+                            Ga4ghBrokersProperties brokersProperties,
+                            PerunAdapter adapter,
+                            JWTSigningAndValidationService jwtService) {
+        super(instanceProperties, brokersProperties, adapter, jwtService);
+        this.idAttribute = instanceProperties.getIdentifierAttribute();
+        this.source = instanceProperties.getSource();
+        if (instanceProperties.getWhitelistedLinkedIdentitySources() != null) {
+            this.ignoreLastAccessIdps.addAll(
+                instanceProperties.getWhitelistedLinkedIdentitySources());
+        }
+    }
+
+    @Override
+    protected String getSubAttribute() {
+        return idAttribute;
+    }
+
+    @Override
+    protected Long getCommunityVoId() {
+        return null;
+    }
+
+    @Override
+    protected void addAffiliationAndRoles(PassportAssemblyContext ctx)
+    {
+        String type = TYPE_AFFILIATION_AND_ROLE;
+        logAddingVisas(type);
+
+        if (ctx.getIdentityAffiliations() == null || ctx.getIdentityAffiliations().isEmpty()) {
+            log.debug("No affiliations available, not adding any visas");
+            return;
+        }
+
+        for (Affiliation affiliation: ctx.getIdentityAffiliations()) {
+            long expires = Utils.getOneYearExpires(affiliation.getAsserted());
+            Ga4ghPassportVisa visa = createVisa(
+                VisaAssemblyParameters.builder()
+                    .type(type)
+                    .sub(ctx.getSubject())
+                    .userId(ctx.getPerunUserId())
+                    .value(affiliation.getValue())
+                    .source(affiliation.getSource())
+                    .by(BY_SYSTEM)
+                    .asserted(affiliation.getAsserted())
+                    .expires(expires)
+                    .conditions(null)
+                    .build()
+            );
+
+            if (visa != null) {
+                ctx.getResultVisas().add(visa);
+                logAddedVisa(type, affiliation.getValue());
+            }
+        }
+    }
+
+    @Override
+    protected void addAcceptedTermsAndPolicies(PassportAssemblyContext ctx) {
+        logAddingVisas(TYPE_ACCEPTED_TERMS_AND_POLICIES);
+        // no policies - extend with Perun AUP?
+    }
+
+    @Override
+    protected void addControlledAccessGrants(PassportAssemblyContext ctx) {
+        logAddingVisas(TYPE_CONTROLLED_ACCESS_GRANTS);
+        // no repositories
+    }
+
+    @Override
+    protected void addLinkedIdentities(PassportAssemblyContext ctx) {
+        String type = TYPE_LINKED_IDENTITIES;
+        logAddingVisas(type);
+
+        List<UserExtSource> userExtSources = adapter.getAdapterRpc().getIdpUserExtSources(ctx.getPerunUserId());
+        for (UserExtSource ues: userExtSources) {
+            long asserted = ues.getLastAccess().getTime() / 1000L;
+            long expires = Utils.getOneYearExpires(asserted);
+
+            String idp = ues.getExtSource().getName();
+            if (ignoreLastAccessIdps.contains(idp)) {
+                expires = Utils.getExpires(asserted, 100L);
+            }
+            String value = Utils.constructLinkedIdentity(ues.getLogin(), ues.getExtSource().getName());
+            Ga4ghPassportVisa visa = createVisa(
+                VisaAssemblyParameters.builder()
+                    .type(type)
+                    .sub(ctx.getSubject())
+                    .userId(ctx.getPerunUserId())
+                    .value(value)
+                    .source(this.source)
+                    .by(BY_SYSTEM)
+                    .asserted(asserted)
+                    .expires(expires)
+                    .conditions(null)
+                    .build()
+            );
+
+            if (visa != null) {
+                ctx.getResultVisas().add(visa);
+                logAddedVisa(type, value);
+            }
+        }
+    }
+
+    @Override
+    protected void addResearcherStatuses(PassportAssemblyContext ctx)
+    {
+        logAddingVisas(TYPE_RESEARCHER_STATUS);
+        // Perun does not have researcher status defined
+    }
+
+}
diff --git a/src/main/java/cz/muni/ics/ga4gh/utils/Utils.java b/src/main/java/cz/muni/ics/ga4gh/utils/Utils.java
deleted file mode 100644
index 7037cb9..0000000
--- a/src/main/java/cz/muni/ics/ga4gh/utils/Utils.java
+++ /dev/null
@@ -1,218 +0,0 @@
-package cz.muni.ics.ga4gh.utils;
-
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.nimbusds.jose.Payload;
-import com.nimbusds.jose.crypto.RSASSAVerifier;
-import com.nimbusds.jose.jwk.JWK;
-import com.nimbusds.jose.jwk.JWKMatcher;
-import com.nimbusds.jose.jwk.JWKSelector;
-import com.nimbusds.jose.jwk.RSAKey;
-import com.nimbusds.jose.jwk.source.RemoteJWKSet;
-import com.nimbusds.jose.proc.SecurityContext;
-import com.nimbusds.jwt.JWTParser;
-import com.nimbusds.jwt.SignedJWT;
-import cz.muni.ics.ga4gh.config.Ga4ghConfig;
-import cz.muni.ics.ga4gh.model.Ga4ghClaimRepository;
-import cz.muni.ics.ga4gh.model.Ga4ghPassportVisa;
-import cz.muni.ics.ga4gh.model.Repo;
-import cz.muni.ics.ga4gh.model.RepoHeader;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.http.client.ClientHttpRequestInterceptor;
-import org.springframework.http.client.InterceptingClientHttpRequestFactory;
-import org.springframework.web.client.RestTemplate;
-
-import java.io.IOException;
-import java.net.MalformedURLException;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.net.URL;
-import java.net.URLEncoder;
-import java.nio.charset.StandardCharsets;
-import java.time.Instant;
-import java.time.ZoneId;
-import java.time.ZonedDateTime;
-import java.time.format.DateTimeFormatter;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-
-@Slf4j
-public class Utils {
-
-    public static void parseConfigFile(Ga4ghConfig config,
-                                       List<Ga4ghClaimRepository> claimRepositories,
-                                       Map<URI, RemoteJWKSet<SecurityContext>> remoteJwkSets,
-                                       Map<URI, String> signers)
-    {
-        for (Repo repo : config.getRepos()) {
-            initializeRepo(repo, claimRepositories);
-            initializeSigner(signers, remoteJwkSets, repo.getName(), repo.getJwks());
-        }
-    }
-
-    public static Ga4ghPassportVisa parseAndVerifyVisa(String jwtString,
-                                                       Map<URI, String> signers,
-                                                       Map<URI, RemoteJWKSet<SecurityContext>> remoteJwkSets,
-                                                       ObjectMapper mapper)
-    {
-        Ga4ghPassportVisa visa = new Ga4ghPassportVisa(jwtString);
-
-        try {
-            SignedJWT signedJWT = (SignedJWT) JWTParser.parse(jwtString);
-            URI jku = signedJWT.getHeader().getJWKURL();
-
-            if (jku == null) {
-                log.error("JKU is missing in JWT header");
-                return visa;
-            }
-
-            visa.setSigner(signers.get(jku));
-            RemoteJWKSet<SecurityContext> remoteJWKSet = remoteJwkSets.get(jku);
-
-            if (remoteJWKSet == null) {
-                log.error("JKU '{}' is not among trusted key sets", jku);
-                return visa;
-            }
-
-            List<JWK> keys = remoteJWKSet.get(new JWKSelector(
-                    new JWKMatcher.Builder().keyID(signedJWT.getHeader().getKeyID()).build()), null);
-
-            RSASSAVerifier verifier = new RSASSAVerifier(((RSAKey) keys.get(0)).toRSAPublicKey());
-            visa.setVerified(signedJWT.verify(verifier));
-
-            if (visa.isVerified()) {
-                Utils.processPayload(mapper, visa, signedJWT.getPayload());
-            }
-        } catch (Exception ex) {
-            log.error("Visa '{}' cannot be parsed and verified", jwtString, ex);
-        }
-        return visa;
-    }
-
-    public static void processPayload(ObjectMapper mapper, Ga4ghPassportVisa visa, Payload payload)
-            throws IOException
-    {
-        JsonNode doc = mapper.readValue(payload.toString(), JsonNode.class);
-        checkVisaKey(visa, doc, Ga4ghPassportVisa.SUB);
-        checkVisaKey(visa, doc, Ga4ghPassportVisa.EXP);
-        checkVisaKey(visa, doc, Ga4ghPassportVisa.ISS);
-
-        JsonNode visa_v1 = doc.path(Ga4ghPassportVisa.GA4GH_VISA_V1);
-        if (visa_v1.isMissingNode() || visa_v1.isNull() || visa_v1.isEmpty()) {
-            log.warn("Nothing available in '{}', considering visa as not verified", Ga4ghPassportVisa.GA4GH_VISA_V1);
-            visa.setVerified(false);
-            return;
-        }
-
-        checkVisaKey(visa, visa_v1, Ga4ghPassportVisa.TYPE);
-        checkVisaKey(visa, visa_v1, Ga4ghPassportVisa.ASSERTED);
-        checkVisaKey(visa, visa_v1, Ga4ghPassportVisa.VALUE);
-        checkVisaKey(visa, visa_v1, Ga4ghPassportVisa.SOURCE);
-        checkVisaKey(visa, visa_v1, Ga4ghPassportVisa.BY);
-
-        if (!visa.isVerified()) {
-            return;
-        }
-
-        long exp = doc.get(Ga4ghPassportVisa.EXP).asLong();
-        if (exp < Instant.now().getEpochSecond()) {
-            log.warn("visa expired on {}", isoDateTime(exp));
-            visa.setVerified(false);
-            return;
-        }
-
-        visa.setLinkedIdentity(URLEncoder.encode(doc.get(Ga4ghPassportVisa.SUB).asText(), StandardCharsets.UTF_8) +
-                ',' + URLEncoder.encode(doc.get(Ga4ghPassportVisa.ISS).asText(), StandardCharsets.UTF_8));
-
-        visa.setPrettyPayload(
-                visa_v1.get(Ga4ghPassportVisa.TYPE).asText() + ": '"
-                        + visa_v1.get(Ga4ghPassportVisa.VALUE).asText() + "' asserted at '"
-                        + isoDate(visa_v1.get(Ga4ghPassportVisa.ASSERTED).asLong()) + '\''
-        );
-    }
-
-    public static long getOneYearExpires(long asserted) {
-        return getExpires(asserted, 1L);
-    }
-
-    public static long getExpires(long asserted, long addYears) {
-        return Instant.ofEpochSecond(asserted).atZone(ZoneId.systemDefault()).plusYears(addYears).toEpochSecond();
-    }
-
-    private static void initializeSigner(Map<URI, String> signers,
-                                         Map<URI, RemoteJWKSet<SecurityContext>> remoteJwkSets,
-                                         String name,
-                                         String jwks)
-    {
-        try {
-            URL jku = new URL(jwks);
-            remoteJwkSets.put(jku.toURI(), new RemoteJWKSet<>(jku));
-            signers.put(jku.toURI(), name);
-
-            log.info("JWKS Signer '{}' added with keys '{}'", name, jwks);
-        } catch (MalformedURLException | URISyntaxException e) {
-            log.error("cannot add to RemoteJWKSet map: '{}' -> '{}'", name, jwks, e);
-        }
-    }
-
-    private static void initializeRepo(Repo repo, List<Ga4ghClaimRepository> claimRepositories) {
-        String name = repo.getName();
-        String actionURL = repo.getUrl();
-        List<RepoHeader> headers = repo.getHeaders();
-
-        if (actionURL == null || headers.isEmpty()) {
-            log.error("claim repository '{}' not defined with url|auth_header|auth_value", repo);
-            return;
-        }
-
-        RestTemplate restTemplate = new RestTemplate();
-        restTemplate.setRequestFactory(
-                new InterceptingClientHttpRequestFactory(restTemplate.getRequestFactory(), getClientHttpRequestInterceptors(headers))
-        );
-
-        claimRepositories.add(new Ga4ghClaimRepository(name, actionURL, restTemplate));
-        log.info("GA4GH Claims Repository '{}' configured at '{}'", name, actionURL);
-    }
-
-    private static void checkVisaKey(Ga4ghPassportVisa visa, JsonNode jsonNode, String key) {
-        if (jsonNode.path(key).isMissingNode()) {
-            log.warn("Key '{}' is missing in the Visa, therefore cannot be verified", key);
-            visa.setVerified(false);
-        } else {
-            switch (key) {
-                case Ga4ghPassportVisa.SUB:
-                    visa.setSub(jsonNode.path(key).asText());
-                    break;
-                case Ga4ghPassportVisa.ISS:
-                    visa.setIss(jsonNode.path(key).asText());
-                    break;
-                case Ga4ghPassportVisa.TYPE:
-                    visa.setType(jsonNode.path(key).asText());
-                    break;
-                case Ga4ghPassportVisa.VALUE:
-                    visa.setValue(jsonNode.path(key).asText());
-                    break;
-                default:
-                    log.warn("Unknown visa key: {}", key);
-            }
-        }
-    }
-
-    private static String isoDate(long linuxTime) {
-        return isoFormat(linuxTime, DateTimeFormatter.ISO_LOCAL_DATE);
-    }
-
-    private static String isoDateTime(long linuxTime) {
-        return isoFormat(linuxTime, DateTimeFormatter.ISO_DATE_TIME);
-    }
-
-    private static String isoFormat(long linuxTime, DateTimeFormatter formatter) {
-        ZonedDateTime zdt = ZonedDateTime.ofInstant(Instant.ofEpochSecond(linuxTime), ZoneId.systemDefault());
-        return formatter.format(zdt);
-    }
-
-    private static List<ClientHttpRequestInterceptor> getClientHttpRequestInterceptors(List<RepoHeader> headers) {
-        return new ArrayList<>(headers);
-    }
-}
diff --git a/src/main/java/cz/muni/ics/ga4gh/web/controllers/ExceptionTranslator.java b/src/main/java/cz/muni/ics/ga4gh/web/controllers/ExceptionTranslator.java
new file mode 100644
index 0000000..10b950a
--- /dev/null
+++ b/src/main/java/cz/muni/ics/ga4gh/web/controllers/ExceptionTranslator.java
@@ -0,0 +1,29 @@
+package cz.muni.ics.ga4gh.web.controllers;
+
+import cz.muni.ics.ga4gh.base.exceptions.InvalidRequestParametersException;
+import cz.muni.ics.ga4gh.base.exceptions.UserNotFoundException;
+import cz.muni.ics.ga4gh.base.exceptions.UserNotUniqueException;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.ControllerAdvice;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+
+@ControllerAdvice
+public class ExceptionTranslator {
+
+    @ExceptionHandler({InvalidRequestParametersException.class, UserNotUniqueException.class})
+    public ResponseEntity<Object> badRequest(Exception e) {
+        return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST);
+    }
+
+    @ExceptionHandler({UserNotFoundException.class})
+    public ResponseEntity<Object> notFound(Exception e) {
+        return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND);
+    }
+
+    @ExceptionHandler({Exception.class})
+    public ResponseEntity<Object> exception(Exception e) {
+        return new ResponseEntity<>(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
+    }
+
+}
diff --git a/src/main/java/cz/muni/ics/ga4gh/web/controllers/Ga4ghBrokerController.java b/src/main/java/cz/muni/ics/ga4gh/web/controllers/Ga4ghBrokerController.java
new file mode 100644
index 0000000..70c89e0
--- /dev/null
+++ b/src/main/java/cz/muni/ics/ga4gh/web/controllers/Ga4ghBrokerController.java
@@ -0,0 +1,39 @@
+package cz.muni.ics.ga4gh.web.controllers;
+
+import static cz.muni.ics.ga4gh.web.security.WebSecurityConfigurer.GA4GH_ENDPOINTS_PATH;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import cz.muni.ics.ga4gh.base.exceptions.InvalidRequestParametersException;
+import cz.muni.ics.ga4gh.base.exceptions.UserNotFoundException;
+import cz.muni.ics.ga4gh.base.exceptions.UserNotUniqueException;
+import cz.muni.ics.ga4gh.facade.Ga4ghBrokerFacade;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.MediaType;
+import org.springframework.util.StringUtils;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping(GA4GH_ENDPOINTS_PATH)
+public class Ga4ghBrokerController {
+
+    private final Ga4ghBrokerFacade ga4GhBrokerFacade;
+
+    @Autowired
+    public Ga4ghBrokerController(Ga4ghBrokerFacade ga4ghBrokerFacade) {
+        this.ga4GhBrokerFacade = ga4ghBrokerFacade;
+    }
+
+    @GetMapping(value = "/{user_id}", produces = MediaType.APPLICATION_JSON_VALUE)
+    public JsonNode getGa4ghPassport(@PathVariable(name = "user_id") String userIdentifier)
+        throws UserNotFoundException, UserNotUniqueException, InvalidRequestParametersException
+    {
+        if (!StringUtils.hasText(userIdentifier)) {
+            throw new InvalidRequestParametersException("No user identifier specified");
+        }
+        return ga4GhBrokerFacade.getGa4ghPassport(userIdentifier);
+    }
+
+}
diff --git a/src/main/java/cz/muni/ics/ga4gh/controllers/JwkSetPublishingEndpoint.java b/src/main/java/cz/muni/ics/ga4gh/web/controllers/JwkSetPublishingEndpoint.java
similarity index 77%
rename from src/main/java/cz/muni/ics/ga4gh/controllers/JwkSetPublishingEndpoint.java
rename to src/main/java/cz/muni/ics/ga4gh/web/controllers/JwkSetPublishingEndpoint.java
index 90bd9c7..16e6b67 100644
--- a/src/main/java/cz/muni/ics/ga4gh/controllers/JwkSetPublishingEndpoint.java
+++ b/src/main/java/cz/muni/ics/ga4gh/web/controllers/JwkSetPublishingEndpoint.java
@@ -1,29 +1,26 @@
-package cz.muni.ics.ga4gh.controllers;
+package cz.muni.ics.ga4gh.web.controllers;
+
+import static cz.muni.ics.ga4gh.web.security.WebSecurityConfigurer.PUBLIC_ENDPOINTS_PATH;
 
 import com.nimbusds.jose.jwk.JWK;
 import com.nimbusds.jose.jwk.JWKSet;
 import cz.muni.ics.ga4gh.service.JWTSigningAndValidationService;
-import lombok.Getter;
-import lombok.Setter;
+import java.util.ArrayList;
+import java.util.Map;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.http.MediaType;
 import org.springframework.web.bind.annotation.CrossOrigin;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 
-import java.util.ArrayList;
-import java.util.Map;
-
 @CrossOrigin(originPatterns = "*")
 @RestController
-@RequestMapping("/public")
-@Getter
-@Setter
+@RequestMapping(PUBLIC_ENDPOINTS_PATH)
 public class JwkSetPublishingEndpoint {
 
     public static final String URL = "jwk";
 
-    private JWTSigningAndValidationService jwtService;
+    private final JWTSigningAndValidationService jwtService;
 
     @Autowired
     public JwkSetPublishingEndpoint(JWTSigningAndValidationService jwtService) {
@@ -33,7 +30,7 @@ public class JwkSetPublishingEndpoint {
     @RequestMapping(value = "/" + URL, produces = MediaType.APPLICATION_JSON_VALUE)
     public String getJwk() {
         // map from key id to key
-        Map<String, JWK> keys = jwtService.getAllPublicKeys();
+        Map<String, JWK> keys = jwtService.getPublicKeys();
         JWKSet jwkSet = new JWKSet(new ArrayList<>(keys.values()));
         return jwkSet.toString();
     }
diff --git a/src/main/java/cz/muni/ics/ga4gh/security/WebSecurityConfigurer.java b/src/main/java/cz/muni/ics/ga4gh/web/security/WebSecurityConfigurer.java
similarity index 51%
rename from src/main/java/cz/muni/ics/ga4gh/security/WebSecurityConfigurer.java
rename to src/main/java/cz/muni/ics/ga4gh/web/security/WebSecurityConfigurer.java
index 20d62d1..13e5dae 100644
--- a/src/main/java/cz/muni/ics/ga4gh/security/WebSecurityConfigurer.java
+++ b/src/main/java/cz/muni/ics/ga4gh/web/security/WebSecurityConfigurer.java
@@ -1,10 +1,13 @@
-package cz.muni.ics.ga4gh.security;
+package cz.muni.ics.ga4gh.web.security;
 
-import cz.muni.ics.ga4gh.config.BasicAuthConfig;
+import cz.muni.ics.ga4gh.base.model.BasicAuthCredentials;
+import cz.muni.ics.ga4gh.base.properties.BasicAuthProperties;
+import java.util.List;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
+import org.springframework.security.config.annotation.authentication.configurers.provisioning.InMemoryUserDetailsManagerConfigurer;
 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
 import org.springframework.security.crypto.password.PasswordEncoder;
 import org.springframework.security.web.SecurityFilterChain;
@@ -13,30 +16,35 @@ import org.springframework.security.web.SecurityFilterChain;
 public class WebSecurityConfigurer {
 
     private static final String ROLE_USER = "ROLE_USER";
-    private static final String PUBLIC_ENDPOINTS_PREFIX = "/public/**";
 
-    private final String username;
-    private final String password;
+    public static final String PUBLIC_ENDPOINTS_PATH = "/public";
+    public static final String GA4GH_ENDPOINTS_PATH = "/ga4gh";
+    private static final String PUBLIC_ENDPOINTS_PREFIX = PUBLIC_ENDPOINTS_PATH + "/**";
+
+    private final List<BasicAuthCredentials> basicAuthCredentialsList;
 
     private final PasswordEncoder passwordEncoder;
 
     @Autowired
-    public WebSecurityConfigurer(BasicAuthConfig config, PasswordEncoder passwordEncoder) {
-        this.username = config.getUsername();
-        this.password = config.getPassword();
+    public WebSecurityConfigurer(BasicAuthProperties basicAuthProperties,
+                                 PasswordEncoder passwordEncoder)
+    {
+        this.basicAuthCredentialsList = basicAuthProperties.getCredentials();
         this.passwordEncoder = passwordEncoder;
     }
 
     @Autowired
     public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
-        auth.inMemoryAuthentication()
-                .withUser(username).password(passwordEncoder.encode(password))
+        InMemoryUserDetailsManagerConfigurer<AuthenticationManagerBuilder> configurer = auth.inMemoryAuthentication();
+        for (BasicAuthCredentials credentials: basicAuthCredentialsList) {
+            configurer.withUser(credentials.getUsername())
+                .password(passwordEncoder.encode(credentials.getPassword()))
                 .authorities(ROLE_USER);
+        }
     }
 
     @Bean
     public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
-
         http.authorizeRequests()
                 .antMatchers(PUBLIC_ENDPOINTS_PREFIX).permitAll()
                 .anyRequest().authenticated()
diff --git a/src/main/resources/application-bbmri.yml b/src/main/resources/application-bbmri.yml
deleted file mode 100644
index 39cd3d1..0000000
--- a/src/main/resources/application-bbmri.yml
+++ /dev/null
@@ -1,77 +0,0 @@
----
-
-spring:
-  profiles: bbmri
-
-broker:
-  bona-fide-status-attr: bona_fide_status
-  bona-fide-status-rems-attr: bona_fide_status_rems
-  group-affiliations-attr: groupAffiliations
-  terms-and-policies-group-id: 1
-  affiliations-attr: affiliations
-  org-url-attr: orgUrl
-  ext-source-name: ext-source-name
-  issuer: issuer
-  path-to-jwk-file: path
-adapter:
-  adapter-primary: rpc
-  call-fallback: false
-ldap:
-  host: host
-  user: user
-  password: password
-  base-dn: dn
-  use-tls: false
-  use-ssl: true
-  allow-untrusted-ssl: false
-  timeout-secs: 10
-  port: 636
-rpc:
-  enabled: true
-  url: url
-  username: username
-  password: password
-  serializer: json
-attributes:
-  attributeMappings:
-    bona_fide_status:
-      internal-name: bona_fide_status
-      rpc-name: urn:perun:user:attribute-def:def:bonaFideStatus
-      ldap-name: bonaFideStatus
-    bona_fide_status_rems:
-      internal-name: bona_fide_status_rems
-      rpc-name: urn:perun:user:attribute-def:def:elixirBonaFideStatusREMS
-      ldap-name: bonaFideStatusREMS
-    groupAffiliations:
-      internal-name: groupAffiliations
-      rpc-name: urn:perun:group:attribute-def:def:groupAffiliations
-      ldap-name: groupAffiliations
-    affiliations:
-      internal-name: affiliations
-      rpc-name: urn:perun:ues:attribute-def:def:affiliation
-      ldap-name:
-    orgUrl:
-      internal-name: orgUrl
-      rpc-name: urn:perun:ues:attribute-def:def:organizationURL
-      ldap-name:
-ga4gh:
-  repos:
-        -
-          name: repo1
-          url: url1
-          jwks: jwks1
-          headers:
-            -
-              header: header
-              value: value
-        -
-          name: repo2
-          url: url2
-          jwks: jwks2
-          headers:
-            -
-              header: header
-              value: value
-basic-auth:
-  username: Honza
-  password: Lojza
diff --git a/src/main/resources/application-elixir.yml b/src/main/resources/application-elixir.yml
deleted file mode 100644
index 881f452..0000000
--- a/src/main/resources/application-elixir.yml
+++ /dev/null
@@ -1,92 +0,0 @@
----
-
-spring:
-  config:
-    activate:
-      on-profile: elixir
-  mvc:
-    pathmatch:
-      matching-strategy: ant_path_matcher
-
-broker:
-  bona-fide-status-attr: bona_fide_status
-  bona-fide-status-rems-attr: bona_fide_status_rems
-  group-affiliations-attr: groupAffiliations
-  terms-and-policies-group-id: 1
-  affiliations-attr: affiliations
-  org-url-attr: orgUrl
-  attributes-to-search:
-    - "elixir-persistent-shadow"
-  issuer: issuer
-  jku: jku-url
-  path-to-jwk-file: path
-adapter:
-  adapter-primary: ldap
-  call-fallback: false
-ldap:
-  host: host
-  user: user
-  password: password
-  base-dn: dn
-  use-tls: false
-  use-ssl: true
-  allow-untrusted-ssl: false
-  timeout-secs: 10
-  port: 636
-rpc:
-  enabled: true
-  url: url
-  username: username
-  password: password
-  serializer: json
-attributes:
-  attributeMappings:
-    bona_fide_status:
-      internal-name: bona_fide_status
-      rpc-name: name
-      ldap-name: name
-    bona_fide_status_rems:
-      internal-name: bona_fide_status_rems
-      rpc-name: name
-      ldap-name: name
-    groupAffiliations:
-      internal-name: groupAffiliations
-      rpc-name: name
-      ldap-name: name
-    affiliations:
-      internal-name: affiliations
-      rpc-name: name
-      ldap-name: name
-    orgUrl:
-      internal-name: orgUrl
-      rpc-name: name
-      ldap-name: name
-    elixir-persistent-shadow:
-      internal-name: elixir-persistent-shadow
-      rpc-name: name
-      ldap-name: name
-    preferred-mail:
-      internal-name: preferred-mail
-      rpc-name: name
-      ldap-name: name
-ga4gh:
-  repos:
-    -
-      name: repo1
-      url: url1
-      jwks: jwks1
-      headers:
-        -
-          header: header
-          value: value
-    -
-      name: repo2
-      url: url2
-      jwks: jwks2
-      headers:
-        -
-          header: header
-          value: value
-basic-auth:
-  username: username
-  password: password
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index 63d98d7..dd9c58c 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -3,11 +3,137 @@
 spring:
   main:
     allow-bean-definition-overriding: true
-  profiles:
-    active: elixir
+  mvc:
+    pathmatch:
+      matching-strategy: ant_path_matcher
 
 logging:
   file:
-    path: /var/lib/tomcat9/logs/broker.log
+    path: /var/lib/tomcat9/logs/ga4gh-passport-broker
+    name: broker.log
   level:
-    root: debug
\ No newline at end of file
+    root: error
+    org.springframework: warn
+    cz.muni.ics.ga4gh: debug
+
+
+broker:
+  user-identification-attributes:
+    - "elixir-persistent-shadow"
+  issuer: issuer
+  jku: jku-url
+  path-to-jwk-file: path
+  brokers:
+    - name: elixir
+      broker-class: ElixirGa4ghBroker
+      bona-fide-status-attr: bona_fide_status
+      bona-fide-status-rems-attr: bona_fide_status_rems
+      group-affiliations-attr: groupAffiliations
+      terms-and-policies-group-id: 1
+      affiliations-attr: affiliations
+      org-url-attr: orgUrl
+      identifier-attribute: elixir-persistent-shadow
+      membership-vo-id: 1
+      passport-repositories:
+        - name: repo1
+          url: url1
+          jwks: jwks1
+          headers:
+            - header: header
+              value: value
+        - name: repo2
+          url: url2
+          jwks: jwks2
+          headers:
+            - header: header
+              value: value
+    - name: bbmri
+      broker-class: BbmriGa4ghBroker
+      bona-fide-status-attr: bona_fide_status
+      bona-fide-status-rems-attr: bona_fide_status_rems
+      group-affiliations-attr: groupAffiliations
+      terms-and-policies-group-id: 2
+      affiliations-attr: affiliations
+      org-url-attr: orgUrl
+      identifier-attribute: bbmri-persistent-shadow
+      membership-vo-id: 2
+      passport-repositories:
+        - name: repo1
+          url: url1
+          jwks: jwks1
+          headers:
+            - header: header
+              value: value
+        - name: repo2
+          url: url2
+          jwks: jwks2
+          headers:
+            - header: header
+              value: value
+    - name: perun
+      broker-class: PerunGa4ghBroker
+      identifier-attribute: lifescience-id
+      source: https://perun-aai.org/
+      affiliations-attr: affiliations
+      org-url-attr: org_url
+      whitelisted-linked-identity-sources:
+        - "https://login.elixir-czech.org/idp/"
+        - "https://proxy.aai.lifescience-ri.eu/proxy"
+
+perun:
+  adapter:
+    primary: ldap
+    call-fallback: false
+  connector:
+    ldap:
+      host: host
+      user: user
+      password: password
+      base-dn: dn
+      use-tls: false
+      use-ssl: true
+      allow-untrusted-ssl: false
+      timeout-secs: 10
+      port: 636
+    rpc:
+      enabled: true
+      url: url
+      username: username
+      password: password
+      serializer: json
+
+attributes:
+  attribute_mappings:
+    bona_fide_status:
+      internal-name: bona_fide_status
+      rpc-name: name
+      ldap-name: name
+    bona_fide_status_rems:
+      internal-name: bona_fide_status_rems
+      rpc-name: name
+      ldap-name: name
+    groupAffiliations:
+      internal-name: groupAffiliations
+      rpc-name: name
+      ldap-name: name
+    affiliations:
+      internal-name: affiliations
+      rpc-name: name
+      ldap-name: name
+    orgUrl:
+      internal-name: orgUrl
+      rpc-name: name
+      ldap-name: name
+    elixir-persistent-shadow:
+      internal-name: elixir-persistent-shadow
+      rpc-name: name
+      ldap-name: name
+    preferred-mail:
+      internal-name: preferred-mail
+      rpc-name: name
+      ldap-name: name
+
+basic-auth:
+  credentials:
+    - username: username
+      password: password
-- 
GitLab