diff --git a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/adapters/PerunAdapterMethods.java b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/adapters/PerunAdapterMethods.java
index 0cdac9e78a308b0460fe916f472447202a8da4f4..d823ea00892f6c5a854052d00cf72dde6126e673 100644
--- a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/adapters/PerunAdapterMethods.java
+++ b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/adapters/PerunAdapterMethods.java
@@ -341,4 +341,6 @@ public interface PerunAdapterMethods {
 
 	PerunUser getPerunUser(Long userId);
 
+	Set<Long> getUserVoIds(Long userId);
+
 }
diff --git a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/adapters/impl/PerunAdapterImpl.java b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/adapters/impl/PerunAdapterImpl.java
index 489c124bb0951e2e8a0bbb55bce70cc636d2623d..33ecd0a2bbaf1de67da66ace9cae886c7b54668d 100644
--- a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/adapters/impl/PerunAdapterImpl.java
+++ b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/adapters/impl/PerunAdapterImpl.java
@@ -414,4 +414,17 @@ public class PerunAdapterImpl extends PerunAdapter {
         }
     }
 
+    @Override
+    public Set<Long> getUserVoIds(Long userId) {
+        try {
+            return this.getAdapterPrimary().getUserVoIds(userId);
+        } catch (UnsupportedOperationException e) {
+            if (this.isCallFallback()) {
+                return this.getAdapterFallback().getUserVoIds(userId);
+            } else {
+                throw e;
+            }
+        }
+    }
+
 }
diff --git a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/adapters/impl/PerunAdapterLdap.java b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/adapters/impl/PerunAdapterLdap.java
index 5216f099de5e8497f84a34dc5bfdb122606f848c..ba6b673e17f0f33c0968a48274f08568c260bc05 100644
--- a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/adapters/impl/PerunAdapterLdap.java
+++ b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/adapters/impl/PerunAdapterLdap.java
@@ -531,6 +531,28 @@ public class PerunAdapterLdap extends PerunAdapterWithMappingServices implements
 		return getPerunUser(filter);
 	}
 
+	@Override
+	public Set<Long> getUserVoIds(Long userId) {
+		if (userId == null) {
+			throw new IllegalArgumentException("No userId");
+		}
+		SearchScope scope = SearchScope.ONELEVEL;
+		final String[] attributes = {PERUN_VO_ID};
+
+		String uniqueMember = getDnPrefixForUserId(userId) + ',' + this.connectorLdap.getBaseDN();
+		FilterBuilder filter = and(equal(UNIQUE_MEMBER, uniqueMember), equal(OBJECT_CLASS, PERUN_VO));
+		EntryMapper<Long> mapper = e -> {
+			if (!checkHasAttributes(e, attributes)) {
+				return null;
+			}
+
+			return Long.valueOf(e.get(PERUN_VO_ID).getString());
+		};
+
+		List<Long> voIds = connectorLdap.search(null, filter, scope, attributes, mapper);
+		return voIds.stream().filter(Objects::nonNull).collect(Collectors.toSet());
+	}
+
 	private PerunUser getPerunUser(FilterBuilder filter) {
 		SearchScope scope = SearchScope.ONELEVEL;
 		String[] attributes = new String[]{PERUN_USER_ID, GIVEN_NAME, SN};
diff --git a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/adapters/impl/PerunAdapterRpc.java b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/adapters/impl/PerunAdapterRpc.java
index 4f50438203a56101db29c2c9f48f6ae8a6741493..68be610d78885e878a27c757aed4819c5b5f0777 100644
--- a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/adapters/impl/PerunAdapterRpc.java
+++ b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/adapters/impl/PerunAdapterRpc.java
@@ -950,6 +950,23 @@ public class PerunAdapterRpc extends PerunAdapterWithMappingServices implements
 		return RpcMapper.mapPerunUser(response);
 	}
 
+	@Override
+	public Set<Long> getUserVoIds(Long userId) {
+		if (!this.connectorRpc.isEnabled()) {
+			return Collections.emptySet();
+		} else if (userId == null) {
+			throw new IllegalArgumentException("No userId");
+		}
+		List<Member> members = getMembersByUser(userId);
+		Set<Long> voIds = new HashSet<>();
+		for (Member member: members) {
+			if (VALID == member.getStatus()) {
+				voIds.add(member.getVoId());
+			}
+		}
+		return voIds;
+	}
+
 	private Member getMemberByUser(Long userId, Long voId) {
 		if (!this.connectorRpc.isEnabled()) {
 			return null;
diff --git a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/claims/sources/VoBasedEdupersonScopedAffiliationsClaimSource.java b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/claims/sources/VoBasedEdupersonScopedAffiliationsClaimSource.java
index 63ff439f43c799abe8a1b02bc8b0aed1d568f2ec..262e77d78aa84eb561b6e0e29344b2bf901635c4 100644
--- a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/claims/sources/VoBasedEdupersonScopedAffiliationsClaimSource.java
+++ b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/claims/sources/VoBasedEdupersonScopedAffiliationsClaimSource.java
@@ -4,55 +4,59 @@ import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.node.ArrayNode;
 import com.fasterxml.jackson.databind.node.JsonNodeFactory;
 import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
+import cz.muni.ics.oidc.exceptions.ConfigurationException;
 import cz.muni.ics.oidc.server.claims.ClaimSource;
 import cz.muni.ics.oidc.server.claims.ClaimSourceInitContext;
 import cz.muni.ics.oidc.server.claims.ClaimSourceProduceContext;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.util.StringUtils;
 
 import java.io.File;
 import java.io.IOException;
 import java.util.ArrayList;
 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.regex.Matcher;
+import java.util.regex.Pattern;
 
 /**
- * Claim source for eduperson_scoped_affiliations MUNI.
+ * Claim source for generating affiliation values based on VO membership(s).
  *
  * Configuration (replace [claimName] with the name of the claim):
  * <ul>
  *     <li>
- *         <b>custom.claim.[claimName].source.config_file</b> - path to the YML config file, see
- *         'eduperson_scoped_affiliations_mu_source.yml' for example configuration
+ *         <b>custom.claim.[claimName].source.valueMap</b> - Mapping of voIds to affiliation values. Has to be specified
+ *         in a format 'voId:aff,aff|voId:aff,aff', where 'voId' is an ID of the VO and 'aff' is the value of an
+ *         affiliation to be added to the output if the user is a valid member of the respective VO with the specified
+ *         identifier.
  *     </li>
  * </ul>
  *
- * @author Dominik Baránek <baranek@ics.muni.cz>
  * @author Dominik Frantisek Bucik <bucik@ics.muni.cz>
  */
 @Slf4j
-public class EdupersonScopedAffiliationsMUSource extends ClaimSource {
+public class VoBasedEdupersonScopedAffiliationsClaimSource extends ClaimSource {
 
-	private static final String CONFIG_FILE = "config_file";
-	private static final String KEY_SCOPE = "scope";
-	private static final String KEY_VO_ID = "voId";
-	private static final String KEY_AFFILIATIONS = "affiliations";
-	private static final String KEY_VALUE = "value";
-	private static final String KEY_GROUPS = "groups";
+	private final Pattern epsaPattern = Pattern.compile(
+			"(member|student|faculty|staff|alum|affiliate|unknown|library-walk-in)@.+"
+	);
 
-	private static final String DEFAULT_PATH = "/etc/perun/eduperson_scoped_affiliations_mu_source.yml";
+	private static final String KEY_VALUE_MAP = "valueMap";
 
-	private final Map<List<Long>, String> affiliations = new HashMap<>();
-	private Long voId = 363L;
-	private String valueScope = "muni.cz";
+	private final Map<Long, Set<String>> voIdValuesMap = new HashMap<>();
 
-	public EdupersonScopedAffiliationsMUSource(ClaimSourceInitContext ctx) {
+	public VoBasedEdupersonScopedAffiliationsClaimSource(ClaimSourceInitContext ctx) {
 		super(ctx);
-		parseConfigFile(ctx.getProperty(CONFIG_FILE, DEFAULT_PATH));
-		log.debug("{} - affiliations: '{}', voId: '{}', valueScope: '{}'",
-				getClaimName(), affiliations, voId, valueScope);
+		String valueMapProp = ctx.getProperty(KEY_VALUE_MAP, null);
+		if (!StringUtils.hasText(valueMapProp)) {
+			throw new ConfigurationException("Invalid configuration for claim " + getClaimName() + ": valueMap must be provided");
+		}
+		voIdValuesMap.putAll(parseValueMap(valueMapProp));
+		log.debug("{} - voIdAffiliationsMap: '{}'", getClaimName(), voIdValuesMap);
 	}
 
 	@Override
@@ -63,24 +67,18 @@ public class EdupersonScopedAffiliationsMUSource extends ClaimSource {
 	@Override
 	public JsonNode produceValue(ClaimSourceProduceContext pctx) {
 		Long userId = pctx.getPerunUserId();
-		ArrayNode result = JsonNodeFactory.instance.arrayNode();
-		Set<Long> groups = pctx.getPerunAdapter().getUserGroupsIds(userId, voId);
-		for (Map.Entry<List<Long>, String> entry : affiliations.entrySet()) {
-			for (Long id: entry.getKey()) {
-				if (groups.contains(id)) {
-					String affiliation = entry.getValue() + '@' + valueScope;
-					log.trace("{} - added affiliation '{}' due to membership in group '{}'",
-							getClaimName(), affiliation, id);
-					result.add(affiliation);
-					break;
-				}
+		Set<String> userAffiliations = new HashSet<>();
+		Set<Long> userVoIds = pctx.getPerunAdapter().getUserVoIds(userId);
+		for (Long userVoId: userVoIds) {
+			Set<String> affiliationsToBeAdded = voIdValuesMap.getOrDefault(userVoId, new HashSet<>());
+			if (!affiliationsToBeAdded.isEmpty()) {
+				log.trace("{} - added affiliations '{}' due to membership in vo '{}'",
+						getClaimName(), affiliationsToBeAdded, userVoId);
+				userAffiliations.addAll(affiliationsToBeAdded);
 			}
 		}
-
-		if (result.size() == 0) {
-			String affiliation = "affiliate@" + valueScope;
-			log.trace("{} - user is not a member in any special groups, added default affiliation: '{}'",
-					getClaimName(), affiliation);
+		ArrayNode result = JsonNodeFactory.instance.arrayNode();
+		for (String affiliation : userAffiliations) {
 			result.add(affiliation);
 		}
 
@@ -88,23 +86,69 @@ public class EdupersonScopedAffiliationsMUSource extends ClaimSource {
 		return result;
 	}
 
-	private void parseConfigFile(String file) {
-		log.trace("{} - Loading config file {}", getClaimName(), file);
-		YAMLMapper mapper = new YAMLMapper();
-		try {
-			JsonNode root = mapper.readValue(new File(file), JsonNode.class);
-			valueScope = root.get(KEY_SCOPE).asText();
-			voId = root.get(KEY_VO_ID).longValue();
-			for (JsonNode affiliationMapping : root.path(KEY_AFFILIATIONS)) {
-				String value = affiliationMapping.path(KEY_VALUE).asText();
-				List<Long> gids = new ArrayList<>();
-				for (JsonNode gid : affiliationMapping.path(KEY_GROUPS)) {
-					gids.add(gid.asLong());
-				}
-				affiliations.put(gids, value);
+	private Map<Long, Set<String>> parseValueMap(String valueMapProp) {
+		String[] valueMapParts = valueMapProp.split("\\|");
+		if (valueMapParts.length == 0) {
+			throw getConfigurationException(
+					"Could not parse valueMap property. Needs to be in format voId1:aff1,aff2|voId2:aff3"
+			);
+		}
+		for (String idValue: valueMapParts) {
+			if (!StringUtils.hasText(idValue)) {
+				throw getConfigurationException(
+						"Could not parse id and affiliations mapping, empty String encountered"
+				);
 			}
-		} catch (IOException ex) {
-			log.warn("{} - cannot read claim configuration file: '{}'", getClaimName(), file);
+			String[] idValueParts = idValue.split(":");
+			if (idValueParts.length != 2) {
+				throw getConfigurationException(
+						"Could not parse id and affiliations mapping. Needs to be in format voId:aff1,aff2"
+				);
+			}
+			long voId;
+			try {
+				voId = Long.parseLong(idValueParts[0]);
+			} catch (NumberFormatException ex) {
+				throw getConfigurationException("Could not parse VO id out of subcomponent "  + idValue, ex);
+			}
+			Set<String> voAffiliations = parseAffiliations(idValueParts[1]);
+			if (voAffiliations.isEmpty()) {
+				throw getConfigurationException("No affiliation values found for voId " + voId);
+			}
+			voIdValuesMap.put(voId, voAffiliations);
+		}
+		return voIdValuesMap;
+	}
+
+	private Set<String> parseAffiliations(String idValuePart) {
+		String[] affiliations = idValuePart.split(",");
+		Set<String> resolvedAffiliations = new HashSet<>();
+		for (String affiliation : affiliations) {
+			if (!epsaPattern.matcher(affiliation).matches()) {
+				throw getConfigurationException(
+						"Value '" + affiliation + "' is not a valid eduPersonScopedAffiliation value"
+				);
+			}
+			resolvedAffiliations.add(affiliation);
+		}
+		return resolvedAffiliations;
+	}
+
+	private ConfigurationException getConfigurationException(String message) {
+		return getConfigurationException(message, null);
+	}
+
+	private ConfigurationException getConfigurationException(String message, Throwable cause) {
+		StringBuilder fullMessage = new StringBuilder();
+		fullMessage.append("Invalid configuration for claim ").append(getClaimName());
+		if (StringUtils.hasText(message)) {
+			fullMessage.append(": ").append(message);
+		}
+
+		if (cause != null) {
+			throw new ConfigurationException(fullMessage.toString(), cause);
+		} else {
+			throw new ConfigurationException(fullMessage.toString());
 		}
 	}