-
BREAKING CHANGE: dropped rate limiting support
BREAKING CHANGE: dropped rate limiting support
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
PrivacyideaAuthSource.php 13.90 KiB
<?php
declare(strict_types=1);
namespace SimpleSAML\Module\privacyidea\Auth\Source;
use PrivacyIDEA\PHPClient\PIResponse;
use SimpleSAML\Auth\Source;
use SimpleSAML\Auth\State;
use SimpleSAML\Error\ConfigurationError;
use SimpleSAML\Logger;
use SimpleSAML\Module;
use SimpleSAML\Module\core\Auth\UserPassBase;
use SimpleSAML\Module\privacyidea\Auth\Utils;
use SimpleSAML\Session;
use SimpleSAML\Utils\HTTP;
const DEFAULT_UID_KEYS = ['username', 'surname', 'email', 'givenname', 'mobile', 'phone', 'realm', 'resolver'];
/**
* privacyIDEA authentication module.
*/
class PrivacyideaAuthSource extends UserPassBase
{
/**
* @var array The serverconfig is listed in this array
*/
public $authSourceConfig;
/**
* @var PrivacyIDEA object representing the privacyIDEA authentication server
*/
public $pi;
/**
* Constructor for this authentication source.
*
* @param array $info information about this authentication source
* @param array $config Configuration set in authsources.php
*/
public function __construct(array $info, array $config)
{
assert(gettype($info) === 'array');
assert(gettype($config) === 'array');
parent::__construct($info, $config);
if (!array_key_exists('attributemap', $config)) {
$config['attributemap'] = [];
}
if (!array_key_exists('detailmap', $config)) {
$config['detailmap'] = [];
}
if (!array_key_exists('concatenationmap', $config)) {
$config['concatenationmap'] = [];
}
$this->authSourceConfig = $config;
$this->pi = Utils::createPrivacyIDEAInstance($this->authSourceConfig);
if ($this->pi === null) {
throw new ConfigurationError('privacyIDEA: Initialization failed.');
}
}
/**
* Initialize login. This function saves the information about the login, and redirects to the login page.
*
* @override
*
* @param array $state Information about the current authentication
*/
public function authenticate(&$state)
{
assert(gettype($state) === 'array');
Logger::debug('privacyIDEA AuthSource authenticate');
// SSO check if authentication should be skipped
if (
array_key_exists('SSO', $this->authSourceConfig)
&& $this->authSourceConfig['SSO'] === true
&& Utils::checkForValidSSO($state)
) {
$session = Session::getSessionFromRequest();
$attributes = $session->getData('privacyidea:privacyidea', 'attributes');
Logger('privacyIDEA: SSO retrieved attributes from session: ' . print_r($attributes, true));
$state['Attributes'] = $attributes;
Source::completeAuth($state);
}
$state['privacyidea:privacyidea'] = $this->authSourceConfig;
// We are going to need the authID in order to retrieve this authentication source later.
$state['privacyidea:privacyidea']['AuthId'] = self::getAuthId();
Logger::debug('privacyIDEA AuthSource authId: ' . $state['privacyidea:privacyidea']['AuthId']);
$state['privacyidea:privacyidea']['transactionID'] = '';
$state['privacyidea:privacyidea']['authenticationMethod'] = 'authsource';
$state['privacyidea:privacyidea:ui']['step'] = '1';
$state['privacyidea:privacyidea:ui']['pushAvailable'] = false;
$state['privacyidea:privacyidea:ui']['otpAvailable'] = true;
$state['privacyidea:privacyidea:ui']['message'] = '';
$state['privacyidea:privacyidea:ui']['webAuthnSignRequest'] = '';
$state['privacyidea:privacyidea:ui']['u2fSignRequest'] = '';
$state['privacyidea:privacyidea:ui']['mode'] = 'otp';
$state['privacyidea:privacyidea:ui']['otpFieldHint'] = $this->authSourceConfig['otpFieldHint'] ?? '';
$state['privacyidea:privacyidea:ui']['passFieldHint'] = $this->authSourceConfig['passFieldHint'] ?? '';
$state['privacyidea:privacyidea:ui']['loadCounter'] = '1';
$state['privacyidea:privacyidea:ui']['messageOverride'] = $this->authSourceConfig['messageOverride'] ?? null;
$stateId = State::saveState($state, 'privacyidea:privacyidea');
$url = Module::getModuleURL('privacyidea/FormBuilder.php');
HTTP::redirectTrustedURL(
$url,
[
'stateId' => $stateId,
]
);
}
/**
* This function process the login for auth source.
*
* @param string $stateId
* @param array $formParams
*/
public static function authSourceLogin($stateId, $formParams)
{
assert(gettype($stateId) === 'array');
assert(gettype($formParams) === 'array');
$state = State::loadState($stateId, 'privacyidea:privacyidea');
$step = $state['privacyidea:privacyidea:ui']['step'];
$source = Source::getById($state['privacyidea:privacyidea']['AuthId']);
if (!$source) {
throw new Exception('Could not find authentication source with ID ' . $state['AuthId']);
}
// If it is the first step, trigger challenges or send the password if configured
$username = $formParams['username'];
$password = '';
if (!empty($formParams['pass'])) {
$password = $formParams['pass'];
}
$response = null;
if ($step === 1) {
$state['privacyidea:privacyidea']['username'] = $username;
$stateId = State::saveState($state, 'privacyidea:privacyidea');
if (
array_key_exists('doTriggerChallenge', $source->authSourceConfig)
&& $source->authSourceConfig['doTriggerChallenge'] === true
) {
if (!empty($username) && $source->pi->serviceAccountAvailable()) {
try {
$response = $source->pi->triggerChallenge($username);
} catch (Exception $e) {
Utils::handlePrivacyIDEAException($e, $state);
}
}
} elseif (
array_key_exists('doSendPassword', $source->authSourceConfig)
&& $source->authSourceConfig['doSendPassword'] === true
) {
if (!empty($username)) {
try {
$response = $source->pi->validateCheck($username, $password);
} catch (Exception $e) {
Utils::handlePrivacyIDEAException($e, $state);
}
}
}
// Save the state at the end of step
$stateId = State::saveState($state, 'privacyidea:privacyidea');
} elseif ($step > 1) {
try {
$response = Utils::authenticatePI($state, $formParams);
} catch (Exception $e) {
Utils::handlePrivacyIDEAException($e, $state);
}
$stateId = State::saveState($state, 'privacyidea:privacyidea');
} else {
Logger::error('privacyIDEA: UNDEFINED STEP: ' . $step);
}
if ($response !== null) {
$stateId = Utils::processPIResponse($stateId, $response, $source->authSourceConfig);
}
$state = State::loadState($stateId, 'privacyidea:privacyidea');
// Increase steps counter
if (empty($state['privacyidea:privacyidea']['errorMessage'])) {
$state['privacyidea:privacyidea:ui']['step'] = $step + 1;
}
//Logger::error("NEW STEP: " . $state['privacyidea:privacyidea:ui']['step']);
$stateId = State::saveState($state, 'privacyidea:privacyidea');
$url = Module::getModuleURL('privacyidea/FormBuilder.php');
HTTP::redirectTrustedURL(
$url,
[
'stateId' => $stateId,
]
);
}
/**
* Check if the attributes that are required by SSP are contained in the response of privacyIDEA. They are then
* merged with the attributes specified in the configuration before returning control to SSP. If the authentication
* is complete, this function does not return. If SSO is enabled, this will also register the loginCompletedHandler
* that will write the necessary data.
*
* @param array $state
* @param $authSourceConfig
*/
public static function checkAuthenticationComplete($state, PIResponse $piResponse, $authSourceConfig)
{
$attributes = $piResponse->detailAndAttributes;
if (!empty($attributes)) {
$userAttributes = $attributes['attributes'];
$detailAttributes = $attributes['detail'];
$completeAttributes = self::mergeAttributes($userAttributes, $detailAttributes, $authSourceConfig);
$state['Attributes'] = $completeAttributes;
if (array_key_exists('SSO', $authSourceConfig) && $authSourceConfig['SSO'] === true) {
/*
* In order to be able to register a logout handler for the session (mandatory for SSO to work),
* the authority is required in the session's authData.
* The authority can be put there by invoking Session::doLogin,
* which should be done by the LoginCompletedHandler.
* To be able to do something after Session::doLogin, the LoginCompletedHandler has to be replaced with
* an implementation that writes the SSO data and attributes in this case (AuthSource) to the session.
*/
$state['LoginCompletedHandler'] = [
'SimpleSAML\Module\privacyidea\Auth\Source\PrivacyideaAuthSource',
'loginCompletedWriteSSO',
];
}
// Return control to simpleSAMLphp after successful authentication.
Source::completeAuth($state);
}
}
/**
* Copy of the original loginCompletedHandler that will additionally write the SSO data and user attributes to the
* session after performing the standard login.
*
* @param array $state the state after the login has completed
*/
public static function loginCompletedWriteSSO(array $state)
{
Logger::debug('privacyIDEA: loginCompletedWriteSSO');
assert(array_key_exists('\SimpleSAML\Auth\Source.Return', $state));
assert(array_key_exists('\SimpleSAML\Auth\Source.id', $state));
assert(array_key_exists('Attributes', $state));
assert(!array_key_exists('LogoutState', $state) || is_array($state['LogoutState']));
$return = $state['\SimpleSAML\Auth\Source.Return'];
// save session state
$session = Session::getSessionFromRequest();
$authId = $state['\SimpleSAML\Auth\Source.id'];
$session->doLogin($authId, State::getPersistentAuthData($state));
// In addition to the SSO data, the attributes have to be written to the session so that they can be retrieved
// and used on the next login
Utils::tryWriteSSO();
$session->setData('privacyidea:privacyidea', 'attributes', $state['Attributes']);
if (is_string($return)) {
// redirect...
HTTP::redirectTrustedURL($return);
} else {
call_user_func($return, $state);
}
assert(false);
}
/**
* Attempt to log in using the given username and password.
*
* @override
*
* @param string $username the username the user wrote
* @param string $password the password the user wrote
*/
protected function login($username, $password)
{
// Stub.
Logger::debug('privacyIDEA AuthSource login stub');
}
/**
* This function merge all attributes and detail which SimpleSAMLphp needs.
*
* @param $userAttributes
* @param $detailAttributes
* @param $authSourceConfig
*
* @return array
*/
protected static function mergeAttributes($userAttributes, $detailAttributes, $authSourceConfig)
{
// Prepare attributes array to return
$attributes = [];
// attributemap is set in config/authsources.php
$keys = array_merge(array_keys($authSourceConfig['attributemap']), DEFAULT_UID_KEYS);
$keys = array_unique($keys);
// Keep all reservations from attributemap to translate PI attributes names to SAML attributes names.
foreach ($keys as $key) {
$attributeValue = $userAttributes[$key];
if ($attributeValue) {
$attributeKey = ($authSourceConfig['attributemap'][$key] ?? null) ?: $key;
$attributes[$attributeKey] = is_array($attributeValue) ? $attributeValue : [$attributeValue];
}
}
// Keep all reservations from detailmap to know which attributes are set to show in UI.
// Detailmap was set in config/authsources.php
foreach ($authSourceConfig['detailmap'] as $key => $mappedKey) {
$attributeValue = $detailAttributes->{$key};
$attributes[$mappedKey] = is_array($attributeValue) ? $attributeValue : [$attributeValue];
}
// Keep all reservations from concatenationmap to fuse some attributes together.
// Concatenationmap was set in config/authsources.php
foreach ($authSourceConfig['concatenationmap'] as $key => $mappedKey) {
$concatenationArr = explode(',', $key);
$concatenationValues = [];
foreach ($concatenationArr as $item) {
$concatenationValues[] = $userAttributes->{$item};
}
$concatenationString = implode(' ', $concatenationValues);
$attributes[$mappedKey] = [$concatenationString];
}
Logger::debug('privacyIDEA: Attributes returned: ' . print_r($attributes, true));
return $attributes;
}
/**
* Check if url is allowed.
*
* @param $id
*/
private static function checkIdLegality($id)
{
$sid = State::parseStateID($id);
if ($sid['url'] !== null) {
HTTP::checkURLAllowed($sid['url']);
}
}
}