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

Merge pull request #46 from CESNET/disco-utils

utility class for proxy, configurable lists of AuthnContextClassRefs
parents 022acb67 7e12021b
No related branches found
No related tags found
No related merge requests found
......@@ -77,6 +77,14 @@ Add an instance of the auth proc filter with example configuration `authswitcher
'preferred_filter' => 'privacyidea:PrivacyideaAuthProc',
'max_user_capability_attr' => 'maxUserCapability',
'max_auth' => 'https://id.muni.cz/profile/maxAuth',
//'password_contexts' => array_merge(AuthSwitcher::PASSWORD_CONTEXTS, [
// 'my-custom-authn-context-for-password',
// '/^my-regex-.*/',
//]),
//'mfa_contexts' => array_merge(AuthSwitcher::MFA_CONTEXTS, [
// 'my-custom-authn-context-for-mfa',
//]),
//'contexts_regex' => true,
],
'configs' => [
'totp:Totp' => [
......@@ -117,6 +125,8 @@ Add an instance of the auth proc filter with example configuration `authswitcher
],
```
You can override which AuthnContextClassRefs are treated as password authentication (`password_contexts`) and MFA authentication (`mfa_contexts`). It is recommended to keep the contexts supported by default, e.g. by merging arrays. If you set `contexts_regex` to `true` and a value in one of these options is a regular expression (wrapped in `/`), all contexts matching the expression are matched (but the regular expression is never used as a response).
## MFA tokens
This module expects that there will be a user attribute (`$attributes` aka `$state['Attributes']`) with
......@@ -150,8 +160,9 @@ filter.
## Running in proxy mode
In the proxy mode, it is assumed that the upstream IdP used for authentication could handle the requested `AuthnContext`
already. You just need to set the `proxy_mode` configuration option to `true`:
In proxy mode, you need to make a couple of changes.
First, set the `proxy_mode` configuration option to `true`:
```php
53 => [
......@@ -164,6 +175,14 @@ already. You just need to set the `proxy_mode` configuration option to `true`:
]
```
If you want to modify `password_contexts` or `mfa_contexts`, move the contents of the `config` array into a new file called `config/module_authswitcher.php`. See `config-templates/module_authswitcher.php` for an example. If you do not want to modify these two options, you can keep the config inside the auth proc filter.
You also need to call `DiscoUtils::setUpstreamRequestedAuthnContext($state)` before the user is redirected to upstream IdP, e.g. in the discovery page's code, so that correct AuthnContext is sent to the upstream IdP.
If you only modified the requested AuthnContextClassRef by using the `AuthnContextClassRef` option in `config/authsources.php`, the login at upstream IdP will work, but authswitcher won't be able to process the originally requested AuthnContextClassRefs (because they would be overwriten by the config option).
The last but very important requirement is that you need to modify SimpleSAMLphp by including this patch: https://github.com/simplesamlphp/simplesamlphp/pull/833/files which adds support for passing AuthnContextClassRef to upstream IdP and getting the returned one. To enable the patch, add `'proxymode.passAuthnContextClassRef' => true,` to your `config/config.php`.
## Enforce MFA per user
If a user should only use MFA, set `mfaEnforced` user attribute to a non-empty value. You can fill this attribute any way you like, for example from LDAP or from database.
......@@ -182,7 +201,6 @@ This filter sets attributes based on whether MFA was in fact performed (at upstr
`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.
Also, you can add a `proxy_mode` option as mentioned above.
```php
55 => [
......
<?php
declare(strict_types=1);
$config = [
'type_filter_array' => [
'TOTP' => 'privacyidea:PrivacyideaAuthProc',
'WebAuthn' => 'privacyidea:PrivacyideaAuthProc',
],
'token_type_attr' => 'type',
'preferred_filter' => 'privacyidea:PrivacyideaAuthProc',
'max_user_capability_attr' => 'maxUserCapability',
'max_auth' => 'https://id.muni.cz/profile/maxAuth',
//'password_contexts' => array_merge(AuthSwitcher::PASSWORD_CONTEXTS, [
// 'my-custom-authn-context-for-password',
// '/^my-regex-.*/',
//]),
//'mfa_contexts' => array_merge(AuthSwitcher::MFA_CONTEXTS, [
// 'my-custom-authn-context-for-mfa',
//]),
//'contexts_regex' => true,
];
......@@ -8,7 +8,7 @@ use Symplify\EasyCodingStandard\Config\ECSConfig;
use Symplify\EasyCodingStandard\ValueObject\Set\SetList;
return static function (ECSConfig $ecsConfig): void {
$ecsConfig->paths([__DIR__ . '/ecs.php', __DIR__ . '/lib', __DIR__ . '/www']);
$ecsConfig->paths([__DIR__ . '/config-templates', __DIR__ . '/ecs.php', __DIR__ . '/lib', __DIR__ . '/www']);
$ecsConfig->sets([
SetList::CLEAN_CODE,
......
......@@ -5,8 +5,6 @@ declare(strict_types=1);
namespace SimpleSAML\Module\authswitcher\Auth\Process;
use SimpleSAML\Configuration;
use SimpleSAML\Module\authswitcher\ProxyHelper;
use SimpleSAML\Module\authswitcher\Utils;
class AddAdditionalAttributesAfterMfa extends \SimpleSAML\Auth\ProcessingFilter
{
......@@ -14,8 +12,6 @@ class AddAdditionalAttributesAfterMfa extends \SimpleSAML\Auth\ProcessingFilter
private $customAttrs;
private $proxyMode = false;
public function __construct($config, $reserved)
{
parent::__construct($config, $reserved);
......@@ -23,18 +19,11 @@ class AddAdditionalAttributesAfterMfa extends \SimpleSAML\Auth\ProcessingFilter
$config = Configuration::loadFromArray($config['config']);
$this->customAttrs = $config->getArray('custom_attrs');
$this->proxyMode = $config->getBoolean('proxy_mode', $this->proxyMode);
}
public function process(&$state)
{
if ($this->proxyMode) {
$upstreamContext = ProxyHelper::fetchContextFromUpstreamIdp($state);
} else {
$upstreamContext = null;
}
if (Utils::wasMFAPerformed($state, $upstreamContext)) {
if ($state[AuthSwitcher::MFA_PERFORMED]) {
foreach ($this->customAttrs as $key => $value) {
$state['Attributes'][$key] = $value;
}
......
......@@ -11,6 +11,7 @@ use SimpleSAML\Error\Exception;
use SimpleSAML\Logger;
use SimpleSAML\Module\authswitcher\AuthnContextHelper;
use SimpleSAML\Module\authswitcher\AuthSwitcher;
use SimpleSAML\Module\authswitcher\ContextSettings;
use SimpleSAML\Module\authswitcher\ProxyHelper;
use SimpleSAML\Module\authswitcher\Utils;
......@@ -78,7 +79,9 @@ class SwitchAuth extends \SimpleSAML\Auth\ProcessingFilter
$this->config = $config;
$this->reserved = $reserved;
$config = Configuration::loadFromArray($config['config']);
$config = isset($config['config']) ? Configuration::loadFromArray(
$config['config']
) : Configuration::getOptionalConfig('module_authswitcher.php');
$this->type_filter_array = $config->getArray('type_filter_array', $this->type_filter_array);
$this->mobile_friendly_filters = $config->getArray('mobile_friendly_filters', $this->mobile_friendly_filters);
$this->token_type_attr = $config->getString('token_type_attr', $this->token_type_attr);
......@@ -96,6 +99,17 @@ 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);
list($this->password_contexts, $this->mfa_contexts, $password_contexts_patterns, $mfa_contexts_patterns) = ContextSettings::parse_config(
$config
);
$this->authnContextHelper = new AuthnContextHelper(
$this->password_contexts,
$this->mfa_contexts,
$password_contexts_patterns,
$mfa_contexts_patterns
);
}
/**
......@@ -118,11 +132,12 @@ class SwitchAuth extends \SimpleSAML\Auth\ProcessingFilter
if ($this->proxyMode) {
$upstreamContext = ProxyHelper::fetchContextFromUpstreamIdp($state);
self::info('upstream context: ' . $upstreamContext);
ProxyHelper::recoverSPRequestedContexts($state);
} else {
$upstreamContext = null;
}
$state[AuthSwitcher::SUPPORTED_REQUESTED_CONTEXTS] = AuthnContextHelper::getSupportedRequestedContexts(
$this->supported_requested_contexts = $this->authnContextHelper->getSupportedRequestedContexts(
$usersCapabilities,
$state,
$upstreamContext,
......@@ -130,12 +145,12 @@ class SwitchAuth extends \SimpleSAML\Auth\ProcessingFilter
$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,
]) && ($this->mfa_enforced || AuthnContextHelper::isMFAprefered(
$state[AuthSwitcher::SUPPORTED_REQUESTED_CONTEXTS]
]) && ($this->mfa_enforced || $this->authnContextHelper->isMFAprefered(
$this->supported_requested_contexts
));
if ($this->mfa_preferred_privacyidea_fail && !empty($state[AuthSwitcher::PRIVACY_IDEA_FAIL]) && $shouldPerformMFA) {
......@@ -143,11 +158,13 @@ class SwitchAuth extends \SimpleSAML\Auth\ProcessingFilter
}
// 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 = '';
if (in_array(AuthSwitcher::MFA, $usersCapabilities, true) || AuthnContextHelper::MFAin([$upstreamContext])) {
$maxUserCapability = AuthSwitcher::MFA;
if (in_array(AuthSwitcher::REFEDS_MFA, $usersCapabilities, true) || $this->authnContextHelper->MFAin([
$upstreamContext,
])) {
$maxUserCapability = AuthSwitcher::REFEDS_MFA;
} elseif (count($usersCapabilities) === 1) {
$maxUserCapability = $usersCapabilities[0];
}
......@@ -163,19 +180,19 @@ class SwitchAuth extends \SimpleSAML\Auth\ProcessingFilter
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;
}
$possibleReplies = $mfaPerformed ? array_merge(
AuthSwitcher::REPLY_CONTEXTS_MFA,
AuthSwitcher::REPLY_CONTEXTS_SFA
) : AuthSwitcher::REPLY_CONTEXTS_SFA;
$possibleReplies = array_values(
array_intersect($possibleReplies, $state[AuthSwitcher::SUPPORTED_REQUESTED_CONTEXTS])
);
$possibleReplies = $state[AuthSwitcher::MFA_PERFORMED] ? array_merge(
$this->mfa_contexts,
$this->password_contexts
) : $this->password_contexts;
$possibleReplies = array_values(array_intersect($possibleReplies, $this->supported_requested_contexts));
if (empty($possibleReplies)) {
AuthnContextHelper::noAuthnContextResponder($state);
} else {
......@@ -267,7 +284,7 @@ class SwitchAuth extends \SimpleSAML\Auth\ProcessingFilter
foreach ($this->type_filter_array as $type => $method) {
if ($mfaToken['revoked'] === false && $mfaToken[$this->token_type_attr] === $type) {
$result[] = AuthSwitcher::MFA;
$result[] = AuthSwitcher::REFEDS_MFA;
break;
}
}
......@@ -276,7 +293,7 @@ class SwitchAuth extends \SimpleSAML\Auth\ProcessingFilter
}
}
}
$result[] = AuthSwitcher::SFA;
$result[] = AuthSwitcher::REFEDS_SFA;
return $result;
}
......
......@@ -12,24 +12,24 @@ use SAML2\Constants;
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.
*/
public const SFA = 'https://refeds.org/profile/sfa';
public const REFEDS_SFA = 'https://refeds.org/profile/sfa';
/**
* 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.
......@@ -37,38 +37,20 @@ class AuthSwitcher
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];
/**
* 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 PASSWORD_CONTEXTS = [self::REFEDS_SFA, Constants::AC_PASSWORD_PROTECTED_TRANSPORT];
public const ERROR_STATE_ID = 'authswitcher_error_state_id';
public const ERROR_STATE_STAGE = 'authSwitcher:errorState';
public const PRIVACY_IDEA_FAIL = 'PrivacyIDEAFail';
public const SP_REQUESTED_CONTEXTS = 'AUTHSWITCHER_SP_REQUESTED_CONTEXTS';
}
......@@ -14,21 +14,35 @@ use SimpleSAML\Module\saml\Error\NoAuthnContext;
*/
class AuthnContextHelper
{
public static function MFAin($contexts)
public function __construct(
$password_contexts,
$mfa_contexts,
$password_contexts_patterns = [],
$mfa_contexts_patterns = []
) {
$this->password_contexts = $password_contexts;
$this->password_contexts_patterns = $password_contexts_patterns;
$this->mfa_contexts = $mfa_contexts;
$this->mfa_contexts_patterns = $mfa_contexts_patterns;
$this->supported_contexts = array_merge($this->mfa_contexts, $this->password_contexts);
$this->default_requested_contexts = array_merge($this->password_contexts, $this->mfa_contexts);
}
public function MFAin($contexts)
{
return array_intersect(AuthSwitcher::MFA_CONTEXTS, $contexts);
return $this->contextsMatch($contexts, $this->mfa_contexts, $this->mfa_contexts_patterns);
}
public static function isMFAprefered($supportedRequestedContexts = [])
public function isMFAprefered($supportedRequestedContexts = [])
{
return count($supportedRequestedContexts) > 0 && in_array(
$supportedRequestedContexts[0],
AuthSwitcher::MFA_CONTEXTS,
$this->mfa_contexts,
true
);
}
public static function getSupportedRequestedContexts(
public function getSupportedRequestedContexts(
$usersCapabilities,
$state,
$upstreamContext,
......@@ -39,14 +53,14 @@ class AuthnContextHelper
if (empty($requestedContexts)) {
Logger::info(
'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) {
$supportedRequestedContexts = array_diff($supportedRequestedContexts, [Authswitcher::SFA]);
$supportedRequestedContexts = array_diff($supportedRequestedContexts, [Authswitcher::REFEDS_SFA]);
Logger::info(
'authswitcher: SFA password entropy level isn\'t satisfied. Remove SFA from SupportedRequestedContext.'
);
......@@ -63,7 +77,7 @@ class AuthnContextHelper
// check for unsatisfiable combinations
if (
!self::testComparison(
!$this->testComparison(
$usersCapabilities,
$supportedRequestedContexts,
$state['saml:RequestedAuthnContext']['Comparison'] ?? Constants::COMPARISON_EXACT,
......@@ -85,17 +99,31 @@ class AuthnContextHelper
self::noAuthnContext($state, Constants::STATUS_RESPONDER);
}
public static function SFAin($contexts)
public function SFAin($contexts)
{
return $this->contextsMatch($contexts, $this->password_contexts, $this->password_contexts_patterns);
}
private static function inPatterns($patterns, $contexts)
{
foreach (AuthSwitcher::SFA_CONTEXTS as $sfa_context) {
if (in_array($sfa_context, $contexts, true)) {
return true;
foreach ($patterns as $pattern) {
foreach ($contexts as $context) {
if (preg_match($pattern, $context)) {
return true;
}
}
}
return false;
}
private function contextsMatch($inputContexts, $matchedContexts, $matchedPatterns)
{
return !empty(array_intersect($matchedContexts, $inputContexts)) || self::inPatterns(
$matchedPatterns,
$inputContexts
);
}
/**
* If the Comparison attribute is set to “better”, “minimum”, or “maximum”, the method of authentication
* must be stronger than, at least as strong as, or no stronger than one of the specified authentication classes.
......@@ -106,21 +134,21 @@ class AuthnContextHelper
* @param mixed|null $upstreamContext
* @param mixed $mfaEnforced
*/
private static function testComparison(
private function testComparison(
$usersCapabilities,
$supportedRequestedContexts,
$comparison,
$upstreamContext = null,
$mfaEnforced = false
) {
$upstreamMFA = $upstreamContext === null ? false : self::MFAin([$upstreamContext]);
$upstreamSFA = $upstreamContext === null ? false : self::SFAin([$upstreamContext]);
$upstreamMFA = $upstreamContext === null ? false : $this->MFAin([$upstreamContext]);
$upstreamSFA = $upstreamContext === null ? false : $this->SFAin([$upstreamContext]);
$requestedSFA = self::SFAin($supportedRequestedContexts);
$requestedMFA = self::MFAin($supportedRequestedContexts);
$requestedSFA = $this->SFAin($supportedRequestedContexts);
$requestedMFA = $this->MFAin($supportedRequestedContexts);
$userCanSFA = self::SFAin($usersCapabilities);
$userCanMFA = self::MFAin($usersCapabilities);
$userCanSFA = $this->SFAin($usersCapabilities);
$userCanMFA = $this->MFAin($usersCapabilities);
switch ($comparison) {
case Constants::COMPARISON_BETTER:
......
<?php
declare(strict_types=1);
namespace SimpleSAML\Module\authswitcher;
class ContextSettings
{
public static function parse_config($config)
{
$contexts_regex = $config->getBoolean('contexts_regex', false);
$password_contexts = $config->getArray('password_contexts', AuthSwitcher::PASSWORD_CONTEXTS);
$mfa_contexts = $config->getArray('mfa_contexts', AuthSwitcher::MFA_CONTEXTS);
if ($contexts_regex) {
$password_contexts_patterns = array_filter(self::is_regex, $password_contexts);
$password_contexts = array_diff($password_contexts, $password_contexts_patterns);
$mfa_contexts_patterns = array_filter(self::is_regex, $mfa_contexts);
$mfa_contexts = array_diff($mfa_contexts, $mfa_contexts_patterns);
} else {
$password_contexts_patterns = [];
$mfa_contexts_patterns = [];
}
return [$password_contexts, $mfa_contexts, $password_contexts_patterns, $mfa_contexts_patterns];
}
private static function is_regex($str)
{
return strlen($str) > 2 && substr($str, 0, 1) === '/' && substr($str, -1) === '/';
}
}
<?php
declare(strict_types=1);
namespace SimpleSAML\Module\authswitcher;
use SimpleSAML\Configuration;
use SimpleSAML\Logger;
/**
* Utility class for handling AuthnContextClassRef on a proxy.
*
* The setUpstreamRequestedAuthnContext method needs to be called from a disco page (before the user is sent to upstream
* IdP).
*/
class DiscoUtils
{
private const DEBUG_PREFIX = 'authswitcher:DiscoUtils: ';
/**
* Store requested AuthnContextClassRef from SP and modify them before sending to upstream IdP.
*
* Contexts for password and MFA authentication are added to non-empty requested contexts so that upstream IdP does
* not fail with an error.
*
* @param array $state global state (request)
*/
public static function setUpstreamRequestedAuthnContext(array &$state)
{
$config = Configuration::getOptionalConfig('module_authswitcher.php');
list($password_contexts, $mfa_contexts, $password_contexts_patterns, $mfa_contexts_patterns) = ContextSettings::parse_config(
$config
);
$authnContextHelper = new AuthnContextHelper(
$password_contexts,
$mfa_contexts,
$password_contexts_patterns,
$mfa_contexts_patterns
);
$spRequestedContexts = $state['saml:RequestedAuthnContext']['AuthnContextClassRef'] ?? [];
// store originally requested contexts for correct handling in SwitchAuth
$state[AuthSwitcher::SP_REQUESTED_CONTEXTS] = $spRequestedContexts;
$upstreamRequestedContexts = [];
if (empty($spRequestedContexts)) {
Logger::debug(self::DEBUG_PREFIX . 'No AuthnContextClassRef requested, not sending any to upstream IdP.');
} elseif ($authnContextHelper->MFAin($spRequestedContexts)) {
Logger::debug(self::DEBUG_PREFIX . 'SP requested MFA, will prefer MFA at upstream IdP.');
$upstreamRequestedContexts = array_values(
array_unique(array_merge($mfa_contexts, $spRequestedContexts, $password_contexts))
);
} else {
Logger::debug(self::DEBUG_PREFIX . 'SP did not request MFA, will prefer SFA at upstream IdP.');
$upstreamRequestedContexts = array_values(
array_unique(array_merge($spRequestedContexts, $password_contexts, $mfa_contexts))
);
}
if (!empty($upstreamRequestedContexts)) {
Logger::debug(
self::DEBUG_PREFIX . 'AuthnContextClassRefs sent to upstream IdP: ' . join(
',',
$upstreamRequestedContexts
)
);
$state['saml:RequestedAuthnContext']['AuthnContextClassRef'] = $upstreamRequestedContexts;
}
}
}
......@@ -4,16 +4,18 @@ declare(strict_types=1);
namespace SimpleSAML\Module\authswitcher;
use SimpleSAML\Logger;
class ProxyHelper
{
/**
* Return context from IdP if replied with valid one.
*/
public static function fetchContextFromUpstreamIdp($state)
{
if (
isset($state['saml:RequestedAuthnContext'], $state['saml:RequestedAuthnContext']['AuthnContextClassRef'], $state['saml:sp:AuthnContext'])
) {
if (isset($state['saml:RequestedAuthnContext']['AuthnContextClassRef']) && isset($state['saml:sp:AuthnContext'])) {
$upstreamIdpAuthnContextClassRef = $state['saml:sp:AuthnContext'];
$requestedAuthnContext = $state['saml:RequestedAuthnContext'];
$requestedContexts = $requestedAuthnContext['AuthnContextClassRef'];
$requestedContexts = $state['saml:RequestedAuthnContext']['AuthnContextClassRef'];
if (
in_array($upstreamIdpAuthnContextClassRef, $requestedContexts, true)
&& !empty($upstreamIdpAuthnContextClassRef)
......@@ -22,6 +24,16 @@ class ProxyHelper
}
}
// IdP returned a context which was not requested, ignore it
return null;
}
public static function recoverSPRequestedContexts(&$state)
{
if (isset($state[AuthSwitcher::SP_REQUESTED_CONTEXTS])) {
$state['saml:RequestedAuthnContext']['AuthnContextClassRef'] = $state[AuthSwitcher::SP_REQUESTED_CONTEXTS];
} else {
Logger::error('authswitcher: running in proxy mode but setUpstreamRequestedAuthnContext was not called.');
}
}
}
......@@ -29,17 +29,6 @@ class Utils
$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)
{
$invalidModules = [];
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment