diff --git a/docs/simplesamlphp-changelog.txt b/docs/simplesamlphp-changelog.txt
index 81c11793ad05f3c050d71ef5e79d805f5018907a..0aa49e8f644123041a281d7b9d0323ad53a65d1d 100644
--- a/docs/simplesamlphp-changelog.txt
+++ b/docs/simplesamlphp-changelog.txt
@@ -1,11 +1,92 @@
-simpleSAMLphp changelog
+SimpleSAMLphp changelog
 =======================
 
 <!-- {{TOC}} -->
 
-This document lists the changes between versions of simpleSAMLphp.
+This document lists the changes between versions of SimpleSAMLphp.
 See the upgrade notes for specific information about upgrading.
 
+## Version 1.14.0
+
+Released TBD
+
+### Security
+
+  * Resolved a security issue with multiple modules that were not validating the URLs they were redirecting to.
+  * Added a security check to disable loading external entities in XML documents.
+  * Enforced admin access to the metadata converter tool.
+  * Changed `xmlseclibs` dependency to point to `robrichards/xmlseclibs` version 1.4.1.
+
+### New features
+
+  * Allow setting the location of the configuration directory with an environment variable.
+  * Added support for the Metadata Query Protocol by means of the new MDX metadata storage handler.
+  * Added support for the Sender-Vouches method.
+  * Added support for WantAssertionsSigned and AuthnRequestsSigned in SAML 2.0 SP metadata.
+  * Added support for file uploads in the metadata converter.
+  * Added support for the Hide From Discovery REFEDS Entity Category.
+  * Added the SAML NameID to the attributes status page, when available.
+  * Added attribute definitions for schacGender (schac), sisSchoolGrade and sisLegalGuardianFor (skolfederation.se).
+  * Attributes required in metadata are now taken into account when parsing.
+
+### Bug fixes
+
+  * Fixed an issue with friendly names in the attributes released.
+  * Fixed an issue with memcache that would result in a push for every fetch, when several servers configured.
+  * Fixed an issue with HTML escaping in error reports.
+  * Fixed an issue with the 'admin.protectmetadata' option not being enforced for SP metadata.
+  * Fixed an issue with SAML 1.X SSO authentications that removed the NameID of the subject from available data.
+  * Fixed an issue with the login form that resulted in a `NOSTATE` error if the user clicked the login button twice.
+  * Fixed an issue with replay detection in IdP-initiated flows.
+  * Fixed an issue that prevented the SAML 1.X IdP to restart when the session is lost.
+  * Fixed an issue that prevented classes using namespaces to be loaded automatically.
+  * Fixed an issue that prevented certain metadata signatures to be verified (fixed upstream in `xmlseclibs`).
+  * Other bug fixes and numerous documentation enhancements.
+
+### API and user interface
+
+  * Added a new and simple database class to serve as PDO interface for all the database needs.
+  * Removed the old, unused `pack` installer tool.
+  * Improved usability by telling users the endpoints are not to be accessed directly.
+  * Moved the hostname, port and protocol diagnostics tool to the admin directory.
+  * Several classes and functions deprecated.
+  * Changed the signature of several functions.
+  * Deleted old and deprecated code, interfaces and endpoints.
+  * Deleted old jQuery remnants.
+  * Deleted the undocumented dynamic XML metadata storage handler.
+  * Deleted the backwards-compatible authentication source.
+
+### `authcrypt`
+
+  * Added whitehat101/apr1-md5 as a dependency for Apache htpasswd.
+
+### `authX509`
+
+  * Added an authentication processing filter to warn about certificate expiration.
+
+### `core`
+
+  * The PHP authentication processing filter now accepts a new option called `function` to define an anonymous function.
+
+### `ldap`
+
+  * Added a new `port` configuration option.
+  * Better error reporting.
+
+### `metaedit`
+
+  * Removed the `admins` configuration option.
+
+### `metarefresh`
+
+  * Added the possibility to specify which types of entities to load.
+  * Added the possibility to verify metadata signatures by using the public key present in a certificate.
+  * Fix `certificate` precedence over `fingerprint` in the configuration options when verifying metadata signatures.
+
+### `smartnameattribute`
+
+  * This module was deprecated long time ago and has now been removed. Use the `smartattributes` module instead.
+
 ## Version 1.13.2
 
 Released 2014-11-04
@@ -846,7 +927,7 @@ Released 2010-01-08.
 
   * Fix security vulnerability due to insecure temp file creation:
     * statistics: The logcleaner script outputs to a file in /tmp.
-    * InfoCard: Saves state directly in /tmp. Changed to the simpleSAMLphp temp directory.
+    * InfoCard: Saves state directly in /tmp. Changed to the SimpleSAMLphp temp directory.
     * openidProvider: Default configuration saves state information in /tmp.
       Changed to '/var/lib/simplesamlphp-openid-provider'.
     * SAML 1 artifact support: Saves certificates temporarily in '/tmp/simplesaml', but directory creation was insecure.
@@ -872,7 +953,7 @@ Released 2009-11-05. Revision 1937.
   * Make use of the portal module on the frontpage.
   * SQL datastore.
   * Support for setting timezone in config (instead of php.ini).
-  * Logging of PHP errors and notices to simpleSAMLphp log file.
+  * Logging of PHP errors and notices to SimpleSAMLphp log file.
   * Improve handling of unhandled errors and exceptions.
   * Admin authentication through authentication sources.
   * Various bugfixes & cleanups.
@@ -1002,12 +1083,12 @@ Updates to `config.php`. Please check for updates in your local modified configu
     * AttributeMap
     * Smartname. does it best to guess the full name of the user based on several attributes.
     * Language adaptor: allow adopting UI by preferredLanguage SAML 2.0 Attribute both on the IdP and the SP. And if the user selects a lanauge, this can be sent to the SP as an attribute.
-  * New module: portal, allows you to created tabbed interface for custom pages within simpleSAMLphp. In example user consent management and attribute viewer.
+  * New module: portal, allows you to created tabbed interface for custom pages within SimpleSAMLphp. In example user consent management and attribute viewer.
   * New module: ldapstatus. Used by Feide to monitor connections to a large list of LDAP connections. Contact Feide on details on how to use.
   * ldapstatus also got certificate check capabilities.
   * New module: MemcacheMonitor: Show statistics for memcache servers.
   * New module: DiscoPower. A tabbed discovery service module with alot of functionality.
-  * New module: SAML 2.0 Debugginer. An improved version of the one found on rnd.feide.no earlier is not included in simpleSAMLphp allowing you to run it locally.
+  * New module: SAML 2.0 Debugginer. An improved version of the one found on rnd.feide.no earlier is not included in SimpleSAMLphp allowing you to run it locally.
   * New module: Simple Consent Amdin module that have one button to remove all consent for one user.
   * New module: Consent Administration. Contribution from Wayf.
   * We also have a consent adminstration module that we use in Feide that is not checked in to subversion.
@@ -1030,7 +1111,7 @@ Updates to `config.php`. Please check for updates in your local modified configu
   * More localized UI.
   * New login as administrator link on frontpage.
   * Tabbed frontpage. Restructured.
-  * Simplifications to the theming and updated documentation on theming simpleSAMLphp.
+  * Simplifications to the theming and updated documentation on theming SimpleSAMLphp.
   * Attribute presentation hook allows you to tweak attributes before presentation in the attribute viewers. Used by Feide to group orgUnit information in a hieararchy.
   * Verification of the Receipient attribute in the response. Will improve security if for some reason an IdP is not includeding sufficient Audience restrictions.
   * Added hook to let modules tell about themself moduleinfo hook.
@@ -1174,7 +1255,7 @@ New localizations in version 1.1: Sami, Svenska (swedish), Suomeksi (finnish), N
   * Add support for external IdP discovery services.
   * Support password encrypted private keys.
   * Added PHP autoloading as the preferred way of loading the
-    simpleSAMLphp library.
+    SimpleSAMLphp library.
   * New error report script which will report errors to the
     `technicalcontact_email` address.
   * Support lookup of the DN of the user who is logging in by searching
diff --git a/docs/simplesamlphp-upgrade-notes-1.14.txt b/docs/simplesamlphp-upgrade-notes-1.14.txt
new file mode 100644
index 0000000000000000000000000000000000000000..aa6e90fe648e0d2d2e63b8aa64d5eb380123977d
--- /dev/null
+++ b/docs/simplesamlphp-upgrade-notes-1.14.txt
@@ -0,0 +1,176 @@
+Upgrade notes for simpleSAMLphp 1.14
+====================================
+
+The `mcrypt` extension is no longer required by SimpleSAMLphp, so if no signatures or encryption are being used, it
+can be skipped. It is still a requirement for `xmlseclibs` though, so for those verifying or creating signed
+documents, or using encryption, is is still needed.
+
+PHP session cookies are now set to HTTP-only by default. This relates to the `session.phpsession.httponly`
+configuration option.
+
+The following deprecated files, directories and endpoints have been removed:
+
+    * `bin/pack.php`
+    * `docs/pack.txt`
+    * `docs/simplesamlphp-features.txt`
+    * `docs/simplesamlphp-reference-sp-hosted.txt`
+    * `docs/simplesamlphp-subversion.txt`
+    * `lib/SimpleSAML/Auth/BWC.php` (`SimpleSAML_Auth_BWC`)
+    * `lib/SimpleSAML/MemcacheStore.php` (`SimpleSAML_MemcacheStore`)
+    * `lib/SimpleSAML/Metadata/MetaDataStorageHandlerDynamicXML.php` (`SimpleSAML_Metadata_MetaDataStorageHandlerDynamicXML`)
+    * `modules/aselect/www/linkback.php`
+    * `modules/core/lib/ModuleDefinition.php` (`sspmod_core_ModuleDefinition`)
+    * `modules/core/lib/ModuleInstaller.php` (`sspmod_core_ModuleInstaller`)
+    * `modules/core/www/bwc_resumeauth.php`
+    * `modules/core/www/idp/resumeauth.php`
+    * `modules/oauth/lib/OauthSignatureMethodRSASHA1.php` (`sspmod_oauth_OauthSignatureMethodRSASHA1`)
+    * `modules/oauth/www/accessToken.php`
+    * `modules/oauth/www/authorize.php`
+    * `modules/oauth/www/requestToken.php`
+    * `modules/smartnameattribute/`
+    * `www/resources/jquery.js`
+    * `www/resources/jquery-ui.js`
+    * `www/resources/uitheme/`
+    * `www/shib13/sp/`
+    * `www/saml2/idp/idpInitSingleLogoutServiceiFrame.php`
+    * `www/saml2/idp/SingleLogoutServiceiFrame.php`
+    * `www/saml2/idp/SingleLogoutServiceiFrameResponse.php`
+    * `www/saml2/sp/`
+    * `www/wsfed/`
+    * `www/example-simple/`
+    * `www/auth/`
+
+The following deprecated methods and constants have been removed:
+
+   * `SimpleSAML_AuthMemCookie::getLoginMethod()`
+   * `SimpleSAML_Session::DATA_TIMEOUT_LOGOUT`
+   * `SimpleSAML_Session::expireDataLogout()`
+   * `SimpleSAML_Session::get_sp_list()`
+   * `SimpleSAML_Session::getAttribute()`
+   * `SimpleSAML_Session::getAttributes()`
+   * `SimpleSAML_Session::getAuthnInstant()`
+   * `SimpleSAML_Session::getAuthnRequest()`
+   * `SimpleSAML_Session::getAuthority()`
+   * `SimpleSAML_Session::getIdP()`
+   * `SimpleSAML_Session::getInstance()`
+   * `SimpleSAML_Session::getLogoutState()`
+   * `SimpleSAML_Session::getNameID()`
+   * `SimpleSAML_Session::getSessionIndex()`
+   * `SimpleSAML_Session::getSize()`
+   * `SimpleSAML_Session::isAuthenticated()`
+   * `SimpleSAML_Session::remainingTime()`
+   * `SimpleSAML_Session::setAttribute()`
+   * `SimpleSAML_Session::setAttributes()`
+   * `SimpleSAML_Session::setAuthnRequest()`
+   * `SimpleSAML_Session::setIdP()`
+   * `SimpleSAML_Session::setLogoutState()`
+   * `SimpleSAML_Session::setNameID()`
+   * `SimpleSAML_Session::setSessionDuration()`
+   * `SimpleSAML_Session::setSessionIndex()`
+   * `SimpleSAML_Utilities::generateRandomBytesMTrand()`
+
+The following methods have changed their signature. Refer to the code for the updated signatures:
+
+    * `SimpleSAML_Auth_Default::initLogout()`
+    * `SimpleSAML_Auth_Default::initLogoutReturn()`
+    * `SimpleSAML_Metadata_MetaDataStorageHandler::getGenerated()`
+    * `SimpleSAML_Metadata_MetaDataStorageHandler::getMetaData()`
+    * `SimpleSAML_Metadata_MetaDataStorageHandler::getMetaDataCurrent()`
+    * `SimpleSAML_Metadata_MetaDataStorageHandler::getMetaDataCurrentEntityID()`
+    * `SimpleSAML_Session::doLogout()`
+    * `SimpleSAML_Session::getAuthState()`
+    * `SimpleSAML_Session::registerLogoutHandler()`
+    * `SimpleSAML_Utilities::generateRandomBytes()`
+    * `SimpleSAML_XML_Shib13_AuthnRequest::createRedirect()`
+
+The following methods and classes have been deprecated. Refer to the code for alternatives:
+
+    * `SimpleSAML_Auth_Default`
+    * `SimpleSAML_Auth_Default::extractPersistentAuthState()`
+    * `SimpleSAML_Auth_Default::handleUnsolicitedAuth()`
+    * `SimpleSAML_Auth_Default::initLogin()`
+    * `SimpleSAML_Auth_Default::loginCompleted()`
+    * `SimpleSAML_Utilities`
+    * `SimpleSAML_Utilities::addURLParameter()`
+    * `SimpleSAML_Utilities::aesDecrypt()`
+    * `SimpleSAML_Utilities::aesEncrypt()`
+    * `SimpleSAML_Utilities::arrayize()`
+    * `SimpleSAML_Utilities::checkCookie()`
+    * `SimpleSAML_Utilities::checkDateConditions()`
+    * `SimpleSAML_Utilities::checkURLAllowed()`
+    * `SimpleSAML_Utilities::createHttpPostRedirectLink()`
+    * `SimpleSAML_Utilities::createPostRedirectLink()`
+    * `SimpleSAML_Utilities::debugMessage()`
+    * `SimpleSAML_Utilities::doRedirect()`
+    * `SimpleSAML_Utilities::fatalError()`
+    * `SimpleSAML_Utilities::fetch()`
+    * `SimpleSAML_Utilities::formatDOMElement()`
+    * `SimpleSAML_Utilities::formatXMLString()`
+    * `SimpleSAML_Utilities::generateID()`
+    * `SimpleSAML_Utilities::generateRandomBytes()`
+    * `SimpleSAML_Utilities::generateTimestamp()`
+    * `SimpleSAML_Utilities::getAcceptLanguage()`
+    * `SimpleSAML_Utilities::getAdminLogoutURL()`
+    * `SimpleSAML_Utilities::getBaseURL()`
+    * `SimpleSAML_Utilities::getDefaultEndpoint()`
+    * `SimpleSAML_Utilities::getDOMChildren()`
+    * `SimpleSAML_Utilities::getDOMText()`
+    * `SimpleSAML_Utilities::getFirstPathElement()`
+    * `SimpleSAML_Utilities::getLastError()`
+    * `SimpleSAML_Utilities::getSecretSalt()`
+    * `SimpleSAML_Utilities::getSelfHost()`
+    * `SimpleSAML_Utilities::getSelfHostWithPath()`
+    * `SimpleSAML_Utilities::getTempDir()`
+    * `SimpleSAML_Utilities::initTimezone()`
+    * `SimpleSAML_Utilities::ipCIDRcheck()`
+    * `SimpleSAML_Utilities::isAdmin()`
+    * `SimpleSAML_Utilities::isDOMElementOfType()`
+    * `SimpleSAML_Utilities::isHTTPS()`
+    * `SimpleSAML_Utilities::isWindowsOS()`
+    * `SimpleSAML_Utilities::loadPrivateKey()`
+    * `SimpleSAML_Utilities::loadPublicKey()`
+    * `SimpleSAML_Utilities::maskErrors()`
+    * `SimpleSAML_Utilities::normalizeURL()`
+    * `SimpleSAML_Utilities::parseAttributes()`
+    * `SimpleSAML_Utilities::parseDuration()`
+    * `SimpleSAML_Utilities::parseQueryString()`
+    * `SimpleSAML_Utilities::parseStateID()`
+    * `SimpleSAML_Utilities::popErrorMask()`
+    * `SimpleSAML_Utilities::postRedirect()`
+    * `SimpleSAML_Utilities::redirect()`
+    * `SimpleSAML_Utilities::redirectTrustedURL()`
+    * `SimpleSAML_Utilities::redirectUntrustedURL()`
+    * `SimpleSAML_Utilities::requireAdmin()`
+    * `SimpleSAML_Utilities::resolveCert()`
+    * `SimpleSAML_Utilities::resolvePath()`
+    * `SimpleSAML_Utilities::resolveURL()`
+    * `SimpleSAML_Utilities::selfURL()`
+    * `SimpleSAML_Utilities::selfURLHost()`
+    * `SimpleSAML_Utilities::selfURLNoQuery()`
+    * `SimpleSAML_Utilities::setCookie()`
+    * `SimpleSAML_Utilities::stringToHex()`
+    * `SimpleSAML_Utilities::transposeArray()`
+    * `SimpleSAML_Utilities::validateCA()`
+    * `SimpleSAML_Utilities::validateXML()`
+    * `SimpleSAML_Utilities::validateXMLDocument()`
+    * `SimpleSAML_Utilities::writeFile()`
+
+The following modules will no longer be shipped with the next version of SimpleSAMLphp:
+
+    * `aggregator`
+    * `aggregator2`
+    * `aselect`
+    * `autotest`
+    * `casserver`
+    * `consentSimpleAdmin`
+    * `discojuice`
+    * `InfoCard`
+    * `logpeek`
+    * `metaedit`
+    * `modinfo`
+    * `papi`
+    * `oauth`
+    * `openid`
+    * `openidProvider`
+    * `saml2debug`
+    * `themefeidernd`
diff --git a/lib/SimpleSAML/Auth/Default.php b/lib/SimpleSAML/Auth/Default.php
index 049855432538f4b7d9d2cd231e9f5c6106ed03cf..5f2a6fe4b1a3f2386c06c5b10b1eb043d4dc16a7 100644
--- a/lib/SimpleSAML/Auth/Default.php
+++ b/lib/SimpleSAML/Auth/Default.php
@@ -8,110 +8,36 @@
  *
  * @author Olav Morken, UNINETT AS.
  * @package simpleSAMLphp
+ *
+ * @deprecated This class will be removed in SSP 2.0.
  */
 class SimpleSAML_Auth_Default {
 
 
 	/**
-	 * Start authentication.
-	 *
-	 * This function never returns.
-	 *
-	 * @param string $authId  The identifier of the authentication source.
-	 * @param string|array $return The URL or function we should direct the
-	 * user to after authentication. If using a URL obtained from user input,
-	 * please make sure to check it by calling
-	 * \SimpleSAML\Utils\HTTP::checkURLAllowed().
-	 * @param string|NULL $errorURL The URL we should direct the user to after
-	 * failed authentication. Can be NULL, in which case a standard error page
-	 * will be shown. If using a URL obtained from user input, please make sure
-	 * to check it by calling \SimpleSAML\Utils\HTTP::checkURLAllowed().
-	 * @param array $params Extra information about the login. Different
-	 * authentication requestors may provide different information. Optional,
-	 * will default to an empty array.
+	 * @deprecated This method will be removed in SSP 2.0.
 	 */
 	public static function initLogin($authId, $return, $errorURL = NULL,
 		array $params = array()) {
 
-		assert('is_string($authId)');
-		assert('is_string($return) || is_array($return)');
-		assert('is_string($errorURL) || is_null($errorURL)');
-
-		$state = array_merge($params, array(
-			'SimpleSAML_Auth_Default.id' => $authId,
-			'SimpleSAML_Auth_Default.Return' => $return,
-			'SimpleSAML_Auth_Default.ErrorURL' => $errorURL,
-			'LoginCompletedHandler' => array(get_class(), 'loginCompleted'),
-			'LogoutCallback' => array(get_class(), 'logoutCallback'),
-			'LogoutCallbackState' => array(
-				'SimpleSAML_Auth_Default.logoutSource' => $authId,
-			),
-		));
-
-		if (is_string($return)) {
-			$state['SimpleSAML_Auth_Default.ReturnURL'] = $return;
-		}
-
-		if ($errorURL !== NULL) {
-			$state[SimpleSAML_Auth_State::EXCEPTION_HANDLER_URL] = $errorURL;
-		}
-
 		$as = SimpleSAML_Auth_Source::getById($authId);
-		if ($as === NULL) {
-			throw new Exception('Invalid authentication source: ' . $authId);
-		}
-
-		try {
-			$as->authenticate($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);
-		}
-		self::loginCompleted($state);
+		$as->initLogin($return, $errorURL, $params);
 	}
 
 
 	/**
-	 * Extract the persistent authentication state from the state array.
-	 *
-	 * @param array $state  The state after the login.
-	 * @return array  The persistent authentication state.
+	 * @deprecated This method will be removed in SSP 2.0. Please use
+	 * SimpleSAML_Auth_State::extractPersistentAuthState() instead.
 	 */
 	public static function extractPersistentAuthState(array &$state) {
 
-		/* Save persistent authentication data. */
-		$persistentAuthState = array();
-
-		if (isset($state['IdP'])) {
-			/* For backwards compatibility. */
-			$persistentAuthState['saml:sp:IdP'] = $state['IdP'];
-		}
-
-		if (isset($state['PersistentAuthData'])) {
-			foreach ($state['PersistentAuthData'] as $key) {
-				if (isset($state[$key])) {
-					$persistentAuthState[$key] = $state[$key];
-				}
-			}
-		}
-
-		/* Add those that should always be included. */
-		foreach (array('Attributes', 'Expire', 'LogoutState', 'AuthnInstant', 'RememberMe', 'saml:sp:NameID') as $a) {
-			if (isset($state[$a])) {
-				$persistentAuthState[$a] = $state[$a];
-			}
-		}
-
-		return $persistentAuthState;
+		$state = SimpleSAML_Auth_State::extractPersistentAuthState($state);
+		return $state;
 	}
 
 
 	/**
-	 * Called when a login operation has finished.
-	 *
-	 * @param array $state  The state after the login.
+	 * @deprecated This method will be removed in SSP 2.0.
 	 */
 	public static function loginCompleted($state) {
 		assert('is_array($state)');
@@ -124,7 +50,9 @@ class SimpleSAML_Auth_Default {
 
 		/* Save session state. */
 		$session = SimpleSAML_Session::getSessionFromRequest();
-		$session->doLogin($state['SimpleSAML_Auth_Default.id'], self::extractPersistentAuthState($state));
+		$authId = $state['SimpleSAML_Auth_Default.id'];
+		$state = SimpleSAML_Auth_State::extractPersistentAuthState($state);
+		$session->doLogin($authId, $state);
 
 		if (is_string($return)) {
 			/* Redirect... */
@@ -242,30 +170,11 @@ class SimpleSAML_Auth_Default {
 
 
 	/**
-	 * Handle a unsolicited login operations.
-	 *
-	 * This function creates a session from the received information. It
-	 * will then redirect to the given URL.
-	 *
-	 * This is used to handle IdP initiated SSO.
-	 *
-	 * @param string $authId The id of the authentication source that received
-	 * the request.
-	 * @param array $state A state array.
-	 * @param string $redirectTo The URL we should redirect the user to after
-	 * updating the session. The function will check if the URL is allowed, so
-	 * there is no need to manually check the URL on beforehand. Please refer
-	 * to the 'trusted.url.domains' configuration directive for more
-	 * information about allowing (or disallowing) URLs.
+	 * @deprecated This method will be removed in SSP 2.0. Please use
+	 * sspmod_saml_Auth_Source_SP::handleUnsolicitedAuth() instead.
 	 */
 	public static function handleUnsolicitedAuth($authId, array $state, $redirectTo) {
-		assert('is_string($authId)');
-		assert('is_string($redirectTo)');
-
-		$session = SimpleSAML_Session::getSessionFromRequest();
-		$session->doLogin($authId, self::extractPersistentAuthState($state));
-
-		\SimpleSAML\Utils\HTTP::redirectUntrustedURL($redirectTo);
+		sspmod_saml_Auth_Source_SP::handleUnsolicitedAuth($authId, $state, $redirectTo);
 	}
 
 }
diff --git a/lib/SimpleSAML/Auth/LDAP.php b/lib/SimpleSAML/Auth/LDAP.php
index f9d543905001a6bd02fe1c0fc4efabbf01dd53a2..933b2eac7cf101b14014707634dcad6cbf641d70 100644
--- a/lib/SimpleSAML/Auth/LDAP.php
+++ b/lib/SimpleSAML/Auth/LDAP.php
@@ -150,7 +150,7 @@ class SimpleSAML_Auth_LDAP {
 		}else{
 			if ($errNo !== 0) {
 				$description .= '; cause: \'' . ldap_error($this->ldap) . '\' (0x' . dechex($errNo) . ')';
-				if (@ldap_get_option($this->ldap, LDAP_OPT_DIAGNOSTIC_MESSAGE, $extendedError) and !empty($extendedError)) {
+				if (@ldap_get_option($this->ldap, LDAP_OPT_DIAGNOSTIC_MESSAGE, $extendedError) && !empty($extendedError)) {
 					$description .= '; additional: \'' . $extendedError . '\'';
 				}
 			}
diff --git a/lib/SimpleSAML/Auth/Simple.php b/lib/SimpleSAML/Auth/Simple.php
index a82419f2ffbed1015c19b9026ad06b8e5823e09e..e1ece69972f76b46363ca5b36289df6dc71ae6ba 100644
--- a/lib/SimpleSAML/Auth/Simple.php
+++ b/lib/SimpleSAML/Auth/Simple.php
@@ -133,7 +133,8 @@ class SimpleSAML_Auth_Simple {
 			$params[SimpleSAML_Auth_State::RESTART] = $restartURL;
 		}
 
-		SimpleSAML_Auth_Default::initLogin($this->authSource, $returnTo, $errorURL, $params);
+		$as = $this->getAuthSource();
+		$as->initLogin($returnTo, $errorURL, $params);
 		assert('FALSE');
 	}
 
diff --git a/lib/SimpleSAML/Auth/Source.php b/lib/SimpleSAML/Auth/Source.php
index 4f071fa71e0a4a5444218ef3a437736f637017ab..b47f53aff44586c2d93d8aedf01f6f3f7ec1a7db 100644
--- a/lib/SimpleSAML/Auth/Source.php
+++ b/lib/SimpleSAML/Auth/Source.php
@@ -147,6 +147,88 @@ abstract class SimpleSAML_Auth_Source
     }
 
 
+    /**
+     * Start authentication.
+     *
+     * This method never returns.
+     *
+     * @param string|array $return The URL or function we should direct the user to after authentication. If using a
+     * URL obtained from user input, please make sure to check it by calling \SimpleSAML\Utils\HTTP::checkURLAllowed().
+     * @param string|null $errorURL The URL we should direct the user to after failed authentication. Can be null, in
+     * which case a standard error page will be shown. If using a URL obtained from user input, please make sure to
+     * check it by calling \SimpleSAML\Utils\HTTP::checkURLAllowed().
+     * @param array $params Extra information about the login. Different authentication requestors may provide different
+     * information. Optional, will default to an empty array.
+     */
+    public function initLogin($return, $errorURL = null, array $params = array())
+    {
+        assert('is_string($authId)');
+        assert('is_string($return) || is_array($return)');
+        assert('is_string($errorURL) || is_null($errorURL)');
+
+        $state = array_merge($params, array(
+            'SimpleSAML_Auth_Default.id' => $this->authId,
+            'SimpleSAML_Auth_Default.Return' => $return,
+            'SimpleSAML_Auth_Default.ErrorURL' => $errorURL,
+            'LoginCompletedHandler' => array(get_class(), 'loginCompleted'),
+            'LogoutCallback' => array(get_class(), 'logoutCallback'),
+            'LogoutCallbackState' => array(
+                'SimpleSAML_Auth_Default.logoutSource' => $this->authId,
+            ),
+        ));
+
+        if (is_string($return)) {
+            $state['SimpleSAML_Auth_Default.ReturnURL'] = $return;
+        }
+
+        if ($errorURL !== null) {
+            $state[SimpleSAML_Auth_State::EXCEPTION_HANDLER_URL] = $errorURL;
+        }
+
+        try {
+            $this->authenticate($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);
+        }
+        self::loginCompleted($state);
+    }
+
+
+    /**
+     * Called when a login operation has finished.
+     *
+     * This method never returns.
+     *
+     * @param array $state The state after the login has completed.
+     */
+    protected static function loginCompleted($state)
+    {
+        assert('is_array($state)');
+        assert('array_key_exists("SimpleSAML_Auth_Default.Return", $state)');
+        assert('array_key_exists("SimpleSAML_Auth_Default.id", $state)');
+        assert('array_key_exists("Attributes", $state)');
+        assert('!array_key_exists("LogoutState", $state) || is_array($state["LogoutState"])');
+
+        $return = $state['SimpleSAML_Auth_Default.Return'];
+
+        // save session state
+        $session = SimpleSAML_Session::getSessionFromRequest();
+        $authId = $state['SimpleSAML_Auth_Default.id'];
+        $state = SimpleSAML_Auth_State::extractPersistentAuthState($state);
+        $session->doLogin($authId, $state);
+
+        if (is_string($return)) { // redirect...
+            \SimpleSAML\Utils\HTTP::redirectTrustedURL($return);
+        } else {
+            call_user_func($return, $state);
+        }
+        assert('false');
+    }
+
+
     /**
      * Log out from this authentication source.
      *
diff --git a/lib/SimpleSAML/Auth/State.php b/lib/SimpleSAML/Auth/State.php
index 4f5e263be43e255900a6f1d7f87970435e3a58cf..5d6ebc6148e7d094c1332a54a149522217786962 100644
--- a/lib/SimpleSAML/Auth/State.php
+++ b/lib/SimpleSAML/Auth/State.php
@@ -91,6 +91,44 @@ class SimpleSAML_Auth_State {
 	private static $stateTimeout = NULL;
 
 
+	/**
+	 * Extract the persistent authentication state from the state array.
+	 *
+	 * @param array $state The state array to analyze.
+	 * @return array The persistent authentication state.
+	 */
+	public static function extractPersistentAuthState(array $state)
+	{
+		// save persistent authentication data
+		$persistent = array();
+
+		if (array_key_exists('PersistentAuthData', $state)) {
+			foreach ($state['PersistentAuthData'] as $key) {
+				if (isset($state[$key])) {
+					$persistent[$key] = $state[$key];
+				}
+			}
+		}
+
+		// add those that should always be included
+		$mandatory = array(
+			'Attributes',
+			'Expire',
+			'LogoutState',
+			'AuthInstant',
+			'RememberMe',
+			'saml:sp:NameID'
+		);
+		foreach ($mandatory as $key) {
+			if (isset($state[$key])) {
+				$persistent[$key] = $state[$key];
+			}
+		}
+
+		return $persistent;
+	}
+
+
 	/**
 	 * Retrieve the ID of a state array.
 	 *
diff --git a/lib/SimpleSAML/Utilities.php b/lib/SimpleSAML/Utilities.php
index 0313330efae0c8beaa771ebe56a50e53c60fa64a..c7eb315b1212729082dab7ab2269fe51284379af 100644
--- a/lib/SimpleSAML/Utilities.php
+++ b/lib/SimpleSAML/Utilities.php
@@ -6,6 +6,8 @@
  *
  * @author Andreas Ã…kre Solberg, UNINETT AS. <andreas.solberg@uninett.no>
  * @package simpleSAMLphp
+ *
+ * @deprecated This entire class will be removed in SimpleSAMLphp 2.0.
  */
 class SimpleSAML_Utilities
 {
diff --git a/modules/oauth/www/registry.edit.php b/modules/oauth/www/registry.edit.php
index 555e77b678e43a55ba6e96765f27bf68de372158..6efd59beb6937e16fd1611bb88f7665967068111 100644
--- a/modules/oauth/www/registry.edit.php
+++ b/modules/oauth/www/registry.edit.php
@@ -17,7 +17,8 @@ if ($session->isValid($authsource)) {
 		throw new Exception('User ID is missing');
 	$userid = $attributes[$useridattr][0];
 } else {
-	SimpleSAML_Auth_Default::initLogin($authsource, \SimpleSAML\Utils\HTTP::getSelfURL());
+	$as = SimpleSAML_Auth_Source::getById($authsource);
+	$as->initLogin(\SimpleSAML\Utils\HTTP::getSelfURL());
 }
 
 function requireOwnership($entry, $userid) {
diff --git a/modules/oauth/www/registry.php b/modules/oauth/www/registry.php
index bd36ac448ee17454e8c68f94dfd1b86f3f1c59b6..d611c3e052bd7ff4ab44c59352fddea827dac08d 100644
--- a/modules/oauth/www/registry.php
+++ b/modules/oauth/www/registry.php
@@ -17,7 +17,8 @@ if ($session->isValid($authsource)) {
 		throw new Exception('User ID is missing');
 	$userid = $attributes[$useridattr][0];
 } else {
-	SimpleSAML_Auth_Default::initLogin($authsource, \SimpleSAML\Utils\HTTP::getSelfURL());
+	$as = SimpleSAML_Auth_Source::getById($authsource);
+	$as->initLogin(\SimpleSAML\Utils\HTTP::getSelfURL());
 }
 
 function requireOwnership($entry, $userid) {
diff --git a/modules/saml/lib/Auth/Source/SP.php b/modules/saml/lib/Auth/Source/SP.php
index e5140681ef8b4bf475e17aa2c1364abb6d4025bc..cb926f6281e960cb1290e2611fea25171e2a3f7b 100644
--- a/modules/saml/lib/Auth/Source/SP.php
+++ b/modules/saml/lib/Auth/Source/SP.php
@@ -439,7 +439,9 @@ class sspmod_saml_Auth_Source_SP extends SimpleSAML_Auth_Source {
 
 		// Update session state
 		$session = SimpleSAML_Session::getSessionFromRequest();
-		$session->doLogin($state['saml:sp:AuthId'], SimpleSAML_Auth_Default::extractPersistentAuthState($state));
+		$authId = $state['saml:sp:AuthId'];
+		$state = SimpleSAML_Auth_State::extractPersistentAuthState($state);
+		$session->doLogin($authId, $state);
 
 		// resume the login process
 		call_user_func($state['ReturnCallback'], $state);
@@ -577,6 +579,29 @@ class sspmod_saml_Auth_Source_SP extends SimpleSAML_Auth_Source {
 	}
 
 
+	/**
+	 * Handle an unsolicited login operations.
+	 *
+	 * This method creates a session from the information received. It will then redirect to the given URL. This is used
+	 * to handle IdP initiated SSO. This method will never return.
+	 *
+	 * @param string $authId The id of the authentication source that received the request.
+	 * @param array $state A state array.
+	 * @param string $redirectTo The URL we should redirect the user to after updating the session. The function will
+	 * check if the URL is allowed, so there is no need to manually check the URL on beforehand. Please refer to the
+	 * 'trusted.url.domains' configuration directive for more information about allowing (or disallowing) URLs.
+	 */
+	public static function handleUnsolicitedAuth($authId, array $state, $redirectTo) {
+		assert('is_string($authId)');
+		assert('is_string($redirectTo)');
+
+		$session = SimpleSAML_Session::getSessionFromRequest();
+		$session->doLogin($authId, SimpleSAML_Auth_State::extractPersistentAuthState($state));
+
+		\SimpleSAML\Utils\HTTP::redirectUntrustedURL($redirectTo);
+	}
+
+
 	/**
 	 * Called when we have completed the procssing chain.
 	 *
@@ -607,7 +632,7 @@ class sspmod_saml_Auth_Source_SP extends SimpleSAML_Auth_Source {
 			} else {
 				$redirectTo = $source->getMetadata()->getString('RelayState', '/');
 			}
-			SimpleSAML_Auth_Default::handleUnsolicitedAuth($sourceId, $state, $redirectTo);
+			self::handleUnsolicitedAuth($sourceId, $state, $redirectTo);
 		}
 
 		SimpleSAML_Auth_Source::completeAuth($state);
diff --git a/tests/lib/SimpleSAML/Auth/StateTest.php b/tests/lib/SimpleSAML/Auth/StateTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..741acdaaba5922b1cbd70ae229a9fe6b5825cc07
--- /dev/null
+++ b/tests/lib/SimpleSAML/Auth/StateTest.php
@@ -0,0 +1,81 @@
+<?php
+
+
+/**
+ * Tests for SimpleSAML_Auth_State
+ */
+class Auth_StateTest extends PHPUnit_Framework_TestCase
+{
+
+
+    /**
+     * Test the extractPersistentAuthState() function.
+     */
+    public function testExtractPersistentAuthState()
+    {
+
+        $mandatory = array(
+            'Attributes' => array(),
+            'Expire' => 1234,
+            'LogoutState' => 'logoutState',
+            'AuthInstant' => 123456,
+            'RememberMe' => true,
+            'saml:sp:NameID' => 'nameID',
+        );
+
+        // check just mandatory parameters
+        $state = $mandatory;
+        $expected = $mandatory;
+        $this->assertEquals(
+            $expected,
+            SimpleSAML_Auth_State::extractPersistentAuthState($state),
+            'Mandatory state attributes did not survive as expected'.print_r($expected, true)
+        );
+
+        // check missing mandatory parameters
+        unset($state['LogoutState']);
+        unset($state['RememberMe']);
+        $expected = $state;
+        $this->assertEquals(
+            $expected,
+            SimpleSAML_Auth_State::extractPersistentAuthState($state),
+            'Some error occurred with missing mandatory parameters'
+        );
+
+        // check additional non-persistent parameters
+        $additional = array(
+            'additional1' => 1,
+            'additional2' => 2,
+        );
+        $state = array_merge($mandatory, $additional);
+        $expected = $mandatory;
+        $this->assertEquals(
+            $expected,
+            SimpleSAML_Auth_State::extractPersistentAuthState($state),
+            'Additional parameters survived'
+        );
+
+        // check additional persistent parameters
+        $additional['PersistentAuthData'] = array('additional1');
+        $state = array_merge($mandatory, $additional);
+        $expected = $state;
+        unset($expected['additional2']);
+        unset($expected['PersistentAuthData']);
+        $this->assertEquals(
+            $expected,
+            SimpleSAML_Auth_State::extractPersistentAuthState($state),
+            'Some error occurred with additional, persistent parameters'
+        );
+
+        // check only additional persistent parameters
+        $state = $additional;
+        $expected = $state;
+        unset($expected['additional2']);
+        unset($expected['PersistentAuthData']);
+        $this->assertEquals(
+            $expected,
+            SimpleSAML_Auth_State::extractPersistentAuthState($state),
+            'Some error occurred with additional, persistent parameters, and no mandatory ones'
+        );
+    }
+}
diff --git a/tests/modules/core/lib/Auth/Process/AttributeAddTest.php b/tests/modules/core/lib/Auth/Process/AttributeAddTest.php
index 20748fafe0116b0e5c047a28aae45978d31241eb..3db1f388d3fac58df6c3165c538b5724de355f5a 100644
--- a/tests/modules/core/lib/Auth/Process/AttributeAddTest.php
+++ b/tests/modules/core/lib/Auth/Process/AttributeAddTest.php
@@ -6,7 +6,7 @@
 class Test_Core_Auth_Process_AttributeAdd extends PHPUnit_Framework_TestCase
 {
 
-    /*
+    /**
      * Helper function to run the filter with a given configuration.
      *
      * @param array $config  The filter configuration.
@@ -20,7 +20,7 @@ class Test_Core_Auth_Process_AttributeAdd extends PHPUnit_Framework_TestCase
         return $request;
     }
 
-    /*
+    /**
      * Test the most basic functionality.
      */
     public function testBasic()
@@ -37,7 +37,7 @@ class Test_Core_Auth_Process_AttributeAdd extends PHPUnit_Framework_TestCase
         $this->assertEquals($attributes['test'], array('value1', 'value2'));
     }
 
-    /*
+    /**
      * Test that existing attributes are left unmodified.
      */
     public function testExistingNotModified()
@@ -61,7 +61,7 @@ class Test_Core_Auth_Process_AttributeAdd extends PHPUnit_Framework_TestCase
         $this->assertEquals($attributes['original2'], array('original_value2'));
     }
 
-    /*
+    /**
      * Test single string as attribute value.
      */
     public function testStringValue()
@@ -78,8 +78,8 @@ class Test_Core_Auth_Process_AttributeAdd extends PHPUnit_Framework_TestCase
         $this->assertEquals($attributes['test'], array('value'));
     }
 
-    /*
-     * Test the most basic functionality.
+    /**
+     * Test adding multiple attributes in one config.
      */
     public function testAddMultiple()
     {
@@ -98,7 +98,7 @@ class Test_Core_Auth_Process_AttributeAdd extends PHPUnit_Framework_TestCase
         $this->assertEquals($attributes['test2'], array('value2'));
     }
 
-    /*
+    /**
      * Test behavior when appending attribute values.
      */
     public function testAppend()
@@ -116,7 +116,7 @@ class Test_Core_Auth_Process_AttributeAdd extends PHPUnit_Framework_TestCase
         $this->assertEquals($attributes['test'], array('value1', 'value2'));
     }
 
-    /*
+    /**
      * Test replacing attribute values.
      */
     public function testReplace()
@@ -135,4 +135,60 @@ class Test_Core_Auth_Process_AttributeAdd extends PHPUnit_Framework_TestCase
         $this->assertEquals($attributes['test'], array('value2'));
     }
 
+    /**
+     * Test wrong usage generates exceptions
+     *
+     * @expectedException Exception
+     */
+    public function testWrongFlag()
+    {
+        $config = array(
+            '%nonsense',
+            'test' => array('value2'),
+        );
+        $request = array(
+            'Attributes' => array(
+                'test' => array('value1'),
+            ),
+        );
+        $result = self::processFilter($config, $request);
+    }
+
+    /**
+     * Test wrong attribute name
+     *
+     * @expectedException Exception
+     */
+    public function testWrongAttributeName()
+    {
+        $config = array(
+            '%replace',
+             true => array('value2'),
+        );
+        $request = array(
+            'Attributes' => array(
+                'test' => array('value1'),
+            ),
+        );
+        $result = self::processFilter($config, $request);
+    }
+
+    /**
+     * Test wrong attribute value
+     *
+     * @expectedException Exception
+     */
+    public function testWrongAttributeValue()
+    {
+        $config = array(
+            '%replace',
+            'test' => array(true),
+        );
+        $request = array(
+            'Attributes' => array(
+                'test' => array('value1'),
+            ),
+        );
+        $result = self::processFilter($config, $request);
+    }
 }
diff --git a/tests/modules/core/lib/Auth/Process/AttributeCopyTest.php b/tests/modules/core/lib/Auth/Process/AttributeCopyTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..29e696400417a31d7e0b4631e3fb946c9db80ce8
--- /dev/null
+++ b/tests/modules/core/lib/Auth/Process/AttributeCopyTest.php
@@ -0,0 +1,140 @@
+<?php
+
+/**
+ * Test for the core:AttributeCopy filter.
+ */
+class Test_Core_Auth_Process_AttributeCopy extends PHPUnit_Framework_TestCase
+{
+
+    /**
+     * Helper function to run the filter with a given configuration.
+     *
+     * @param array $config  The filter configuration.
+     * @param array $request  The request state.
+     * @return array  The state array after processing.
+     */
+    private static function processFilter(array $config, array $request)
+    {
+        $filter = new sspmod_core_Auth_Process_AttributeCopy($config, NULL);
+        $filter->process($request);
+        return $request;
+    }
+
+    /**
+     * Test the most basic functionality.
+     */
+    public function testBasic()
+    {
+        $config = array(
+            'test' => 'testnew',
+        );
+        $request = array(
+            'Attributes' => array('test' => array('AAP')),
+        );
+        $result = self::processFilter($config, $request);
+        $attributes = $result['Attributes'];
+        $this->assertArrayHasKey('test', $attributes);
+        $this->assertArrayHasKey('testnew', $attributes);
+        $this->assertEquals($attributes['testnew'], array('AAP'));
+    }
+
+    /**
+     * Test that existing attributes are left unmodified.
+     */
+    public function testExistingNotModified()
+    {
+        $config = array(
+            'test' => 'testnew',
+        );
+        $request = array(
+            'Attributes' => array(
+                'test' => array('AAP'),
+                'original1' => array('original_value1'),
+                'original2' => array('original_value2'),
+            ),
+        );
+        $result = self::processFilter($config, $request);
+        $attributes = $result['Attributes'];
+        $this->assertArrayHasKey('testnew', $attributes);
+        $this->assertEquals($attributes['test'], array('AAP'));
+        $this->assertArrayHasKey('original1', $attributes);
+        $this->assertEquals($attributes['original1'], array('original_value1'));
+        $this->assertArrayHasKey('original2', $attributes);
+        $this->assertEquals($attributes['original2'], array('original_value2'));
+    }
+
+    /**
+     * Test copying multiple attributes
+     */
+    public function testCopyMultiple()
+    {
+        $config = array(
+            'test1' => 'new1',
+            'test2' => 'new2',
+        );
+        $request = array(
+            'Attributes' => array('test1' => array('val1'), 'test2' => array('val2.1','val2.2')),
+        );
+        $result = self::processFilter($config, $request);
+        $attributes = $result['Attributes'];
+        $this->assertArrayHasKey('new1', $attributes);
+        $this->assertEquals($attributes['new1'], array('val1'));
+        $this->assertArrayHasKey('new2', $attributes);
+        $this->assertEquals($attributes['new2'], array('val2.1','val2.2'));
+    }
+
+    /**
+     * Test behaviour when target attribute exists (should be replaced).
+     */
+    public function testCopyClash()
+    {
+        $config = array(
+            'test' => 'new1',
+        );
+        $request = array(
+            'Attributes' => array(
+                'test' => array('testvalue1'),
+                'new1' => array('newvalue1'),
+            ),
+        );
+        $result = self::processFilter($config, $request);
+        $attributes = $result['Attributes'];
+        $this->assertEquals($attributes['new1'], array('testvalue1'));
+    }
+
+    /**
+     * Test wrong attribute name
+     *
+     * @expectedException Exception
+     */
+    public function testWrongAttributeName()
+    {
+        $config = array(
+            array('value2'),
+        );
+        $request = array(
+            'Attributes' => array(
+                'test' => array('value1'),
+            ),
+        );
+        $result = self::processFilter($config, $request);
+    }
+
+    /**
+     * Test wrong attribute value
+     *
+     * @expectedException Exception
+     */
+    public function testWrongAttributeValue()
+    {
+        $config = array(
+            'test' => array('test2'),
+        );
+        $request = array(
+            'Attributes' => array(
+                'test' => array('value1'),
+            ),
+        );
+        $result = self::processFilter($config, $request);
+    }
+}
diff --git a/tests/modules/core/lib/Auth/Process/ScopeAttributeTest.php b/tests/modules/core/lib/Auth/Process/ScopeAttributeTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..31e151636028a1901b710bd97011c0fbbbc4ff6a
--- /dev/null
+++ b/tests/modules/core/lib/Auth/Process/ScopeAttributeTest.php
@@ -0,0 +1,194 @@
+<?php
+
+/**
+ * Test for the core:ScopeAttribute filter.
+ */
+class Test_Core_Auth_Process_ScopeAttribute extends PHPUnit_Framework_TestCase
+{
+
+    /*
+     * Helper function to run the filter with a given configuration.
+     *
+     * @param array $config  The filter configuration.
+     * @param array $request  The request state.
+     * @return array  The state array after processing.
+     */
+    private static function processFilter(array $config, array $request)
+    {
+        $filter = new sspmod_core_Auth_Process_ScopeAttribute($config, NULL);
+        $filter->process($request);
+        return $request;
+    }
+
+    /*
+     * Test the most basic functionality.
+     */
+    public function testBasic()
+    {
+        $config = array(
+            'scopeAttribute' => 'eduPersonPrincipalName',
+            'sourceAttribute' => 'eduPersonAffiliation',
+            'targetAttribute' => 'eduPersonScopedAffiliation',
+        );
+        $request = array(
+            'Attributes' => array(
+                'eduPersonPrincipalName' => array('jdoe@example.com'),
+                'eduPersonAffiliation' => array('member'),
+            )
+        );
+        $result = self::processFilter($config, $request);
+        $attributes = $result['Attributes'];
+        $this->assertArrayHasKey('eduPersonScopedAffiliation', $attributes);
+        $this->assertEquals($attributes['eduPersonScopedAffiliation'], array('member@example.com'));
+    }
+
+    /*
+     * If target attribute already set, module must add, not overwrite.
+     */
+    public function testNoOverwrite()
+    {
+        $config = array(
+            'scopeAttribute' => 'eduPersonPrincipalName',
+            'sourceAttribute' => 'eduPersonAffiliation',
+            'targetAttribute' => 'eduPersonScopedAffiliation',
+        );
+        $request = array(
+            'Attributes' => array(
+                'eduPersonPrincipalName' => array('jdoe@example.com'),
+                'eduPersonAffiliation' => array('member'),
+                'eduPersonScopedAffiliation' => array('library-walk-in@example.edu'),
+            )
+        );
+        $result = self::processFilter($config, $request);
+        $attributes = $result['Attributes'];
+        $this->assertEquals($attributes['eduPersonScopedAffiliation'], array('library-walk-in@example.edu', 'member@example.com'));
+    }
+
+    /*
+     * If same scope already set, module must do nothing, not duplicate value.
+     */
+    public function testNoDuplication()
+    {
+        $config = array(
+            'scopeAttribute' => 'eduPersonPrincipalName',
+            'sourceAttribute' => 'eduPersonAffiliation',
+            'targetAttribute' => 'eduPersonScopedAffiliation',
+        );
+        $request = array(
+            'Attributes' => array(
+                'eduPersonPrincipalName' => array('jdoe@example.com'),
+                'eduPersonAffiliation' => array('member'),
+                'eduPersonScopedAffiliation' => array('member@example.com'),
+            )
+        );
+        $result = self::processFilter($config, $request);
+        $attributes = $result['Attributes'];
+        $this->assertEquals($attributes['eduPersonScopedAffiliation'], array('member@example.com'));
+    }
+
+
+    /*
+     * If source attribute not set, nothing happens
+     */
+    public function testNoSourceAttribute()
+    {
+        $config = array(
+            'scopeAttribute' => 'eduPersonPrincipalName',
+            'sourceAttribute' => 'eduPersonAffiliation',
+            'targetAttribute' => 'eduPersonScopedAffiliation',
+        );
+        $request = array(
+            'Attributes' => array(
+                'mail' => array('j.doe@example.edu', 'john@example.org'),
+                'eduPersonAffiliation' => array('member'),
+                'eduPersonScopedAffiliation' => array('library-walk-in@example.edu'),
+            )
+        );
+        $result = self::processFilter($config, $request);
+        $this->assertEquals($request['Attributes'], $result['Attributes']);
+    }
+
+    /*
+     * If scope attribute not set, nothing happens
+     */
+    public function testNoScopeAttribute()
+    {
+        $config = array(
+            'scopeAttribute' => 'eduPersonPrincipalName',
+            'sourceAttribute' => 'eduPersonAffiliation',
+            'targetAttribute' => 'eduPersonScopedAffiliation',
+        );
+        $request = array(
+            'Attributes' => array(
+                'mail' => array('j.doe@example.edu', 'john@example.org'),
+                'eduPersonScopedAffiliation' => array('library-walk-in@example.edu'),
+                'eduPersonPrincipalName' => array('jdoe@example.com'),
+            )
+        );
+        $result = self::processFilter($config, $request);
+        $this->assertEquals($request['Attributes'], $result['Attributes']);
+    }
+
+    /*
+     * When multiple @ signs in attribute, will use the first one.
+     */
+    public function testMultiAt()
+    {
+        $config = array(
+            'scopeAttribute' => 'eduPersonPrincipalName',
+            'sourceAttribute' => 'eduPersonAffiliation',
+            'targetAttribute' => 'eduPersonScopedAffiliation',
+        );
+        $request = array(
+            'Attributes' => array(
+                'eduPersonPrincipalName' => array('john@doe@example.com'),
+                'eduPersonAffiliation' => array('member'),
+            )
+        );
+        $result = self::processFilter($config, $request);
+        $attributes = $result['Attributes'];
+        $this->assertEquals($attributes['eduPersonScopedAffiliation'], array('member@doe@example.com'));
+    }
+
+    /*
+     * When multiple values in source attribute, should render multiple targets.
+     */
+    public function testMultivaluedSource()
+    {
+        $config = array(
+            'scopeAttribute' => 'eduPersonPrincipalName',
+            'sourceAttribute' => 'eduPersonAffiliation',
+            'targetAttribute' => 'eduPersonScopedAffiliation',
+        );
+        $request = array(
+            'Attributes' => array(
+                'eduPersonPrincipalName' => array('jdoe@example.com'),
+                'eduPersonAffiliation' => array('member','staff','faculty'),
+            )
+        );
+        $result = self::processFilter($config, $request);
+        $attributes = $result['Attributes'];
+        $this->assertEquals($attributes['eduPersonScopedAffiliation'], array('member@example.com','staff@example.com','faculty@example.com'));
+    }
+
+    /*
+     * When the source attribute doesn't have a scope, the entire value is used.
+     */
+    public function testNoAt()
+    {
+        $config = array(
+            'scopeAttribute' => 'schacHomeOrganization',
+            'sourceAttribute' => 'eduPersonAffiliation',
+            'targetAttribute' => 'eduPersonScopedAffiliation',
+        );
+        $request = array(
+            'Attributes' => array(
+                'schacHomeOrganization' => array('example.org'),
+                'eduPersonAffiliation' => array('student'),
+            )
+        );
+        $result = self::processFilter($config, $request);
+        $attributes = $result['Attributes'];
+        $this->assertEquals($attributes['eduPersonScopedAffiliation'], array('student@example.org'));
+    }
+}
diff --git a/tests/modules/core/lib/Auth/Process/ScopeFromAttributeTest.php b/tests/modules/core/lib/Auth/Process/ScopeFromAttributeTest.php
index 955d908c10ecd1b61d54b4fb8b0aef9a1be9f27f..f6ef1bf90414abb094f6e92db22c2d75fda23fd1 100644
--- a/tests/modules/core/lib/Auth/Process/ScopeFromAttributeTest.php
+++ b/tests/modules/core/lib/Auth/Process/ScopeFromAttributeTest.php
@@ -103,7 +103,7 @@ class Test_Core_Auth_Process_ScopeFromAttribute extends PHPUnit_Framework_TestCa
      * NOTE: currently disabled: this triggers a warning and a warning
      * wants to start a session which we cannot do in phpunit. How to fix?
      */
-/*    public function testNoAt()
+    public function testNoAt()
     {
         $config = array(
             'sourceAttribute' => 'eduPersonPrincipalName',
@@ -118,5 +118,5 @@ class Test_Core_Auth_Process_ScopeFromAttribute extends PHPUnit_Framework_TestCa
         $attributes = $result['Attributes'];
 
         $this->assertArrayNotHasKey('scope', $attributes);
-    } */
+    }
 }