Skip to content
Snippets Groups Projects
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
IdP.php 15.67 KiB
<?php

/**
 * IdP class.
 *
 * This class implements the various functions used by IdP.
 *
 * @package simpleSAMLphp
 * @version $Id$
 */
class SimpleSAML_IdP {

	/**
	 * A cache for resolving IdP id's.
	 *
	 * @var array
	 */
	private static $idpCache = array();


	/**
	 * The identifier for this IdP.
	 *
	 * @var string
	 */
	private $id;


	/**
	 * The "association group" for this IdP.
	 *
	 * We use this to support cross-protocol logout until
	 * we implement a cross-protocol IdP.
	 *
	 * @var string
	 */
	private $associationGroup;


	/**
	 * The configuration for this IdP.
	 *
	 * @var SimpleSAML_Configuration
	 */
	private $config;


	/**
	 * Initialize an IdP.
	 *
	 * @param string $id  The identifier of this IdP.
	 */
	private function __construct($id) {
		assert('is_string($id)');

		$this->id = $id;

		$metadata = SimpleSAML_Metadata_MetaDataStorageHandler::getMetadataHandler();
		$globalConfig = SimpleSAML_Configuration::getInstance();

		if (substr($id, 0, 6) === 'saml2:') {
			if (!$globalConfig->getBoolean('enable.saml20-idp', FALSE)) {
				throw new SimpleSAML_Error_Exception('enable.saml20-idp disabled in config.php.');
			}
			$this->config = $metadata->getMetaDataConfig(substr($id, 6), 'saml20-idp-hosted');
		} elseif (substr($id, 0, 6) === 'saml1:') {
			if (!$globalConfig->getBoolean('enable.shib13-idp', FALSE)) {
				throw new SimpleSAML_Error_Exception('enable.shib13-idp disabled in config.php.');
			}
			$this->config = $metadata->getMetaDataConfig(substr($id, 6), 'shib13-idp-hosted');
		} elseif (substr($id, 0, 5) === 'adfs:') {
			if (!$globalConfig->getBoolean('enable.adfs-idp', FALSE)) {
				throw new SimpleSAML_Error_Exception('enable.adfs-idp disabled in config.php.');
			}
			$this->config = $metadata->getMetaDataConfig(substr($id, 5), 'adfs-idp-hosted');

			try {
				/* This makes the ADFS IdP use the same SP associations as the SAML 2.0 IdP. */
				$saml2EntityId = $metadata->getMetaDataCurrentEntityID('saml20-idp-hosted');
				$this->associationGroup = 'saml2:' . $saml2EntityId;
								
			} catch (Exception $e) {
				/* Probably no SAML 2 IdP configured for this host. Ignore the error. */
			}
		} else {
			assert(FALSE);
		}

		if ($this->associationGroup === NULL) {
			$this->associationGroup = $this->id;
		}

	}


	/**
	 * Retrieve the ID of this IdP.
	 *
	 * @return string  The ID of this IdP.
	 */
	public function getId() {
		return $this->id;
	}


	/**
	 * Retrieve an IdP by ID.
	 *
	 * @param string $id  The identifier of the IdP.
	 * @return SimpleSAML_IdP  The IdP.
	 */
	public static function getById($id) {
		assert('is_string($id)');

		if (isset(self::$idpCache[$id])) {
			return self::$idpCache[$id];
		}

		$idp = new self($id);
		self::$idpCache[$id] = $idp;
		return $idp;
	}


	/**
	 * Retrieve the IdP "owning" the state.
	 *
	 * @param array &$state  The state array.
	 * @return SimpleSAML_IdP  The IdP.
	 */
	public static function getByState(array &$state) {
		assert('isset($state["core:IdP"])');

		return self::getById($state['core:IdP']);
	}


	/**
	 * Retrieve the configuration for this IdP.
	 *
	 * @return SimpleSAML_Configuration  The configuration object.
	 */
	public function getConfig() {

		return $this->config;
	}


	/**
	 * Get SP name.
	 *
	 * @param string $assocId  The association identifier.
	 * @return array|NULL  The name of the SP, as an associative array of language=>text, or NULL if this isn't an SP.
	 */
	public function getSPName($assocId) {
		assert('is_string($assocId)');

		$prefix = substr($assocId, 0, 4);
		$spEntityId = substr($assocId, strlen($prefix) + 1);
		$metadata = SimpleSAML_Metadata_MetaDataStorageHandler::getMetadataHandler();

		if ($prefix === 'saml') {
			try {
				$spMetadata = $metadata->getMetaDataConfig($spEntityId, 'saml20-sp-remote');
			} catch (Exception $e) {
				try {
					$spMetadata = $metadata->getMetaDataConfig($spEntityId, 'shib13-sp-remote');
				} catch (Exception $e) {
					return NULL;
				}
			}
		} else if ($prefix === 'adfs') {
			$spMetadata = $metadata->getMetaDataConfig($spEntityId, 'adfs-sp-remote');
		} else {
			return NULL;
		}

		if ($spMetadata->hasValue('name')) {
			return $spMetadata->getLocalizedString('name');
		} elseif ($spMetadata->hasValue('OrganizationDisplayName')) {
			return $spMetadata->getLocalizedString('OrganizationDisplayName');
		} else {
			return array('en' => $spEntityId);
		}
	}


	/**
	 * Add an SP association.
	 *
	 * @param array  The SP association.
	 */
	public function addAssociation(array $association) {
		assert('isset($association["id"])');
		assert('isset($association["Handler"])');

		$association['core:IdP'] = $this->id;
		
		$session = SimpleSAML_Session::getInstance();
		$session->addAssociation($this->associationGroup, $association);
	}


	/**
	 * Retrieve list of SP associations.
	 *
	 * @return array  List of SP associations.
	 */
	public function getAssociations() {
		$session = SimpleSAML_Session::getInstance();
		return $session->getAssociations($this->associationGroup);
	}


	/**
	 * Remove an SP association.
	 *
	 * @param string $assocId  The association id.
	 */
	public function terminateAssociation($assocId) {
		assert('is_string($assocId)');

		$session = SimpleSAML_Session::getInstance();
		$session->terminateAssociation($this->associationGroup, $assocId);
	}


	/**
	 * Retrieve the authority for the given IdP metadata.
	 *
	 * This function provides backwards-compatibility with
	 * previous versions of simpleSAMLphp.
	 *
	 * @param array $idpmetadata  The IdP metadata.
	 * @return string  The authority that should be used to validate the session.
	 */
	private function getAuthority() {

		if ($this->config->hasValue('authority')) {
			return $this->config->getString('authority');
		}

		$candidates = array(
			'auth/login-admin.php' => 'login-admin',
			'auth/login-cas-ldap.php' => 'login-cas-ldap',
			'auth/login-ldapmulti.php' => 'login-ldapmulti',
			'auth/login-radius.php' => 'login-radius',
			'auth/login-tlsclient.php' => 'tlsclient',
			'auth/login-wayf-ldap.php' => 'login-wayf-ldap',
			'auth/login.php' => 'login',
		);

		$auth = $this->config->getString('auth');

		if (isset($candidates[$auth])) {
			return $candidates[$auth];
		}
		if (strpos($auth, '/') !== FALSE) {
			/* Probably a file. */
			throw new SimpleSAML_Error_Exception('You need to set \'authority\' in the metadata for ' .
				var_export($this->id, TRUE) . '.');
		} else {
			throw new SimpleSAML_Error_Exception('Unknown authsource ' .
				var_export($auth, TRUE) . '.');
		}
	}


	/**
	 * Is the current user authenticated?
	 *
	 * @return bool  TRUE if the user is authenticated, FALSE if not.
	 */
	public function isAuthenticated() {

		$session = SimpleSAML_Session::getInstance();

		$authority = $this->config->getString('auth');
		if ($session->isValid($authority)) {
			return TRUE;
		}

		/* Maybe the 'auth' option didn't point to an authentication source? */
		if (SimpleSAML_Auth_Source::getById($authority) !== NULL) {
			/* It was an authentication source - the user is therefore not authenticated. */
			return FALSE;
		}

		/* It wasn't an authentication source. */
		$authority = $this->getAuthority();
		return $session->isValid($authority);
	}


	/**
	 * Called after authproc has run.
	 *
	 * @param array $state  The authentication request state array.
	 */
	public static function postAuthProc(array $state) {
		assert('is_callable($state["Responder"])');

		if (isset($state['core:SP'])) {
			$session = SimpleSAML_Session::getInstance();
			$session->setData('core:idp-ssotime', $state['core:IdP'] . ';' . $state['core:SP'],
				time(), SimpleSAML_Session::DATA_TIMEOUT_LOGOUT);
		}

		call_user_func($state['Responder'], $state);
		assert('FALSE');
	}


	/**
	 * The user is authenticated.
	 *
	 * @param array $state  The authentication request state arrray.
	 */
	public static function postAuth(array $state) {

		$idp = SimpleSAML_IdP::getByState($state);

		if (!$idp->isAuthenticated()) {
			throw new SimpleSAML_Error_Exception('Not authenticated.');
		}

		$session = SimpleSAML_Session::getInstance();
		$state['Attributes'] = $session->getAttributes();

		if (isset($state['SPMetadata'])) {
			$spMetadata = $state['SPMetadata'];
		} else {
			$spMetadata = array();
		}

		if (isset($state['core:SP'])) {
			$previousSSOTime = $session->getData('core:idp-ssotime', $state['core:IdP'] . ';' . $state['core:SP']);
			if ($previousSSOTime !== NULL) {
				$state['PreviousSSOTimestamp'] = $previousSSOTime;
			}
		}

		$idpMetadata = $idp->getConfig()->toArray();

		$pc = new SimpleSAML_Auth_ProcessingChain($idpMetadata, $spMetadata, 'idp');

		$state['ReturnCall'] = array('SimpleSAML_IdP', 'postAuthProc');
		$state['Destination'] = $spMetadata;
		$state['Source'] = $idpMetadata;

		$pc->processState($state);

		self::postAuthProc($state);
	}


	/**
	 * Authenticate the user.
	 *
	 * This function authenticates the user.
	 *
	 * @param array &$state  The authentication request state.
	 */
	private function authenticate(array &$state) {

		if (isset($state['isPassive']) && (bool)$state['isPassive']) {
			throw new SimpleSAML_Error_NoPassive('Passive authentication not supported.');
		}

		$auth = $this->config->getString('auth');
		$authSource = SimpleSAML_Auth_Source::getById($auth);
		if ($authSource === NULL) {
			$session = SimpleSAML_Session::getInstance();
			$config = SimpleSAML_Configuration::getInstance();
			$authurl = '/' . $config->getBaseURL() . $auth;

			$authnRequest = array(
				'IsPassive' => isset($state['isPassive']) ? $state['isPassive'] : FALSE,
				'ForceAuthn' => isset($state['ForceAuthn']) ? $state['ForceAuthn'] : FALSE,
				'State' => $state,
				'core:prevSession' => $session->getAuthnInstant(),
			);

			if (isset($state['saml:RequestId'])) {
				$authnRequest['RequestID'] = $state['saml:RequestId'];
			}
			if (isset($state['SPMetadata']['entityid'])) {
				$authnRequest['Issuer'] = $state['SPMetadata']['entityid'];
			}
			if (isset($state['saml:RelayState'])) {
				$authnRequest['RelayState'] = $state['saml:RelayState'];
			}
			if (isset($state['saml:IDPList'])) {
				$authnRequest['IDPList'] = $state['saml:IDPList'];
			}

			$authId = SimpleSAML_Utilities::generateID();
			$session->setAuthnRequest('saml2', $authId, $authnRequest);

			$relayState = SimpleSAML_Module::getModuleURL('core/idp/resumeauth.php', array('RequestID' => $authId));

			SimpleSAML_Utilities::redirect($authurl, array(
				'RelayState' => $relayState,
				'AuthId' => $authId,
				'protocol' => 'saml2',
			));
		}

		$state['IdPMetadata'] = $this->getConfig()->toArray();
		SimpleSAML_Auth_Default::initLogin($auth, array('SimpleSAML_IdP', 'postAuth'), NULL, $state);
	}


	/**
	 * Process authentication requests.
	 *
	 * @param array &$state  The authentication request state.
	 */
	public function handleAuthenticationRequest(array &$state) {
		assert('isset($state["Responder"])');

		$state['core:IdP'] = $this->id;

		if (isset($state['SPMetadata']['entityid'])) {
			$spEntityId = $state['SPMetadata']['entityid'];
		} elseif (isset($state['SPMetadata']['entityID'])) {
			$spEntityId = $state['SPMetadata']['entityID'];
		} else {
			$spEntityId = NULL;
		}
		$state['core:SP'] = $spEntityId;

		/* First, check whether we need to authenticate the user. */
		if (isset($state['ForceAuthn']) && (bool)$state['ForceAuthn']) {
			/* Force authentication is in effect. */
			$needAuth = TRUE;
		} elseif (isset($state['saml:IDPList']) && sizeof($state['saml:IDPList']) > 0) {
			$needAuth = TRUE;
		} else {
			$needAuth = !$this->isAuthenticated();
		}

		try {
			if ($needAuth) {
				$this->authenticate($state);
				assert('FALSE');
			} else {
				$session = SimpleSAML_Session::getInstance();
				foreach ($session->getAuthState() as $k => $v) {
					$state[$k] = $v;
				}
			}
			$this->postAuth($state);
		} catch (SimpleSAML_Error_Exception $e) {
			SimpleSAML_Auth_State::throwException($state, $e);
		} catch (Exception $e) {
			$e = new SimpleSAML_Error_UnserializableException($e);
			SimpleSAML_Auth_State::throwException($state, $e);
		}
	}


	/**
	 * Find the logout handler of this IdP.
	 *
	 * @return string  The logout handler class.
	 */
	public function getLogoutHandler() {

		/* Find the logout handler. */
		$logouttype = $this->getConfig()->getString('logouttype', 'traditional');
		switch ($logouttype) {
		case 'traditional':
			$handler = 'SimpleSAML_IdP_LogoutTraditional';
			break;
		case 'iframe':
			$handler = 'SimpleSAML_IdP_LogoutIFrame';
			break;
		default:
			throw new SimpleSAML_Error_Exception('Unknown logout handler: ' . var_export($logouttype, TRUE));
		}

		return new $handler($this);

	}


	/**
	 * Finish the logout operation.
	 *
	 * This function will never return.
	 *
	 * @param array &$state  The logout request state.
	 */
	public function finishLogout(array &$state) {
		assert('isset($state["Responder"])');

		$idp = SimpleSAML_IdP::getByState($state);
		call_user_func($state['Responder'], $idp, $state);
		assert('FALSE');
	}


	/**
	 * Process a logout request.
	 *
	 * This function will never return.
	 *
	 * @param array &$state  The logout request state.
	 * @param string|NULL $assocId  The association we received the logout request from, or NULL if there was no association.
	 */
	public function handleLogoutRequest(array &$state, $assocId) {
		assert('isset($state["Responder"])');
		assert('is_string($assocId) || is_null($assocId)');

		$state['core:IdP'] = $this->id;
		$state['core:TerminatedAssocId'] = $assocId;

		if ($assocId !== NULL) {
			$this->terminateAssociation($assocId);
		}

		/* Terminate the local session. */
		$session = SimpleSAML_Session::getInstance();
		$authority = $session->getAuthority();
		if ($authority !== NULL) {
			/* We are logged in. */

			$id = SimpleSAML_Auth_State::saveState($state, 'core:Logout:afterbridge');
			$returnTo = SimpleSAML_Module::getModuleURL('core/idp/resumelogout.php',
				array('id' => $id)
			);

			if ($authority === $this->config->getString('auth')) {
				/* This is probably an authentication source. */
				SimpleSAML_Auth_Default::initLogoutReturn($returnTo);
			} elseif ($authority === 'saml2') {
				/* SAML 2 SP which isn't an authentication source. */
				SimpleSAML_Utilities::redirect('/' . $config->getBaseURL() . 'saml2/sp/initSLO.php',
					array('RelayState' => $returnTo)
				);
			} else {
				/* A different old-style authentication file. */
				$session->doLogout();
			}
		}

		$handler = $this->getLogoutHandler();
		$handler->startLogout($state, $assocId);
		assert('FALSE');
	}


	/**
	 * Process a logout response.
	 *
	 * This function will never return.
	 *
	 * @param string $assocId  The association that is terminated.
	 * @param string|NULL $relayState  The RelayState from the start of the logout.
	 * @param SimpleSAML_Error_Exception|NULL $error  The error that occured during session termination (if any).
	 */
	public function handleLogoutResponse($assocId, $relayState, SimpleSAML_Error_Exception $error = NULL) {
		assert('is_string($assocId)');
		assert('is_string($relayState) || is_null($relayState)');

		$handler = $this->getLogoutHandler();
		$handler->onResponse($assocId, $relayState, $error);

		assert('FALSE');
	}


	/**
	 * Log out, then redirect to an URL.
	 *
	 * This function never returns.
	 *
	 * @param string $url  The URL the user should be returned to after logout.
	 */
	public function doLogoutRedirect($url) {
		assert('is_string($url)');

		$state = array(
			'Responder' => array('SimpleSAML_IdP', 'finishLogoutRedirect'),
			'core:Logout:URL' => $url,
		);

		$this->handleLogoutRequest($state, NULL);
		assert('FALSE');
	}


	/**
	 * Redirect to an URL after logout.
	 *
	 * This function never returns.
	 *
	 * @param array &$state  The logout state from doLogoutRedirect().
	 */
	public static function finishLogoutRedirect(SimpleSAML_IdP $idp, array $state) {
		assert('isset($state["core:Logout:URL"])');

		SimpleSAML_Utilities::redirect($state['core:Logout:URL']);
		assert('FALSE');
	}

}