Skip to content
Snippets Groups Projects
Unverified Commit a85e75e2 authored by Pavel Břoušek's avatar Pavel Břoušek Committed by GitHub
Browse files

Merge pull request #47 from CESNET/mfaEnforceSettings

Mfa enforce settings
parents c46afd88 354b516a
Branches
Tags
No related merge requests found
...@@ -85,6 +85,9 @@ Add an instance of the auth proc filter with example configuration `authswitcher ...@@ -85,6 +85,9 @@ Add an instance of the auth proc filter with example configuration `authswitcher
// 'my-custom-authn-context-for-mfa', // 'my-custom-authn-context-for-mfa',
//]), //]),
//'contexts_regex' => true, //'contexts_regex' => true,
//'entityID' => function($request){
// return empty($request["saml:RequesterID"]) ? $request["SPMetadata"]["entityid"] : $request["saml:RequesterID"][0];
//},
], ],
'configs' => [ 'configs' => [
'totp:Totp' => [ 'totp:Totp' => [
...@@ -193,6 +196,18 @@ When the attribute is not empty, multi-factor authentication is always performed ...@@ -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. 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 ## Add additional attributes when MFA is performed
To add attributes only if MFA was performed, you can use a filter called `AddAdditionalAttributesAfterMfa`. To add attributes only if MFA was performed, you can use a filter called `AddAdditionalAttributesAfterMfa`.
......
...@@ -19,4 +19,7 @@ $config = [ ...@@ -19,4 +19,7 @@ $config = [
// 'my-custom-authn-context-for-mfa', // 'my-custom-authn-context-for-mfa',
//]), //]),
//'contexts_regex' => true, //'contexts_regex' => true,
//'entityID' => function($request){
// return empty($request["saml:RequesterID"]) ? $request["SPMetadata"]["entityid"] : $request["saml:RequesterID"][0];
//},
]; ];
...@@ -23,7 +23,7 @@ class GetMfaTokensPrivacyIDEA extends \SimpleSAML\Auth\ProcessingFilter ...@@ -23,7 +23,7 @@ class GetMfaTokensPrivacyIDEA extends \SimpleSAML\Auth\ProcessingFilter
private $timeout; private $timeout;
private $tokens_attr = 'mfaTokens'; private $tokens_attr = AuthSwitcher::MFA_TOKENS;
private $privacy_idea_username; private $privacy_idea_username;
......
...@@ -20,8 +20,6 @@ class SwitchAuth extends \SimpleSAML\Auth\ProcessingFilter ...@@ -20,8 +20,6 @@ class SwitchAuth extends \SimpleSAML\Auth\ProcessingFilter
/* constants */ /* constants */
private const DEBUG_PREFIX = 'authswitcher:SwitchAuth: '; private const DEBUG_PREFIX = 'authswitcher:SwitchAuth: ';
private const MFA_TOKENS = 'mfaTokens';
private $type_filter_array = [ private $type_filter_array = [
'TOTP' => 'privacyidea:PrivacyideaAuthProc', 'TOTP' => 'privacyidea:PrivacyideaAuthProc',
'WebAuthn' => 'privacyidea:PrivacyideaAuthProc', 'WebAuthn' => 'privacyidea:PrivacyideaAuthProc',
...@@ -56,17 +54,14 @@ class SwitchAuth extends \SimpleSAML\Auth\ProcessingFilter ...@@ -56,17 +54,14 @@ class SwitchAuth extends \SimpleSAML\Auth\ProcessingFilter
*/ */
private $max_auth = 'https://id.muni.cz/profile/maxAuth'; 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 $check_entropy = false;
private $sfa_alphabet_attr; private $sfa_alphabet_attr;
private $sfa_len_attr; private $sfa_len_attr;
private $entityID;
/** /**
* @override * @override
* *
...@@ -99,6 +94,7 @@ class SwitchAuth extends \SimpleSAML\Auth\ProcessingFilter ...@@ -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_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->sfa_len_attr = $config->getString('sfa_len_attr', $this->sfa_len_attr);
$this->check_entropy = $config->getBoolean('check_entropy', $this->check_entropy); $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( list($this->password_contexts, $this->mfa_contexts, $password_contexts_patterns, $mfa_contexts_patterns) = ContextSettings::parse_config(
$config $config
...@@ -119,10 +115,10 @@ class SwitchAuth extends \SimpleSAML\Auth\ProcessingFilter ...@@ -119,10 +115,10 @@ class SwitchAuth extends \SimpleSAML\Auth\ProcessingFilter
*/ */
public function process(&$state) public function process(&$state)
{ {
$this->mfa_enforced = !empty($state['Attributes']['mfaEnforced']);
$this->getConfig($this->config); $this->getConfig($this->config);
$mfaEnforced = Utils::isMFAEnforced($state, $this->entityID);
$usersCapabilities = $this->getMFAForUid($state); $usersCapabilities = $this->getMFAForUid($state);
self::info('user capabilities: ' . json_encode($usersCapabilities)); self::info('user capabilities: ' . json_encode($usersCapabilities));
...@@ -142,16 +138,14 @@ class SwitchAuth extends \SimpleSAML\Auth\ProcessingFilter ...@@ -142,16 +138,14 @@ class SwitchAuth extends \SimpleSAML\Auth\ProcessingFilter
$state, $state,
$upstreamContext, $upstreamContext,
!$this->check_entropy || $this->checkSfaEntropy($state['Attributes']), !$this->check_entropy || $this->checkSfaEntropy($state['Attributes']),
$this->mfa_enforced $mfaEnforced
); );
self::info('supported requested contexts: ' . json_encode($this->supported_requested_contexts)); self::info('supported requested contexts: ' . json_encode($this->supported_requested_contexts));
$shouldPerformMFA = !$this->authnContextHelper->MFAin([ $shouldPerformMFA = !$this->authnContextHelper->MFAin([
$upstreamContext, $upstreamContext,
]) && ($this->mfa_enforced || $this->authnContextHelper->isMFAprefered( ]) && ($mfaEnforced || $this->authnContextHelper->isMFAprefered($this->supported_requested_contexts));
$this->supported_requested_contexts
));
if ($this->mfa_preferred_privacyidea_fail && !empty($state[AuthSwitcher::PRIVACY_IDEA_FAIL]) && $shouldPerformMFA) { 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.'); 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 ...@@ -276,8 +270,8 @@ class SwitchAuth extends \SimpleSAML\Auth\ProcessingFilter
private function getMFAForUid($state) private function getMFAForUid($state)
{ {
$result = []; $result = [];
if (!empty($state['Attributes'][self::MFA_TOKENS])) { if (!empty($state['Attributes'][AuthSwitcher::MFA_TOKENS])) {
foreach ($state['Attributes'][self::MFA_TOKENS] as $mfaToken) { foreach ($state['Attributes'][AuthSwitcher::MFA_TOKENS] as $mfaToken) {
if (is_string($mfaToken)) { if (is_string($mfaToken)) {
$mfaToken = json_decode($mfaToken, true); $mfaToken = json_decode($mfaToken, true);
} }
...@@ -301,8 +295,8 @@ class SwitchAuth extends \SimpleSAML\Auth\ProcessingFilter ...@@ -301,8 +295,8 @@ class SwitchAuth extends \SimpleSAML\Auth\ProcessingFilter
private function getActiveMethod(&$state) private function getActiveMethod(&$state)
{ {
$result = []; $result = [];
if (!empty($state['Attributes'][self::MFA_TOKENS])) { if (!empty($state['Attributes'][AuthSwitcher::MFA_TOKENS])) {
foreach ($state['Attributes'][self::MFA_TOKENS] as $mfaToken) { foreach ($state['Attributes'][AuthSwitcher::MFA_TOKENS] as $mfaToken) {
if (is_string($mfaToken)) { if (is_string($mfaToken)) {
$mfaToken = json_decode($mfaToken, true); $mfaToken = json_decode($mfaToken, true);
} }
......
...@@ -53,4 +53,24 @@ class AuthSwitcher ...@@ -53,4 +53,24 @@ class AuthSwitcher
public const PRIVACY_IDEA_FAIL = 'PrivacyIDEAFail'; public const PRIVACY_IDEA_FAIL = 'PrivacyIDEAFail';
public const SP_REQUESTED_CONTEXTS = 'AUTHSWITCHER_SP_REQUESTED_CONTEXTS'; 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';
} }
...@@ -12,6 +12,8 @@ use SimpleSAML\Module; ...@@ -12,6 +12,8 @@ use SimpleSAML\Module;
*/ */
class Utils class Utils
{ {
private const DEBUG_PREFIX = 'authswitcher:Utils: ';
/** /**
* Execute an auth proc filter. * Execute an auth proc filter.
* *
...@@ -48,7 +50,60 @@ class Utils ...@@ -48,7 +50,60 @@ class Utils
public static function checkVariableInStateAttributes($state, $variable) public static function checkVariableInStateAttributes($state, $variable)
{ {
if (!isset($state['Attributes'][$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;
} }
} }
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment