diff --git a/docs/simplesamlphp-ecp-idp.txt b/docs/simplesamlphp-ecp-idp.txt
new file mode 100644
index 0000000000000000000000000000000000000000..28ac7f90a125dc2b14c1915cfa46755d82009453
--- /dev/null
+++ b/docs/simplesamlphp-ecp-idp.txt
@@ -0,0 +1,74 @@
+Adding Enhanced Client or Proxy (ECP) Profile support to the IdP
+===============================================================
+
+This document describes the necessary steps to enable support for the [SAML V2.0 Enhanced Client or Proxy Profile Version 2.0](http://docs.oasis-open.org/security/saml/Post2.0/saml-ecp/v2.0/cs01/saml-ecp-v2.0-cs01.pdf) on a simpleSAMLphp Identity Provider (IdP).
+
+The SAML V2.0 Enhanced Client or Proxy (ECP) profile is a SSO profile for use with HTTP, and clients with the capability to directly contact a principal's identity provider(s) without requiring discovery and redirection by the service provider, as in the case of a browser. It is particularly useful for desktop or server-side HTTP clients.
+
+Limitations
+-----------
+* Authentication must be done via [HTTP Basic authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#Basic_authentication_scheme).
+* Authentication must not require user interaction (e.g. auth filters that require user input).
+* Channel Bindings are unsupported.
+* "Holder of Key" Subject Confirmation is unsupported.
+
+This feature has been tested to work with Microsoft Office 365, but other service providers may require features of the ECP profile that are currently unsupported!
+
+Enabling ECP Profile on the IdP
+-----------------------------------
+
+To enable the IdP to send ECP assertions you must add the `saml20.ecp` option to the `saml20-idp-hosted` metadata file:
+
+    $metadata['__DYNAMIC:1__'] = array(
+        [....]
+        'auth' => 'example-userpass',
+        'saml20.ecp' => true,
+    );
+
+Note: authentication filters that require interaction with the user will not work with ECP.
+
+Add new metadata to SPs
+-----------------------
+
+After enabling the ECP Profile your IdP metadata will change. An additional ECP `SingleSignOnService` endpoint is added.
+You therefore need to update the metadata for your IdP at your SPs.
+The `saml20-idp-remote` metadata for simpleSAMLphp SPs should contain something like the following code:
+
+	'SingleSignOnService' =>
+	  array (
+	    0 =>
+	    array (
+	      'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect',
+	      'Location' => 'https://idp.example.org/simplesaml/saml2/idp/SSOService.php',
+	    ),
+	    1 =>
+	    array (
+	      'index' => 0,
+	      'Location' => 'https://didp.example.org/simplesaml/saml2/idp/SSOService.php',
+	      'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:SOAP',
+	    ),
+	  ),
+
+SP metadata on the IdP
+----------------------
+
+A SP using the ECP Profile must have an `AssertionConsumerService` endpoint supporting that profile.
+This means that you have to use the complex endpoint format in `saml20-sp-remote` metadata.
+In general, this should look like the following code:
+
+	'AssertionConsumerService' =>
+	  array (
+	    0 =>
+	    array (
+	      'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST',
+	      'Location' => 'https://sp.example.org/Shibboleth.sso/SAML2/POST',
+	      'index' => 1,
+	    ),
+	    1 =>
+	    array (
+	      'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:PAOS',
+	      'Location' => 'https://sp.example.org/ECP',
+	      'index' => 2,
+	    ),
+	  ),
+
diff --git a/docs/simplesamlphp-reference-idp-hosted.md b/docs/simplesamlphp-reference-idp-hosted.md
index 0e5cb6298e92a0ba1f393c4d9961955345b81335..712e2b45c4195569992cd5a240e51de5c876e05b 100644
--- a/docs/simplesamlphp-reference-idp-hosted.md
+++ b/docs/simplesamlphp-reference-idp-hosted.md
@@ -255,6 +255,10 @@ The following SAML 2.0 options are available:
 
 :   Note that this requires a configured memcache server.
 
+`saml20.ecp`
+:   Set to `true` to enable the IdP to recieve authnrequests and send responses according the Enhanced Client or Proxy (ECP) Profile. Note: authentication filters that require interaction with the user will not work with ECP.
+    Defaults to `false`.
+
 `saml20.hok.assertion`
 :   Set to `TRUE` to enable the IdP to send responses according the [Holder-of-Key Web Browser SSO Profile](./simplesamlphp-hok-idp).
     Defaults to `FALSE`.
diff --git a/modules/core/lib/Auth/UserPassBase.php b/modules/core/lib/Auth/UserPassBase.php
index a8c7e5445003f27cc175aa3edf9c4e9417930b35..bc05f61967cf976ac80f323a148759dfd215a5be 100644
--- a/modules/core/lib/Auth/UserPassBase.php
+++ b/modules/core/lib/Auth/UserPassBase.php
@@ -31,9 +31,9 @@ abstract class sspmod_core_Auth_UserPassBase extends SimpleSAML_Auth_Source {
 	 * If this is NULL, we won't force any username.
 	 */
 	private $forcedUsername;
-	
+
 	/**
-	 * Links to pages from login page. 
+	 * Links to pages from login page.
 	 * From configuration
 	 */
 	protected $loginLinks;
@@ -183,7 +183,24 @@ abstract class sspmod_core_Auth_UserPassBase extends SimpleSAML_Auth_Source {
 			 * is allowed to change the username.
 			 */
 			$state['forcedUsername'] = $this->forcedUsername;
-		}
+	    }
+
+	  // ECP requests supply authentication credentials with the AUthnRequest
+	  // so we validate them now rather than redirecting
+	  if (isset($state['core:auth:username']) && isset($state['core:auth:password'])) {
+	      $username = $state['core:auth:username'];
+	      $password = $state['core:auth:password'];
+
+	      if (isset($state['forcedUsername'])) {
+	          $username = $state['forcedUsername'];
+	      }
+
+	      $attributes = $this->login($username, $password);
+	      assert('is_array($attributes)');
+	      $state['Attributes'] = $attributes;
+
+	      return;
+	  }
 
 		/* Save the $state-array, so that we can restore it after a redirect. */
 		$id = SimpleSAML_Auth_State::saveState($state, self::STAGEID);
diff --git a/modules/saml/lib/IdP/SAML2.php b/modules/saml/lib/IdP/SAML2.php
index a02c8fc6c04439b19173469daf451a7e1d1d176f..384e7806cb9aa68b1f0bac0430149f5c8c52f821 100644
--- a/modules/saml/lib/IdP/SAML2.php
+++ b/modules/saml/lib/IdP/SAML2.php
@@ -1,6 +1,5 @@
 <?php
 
-
 use RobRichards\XMLSecLibs\XMLSecurityKey;
 
 /**
@@ -258,6 +257,9 @@ class sspmod_saml_IdP_SAML2
         if ($idpMetadata->getBoolean('saml20.hok.assertion', false)) {
             $supportedBindings[] = \SAML2\Constants::BINDING_HOK_SSO;
         }
+        if ($idpMetadata->getBoolean('saml20.ecp', false)) {
+            $supportedBindings[] = \SAML2\Constants::BINDING_PAOS;
+        }
 
         if (isset($_REQUEST['spentityid'])) {
             /* IdP initiated authentication. */
@@ -428,9 +430,25 @@ class sspmod_saml_IdP_SAML2
             'saml:RequestedAuthnContext'  => $authnContext,
         );
 
+        // ECP AuthnRequests need to supply credentials
+        if ($binding instanceof SOAP) {
+            self::processSOAPAuthnRequest($state);
+        }
+
         $idp->handleAuthenticationRequest($state);
     }
 
+    public static function processSOAPAuthnRequest(array &$state)
+    {
+        if (!isset($_SERVER['PHP_AUTH_USER']) || !isset($_SERVER['PHP_AUTH_PW'])) {
+            SimpleSAML_Logger::error("ECP AuthnRequest did not contain Basic Authentication header");
+            // TODO Throw some sort of ECP-specific exception / convert this to SOAP fault
+            throw new SimpleSAML_Error_Error("WRONGUSERPASS");
+        }
+
+        $state['core:auth:username'] = $_SERVER['PHP_AUTH_USER'];
+        $state['core:auth:password'] = $_SERVER['PHP_AUTH_PW'];
+    }
 
     /**
      * Send a logout request to a given association.
diff --git a/tests/modules/core/lib/Auth/UserPassBaseTest.php b/tests/modules/core/lib/Auth/UserPassBaseTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..ff248bdc5459814ed92f3881fe4319ef4dc7c873
--- /dev/null
+++ b/tests/modules/core/lib/Auth/UserPassBaseTest.php
@@ -0,0 +1,52 @@
+<?php
+
+class sspmod_core_Auth_UserPassBaseTest extends \PHPUnit_Framework_TestCase
+{
+    public function testAuthenticateECPCallsLoginAndSetsAttributes()
+    {
+        $state = array();
+        $attributes = array('attrib' => 'val');
+
+        $username = $state['core:auth:username'] = 'username';
+        $password = $state['core:auth:password'] = 'password';
+
+        $stub = $this->getMockBuilder('sspmod_core_Auth_UserPassBase')
+            ->disableOriginalConstructor()
+            ->setMethods(array('login'))
+            ->getMockForAbstractClass();
+
+        $stub->expects($this->once())
+            ->method('login')
+            ->with($username, $password)
+            ->will($this->returnValue($attributes));
+
+        $stub->authenticate($state);
+
+        $this->assertSame($attributes, $state['Attributes']);
+    }
+
+    public function testAuthenticateECPCallsLoginWithForcedUsername()
+    {
+        $state = array();
+        $attributes = array();
+
+        $forcedUsername = 'forcedUsername';
+
+        $state['core:auth:username'] = 'username';
+        $password = $state['core:auth:password'] = 'password';
+
+        $stub = $this->getMockBuilder('sspmod_core_Auth_UserPassBase')
+            ->disableOriginalConstructor()
+            ->setMethods(array('login'))
+            ->getMockForAbstractClass();
+
+        $stub->expects($this->once())
+            ->method('login')
+            ->with($forcedUsername, $password)
+            ->will($this->returnValue($attributes));
+
+        $stub->setForcedUsername($forcedUsername);
+
+        $stub->authenticate($state);
+    }
+}
diff --git a/tests/modules/saml/lib/IdP/SAML2Test.php b/tests/modules/saml/lib/IdP/SAML2Test.php
new file mode 100644
index 0000000000000000000000000000000000000000..4ca2095920eeef09f42f75d7507d94e264462dd7
--- /dev/null
+++ b/tests/modules/saml/lib/IdP/SAML2Test.php
@@ -0,0 +1,38 @@
+<?php
+
+class sspmod_saml_IdP_SAML2Test extends \PHPUnit_Framework_TestCase
+{
+    public function testProcessSOAPAuthnRequest()
+    {
+        $username = $_SERVER['PHP_AUTH_USER'] = 'username';
+        $password = $_SERVER['PHP_AUTH_PW'] = 'password';
+        $state = array();
+
+        sspmod_saml_IdP_SAML2::processSOAPAuthnRequest($state);
+
+        $this->assertEquals($username, $state['core:auth:username']);
+        $this->assertEquals($password, $state['core:auth:password']);
+    }
+
+    public function testProcessSOAPAuthnRequestMissingUsername()
+    {
+        $this->setExpectedException('SimpleSAML_Error_Error', 'WRONGUSERPASS');
+
+        $_SERVER['PHP_AUTH_PW'] = 'password';
+        unset($_SERVER['PHP_AUTH_USER']);
+        $state = array();
+
+        sspmod_saml_IdP_SAML2::processSOAPAuthnRequest($state);
+    }
+
+    public function testProcessSOAPAuthnRequestMissingPassword()
+    {
+        $this->setExpectedException('SimpleSAML_Error_Error', 'WRONGUSERPASS');
+
+        $_SERVER['PHP_AUTH_USER'] = 'username';
+        unset($_SERVER['PHP_AUTH_PW']);
+        $state = array();
+
+        sspmod_saml_IdP_SAML2::processSOAPAuthnRequest($state);
+    }
+}
diff --git a/www/saml2/idp/metadata.php b/www/saml2/idp/metadata.php
index fabe2a2efe4f60816ceb10f4524b114193f931e1..d99aab0fb051c3af56f015323f1a882b1c195cb8 100644
--- a/www/saml2/idp/metadata.php
+++ b/www/saml2/idp/metadata.php
@@ -127,6 +127,14 @@ try {
         ));
     }
 
+    if ($idpmeta->getBoolean('saml20.ecp', false)) {
+        $metaArray['SingleSignOnService'][] = array(
+            'index' => 0,
+            'Binding' => SAML2_Const::BINDING_SOAP,
+            'Location' => SimpleSAML_Utilities::getHostnameURL() . 'saml2/idp/SSOService.php',
+        );
+    }
+
     $metaArray['NameIDFormat'] = $idpmeta->getString(
         'NameIDFormat',
         'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'