Newer
Older
Olav Morken
committed
<?php
namespace SimpleSAML\Module\saml\Auth\Source;
use Psr\Http\Message\RequestInterface;
use SimpleSAML\{Auth, Configuration, Error, IdP, Logger, Module, Session, Store, Utils};
use SimpleSAML\Assert\{Assert, AssertionFailedException};
use SimpleSAML\Metadata\MetaDataStorageHandler;
use SimpleSAML\SAML2\{AuthnRequest, Binding, LogoutRequest};
use SimpleSAML\SAML2\Constants as C;
use SimpleSAML\SAML2\Exception\ArrayValidationException;
use SimpleSAML\SAML2\Exception\Protocol\{NoAvailableIDPException, NoPassiveException, NoSupportedIDPException};
use SimpleSAML\SAML2\XML\md\ContactPerson;
use SimpleSAML\SAML2\XML\saml\NameID;
Tim van Dijen
committed
use SimpleSAML\SAML2\XML\saml\{AuthnContextClassRef};
use SimpleSAML\SAML2\XML\samlp\{Extensions, IDPEntry, IDPList, RequestedAuthnContext, RequesterID, Scoping};
use Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory;
use Symfony\Component\HttpFoundation\{RedirectResponse, Request, Response};
use function array_intersect;
use function array_key_exists;
use function array_keys;
Tim van Dijen
committed
use function array_map;
use function call_user_func;
use function count;
use function in_array;
use function is_a;
use function is_array;
use function is_null;
use function sprintf;
use function urlencode;
use function var_export;
class SP extends Auth\Source
{
/**
* The entity ID of this SP.
*
* @var string
*/
/**
* The global configuration.
*
* @var \SimpleSAML\Configuration
*/
private Configuration $config;
* @var \SimpleSAML\Configuration
/**
* The IdP the user is allowed to log into.
*
* @var string|null The IdP the user can log into, or null if the user can log into all IdPs.
*/
/**
* URL to discovery service.
*
* @var string|null
*/
/**
* Flag to indicate whether to disable sending the Scoping element.
*
/**
* If pass AuthnContextClassRef back to the IdPs in front of the SP/IdP Proxy.
*
* @var bool
*/
private bool $passAuthnContextClassRef;
/**
* A list of supported protocols.
*
/**
* Constructor for SAML SP authentication source.
*
* @param array $info Information about this authentication source.
* @param array $config Configuration.
*/
public function __construct(array $info, array $config)
{
// Call the parent constructor first, as required by the interface
parent::__construct($info, $config);
/* For compatibility with code that assumes that $metadata->getString('entityid')
* gives the entity id. */
$config['entityid'] = $config['entityID'];
$this->metadata = Configuration::loadFromArray(
'authsources[' . var_export($this->authId, true) . ']'
$entityId = $this->metadata->getString('entityID');
Assert::validURI($entityId);
Assert::maxLength(
$entityId,
C::SAML2INT_ENTITYID_MAX_LENGTH,
sprintf('The entityID cannot be longer than %d characters.', C::SAML2INT_ENTITYID_MAX_LENGTH)
Assert::notEq(
$entityId,
'https://myapp.example.org/',
'Please set a valid and unique SP entityID',
$this->config = Configuration::getInstance();
$this->idp = $this->metadata->getOptionalString('idp', null);
$this->discoURL = $this->metadata->getOptionalString('discoURL', null);
$this->disable_scoping = $this->metadata->getOptionalBoolean('disable_scoping', false);
$this->passAuthnContextClassRef = $this->metadata->getOptionalBoolean(
'proxymode.passAuthnContextClassRef',
false
);
/**
* Retrieve the URL to the metadata of this SP.
*
* @return string The metadata URL.
*/
return Module::getModuleURL('saml/sp/metadata/' . urlencode($this->authId));
/**
* Retrieve the entity id of this SP.
*
* @return string The entity id of this SP.
*/
* Retrieve the metadata array of this SP, as a remote IdP would see it.
* @return array The metadata array for its use by a remote IdP.
$entityid = $this->getEntityId();
$metadata = [
'entityid' => $entityid,
'SingleLogoutService' => $this->getSLOEndpoints(),
'AssertionConsumerService' => $this->getACSEndpoints(),
];
// add NameIDPolicy
if ($this->metadata->hasValue('NameIDPolicy')) {
$format = $this->metadata->getArray('NameIDPolicy');
if ($format !== []) {
$metadata['NameIDFormat'] = $format['Format'] ?? C::NAMEID_TRANSIENT;
}
}
// add attributes
$name = $this->metadata->getOptionalLocalizedString('name', null);
$attributes = $this->metadata->getOptionalArray('attributes', []);
if ($name !== null) {
if (!empty($attributes)) {
$metadata['name'] = $name;
$metadata['attributes'] = $attributes;
if ($this->metadata->hasValue('attributes.required')) {
$metadata['attributes.required'] = $this->metadata->getArray('attributes.required');
}
if ($this->metadata->hasValue('description')) {
$metadata['description'] = $this->metadata->getArray('description');
}
if ($this->metadata->hasValue('attributes.NameFormat')) {
$metadata['attributes.NameFormat'] = $this->metadata->getString('attributes.NameFormat');
}
if ($this->metadata->hasValue('attributes.index')) {
$metadata['attributes.index'] = $this->metadata->getInteger('attributes.index');
}
if ($this->metadata->hasValue('attributes.isDefault')) {
$metadata['attributes.isDefault'] = $this->metadata->getBoolean('attributes.isDefault');
}
}
}
// add organization info
$org = $this->metadata->getOptionalLocalizedString('OrganizationName', null);
if ($org !== null) {
$metadata['OrganizationName'] = $org;
$metadata['OrganizationDisplayName'] = $this->metadata->getOptionalLocalizedString(
'OrganizationDisplayName',
$org
);
$metadata['OrganizationURL'] = $this->metadata->getOptionalLocalizedString('OrganizationURL', null);
if ($metadata['OrganizationURL'] === null) {
throw new Error\Exception(
'If OrganizationName is set, OrganizationURL must also be set.'
);
}
}
// add contacts
$contacts = $this->metadata->getOptionalArray('contacts', []);
foreach ($contacts as $contact) {
try {
$metadata['contacts'][] = ContactPerson::fromArray($contact)->toArray();
} catch (ArrayValidationException $e) {
Logger::warning('SP metadata: invalid content found in contact: ' . $e->getMessage());
continue;
}
}
// add technical contact
$email = $this->config->getOptionalString('technicalcontact_email', 'na@example.org');
if (!empty($email) && $email !== 'na@example.org') {
$contact = [
'EmailAddress' => [$email],
'GivenName' => $this->config->getOptionalString('technicalcontact_name', null),
'ContactType' => 'technical',
try {
$metadata['contacts'][] = ContactPerson::fromArray($contact)->toArray();
} catch (ArrayValidationException $e) {
Logger::warning('SP metadata: invalid content found in contact: ' . $e->getMessage());
}
$cryptoUtils = new Utils\Crypto();
// add certificate(s)
$certInfo = $cryptoUtils->loadPublicKey($this->metadata, false, 'new_');
$hasNewCert = false;
if ($certInfo !== null && array_key_exists('certData', $certInfo)) {
$hasNewCert = true;
$metadata['keys'][] = [
'type' => 'X509Certificate',
'signing' => true,
'encryption' => true,
'X509Certificate' => $certInfo['certData'],
'prefix' => 'new_',
'name' => $certInfo['name'] ?? null,
];
}
$certInfo = $cryptoUtils->loadPublicKey($this->metadata);
if ($certInfo !== null && array_key_exists('certData', $certInfo)) {
$metadata['keys'][] = [
'type' => 'X509Certificate',
'signing' => true,
'encryption' => $hasNewCert ? false : true,
'X509Certificate' => $certInfo['certData'],
'prefix' => '',
'name' => $certInfo['name'] ?? null,
];
}
// add EntityAttributes extension
if ($this->metadata->hasValue('EntityAttributes')) {
$metadata['EntityAttributes'] = $this->metadata->getArray('EntityAttributes');
}
// add UIInfo extension
if ($this->metadata->hasValue('UIInfo')) {
$metadata['UIInfo'] = $this->metadata->getArray('UIInfo');
}
// add RegistrationInfo extension
if ($this->metadata->hasValue('RegistrationInfo')) {
$metadata['RegistrationInfo'] = $this->metadata->getArray('RegistrationInfo');
}
// add signature options
if ($this->metadata->hasValue('WantAssertionsSigned')) {
$metadata['saml20.sign.assertion'] = $this->metadata->getBoolean('WantAssertionsSigned');
}
if ($this->metadata->hasValue('redirect.sign')) {
$metadata['redirect.validate'] = $this->metadata->getBoolean('redirect.sign');
} elseif ($this->metadata->hasValue('sign.authnrequest')) {
$metadata['validate.authnrequest'] = $this->metadata->getBoolean('sign.authnrequest');
}
return $metadata;
/**
* Retrieve the metadata of an IdP.
*
* @param \SimpleSAML\Configuration $config The configuration
* @param string $entityId The entity id of the IdP.
* @return \SimpleSAML\Configuration The metadata of the IdP.
public function getIdPMetadata(Configuration $config, string $entityId): Configuration
{
if ($this->idp !== null && $this->idp !== $entityId) {
throw new Error\Exception('Cannot retrieve metadata for IdP ' .
var_export($entityId, true) . ' because it isn\'t a valid IdP for this SP.');
$metadataHandler = MetaDataStorageHandler::getMetadataHandler($config);
return $metadataHandler->getMetaDataConfig($entityId, 'saml20-idp-remote');
/**
* Retrieve the metadata of this SP.
*
* @return \SimpleSAML\Configuration The metadata of this SP.
*/
public function getMetadata(): Configuration
{
return $this->metadata;
}
/**
* Get a list with the protocols supported by this SP.
*
public function getSupportedProtocols(): array
{
return $this->protocols;
}
/**
* Get the AssertionConsumerService endpoints for a given local SP.
*
* @return array
* @throws \Exception
*/
private function getACSEndpoints(): array
// If a list of endpoints is specified in config, take that at face value
if ($this->metadata->hasValue('AssertionConsumerService')) {
return $this->metadata->getArray('AssertionConsumerService');
}
$endpoints = [];
$default = [
if ($this->metadata->getOptionalString('ProtocolBinding', null) === C::BINDING_HOK_SSO) {
$default[] = C::BINDING_HOK_SSO;
$bindings = $this->metadata->getOptionalArray('acs.Bindings', $default);
$index = 0;
foreach ($bindings as $service) {
switch ($service) {
$acs = [
'Location' => Module::getModuleURL('saml/sp/saml2-acs.php/' . $this->getAuthId()),
];
break;
$acs = [
'Location' => Module::getModuleURL('saml/sp/saml2-acs.php/' . $this->getAuthId()),
];
break;
$acs = [
'Location' => Module::getModuleURL('saml/sp/saml2-acs.php/' . $this->getAuthId()),
];
break;
Logger::warning('Unknown acs.Binding value specified, ignoring: ' . $service);
continue 2;
}
$acs['index'] = $index;
$endpoints[] = $acs;
$index++;
}
return $endpoints;
}
/**
* Get the SingleLogoutService endpoints available for a given local SP.
*
* @return array
* @throws \SimpleSAML\Error\CriticalConfigurationError
*/
private function getSLOEndpoints(): array
$storeType = $this->config->getOptionalString('store.type', 'phpsession');
$store = StoreFactory::getInstance($storeType);
$bindings = $this->metadata->getOptionalArray(
'SingleLogoutServiceBinding',
[
]
);
$defaultLocation = Module::getModuleURL('saml/sp/saml2-logout.php/' . $this->getAuthId());
$location = $this->metadata->getOptionalString('SingleLogoutServiceLocation', $defaultLocation);
$endpoints = [];
foreach ($bindings as $binding) {
if ($binding == C::BINDING_SOAP && !($store instanceof Store\SQLStore)) {
// we cannot properly support SOAP logout
continue;
}
$endpoints[] = [
'Binding' => $binding,
'Location' => $location,
];
}
return $endpoints;
}
* Send a SAML2 SSO request to an IdP
* @param \SimpleSAML\Configuration $idpMetadata The metadata of the IdP.
* @param array $state The state array for the current authentication.
*/
private function startSSO2(Configuration $idpMetadata, array $state): Response
{
if (isset($state['saml:ProxyCount']) && $state['saml:ProxyCount'] < 0) {
Auth\State::throwException(
new Module\saml\Error\ProxyCountExceeded(C::STATUS_RESPONDER)
$ar = Module\saml\Message::buildAuthnRequest($this->metadata, $idpMetadata);
$ar->setAssertionConsumerServiceURL(Module::getModuleURL('saml/sp/saml2-acs.php/' . $this->authId));
if (isset($state['\SimpleSAML\Auth\Source.ReturnURL'])) {
$ar->setRelayState($state['\SimpleSAML\Auth\Source.ReturnURL']);
$arrayUtils = new Utils\Arrays();
$accr = null;
if ($idpMetadata->getOptionalString('AuthnContextClassRef', null) !== null) {
$accr = $arrayUtils->arrayize($idpMetadata->getString('AuthnContextClassRef'));
Tim van Dijen
committed
$accr = array_map(fn($value): AuthnContextClassRef => new AuthnContextClassRef($value), $accr);
} elseif (isset($state['saml:AuthnContextClassRef'])) {
$accr = $arrayUtils->arrayize($state['saml:AuthnContextClassRef']);
}
if ($accr !== null) {
if ($idpMetadata->getOptionalString('AuthnContextComparison', null) !== null) {
$comp = $idpMetadata->getString('AuthnContextComparison');
} elseif (
isset($state['saml:AuthnContextComparison'])
&& in_array($state['saml:AuthnContextComparison'], [
C::COMPARISON_EXACT,
C::COMPARISON_MINIMUM,
C::COMPARISON_MAXIMUM,
C::COMPARISON_BETTER,
$comp = $state['saml:AuthnContextComparison'];
}
Tim van Dijen
committed
$ar->setRequestedAuthnContext(
new RequestedAuthnContext($accr, $comp),
);
} elseif (
$this->passAuthnContextClassRef
&& isset($state['saml:RequestedAuthnContext'])
&& isset($state['saml:RequestedAuthnContext']['AuthnContextClassRef'])
) {
if (
isset($state['saml:RequestedAuthnContext']['Comparison'])
&& in_array(
$state['saml:RequestedAuthnContext']['Comparison'],
[
C::COMPARISON_EXACT,
C::COMPARISON_MINIMUM,
C::COMPARISON_MAXIMUM,
C::COMPARISON_BETTER,
],
true
)
) {
// RequestedAuthnContext has been set by an SP behind the proxy so pass it to the upper IdP
$ar->setRequestedAuthnContext(
new RequestedAuthnContext(
[
$state['saml:RequestedAuthnContext']['AuthnContextClassRef'],
],
$state['saml:RequestedAuthnContext']['Comparison'],
),
);
if (isset($state['saml:Audience'])) {
$ar->setAudiences($state['saml:Audience']);
}
$ar->setForceAuthn((bool) $state['ForceAuthn']);
}
if (isset($state['isPassive'])) {
$ar->setIsPassive((bool) $state['isPassive']);
}
if (isset($state['saml:NameID'])) {
if (!is_array($state['saml:NameID']) && !is_a($state['saml:NameID'], NameID::class)) {
throw new Error\Exception('Invalid value of $state[\'saml:NameID\'].');
Tim van Dijen
committed
$nid = NameID::fromArray($state['saml:NameID']);
if (!empty($state['saml:NameIDPolicy'])) {
$proxyCount = $idpList = null;
/* Only check for real info for Scoping element if we are going to send Scoping element */
if ($this->disable_scoping !== true && $idpMetadata->getOptionalBoolean('disable_scoping', false) !== true) {
$idpList = $state['IDPList'];
} elseif (!empty($this->metadata->getOptionalArray('IDPList', []))) {
foreach ($this->metadata->getArray('IDPList') as $entry) {
$idpEntry[] = new IDPEntry($entry);
}
$idpList = new IDPList($idpEntry);
} elseif (!empty($idpMetadata->getOptionalArray('IDPList', []))) {
foreach ($idpMetadata->getArray('IDPList') as $entry) {
$idpEntry[] = new IDPEntry($entry);
}
$idpList = new IDPList($idpEntry);
}
if (isset($state['saml:ProxyCount']) && $state['saml:ProxyCount'] !== null) {
$proxyCount = $state['saml:ProxyCount'];
} elseif ($idpMetadata->hasValue('ProxyCount')) {
$proxyCount = $idpMetadata->getInteger('ProxyCount');
} elseif ($this->metadata->hasValue('ProxyCount')) {
$proxyCount = $this->metadata->getInteger('ProxyCount');
}
$requesterID = [];
if (isset($state['saml:RequesterID'])) {
foreach ($state['saml:RequesterID'] as $requesterId) {
$requesterID[] = new RequesterID($requesterId);
}
}
if (isset($state['core:SP'])) {
$requesterID[] = new RequesterID($state['core:SP']);
Logger::debug('Disabling samlp:Scoping for ' . var_export($idpMetadata->getString('entityid'), true));
$scoping = new Scoping($proxyCount, $idpList, $requesterID);
$ar->setScoping($scoping);
// If the downstream SP has set extensions then use them.
// Otherwise use extensions that might be defined in the local SP (only makes sense in a proxy scenario)
if (isset($state['saml:Extensions']) && count($state['saml:Extensions']) > 0) {
$ar->setExtensions(new Extensions($state['saml:Extensions']));
} elseif ($this->metadata->getOptionalArray('saml:Extensions', null) !== null) {
$ar->setExtensions(new Extensions($this->metadata->getArray('saml:Extensions')));
$providerName = $this->metadata->getOptionalString("ProviderName", null);
if ($providerName !== null) {
$ar->setProviderName($providerName);
}
// save IdP entity ID as part of the state
$state['ExpectedIssuer'] = $idpMetadata->getString('entityid');
$id = Auth\State::saveState($state, 'saml:sp:sso', true);
Logger::debug(
'Sending SAML 2 AuthnRequest to ' . var_export($idpMetadata->getString('entityid'), true)
// Select appropriate SSO endpoint
$dst = $idpMetadata->getDefaultEndpoint(
'SingleSignOnService',
$dst = $idpMetadata->getEndpointPrioritizedByBinding(
'SingleSignOnService',
[
C::BINDING_HTTP_ARTIFACT,
}
$ar->setDestination($dst['Location']);
$b = Binding::getBinding($dst['Binding']);
return $this->sendSAML2AuthnRequest($b, $ar);
/**
* Function to actually send the authentication request.
*
* This function does not return.
*
* @param \SimpleSAML\SAML2\Binding $binding The binding.
* @param \SimpleSAML\SAML2\AuthnRequest $ar The authentication request.
public function sendSAML2AuthnRequest(Binding $binding, AuthnRequest $ar): Response
$response = $binding->send($ar);
return (new HttpFoundationFactory())->createResponse($response);
/**
* Function to actually send the logout request.
*
* This function does not return.
*
* @param \SimpleSAML\SAML2\Binding $binding The binding.
* @param \SimpleSAML\SAML2\LogoutRequest $ar The logout request.
public function sendSAML2LogoutRequest(Binding $binding, LogoutRequest $lr): Response
$psrResponse = $binding->send($lr);
$httpFoundationFactory = new HttpFoundationFactory();
return $httpFoundationFactory->createResponse($psrResponse);
/**
* Send a SSO request to an IdP.
*
* @param \SimpleSAML\Configuration $config The configuration
* @param string $idp The entity ID of the IdP.
* @param array $state The state array for the current authentication.
*/
public function startSSO(Configuration $config, string $idp, array $state): Response
$idpMetadata = $this->getIdPMetadata($config, $idp);
$type = $idpMetadata->getString('metadata-set');
return $this->startSSO2($idpMetadata, $state);
/**
* Start an IdP discovery service operation.
*
* @param array $state The state array.
*/
private function startDisco(array $state): RedirectResponse
$id = Auth\State::saveState($state, 'saml:sp:sso');
$discoURL = $this->discoURL;
if ($discoURL === null) {
// Fallback to internal discovery service
$discoURL = Module::getModuleURL('saml/disco');
$returnTo = Module::getModuleURL('saml/sp/discoResponse', ['AuthID' => $id]);
'entityID' => $this->entityId,
'return' => $returnTo,
'returnIDParam' => 'idpentityid'
if (isset($state['saml:IDPList'])) {
$params['IDPList'] = $state['saml:IDPList'];
}
if (isset($state['isPassive']) && $state['isPassive']) {
$params['isPassive'] = 'true';
}
$httpUtils = new Utils\HTTP();
return $httpUtils->redirectTrustedURL($discoURL, $params);
/**
* Start login.
*
* This function saves the information about the login, and redirects to the IdP.
*
* @param \Symfony\Component\HttpFoundation\Request $request The current request
* @param array &$state Information about the current authentication.
*/
public function authenticate(Request $request, array &$state): Response
// We are going to need the authId in order to retrieve this authentication source later
$state['saml:sp:AuthId'] = $this->authId;
$idp = $this->idp;
if (isset($state['saml:idp'])) {
$idp = (string) $state['saml:idp'];
if (isset($state['saml:IDPList']) && count($state['saml:IDPList']) > 0) {
// we have a SAML IDPList (we are a proxy): filter the list of IdPs available
$mdh = MetaDataStorageHandler::getMetadataHandler($this->config);
$matchedEntities = $mdh->getMetaDataForEntities($state['saml:IDPList'], 'saml20-idp-remote');
if (empty($matchedEntities)) {
'None of the IdPs requested are supported by this proxy.'
);
}
if (!is_null($idp) && !array_key_exists($idp, $matchedEntities)) {
// the IdP is enforced but not in the IDPList
'None of the IdPs requested are available to this proxy.'
);
}
if (is_null($idp) && count($matchedEntities) === 1) {
$idp = key($matchedEntities);
$response = $this->startDisco($state);
} else {
$response = $this->startSSO($this->config, $idp, $state);
return $response;
/**
* Re-authenticate an user.
*
* This function is called by the IdP to give the authentication source a chance to
* interact with the user even in the case when the user is already authenticated.
*
* @param array &$state Information about the current authentication.
*/
public function reauthenticate(array &$state): void
$session = Session::getSessionFromRequest();
$data = $session->getAuthState($this->authId);
if ($data === null) {
throw new Error\NoState();
}
foreach ($data as $k => $v) {
$state[$k] = $v;
}
// check if we have an IDPList specified in the request
&& !in_array($state['saml:sp:IdP'], $state['saml:IDPList'], true)
) {
/*
* The user has an existing, valid session. However, the SP
* provided a list of IdPs it accepts for authentication, and
* the IdP the existing session is related to is not in that list.
*
* First, check if we recognize any of the IdPs requested.
*/
$mdh = MetaDataStorageHandler::getMetadataHandler($this->config);
$known_idps = $mdh->getList();
$intersection = array_intersect($state['saml:IDPList'], array_keys($known_idps));
if (empty($intersection)) {
// all requested IdPs are unknown
'None of the IdPs requested are supported by this proxy.'
);
}
/*
* We have at least one IdP in the IDPList that we recognize, and
* it's not the one currently in use. Let's see if this proxy
* enforces the use of one single IdP.
*/
if (!is_null($this->idp) && !in_array($this->idp, $intersection, true)) {
// an IdP is enforced but not requested
'None of the IdPs requested are available to this proxy.'
);
}
/*
* We need to inform the user, and ask whether we should logout before
* starting the authentication process again with a different IdP, or
* cancel the current SSO attempt.
*/
Logger::warning(sprintf(
"Reauthentication after logout is needed. The IdP '%s' is not in the IDPList "
. "provided by the Service Provider '%s'.",
$state['saml:sp:IdP'],
$state['core:SP']
));
Olav Morken
committed
$state['saml:sp:IdPMetadata'] = $this->getIdPMetadata($this->config, $state['saml:sp:IdP']);
$state['saml:sp:AuthId'] = $this->authId;
$response = self::askForIdPChange($state);
$response->send();
}
}
/**
* Ask the user to log out before being able to log in again with a
* different identity provider. Note that this method is intended for
* instances of SimpleSAMLphp running as a SAML proxy, and therefore
* acting both as an SP and an IdP at the same time.
*
* This method will never return.
*
* @param array $state The state array.
* The following keys must be defined in the array:
* - 'saml:sp:IdPMetadata': a \SimpleSAML\Configuration object containing
* the metadata of the IdP that authenticated the user in the current
* session.
* - 'saml:sp:AuthId': the identifier of the current authentication source.
* - 'core:IdP': the identifier of the local IdP.
* - 'SPMetadata': an array with the metadata of this local SP.
*
* @throws \SimpleSAML\SAML2\Exception\Protocol\NoPassiveException In case the authentication request was passive.
public static function askForIdPChange(array &$state): RedirectResponse
Assert::keyExists($state, 'saml:sp:IdPMetadata');
Assert::keyExists($state, 'saml:sp:AuthId');
Assert::keyExists($state, 'core:IdP');
Assert::keyExists($state, 'SPMetadata');
if (isset($state['isPassive']) && (bool) $state['isPassive']) {
// passive request, we cannot authenticate the user
);
}
// save the state WITHOUT a restart URL, so that we don't try an IdP-initiated login if something goes wrong
$id = Auth\State::saveState($state, 'saml:proxy:invalid_idp', true);
$url = Module::getModuleURL('saml/proxy/invalidSession');
$httpUtils = new Utils\HTTP();
return $httpUtils->redirectTrustedURL($url, ['AuthState' => $id]);
/**
* Log the user out before logging in again.
*
* @param \SimpleSAML\Configuration $config The configuration
* @param array $state The state array.
*/
public static function reauthLogout(Configuration $config, array $state): Response
Logger::debug('Proxy: logging the user out before re-authentication.');
if (isset($state['Responder'])) {
$state['saml:proxy:reauthLogout:PrevResponder'] = $state['Responder'];
}
$state['Responder'] = [SP::class, 'reauthPostLogout'];
$idp = IdP::getByState($config, $state);
return $idp->handleLogoutRequest($state, null);
/**
* Complete login operation after re-authenticating the user on another IdP.
*
* @param array $state The authentication state.
*/
public static function reauthPostLogin(array $state): Response
Assert::keyExists($state, 'ReturnCallback');
$session = Session::getSessionFromRequest();
$session->doLogin($authId, Auth\State::getPersistentAuthData($state));
return call_user_func($state['ReturnCallback'], $state);
/**
* Post-logout handler for re-authentication.
*
* This method will never return.
*
* @param \SimpleSAML\IdP $idp The IdP we are logging out from.
* @param array &$state The state array with the state during logout.
*/
public static function reauthPostLogout(IdP $idp, array $state): Response
Assert::keyExists($state, 'saml:sp:AuthId');
Logger::debug('Proxy: logout completed.');
if (isset($state['saml:proxy:reauthLogout:PrevResponder'])) {
$state['Responder'] = $state['saml:proxy:reauthLogout:PrevResponder'];
}
$sp = Auth\Source::getById($state['saml:sp:AuthId'], self::class);
Logger::debug('Proxy: logging in again.');
$request = Request::createFromGlobals();
return $sp->authenticate($request, $state);
/**
* Start a SAML 2 logout operation.
*
* @param \SimpleSAML\Configuration $config The configuration.
* @param array $state The logout state.
*/
public function startSLO2(Configuration $config, array &$state): ?Response
Assert::keyExists($state, 'saml:logout:IdP');
Assert::keyExists($state, 'saml:logout:NameID');
Assert::keyExists($state, 'saml:logout:SessionIndex');
$id = Auth\State::saveState($state, 'saml:slosent');
$idp = $state['saml:logout:IdP'];
$nameId = $state['saml:logout:NameID'];
$sessionIndex = $state['saml:logout:SessionIndex'];
$idpMetadata = $this->getIdPMetadata($config, $idp);
$endpoint = $idpMetadata->getEndpointPrioritizedByBinding(
'SingleLogoutService',