Skip to content
Snippets Groups Projects
Verified Commit 354b516a authored by Pavel Břoušek's avatar Pavel Břoušek
Browse files

feat: enforcing MFA per user per service

new attribute mfaEnforceSettings instead of mfaEnforced, new option entityID
parent c1a4ee1a
No related branches found
No related tags found
1 merge request!47Mfa enforce settings
......@@ -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`.
......
......@@ -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];
//},
];
......@@ -60,6 +60,8 @@ class SwitchAuth extends \SimpleSAML\Auth\ProcessingFilter
private $sfa_len_attr;
private $entityID;
/**
* @override
*
......@@ -92,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
......@@ -112,10 +115,10 @@ class SwitchAuth extends \SimpleSAML\Auth\ProcessingFilter
*/
public function process(&$state)
{
$mfaEnforced = Utils::isMFAEnforced($state);
$this->getConfig($this->config);
$mfaEnforced = Utils::isMFAEnforced($state, $this->entityID);
$usersCapabilities = $this->getMFAForUid($state);
self::info('user capabilities: ' . json_encode($usersCapabilities));
......
......@@ -63,4 +63,14 @@ class AuthSwitcher
* 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';
}
......@@ -12,6 +12,8 @@ use SimpleSAML\Module;
*/
class Utils
{
private const DEBUG_PREFIX = 'authswitcher:Utils: ';
/**
* Execute an auth proc filter.
*
......@@ -48,11 +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;
}
public static isMFAEnforced($state) {
return !empty($state['Attributes'][AuthSwitcher::MFA_ENFORCED]);
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;
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment