diff --git a/README.md b/README.md
index ab1e2157a3629f4c44f5e137908e50af4d825dbd..d5826d863a3c4c0e5e10a7ebc7f3a58723ab1773 100755
--- a/README.md
+++ b/README.md
@@ -85,6 +85,9 @@ Add an instance of the auth proc filter with example configuration `authswitcher
           //    'my-custom-authn-context-for-mfa',
           //]),
           //'contexts_regex' => true,
+          //'entityID' => function($request){
+          //    return empty($request["saml:RequesterID"]) ? $request["SPMetadata"]["entityid"] : $request["saml:RequesterID"][0];
+          //},
       ],
       'configs' => [
             'totp:Totp' => [
@@ -193,6 +196,18 @@ When the attribute is not empty, multi-factor authentication is always performed
 
 When used with proxy mode, MFA is not forced if it was already done at upstream IdP.
 
+## Enforce MFA per user per service
+
+If some user should use MFA for some services, set `mfaEnforceSettings` user attribute to one of the following JSON-encoded object types:
+
+- `{"all":true}` to force MFA for all services (equivalent to mfaEnforced)
+- `{"include_categories":["category1","category2"]}` to force MFA for all services from the listed categories
+- `{"include_categories":["category1","category2"],"exclude_rps":["entityID1","entityID2"]}` to force MFA for all services from the listed categories except services with entity ID `entityID1` and `entityID2`
+
+For this to work, you must also fill the `rpCategory` user attribute with the appropriate category. If this attribute is empty, the service is assumed to belong to a category named `"other"`.
+
+By default, entity ID is read from the metadata of the current SP. You can override this by specifying the `entityID` config option to either a string (which is used as is) or a callable in the form `function getEntityID($state){return "str";}`. See example configs for more.
+
 ## Add additional attributes when MFA is performed
 
 To add attributes only if MFA was performed, you can use a filter called `AddAdditionalAttributesAfterMfa`.
diff --git a/config-templates/module_authswitcher.php b/config-templates/module_authswitcher.php
index aeeb2db34d00d15a494dcc5fb9bf8bd26a609584..55cbe1c9e014d03d2040d959ac2099f41e6e34b1 100644
--- a/config-templates/module_authswitcher.php
+++ b/config-templates/module_authswitcher.php
@@ -19,4 +19,7 @@ $config = [
     //    'my-custom-authn-context-for-mfa',
     //]),
     //'contexts_regex' => true,
+    //'entityID' => function($request){
+    //    return empty($request["saml:RequesterID"]) ? $request["SPMetadata"]["entityid"] : $request["saml:RequesterID"][0];
+    //},
 ];
diff --git a/lib/Auth/Process/GetMfaTokensPrivacyIDEA.php b/lib/Auth/Process/GetMfaTokensPrivacyIDEA.php
index d3c2b556ad5a498e1b35368966e296aea48c7e0e..610e9f9b0b2544c51fb71be723458f9fa411819e 100644
--- a/lib/Auth/Process/GetMfaTokensPrivacyIDEA.php
+++ b/lib/Auth/Process/GetMfaTokensPrivacyIDEA.php
@@ -23,7 +23,7 @@ class GetMfaTokensPrivacyIDEA extends \SimpleSAML\Auth\ProcessingFilter
 
     private $timeout;
 
-    private $tokens_attr = 'mfaTokens';
+    private $tokens_attr = AuthSwitcher::MFA_TOKENS;
 
     private $privacy_idea_username;
 
diff --git a/lib/Auth/Process/SwitchAuth.php b/lib/Auth/Process/SwitchAuth.php
index cbbe0ddd61bc7d8ace758637861b4d3dbe3b5797..4c9e40956a40443aeca1f7f2c3c89f6815c1bb99 100644
--- a/lib/Auth/Process/SwitchAuth.php
+++ b/lib/Auth/Process/SwitchAuth.php
@@ -20,8 +20,6 @@ class SwitchAuth extends \SimpleSAML\Auth\ProcessingFilter
     /* constants */
     private const DEBUG_PREFIX = 'authswitcher:SwitchAuth: ';
 
-    private const MFA_TOKENS = 'mfaTokens';
-
     private $type_filter_array = [
         'TOTP' => 'privacyidea:PrivacyideaAuthProc',
         'WebAuthn' => 'privacyidea:PrivacyideaAuthProc',
@@ -56,17 +54,14 @@ class SwitchAuth extends \SimpleSAML\Auth\ProcessingFilter
      */
     private $max_auth = 'https://id.muni.cz/profile/maxAuth';
 
-    /**
-     * Whether MFA is enforced for the current user.
-     */
-    private $mfa_enforced;
-
     private $check_entropy = false;
 
     private $sfa_alphabet_attr;
 
     private $sfa_len_attr;
 
+    private $entityID;
+
     /**
      * @override
      *
@@ -99,6 +94,7 @@ class SwitchAuth extends \SimpleSAML\Auth\ProcessingFilter
         $this->sfa_alphabet_attr = $config->getString('sfa_alphabet_attr', $this->sfa_alphabet_attr);
         $this->sfa_len_attr = $config->getString('sfa_len_attr', $this->sfa_len_attr);
         $this->check_entropy = $config->getBoolean('check_entropy', $this->check_entropy);
+        $this->entityID = $config->getValue('entityID', null);
 
         list($this->password_contexts, $this->mfa_contexts, $password_contexts_patterns, $mfa_contexts_patterns) = ContextSettings::parse_config(
             $config
@@ -119,10 +115,10 @@ class SwitchAuth extends \SimpleSAML\Auth\ProcessingFilter
      */
     public function process(&$state)
     {
-        $this->mfa_enforced = !empty($state['Attributes']['mfaEnforced']);
-
         $this->getConfig($this->config);
 
+        $mfaEnforced = Utils::isMFAEnforced($state, $this->entityID);
+
         $usersCapabilities = $this->getMFAForUid($state);
 
         self::info('user capabilities: ' . json_encode($usersCapabilities));
@@ -142,16 +138,14 @@ class SwitchAuth extends \SimpleSAML\Auth\ProcessingFilter
             $state,
             $upstreamContext,
             !$this->check_entropy || $this->checkSfaEntropy($state['Attributes']),
-            $this->mfa_enforced
+            $mfaEnforced
         );
 
         self::info('supported requested contexts: ' . json_encode($this->supported_requested_contexts));
 
         $shouldPerformMFA = !$this->authnContextHelper->MFAin([
             $upstreamContext,
-        ]) && ($this->mfa_enforced || $this->authnContextHelper->isMFAprefered(
-            $this->supported_requested_contexts
-        ));
+        ]) && ($mfaEnforced || $this->authnContextHelper->isMFAprefered($this->supported_requested_contexts));
 
         if ($this->mfa_preferred_privacyidea_fail && !empty($state[AuthSwitcher::PRIVACY_IDEA_FAIL]) && $shouldPerformMFA) {
             throw new Exception(self::DEBUG_PREFIX . 'MFA should be performed but connection to privacyidea failed.');
@@ -276,8 +270,8 @@ class SwitchAuth extends \SimpleSAML\Auth\ProcessingFilter
     private function getMFAForUid($state)
     {
         $result = [];
-        if (!empty($state['Attributes'][self::MFA_TOKENS])) {
-            foreach ($state['Attributes'][self::MFA_TOKENS] as $mfaToken) {
+        if (!empty($state['Attributes'][AuthSwitcher::MFA_TOKENS])) {
+            foreach ($state['Attributes'][AuthSwitcher::MFA_TOKENS] as $mfaToken) {
                 if (is_string($mfaToken)) {
                     $mfaToken = json_decode($mfaToken, true);
                 }
@@ -301,8 +295,8 @@ class SwitchAuth extends \SimpleSAML\Auth\ProcessingFilter
     private function getActiveMethod(&$state)
     {
         $result = [];
-        if (!empty($state['Attributes'][self::MFA_TOKENS])) {
-            foreach ($state['Attributes'][self::MFA_TOKENS] as $mfaToken) {
+        if (!empty($state['Attributes'][AuthSwitcher::MFA_TOKENS])) {
+            foreach ($state['Attributes'][AuthSwitcher::MFA_TOKENS] as $mfaToken) {
                 if (is_string($mfaToken)) {
                     $mfaToken = json_decode($mfaToken, true);
                 }
diff --git a/lib/AuthSwitcher.php b/lib/AuthSwitcher.php
index 1bb80779b5c8f30c4e9dc5f7000c2386f89bb720..232dea79f6892fce7b66270bf86d6710077b7733 100644
--- a/lib/AuthSwitcher.php
+++ b/lib/AuthSwitcher.php
@@ -53,4 +53,24 @@ class AuthSwitcher
     public const PRIVACY_IDEA_FAIL = 'PrivacyIDEAFail';
 
     public const SP_REQUESTED_CONTEXTS = 'AUTHSWITCHER_SP_REQUESTED_CONTEXTS';
+
+    /**
+     * user attribute which contains MFA tokens
+     */
+    public const MFA_TOKENS = 'mfaTokens';
+
+    /**
+     * user attribute which contains boolean whether MFA should be enforced
+     */
+    public const MFA_ENFORCED = 'mfaEnforced';
+
+    /**
+     * user attribute which contains settings object for enforcing MFA
+     */
+    public const MFA_ENFORCE_SETTINGS = 'mfaEnforceSettings';
+
+    /**
+     * user attribute which contains RP category for the current service
+     */
+    public const RP_CATEGORY = 'rpCategory';
 }
diff --git a/lib/Utils.php b/lib/Utils.php
index a5231a9d9edca532aa44f7eef92d2ffcde29199d..40072aa2f25bca213065de46325d97fd020bbdb2 100644
--- a/lib/Utils.php
+++ b/lib/Utils.php
@@ -12,6 +12,8 @@ use SimpleSAML\Module;
  */
 class Utils
 {
+    private const DEBUG_PREFIX = 'authswitcher:Utils: ';
+
     /**
      * Execute an auth proc filter.
      *
@@ -48,7 +50,60 @@ class Utils
     public static function checkVariableInStateAttributes($state, $variable)
     {
         if (!isset($state['Attributes'][$variable])) {
-            throw new Exception('authswitcher:SwitchMfaMethods: ' . $variable . ' missing in state attributes');
+            throw new Exception(self::DEBUG_PREFIX . $variable . ' missing in state attributes');
+        }
+    }
+
+    public static function isMFAEnforced($state, $entityID = null)
+    {
+        if (!empty($state['Attributes'][AuthSwitcher::MFA_ENFORCE_SETTINGS])) {
+            $settings = $state['Attributes'][AuthSwitcher::MFA_ENFORCE_SETTINGS];
+            if (is_string($settings)) {
+                $settings = json_decode($settings, true, 3, JSON_THROW_ON_ERROR);
+            }
+
+            if (!empty($settings['all'])) {
+                Logger::info(self::DEBUG_PREFIX . 'MFA was forced for all services by settings');
+                return true;
+            }
+
+            $rpCategory = $state['Attributes'][AuthSwitcher::RP_CATEGORY][0] ?? 'other';
+
+            $rpIdentifier = self::getEntityID($entityID, $state);
+
+            if (!empty($settings['include_categories']) && in_array(
+                $rpCategory,
+                $settings['include_categories'],
+                true
+            ) && !in_array($rpIdentifier, $settings['exclude_rps'] ?? [], true)) {
+                Logger::info(self::DEBUG_PREFIX . 'MFA was forced for this service by settings');
+                return true;
+            }
+
+            Logger::info(self::DEBUG_PREFIX . 'MFA was not forced by settings');
+            return false;
+        }
+        if (!empty($state['Attributes'][AuthSwitcher::MFA_ENFORCED])) {
+            Logger::info(self::DEBUG_PREFIX . 'MFA was forced for all services by mfaEnforced');
+            return true;
+        }
+        Logger::info(self::DEBUG_PREFIX . 'MFA was not forced');
+        return false;
+    }
+
+    private static function getEntityID($entityID, $request)
+    {
+        if ($entityID === null) {
+            return $request['SPMetadata']['entityid'];
+        }
+        if (is_callable($entityID)) {
+            return call_user_func($entityID, $request);
+        }
+        if (!is_string($entityID)) {
+            throw new Exception(
+                self::DEBUG_PREFIX . 'Invalid configuration option entityID. It must be a string or a callable.'
+            );
         }
+        return $entityID;
     }
 }