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

feat: config options password_contexts and mfa_contexts

also dropped redundant option proxy_mode from additional attributes filter
parent 89ae1552
No related branches found
No related tags found
1 merge request!46utility class for proxy, configurable lists of AuthnContextClassRefs
...@@ -77,6 +77,12 @@ Add an instance of the auth proc filter with example configuration `authswitcher ...@@ -77,6 +77,12 @@ Add an instance of the auth proc filter with example configuration `authswitcher
'preferred_filter' => 'privacyidea:PrivacyideaAuthProc', 'preferred_filter' => 'privacyidea:PrivacyideaAuthProc',
'max_user_capability_attr' => 'maxUserCapability', 'max_user_capability_attr' => 'maxUserCapability',
'max_auth' => 'https://id.muni.cz/profile/maxAuth', 'max_auth' => 'https://id.muni.cz/profile/maxAuth',
//'password_contexts' => array_merge(AuthSwitcher::PASSWORD_CONTEXTS, [
// 'my-custom-authn-context-for-password'
//]),
//'mfa_contexts' => array_merge(AuthSwitcher::MFA_CONTEXTS, [
// 'my-custom-authn-context-for-mfa'
//]),
], ],
'configs' => [ 'configs' => [
'totp:Totp' => [ 'totp:Totp' => [
...@@ -186,7 +192,6 @@ This filter sets attributes based on whether MFA was in fact performed (at upstr ...@@ -186,7 +192,6 @@ This filter sets attributes based on whether MFA was in fact performed (at upstr
`AddAdditionalAttributesAfterMfa` needs to run after the `SwitchAuth` filter. `AddAdditionalAttributesAfterMfa` needs to run after the `SwitchAuth` filter.
In configuration, you just need to add a `custom_attrs` option which contains a map of additional attributes and their values. In configuration, you just need to add a `custom_attrs` option which contains a map of additional attributes and their values.
Also, you can add a `proxy_mode` option as mentioned above.
```php ```php
55 => [ 55 => [
......
...@@ -5,8 +5,6 @@ declare(strict_types=1); ...@@ -5,8 +5,6 @@ declare(strict_types=1);
namespace SimpleSAML\Module\authswitcher\Auth\Process; namespace SimpleSAML\Module\authswitcher\Auth\Process;
use SimpleSAML\Configuration; use SimpleSAML\Configuration;
use SimpleSAML\Module\authswitcher\ProxyHelper;
use SimpleSAML\Module\authswitcher\Utils;
class AddAdditionalAttributesAfterMfa extends \SimpleSAML\Auth\ProcessingFilter class AddAdditionalAttributesAfterMfa extends \SimpleSAML\Auth\ProcessingFilter
{ {
...@@ -14,8 +12,6 @@ class AddAdditionalAttributesAfterMfa extends \SimpleSAML\Auth\ProcessingFilter ...@@ -14,8 +12,6 @@ class AddAdditionalAttributesAfterMfa extends \SimpleSAML\Auth\ProcessingFilter
private $customAttrs; private $customAttrs;
private $proxyMode = false;
public function __construct($config, $reserved) public function __construct($config, $reserved)
{ {
parent::__construct($config, $reserved); parent::__construct($config, $reserved);
...@@ -23,18 +19,11 @@ class AddAdditionalAttributesAfterMfa extends \SimpleSAML\Auth\ProcessingFilter ...@@ -23,18 +19,11 @@ class AddAdditionalAttributesAfterMfa extends \SimpleSAML\Auth\ProcessingFilter
$config = Configuration::loadFromArray($config['config']); $config = Configuration::loadFromArray($config['config']);
$this->customAttrs = $config->getArray('custom_attrs'); $this->customAttrs = $config->getArray('custom_attrs');
$this->proxyMode = $config->getBoolean('proxy_mode', $this->proxyMode);
} }
public function process(&$state) public function process(&$state)
{ {
if ($this->proxyMode) { if ($state[AuthSwitcher::MFA_PERFORMED]) {
$upstreamContext = ProxyHelper::fetchContextFromUpstreamIdp($state);
} else {
$upstreamContext = null;
}
if (Utils::wasMFAPerformed($state, $upstreamContext)) {
foreach ($this->customAttrs as $key => $value) { foreach ($this->customAttrs as $key => $value) {
$state['Attributes'][$key] = $value; $state['Attributes'][$key] = $value;
} }
......
...@@ -96,6 +96,11 @@ class SwitchAuth extends \SimpleSAML\Auth\ProcessingFilter ...@@ -96,6 +96,11 @@ 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->password_contexts = $config->getArray('password_contexts', AuthSwitcher::PASSWORD_CONTEXTS);
$this->mfa_contexts = $config->getArray('mfa_contexts', AuthSwitcher::MFA_CONTEXTS);
$this->authnContextHelper = new AuthnContextHelper($this->password_contexts, $this->mfa_contexts);
} }
/** /**
...@@ -123,7 +128,7 @@ class SwitchAuth extends \SimpleSAML\Auth\ProcessingFilter ...@@ -123,7 +128,7 @@ class SwitchAuth extends \SimpleSAML\Auth\ProcessingFilter
$upstreamContext = null; $upstreamContext = null;
} }
$state[AuthSwitcher::SUPPORTED_REQUESTED_CONTEXTS] = AuthnContextHelper::getSupportedRequestedContexts( $this->supported_requested_contexts = $this->authnContextHelper->getSupportedRequestedContexts(
$usersCapabilities, $usersCapabilities,
$state, $state,
$upstreamContext, $upstreamContext,
...@@ -131,12 +136,12 @@ class SwitchAuth extends \SimpleSAML\Auth\ProcessingFilter ...@@ -131,12 +136,12 @@ class SwitchAuth extends \SimpleSAML\Auth\ProcessingFilter
$this->mfa_enforced $this->mfa_enforced
); );
self::info('supported requested contexts: ' . json_encode($state[AuthSwitcher::SUPPORTED_REQUESTED_CONTEXTS])); self::info('supported requested contexts: ' . json_encode($this->supported_requested_contexts));
$shouldPerformMFA = !AuthnContextHelper::MFAin([ $shouldPerformMFA = !$this->authnContextHelper->MFAin([
$upstreamContext, $upstreamContext,
]) && ($this->mfa_enforced || AuthnContextHelper::isMFAprefered( ]) && ($this->mfa_enforced || $this->authnContextHelper->isMFAprefered(
$state[AuthSwitcher::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) {
...@@ -144,11 +149,13 @@ class SwitchAuth extends \SimpleSAML\Auth\ProcessingFilter ...@@ -144,11 +149,13 @@ class SwitchAuth extends \SimpleSAML\Auth\ProcessingFilter
} }
// switch to MFA if enforced or preferred but not already done if we handle the proxy mode // switch to MFA if enforced or preferred but not already done if we handle the proxy mode
$performMFA = AuthnContextHelper::MFAin($usersCapabilities) && $shouldPerformMFA; $performMFA = $this->authnContextHelper->MFAin($usersCapabilities) && $shouldPerformMFA;
$maxUserCapability = ''; $maxUserCapability = '';
if (in_array(AuthSwitcher::MFA, $usersCapabilities, true) || AuthnContextHelper::MFAin([$upstreamContext])) { if (in_array(AuthSwitcher::REFEDS_MFA, $usersCapabilities, true) || $this->authnContextHelper->MFAin([
$maxUserCapability = AuthSwitcher::MFA; $upstreamContext,
])) {
$maxUserCapability = AuthSwitcher::REFEDS_MFA;
} elseif (count($usersCapabilities) === 1) { } elseif (count($usersCapabilities) === 1) {
$maxUserCapability = $usersCapabilities[0]; $maxUserCapability = $usersCapabilities[0];
} }
...@@ -164,19 +171,19 @@ class SwitchAuth extends \SimpleSAML\Auth\ProcessingFilter ...@@ -164,19 +171,19 @@ class SwitchAuth extends \SimpleSAML\Auth\ProcessingFilter
public function setAuthnContext(&$state, $maxUserCapability, $upstreamContext = null) public function setAuthnContext(&$state, $maxUserCapability, $upstreamContext = null)
{ {
$mfaPerformed = Utils::wasMFAPerformed($state, $upstreamContext); $state[AuthSwitcher::MFA_PERFORMED] = !empty($state[AuthSwitcher::MFA_BEING_PERFORMED]) || $this->authnContextHelper->MFAin([
$upstreamContext,
]);
if ($maxUserCapability === AuthSwitcher::SFA || ($maxUserCapability === AuthSwitcher::MFA && $mfaPerformed)) { if ($maxUserCapability === AuthSwitcher::REFEDS_SFA || ($maxUserCapability === AuthSwitcher::REFEDS_MFA && $state[AuthSwitcher::MFA_PERFORMED])) {
$state['Attributes'][$this->max_user_capability_attr][] = $this->max_auth; $state['Attributes'][$this->max_user_capability_attr][] = $this->max_auth;
} }
$possibleReplies = $mfaPerformed ? array_merge( $possibleReplies = $state[AuthSwitcher::MFA_PERFORMED] ? array_merge(
AuthSwitcher::REPLY_CONTEXTS_MFA, $this->mfa_contexts,
AuthSwitcher::REPLY_CONTEXTS_SFA $this->password_contexts
) : AuthSwitcher::REPLY_CONTEXTS_SFA; ) : $this->password_contexts;
$possibleReplies = array_values( $possibleReplies = array_values(array_intersect($possibleReplies, $this->supported_requested_contexts));
array_intersect($possibleReplies, $state[AuthSwitcher::SUPPORTED_REQUESTED_CONTEXTS])
);
if (empty($possibleReplies)) { if (empty($possibleReplies)) {
AuthnContextHelper::noAuthnContextResponder($state); AuthnContextHelper::noAuthnContextResponder($state);
} else { } else {
...@@ -268,7 +275,7 @@ class SwitchAuth extends \SimpleSAML\Auth\ProcessingFilter ...@@ -268,7 +275,7 @@ class SwitchAuth extends \SimpleSAML\Auth\ProcessingFilter
foreach ($this->type_filter_array as $type => $method) { foreach ($this->type_filter_array as $type => $method) {
if ($mfaToken['revoked'] === false && $mfaToken[$this->token_type_attr] === $type) { if ($mfaToken['revoked'] === false && $mfaToken[$this->token_type_attr] === $type) {
$result[] = AuthSwitcher::MFA; $result[] = AuthSwitcher::REFEDS_MFA;
break; break;
} }
} }
...@@ -277,7 +284,7 @@ class SwitchAuth extends \SimpleSAML\Auth\ProcessingFilter ...@@ -277,7 +284,7 @@ class SwitchAuth extends \SimpleSAML\Auth\ProcessingFilter
} }
} }
} }
$result[] = AuthSwitcher::SFA; $result[] = AuthSwitcher::REFEDS_SFA;
return $result; return $result;
} }
......
...@@ -12,24 +12,24 @@ use SAML2\Constants; ...@@ -12,24 +12,24 @@ use SAML2\Constants;
class AuthSwitcher class AuthSwitcher
{ {
/** /**
* Name of the MFA being performed attribute. * Key into state array for MFA being performed.
*/ */
public const MFA_BEING_PERFORMED = 'mfa_being_performed'; public const MFA_BEING_PERFORMED = 'authswitcher_mfa_being_performed';
/** /**
* Name of the support requested contexts attribute. * Key into state array for MFA was done.
*/ */
public const SUPPORTED_REQUESTED_CONTEXTS = 'authswitcher_supported_requested_contexts'; public const MFA_PERFORMED = 'authswitcher_mfa_performed';
/** /**
* REFEDS profile for SFA. * REFEDS profile for SFA.
*/ */
public const SFA = 'https://refeds.org/profile/sfa'; public const REFEDS_SFA = 'https://refeds.org/profile/sfa';
/** /**
* REFEDS profile for MFA. * REFEDS profile for MFA.
*/ */
public const MFA = 'https://refeds.org/profile/mfa'; public const REFEDS_MFA = 'https://refeds.org/profile/mfa';
/** /**
* Microsoft authentication context for MFA. * Microsoft authentication context for MFA.
...@@ -37,34 +37,14 @@ class AuthSwitcher ...@@ -37,34 +37,14 @@ class AuthSwitcher
public const MS_MFA = 'http://schemas.microsoft.com/claims/multipleauthn'; public const MS_MFA = 'http://schemas.microsoft.com/claims/multipleauthn';
/** /**
* Supported AuthnContexts (pass <= sfa < mfa). * Contexts trusted as multifactor authentication, in the order of preference (for replies).
*/ */
public const SUPPORTED = [Constants::AC_PASSWORD_PROTECTED_TRANSPORT, self::SFA, self::MFA, self::MS_MFA]; public const MFA_CONTEXTS = [self::REFEDS_MFA, self::MS_MFA];
/** /**
* Contexts to assume when request contains none. * Contexts trusted as password authentication, in the order of preference (for replies).
*/ */
public const DEFAULT_REQUESTED_CONTEXTS = [self::SFA, Constants::AC_PASSWORD_PROTECTED_TRANSPORT, self::MFA]; public const PASSWORD_CONTEXTS = [self::REFEDS_SFA, Constants::AC_PASSWORD_PROTECTED_TRANSPORT];
/**
* Contexts to reply when MFA was performed, in the order of preference.
*/
public const REPLY_CONTEXTS_MFA = [self::MFA, self::MS_MFA];
/**
* Contexts to reply when MFA was not performed, in the order of preference.
*/
public const REPLY_CONTEXTS_SFA = [self::SFA, Constants::AC_PASSWORD_PROTECTED_TRANSPORT];
/**
* Contexts which are considered multifactor authentication.
*/
public const MFA_CONTEXTS = self::REPLY_CONTEXTS_MFA;
/**
* Contexts which are considered single factor authentication only.
*/
public const SFA_CONTEXTS = self::REPLY_CONTEXTS_SFA;
public const ERROR_STATE_ID = 'authswitcher_error_state_id'; public const ERROR_STATE_ID = 'authswitcher_error_state_id';
......
...@@ -14,21 +14,29 @@ use SimpleSAML\Module\saml\Error\NoAuthnContext; ...@@ -14,21 +14,29 @@ use SimpleSAML\Module\saml\Error\NoAuthnContext;
*/ */
class AuthnContextHelper class AuthnContextHelper
{ {
public static function MFAin($contexts) public function __construct($password_contexts, $mfa_contexts)
{ {
return array_intersect(AuthSwitcher::MFA_CONTEXTS, $contexts); $this->password_contexts = $password_contexts;
$this->mfa_contexts = $mfa_contexts;
$this->supported_contexts = array_merge($this->mfa_contexts, $this->password_contexts);
$this->default_requested_contexts = array_merge($this->password_contexts, $this->mfa_contexts);
} }
public static function isMFAprefered($supportedRequestedContexts = []) public function MFAin($contexts)
{
return !empty(array_intersect($this->mfa_contexts, $contexts));
}
public function isMFAprefered($supportedRequestedContexts = [])
{ {
return count($supportedRequestedContexts) > 0 && in_array( return count($supportedRequestedContexts) > 0 && in_array(
$supportedRequestedContexts[0], $supportedRequestedContexts[0],
AuthSwitcher::MFA_CONTEXTS, $this->mfa_contexts,
true true
); );
} }
public static function getSupportedRequestedContexts( public function getSupportedRequestedContexts(
$usersCapabilities, $usersCapabilities,
$state, $state,
$upstreamContext, $upstreamContext,
...@@ -39,14 +47,14 @@ class AuthnContextHelper ...@@ -39,14 +47,14 @@ class AuthnContextHelper
if (empty($requestedContexts)) { if (empty($requestedContexts)) {
Logger::info( Logger::info(
'authswitcher: no AuthnContext requested, using default: ' . json_encode( 'authswitcher: no AuthnContext requested, using default: ' . json_encode(
AuthSwitcher::DEFAULT_REQUESTED_CONTEXTS $this->default_requested_contexts
) )
); );
$requestedContexts = AuthSwitcher::DEFAULT_REQUESTED_CONTEXTS; $requestedContexts = $this->default_requested_contexts;
} }
$supportedRequestedContexts = array_values(array_intersect($requestedContexts, AuthSwitcher::SUPPORTED)); $supportedRequestedContexts = array_values(array_intersect($requestedContexts, $this->supported_contexts));
if (!$sfaEntropy) { if (!$sfaEntropy) {
$supportedRequestedContexts = array_diff($supportedRequestedContexts, [Authswitcher::SFA]); $supportedRequestedContexts = array_diff($supportedRequestedContexts, [Authswitcher::REFEDS_SFA]);
Logger::info( Logger::info(
'authswitcher: SFA password entropy level isn\'t satisfied. Remove SFA from SupportedRequestedContext.' 'authswitcher: SFA password entropy level isn\'t satisfied. Remove SFA from SupportedRequestedContext.'
); );
...@@ -63,7 +71,7 @@ class AuthnContextHelper ...@@ -63,7 +71,7 @@ class AuthnContextHelper
// check for unsatisfiable combinations // check for unsatisfiable combinations
if ( if (
!self::testComparison( !$this->testComparison(
$usersCapabilities, $usersCapabilities,
$supportedRequestedContexts, $supportedRequestedContexts,
$state['saml:RequestedAuthnContext']['Comparison'] ?? Constants::COMPARISON_EXACT, $state['saml:RequestedAuthnContext']['Comparison'] ?? Constants::COMPARISON_EXACT,
...@@ -85,15 +93,9 @@ class AuthnContextHelper ...@@ -85,15 +93,9 @@ class AuthnContextHelper
self::noAuthnContext($state, Constants::STATUS_RESPONDER); self::noAuthnContext($state, Constants::STATUS_RESPONDER);
} }
public static function SFAin($contexts) public function SFAin($contexts)
{ {
foreach (AuthSwitcher::SFA_CONTEXTS as $sfa_context) { return !empty(array_intersect($this->password_contexts, $contexts));
if (in_array($sfa_context, $contexts, true)) {
return true;
}
}
return false;
} }
/** /**
...@@ -106,21 +108,21 @@ class AuthnContextHelper ...@@ -106,21 +108,21 @@ class AuthnContextHelper
* @param mixed|null $upstreamContext * @param mixed|null $upstreamContext
* @param mixed $mfaEnforced * @param mixed $mfaEnforced
*/ */
private static function testComparison( private function testComparison(
$usersCapabilities, $usersCapabilities,
$supportedRequestedContexts, $supportedRequestedContexts,
$comparison, $comparison,
$upstreamContext = null, $upstreamContext = null,
$mfaEnforced = false $mfaEnforced = false
) { ) {
$upstreamMFA = $upstreamContext === null ? false : self::MFAin([$upstreamContext]); $upstreamMFA = $upstreamContext === null ? false : $this->MFAin([$upstreamContext]);
$upstreamSFA = $upstreamContext === null ? false : self::SFAin([$upstreamContext]); $upstreamSFA = $upstreamContext === null ? false : $this->SFAin([$upstreamContext]);
$requestedSFA = self::SFAin($supportedRequestedContexts); $requestedSFA = $this->SFAin($supportedRequestedContexts);
$requestedMFA = self::MFAin($supportedRequestedContexts); $requestedMFA = $this->MFAin($supportedRequestedContexts);
$userCanSFA = self::SFAin($usersCapabilities); $userCanSFA = $this->SFAin($usersCapabilities);
$userCanMFA = self::MFAin($usersCapabilities); $userCanMFA = $this->MFAin($usersCapabilities);
switch ($comparison) { switch ($comparison) {
case Constants::COMPARISON_BETTER: case Constants::COMPARISON_BETTER:
......
...@@ -29,17 +29,6 @@ class Utils ...@@ -29,17 +29,6 @@ class Utils
$authFilter->process($state); $authFilter->process($state);
} }
/**
* Check whether MFA was performed, either locally (by running MFA auth proc filters) or at the upstream IdP.
*
* @param mixed $state
* @param mixed|null $upstreamContext
*/
public static function wasMFAPerformed($state, $upstreamContext = null)
{
return !empty($state[AuthSwitcher::MFA_BEING_PERFORMED]) || $upstreamContext === AuthSwitcher::MFA;
}
public static function areFilterModulesEnabled(array $filters) public static function areFilterModulesEnabled(array $filters)
{ {
$invalidModules = []; $invalidModules = [];
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment