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

feat: utility class for proxy, recover originally requested contexts

parent 022acb67
Branches
Tags
1 merge request!46utility class for proxy, configurable lists of AuthnContextClassRefs
......@@ -151,7 +151,7 @@ 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`:
already. You need to set the `proxy_mode` configuration option to `true`:
```php
53 => [
......@@ -164,6 +164,10 @@ already. You just need to set the `proxy_mode` configuration option to `true`:
]
```
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).
## 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.
......
......@@ -118,6 +118,7 @@ class SwitchAuth extends \SimpleSAML\Auth\ProcessingFilter
if ($this->proxyMode) {
$upstreamContext = ProxyHelper::fetchContextFromUpstreamIdp($state);
self::info('upstream context: ' . $upstreamContext);
ProxyHelper::recoverSPRequestedContexts($state);
} else {
$upstreamContext = null;
}
......
......@@ -71,4 +71,6 @@ class AuthSwitcher
public const ERROR_STATE_STAGE = 'authSwitcher:errorState';
public const PRIVACY_IDEA_FAIL = 'PrivacyIDEAFail';
public const SP_REQUESTED_CONTEXTS = 'AUTHSWITCHER_SP_REQUESTED_CONTEXTS';
}
<?php
declare(strict_types=1);
namespace SimpleSAML\Module\authswitcher;
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 static const DEBUG_PREFIX = 'AuthSwitcher: ';
/**
* 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)
{
$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 (!empty(array_diff(AuthSwitcher::REPLY_CONTEXTS_MFA, $spRequestedContexts))) {
Logger::debug(self::DEBUG_PREFIX . 'SP requested MFA, will prefer MFA at upstream IdP.');
$upstreamRequestedContexts = array_values(
array_unique(array_merge(
AuthSwitcher::REPLY_CONTEXTS_MFA,
$spRequestedContexts,
AuthSwitcher::REPLY_CONTEXTS_SFA
))
);
} 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,
AuthSwitcher::REPLY_CONTEXTS_SFA,
AuthSwitcher::REPLY_CONTEXTS_MFA
))
);
}
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.');
}
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment