Pre-Requisite:
1. Installed and Configured Shibboleth IDP 3.02. Installed and Configured Shibboleth native SP 2.5.3 with ECP Enabled
Environment:
1. JDK 1.72. Eclipse
3. Gradle 1.5
4. Open SAML2.6.4
5. Apache Http Client 4.4
6. SL4J 1.7.10
Developing SAMl-ECP client
The following tasks needs to be performed to build the ECP clients:
1. Sending Request from ECP to SP
2. Sending Request from ECP to IDP
3. Sending Request from ECP to SP
1. Sending Request from ECP to SP
Accessing the Secure URL through Enhanced Client Proxy(ECP) using the Get Request. In get request you need to add two header fields in the request:1. Accept: application/vnd.paos+xml
2. PAOS: ver='url:liberty:paos:2003-08';'urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp'
Please refer example code : EcpRequestInterceptor for adding the header variable in the request.
1.1 Send the Get Request Secure URL from ECP to SP with PAOS Header
The PAOS header variables are added in the EcpRequestInterceptor class.
public Envelope accessProtectedPage()
{
Envelope envelope=null;
HttpGet request= new HttpGet(ecp.getProtectedUrl());
try
{
CloseableHttpResponse httpResponse = client.execute(request);
if(httpResponse != null)
{
if(httpResponse.getStatusLine().getStatusCode()==200)
{
HttpEntity entity = httpResponse.getEntity();
envelope=convertRequestToSoap(entity);
if (!envelope.getBody().getUnknownXMLObjects().isEmpty())
{
if(envelope.getBody().getUnknownXMLObjects(AuthnRequest.DEFAULT_ELEMENT_NAME) != null)
{
logger.info("Auethentication Access"+convertSoapToString(envelope));
}
else
{
throw new RuntimeException("Invalid ECP Response");
}
}
else
{
throw new RuntimeException("Invalid ECP Response");
}
EntityUtils.consume(entity);
}
else
{
throw new RuntimeException(httpResponse.getStatusLine().getStatusCode()+" - " +httpResponse.getStatusLine().getReasonPhrase());
}
}
} catch (ClientProtocolException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return envelope;
}
1.2 SP will send the SOAP Response to ECP with PAOS Request in SOAP Headers and Authentication Request Statement in Soap Body.
The Ecp response from the SP is shown below.
<?xml version="1.0" encoding="UTF-8"?>
<S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/">
<S:Header>
<paos:Request xmlns:paos="urn:liberty:paos:2003-08" S:actor="http://schemas.xmlsoap.org/soap/actor/next" S:mustUnderstand="1"
responseConsumerURL="https://shib-sp.example.edu/Shibboleth.sso/SAML2/ECP" service="urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp"/>
<ecp:Request xmlns:ecp="urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp" IsPassive="0" S:actor="http://schemas.xmlsoap.org/soap/actor/next" S:mustUnderstand="1">
<saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">https://shib-sp.example.edu/shibboleth</saml:Issuer>
<samlp:IDPList xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol">
<samlp:IDPEntry ProviderID="https://shib-idp.example.edu/idp/shibboleth"/>
</samlp:IDPList>
</ecp:Request>
<ecp:RelayState xmlns:ecp="urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp" S:actor="http://schemas.xmlsoap.org/soap/actor/next"
S:mustUnderstand="1">ss:mem:ec7e3978ddd1958ca9efb27be7c9445017daf9068b62c647cc398a4dfc73573c</ecp:RelayState>
</S:Header>
<S:Body>
<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
AssertionConsumerServiceURL="https://shib-sp.example.edu/Shibboleth.sso/SAML2/ECP"
ID="_c06ac9aa1e8e172b920bd32435547b05"
IssueInstant="2015-02-27T00:04:38Z"
ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:PAOS"
Version="2.0">
<saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">https://shib-sp.example.edu/shibboleth</saml:Issuer>
<samlp:NameIDPolicy AllowCreate="1"/>
<samlp:Scoping>
<samlp:IDPList>
<samlp:IDPEntry ProviderID="https://shib-idp.example.edu/idp/shibboleth"/>
</samlp:IDPList>
</samlp:Scoping>
</samlp:AuthnRequest>
</S:Body>
</S:Envelope>
2. Sending the Authentication Request from ECP to IDP with PAOS and Authorization Headers
2.1 Detach the SP Soap Body and Create a new Soap Envelop and set the detached soap body to new envelop
You need to remove the SOAP headers from the SP Initial Secure URL response or You can copy the authentication request from the SP Initial Secure URL response and build a new Soap Envelope. I have detached (copy) from the initial soap response and building the new soap request and sample code is given below:
public Envelope buildIdpAuthRequest(Envelope envelope)
{
Envelope newAuthRequest = new EnvelopeBuilder().buildObject();
Body authBody= envelope.getBody();
authBody.detach();
newAuthRequest.setBody(authBody);
return newAuthRequest;
}
{
Envelope newAuthRequest = new EnvelopeBuilder().buildObject();
Body authBody= envelope.getBody();
authBody.detach();
newAuthRequest.setBody(authBody);
return newAuthRequest;
}
Sample Soap XML :
<?xml version="1.0" encoding="UTF-8"?>
<soap11:Envelope xmlns:soap11="http://schemas.xmlsoap.org/soap/envelope/">
<S:Body xmlns:S="http://schemas.xmlsoap.org/soap/envelope/">
<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
AssertionConsumerServiceURL="https://shib-sp.example.edu/Shibboleth.sso/SAML2/ECP"
ID="_b2c92bc8216ff317a5c42d797a7cd8ca" IssueInstant="2015-02-27T18:49:37Z"
ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:PAOS"
Version="2.0">
<saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">https://shib-sp.example.edu/shibboleth</saml:Issuer>
<samlp:NameIDPolicy AllowCreate="1"/>
<samlp:Scoping>
<samlp:IDPList><samlp:IDPEntry ProviderID="https://shib-idp.example.edu/idp/shibboleth"/></samlp:IDPList>
</samlp:Scoping>
</samlp:AuthnRequest>
</S:Body>
</soap11:Envelope>
<soap11:Envelope xmlns:soap11="http://schemas.xmlsoap.org/soap/envelope/">
<S:Body xmlns:S="http://schemas.xmlsoap.org/soap/envelope/">
<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
AssertionConsumerServiceURL="https://shib-sp.example.edu/Shibboleth.sso/SAML2/ECP"
ID="_b2c92bc8216ff317a5c42d797a7cd8ca" IssueInstant="2015-02-27T18:49:37Z"
ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:PAOS"
Version="2.0">
<saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">https://shib-sp.example.edu/shibboleth</saml:Issuer>
<samlp:NameIDPolicy AllowCreate="1"/>
<samlp:Scoping>
<samlp:IDPList><samlp:IDPEntry ProviderID="https://shib-idp.example.edu/idp/shibboleth"/></samlp:IDPList>
</samlp:Scoping>
</samlp:AuthnRequest>
</S:Body>
</soap11:Envelope>
2.2 Send the Authentication Request to IDP with PAOS and Authorization Headers
You need to add the following header variables into the post request because these variables are required for IDP ECP profile:
2. PAOS: ver='url:liberty:paos:2003-08';'urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp'
3. Authorization: Basic + User Name and password encode with base 64.
The sample code is given below:
public Envelope sendAuthenticationToIdp(String authRequestXml)
{
logger.info("Authentication Request from ECP to IDP :" + authRequestXml);
Envelope envelope=null;
HttpPost request= new HttpPost(ecp.getIdpUrl());
request.addHeader(HttpHeaders.AUTHORIZATION, "Basic "+ Base64.encodeBytes((ecp.getUserName()+":"+ecp.getPassword()).getBytes()));
try
{
request.setEntity(new StringEntity(authRequestXml));
} catch (UnsupportedEncodingException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
try
{
CloseableHttpResponse httpResponse = client.execute(request);
if(httpResponse != null)
{
if(httpResponse.getStatusLine().getStatusCode()==200)
{
HttpEntity entity = httpResponse.getEntity();
envelope=convertRequestToSoap(entity);
if (!envelope.getBody().getUnknownXMLObjects().isEmpty())
{
if(envelope.getBody().getUnknownXMLObjects(Response.DEFAULT_ELEMENT_NAME) != null)
{
logger.info("Auethentication Access"+convertSoapToString(envelope));
org.opensaml.saml2.core.Response samlResp=(org.opensaml.saml2.core.Response)envelope.getBody().getUnknownXMLObjects(org.opensaml.saml2.core.Response.DEFAULT_ELEMENT_NAME).get(0);
if(samlResp.getStatus().getStatusCode().getValue().equals(StatusCode.SUCCESS_URI))
{
System.out.println("Status :"+samlResp.getStatus().getStatusCode().getValue());
}
else
{
StatusDetail detail= (StatusDetail)samlResp.getStatus().getStatusDetail().getUnknownXMLObjects(StatusDetail.DEFAULT_ELEMENT_NAME).get(0);
throw new RuntimeException("SAML Authentication Failed "+samlResp.getStatus().getStatusCode().getValue() + " - ");
}
}
else
{
throw new RuntimeException("Invalid IDP ECP Response");
}
}
else
{
throw new RuntimeException("Invalid ECP Response");
}
EntityUtils.consume(entity);
}
else
{
// it will throw 403 error due to invalid password or invalid user name or authentication failed
throw new RuntimeException(httpResponse.getStatusLine().getStatusCode()+" - " +httpResponse.getStatusLine().getReasonPhrase());
}
}
} catch (ClientProtocolException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return envelope;
}
{
logger.info("Authentication Request from ECP to IDP :" + authRequestXml);
Envelope envelope=null;
HttpPost request= new HttpPost(ecp.getIdpUrl());
request.addHeader(HttpHeaders.AUTHORIZATION, "Basic "+ Base64.encodeBytes((ecp.getUserName()+":"+ecp.getPassword()).getBytes()));
try
{
request.setEntity(new StringEntity(authRequestXml));
} catch (UnsupportedEncodingException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
try
{
CloseableHttpResponse httpResponse = client.execute(request);
if(httpResponse != null)
{
if(httpResponse.getStatusLine().getStatusCode()==200)
{
HttpEntity entity = httpResponse.getEntity();
envelope=convertRequestToSoap(entity);
if (!envelope.getBody().getUnknownXMLObjects().isEmpty())
{
if(envelope.getBody().getUnknownXMLObjects(Response.DEFAULT_ELEMENT_NAME) != null)
{
logger.info("Auethentication Access"+convertSoapToString(envelope));
org.opensaml.saml2.core.Response samlResp=(org.opensaml.saml2.core.Response)envelope.getBody().getUnknownXMLObjects(org.opensaml.saml2.core.Response.DEFAULT_ELEMENT_NAME).get(0);
if(samlResp.getStatus().getStatusCode().getValue().equals(StatusCode.SUCCESS_URI))
{
System.out.println("Status :"+samlResp.getStatus().getStatusCode().getValue());
}
else
{
StatusDetail detail= (StatusDetail)samlResp.getStatus().getStatusDetail().getUnknownXMLObjects(StatusDetail.DEFAULT_ELEMENT_NAME).get(0);
throw new RuntimeException("SAML Authentication Failed "+samlResp.getStatus().getStatusCode().getValue() + " - ");
}
}
else
{
throw new RuntimeException("Invalid IDP ECP Response");
}
}
else
{
throw new RuntimeException("Invalid ECP Response");
}
EntityUtils.consume(entity);
}
else
{
// it will throw 403 error due to invalid password or invalid user name or authentication failed
throw new RuntimeException(httpResponse.getStatusLine().getStatusCode()+" - " +httpResponse.getStatusLine().getReasonPhrase());
}
}
} catch (ClientProtocolException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return envelope;
}
2.3 IDP will send the SOAP response to ECP with SAML Response in soap body and ECP response in SOAP Headers.
The sample Authentication Successful response from IDP as follows:
<?xml version="1.0" encoding="UTF-8"?>
<soap11:Envelope xmlns:soap11="http://schemas.xmlsoap.org/soap/envelope/">
<soap11:Header>
<ecp:Response xmlns:ecp="urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp"
AssertionConsumerServiceURL="https://shib-sp.example.edu/Shibboleth.sso/SAML2/ECP"
soap11:actor="http://schemas.xmlsoap.org/soap/actor/next" soap11:mustUnderstand="1"/>
<samlec:GeneratedKey xmlns:samlec="urn:ietf:params:xml:ns:samlec"
soap11:actor="http://schemas.xmlsoap.org/soap/actor/next">Saml keys</samlec:GeneratedKey>
</soap11:Header>
<soap11:Body>
<saml2p:Response xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol"
Destination="https://shib-sp.example.edu/Shibboleth.sso/SAML2/ECP"
ID="_c860f132c4115581218971d92a52f500"
InResponseTo="_b2c92bc8216ff317a5c42d797a7cd8ca"
IssueInstant="2015-02-27T18:49:37.867Z" Version="2.0">
<saml2:Issuer xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">https://shib-idp.example.edu/idp/shibboleth</saml2:Issuer>
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:SignedInfo>
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha512"/>
<ds:Reference URI="#_c860f132c4115581218971d92a52f500">
<ds:Transforms>
<ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
</ds:Transforms>
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha512"/>
<ds:DigestValue>Digest Base 64 encoded value </ds:DigestValue>
</ds:Reference>
</ds:SignedInfo>
<ds:SignatureValue>Signature Value Base 64 encoded</ds:SignatureValue>
<ds:KeyInfo>
<ds:X509Data>
<ds:X509Certificate>Certificate</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</ds:Signature>
<saml2p:Status>
<saml2p:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
</saml2p:Status>
<saml2:EncryptedAssertion xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">
<xenc:EncryptedData xmlns:xenc="http://www.w3.org/2001/04/xmlenc#"
Id="_37f9853b6561b3722b3691392ace994b"
Type="http://www.w3.org/2001/04/xmlenc#Element">
<xenc:EncryptionMethod Algorithm="http://www.w3.org/2009/xmlenc11#aes128-gcm"/>
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<xenc:EncryptedKey Id="_7dc4db379206ea3e491f5e6e15bbf6fb"
Recipient="https://shib-sp.example.edu/shibboleth">
<xenc:EncryptionMethod Algorithm="http://www.w3.org/2009/xmlenc11#rsa-oaep">
<ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
<xenc11:MGF xmlns:xenc11="http://www.w3.org/2009/xmlenc11#"
Algorithm="http://www.w3.org/2009/xmlenc11#mgf1sha1"/>
</xenc:EncryptionMethod>
<ds:KeyInfo>
<ds:X509Data>
<ds:X509Certificate>Certificate</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
<xenc:CipherData>
<xenc:CipherValue>Cipher Value</xenc:CipherValue>
</xenc:CipherData>
</xenc:EncryptedKey>
</ds:KeyInfo>
<xenc:CipherData>
<xenc:CipherValue>Certificate</xenc:CipherValue>
</xenc:CipherData>
</xenc:EncryptedData>
</saml2:EncryptedAssertion>
</saml2p:Response>
</soap11:Body>
</soap11:Envelope>
<soap11:Envelope xmlns:soap11="http://schemas.xmlsoap.org/soap/envelope/">
<soap11:Header>
<ecp:Response xmlns:ecp="urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp"
AssertionConsumerServiceURL="https://shib-sp.example.edu/Shibboleth.sso/SAML2/ECP"
soap11:actor="http://schemas.xmlsoap.org/soap/actor/next" soap11:mustUnderstand="1"/>
<samlec:GeneratedKey xmlns:samlec="urn:ietf:params:xml:ns:samlec"
soap11:actor="http://schemas.xmlsoap.org/soap/actor/next">Saml keys</samlec:GeneratedKey>
</soap11:Header>
<soap11:Body>
<saml2p:Response xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol"
Destination="https://shib-sp.example.edu/Shibboleth.sso/SAML2/ECP"
ID="_c860f132c4115581218971d92a52f500"
InResponseTo="_b2c92bc8216ff317a5c42d797a7cd8ca"
IssueInstant="2015-02-27T18:49:37.867Z" Version="2.0">
<saml2:Issuer xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">https://shib-idp.example.edu/idp/shibboleth</saml2:Issuer>
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:SignedInfo>
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha512"/>
<ds:Reference URI="#_c860f132c4115581218971d92a52f500">
<ds:Transforms>
<ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
</ds:Transforms>
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha512"/>
<ds:DigestValue>Digest Base 64 encoded value </ds:DigestValue>
</ds:Reference>
</ds:SignedInfo>
<ds:SignatureValue>Signature Value Base 64 encoded</ds:SignatureValue>
<ds:KeyInfo>
<ds:X509Data>
<ds:X509Certificate>Certificate</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</ds:Signature>
<saml2p:Status>
<saml2p:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
</saml2p:Status>
<saml2:EncryptedAssertion xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">
<xenc:EncryptedData xmlns:xenc="http://www.w3.org/2001/04/xmlenc#"
Id="_37f9853b6561b3722b3691392ace994b"
Type="http://www.w3.org/2001/04/xmlenc#Element">
<xenc:EncryptionMethod Algorithm="http://www.w3.org/2009/xmlenc11#aes128-gcm"/>
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<xenc:EncryptedKey Id="_7dc4db379206ea3e491f5e6e15bbf6fb"
Recipient="https://shib-sp.example.edu/shibboleth">
<xenc:EncryptionMethod Algorithm="http://www.w3.org/2009/xmlenc11#rsa-oaep">
<ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
<xenc11:MGF xmlns:xenc11="http://www.w3.org/2009/xmlenc11#"
Algorithm="http://www.w3.org/2009/xmlenc11#mgf1sha1"/>
</xenc:EncryptionMethod>
<ds:KeyInfo>
<ds:X509Data>
<ds:X509Certificate>Certificate</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
<xenc:CipherData>
<xenc:CipherValue>Cipher Value</xenc:CipherValue>
</xenc:CipherData>
</xenc:EncryptedKey>
</ds:KeyInfo>
<xenc:CipherData>
<xenc:CipherValue>Certificate</xenc:CipherValue>
</xenc:CipherData>
</xenc:EncryptedData>
</saml2:EncryptedAssertion>
</saml2p:Response>
</soap11:Body>
</soap11:Envelope>
3. Sending the IDP Authentication Response from ECP to SP to access the Secure URL Access.
3.1 Add Initial Secure URL Access Relay State in IDP Soap Response HeadersThe RelayState information is extracted from the initial Sp Secure Access Response from the SP Soap Envelop. This information is required in the soap header because, the SP going to accept the idp authentication SAML response and also make a decision to allow or denied requested secure page.
RelayState relayState= (RelayState)secureResourceResponse.getHeader().getUnknownXMLObjects(RelayState.DEFAULT_ELEMENT_NAME).get(0);
relayState.detach();
Header header = new HeaderBuilder().buildObject();
header.getUnknownXMLObjects().clear();
header.getUnknownXMLObjects().add(relayState);
idpAuthResponse.setHeader(header);
3.2 Extract the AssertionConsumerService URL from the IDP Soap Response
This assertionConsumerUrl parameter extracted from the Soap Header from the IDP Authentication soap response. This assertionConsumerUrl is being used to post the IDP Authentication response with Relay State header from ECP to Service provider post request.
String assertionConsumerUrl=((Response)idpAuthResponse.getHeader().getUnknownXMLObjects(Response.DEFAULT_ELEMENT_NAME).get(0)).getAssertionConsumerServiceURL();
3.3 Send IDP Authentication response to Service Provider with PAOS and Conetent-Type headers.
Sample Code:
public Envelope sendAuthResponseToSp(String authResponseXml,String consumeServiceUrl)
{
Envelope envelope=null;
HttpPost request= new HttpPost(consumeServiceUrl);
request.addHeader(HttpHeaders.CONTENT_TYPE, EcpFlowConstants.PAOS_CONTENT_TYPE);
try
{
request.setEntity(new StringEntity(authResponseXml));
} catch (UnsupportedEncodingException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
try
{
CloseableHttpResponse httpResponse = client.execute(request);
if(httpResponse != null)
{
if(httpResponse.getStatusLine().getStatusCode()==200)
{
HttpEntity entity = httpResponse.getEntity();
entity.writeTo(System.out);
logger.info("Cookies "+store.getCookies());
EntityUtils.consume(entity);
}
else
{
throw new RuntimeException(httpResponse.getStatusLine().getStatusCode()+" - " +httpResponse.getStatusLine().getReasonPhrase());
}
}
} catch (ClientProtocolException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return envelope;
}
Example Code:
I have classified the example code 4 classes. They are
1. ECP Object
This object is being used to capture the ECP Endpoint IDP Url, Protected Resource from the SP, User and Password to authenticate the IDP Server
2. EcpRequestInterceptor
This object is being used to add the ECP headers in each request.
3. EcpClientFlow
This object being used to send the GET, POST request to IDP and SP using apache HttpClient.
4. EcpClient
This object is being used to Initialize the SAML Boot strap, and Interacting with EcpClientFlow object invoking the various request such as Accessing the Protected Page, Authenticating User from the IDP, and Finally Send the Authenticated IDP Response to Service Provider.
The Sample Code is given below:
package edu.example.ecp.client;
public class Ecp
{
private String idpUrl;
private String protectedUrl;
private String userName;
private String password;
public String getIdpUrl() {
return idpUrl;
}
public void setIdpUrl(String idpUrl) {
this.idpUrl = idpUrl;
}
public String getProtectedUrl() {
return protectedUrl;
}
public void setProtectedUrl(String protectedUrl) {
this.protectedUrl = protectedUrl;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
package edu.example.ecp.client;
import java.io.IOException;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.net.ProxySelector;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerFactoryConfigurationError;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHeaders;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.CookieStore;
import org.apache.http.client.config.CookieSpecs;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.client.LaxRedirectStrategy;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.impl.conn.SystemDefaultRoutePlanner;
import org.apache.http.util.EntityUtils;
import org.opensaml.common.SAMLObject;
import org.opensaml.common.xml.SAMLConstants;
import org.opensaml.saml1.core.AuthenticationStatement;
import org.opensaml.saml2.core.Assertion;
import org.opensaml.saml2.core.AuthnRequest;
import org.opensaml.saml2.core.Status;
import org.opensaml.saml2.core.StatusCode;
import org.opensaml.saml2.core.StatusDetail;
import org.opensaml.saml2.ecp.Response;
import org.opensaml.ws.soap.soap11.Body;
import org.opensaml.ws.soap.soap11.Envelope;
import org.opensaml.ws.soap.soap11.impl.EnvelopeBuilder;
import org.opensaml.xml.Configuration;
import org.opensaml.xml.io.Marshaller;
import org.opensaml.xml.io.MarshallingException;
import org.opensaml.xml.io.Unmarshaller;
import org.opensaml.xml.io.UnmarshallingException;
import org.opensaml.xml.parse.BasicParserPool;
import org.opensaml.xml.parse.ParserPool;
import org.opensaml.xml.parse.XMLParserException;
import org.opensaml.xml.util.Base64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
package edu.example.ecp.client;
public class EcpClientFlow
{
private Ecp ecp;
private BasicParserPool pool;
private CloseableHttpClient client;
private CookieStore store;
private Logger logger= LoggerFactory.getLogger(EcpClientFlow.class);
public EcpClientFlow(Ecp ecp)
{
this.ecp=ecp;
//Creating the Parse Object
pool= new BasicParserPool();
pool.setNamespaceAware(true);
//Creating the Pooled Http Connection Manager
PoolingHttpClientConnectionManager manager= new PoolingHttpClientConnectionManager();
manager.setMaxTotal(20);
manager.setDefaultMaxPerRoute(10);
//Creating the Cookie Store store the Cookies
store= new BasicCookieStore();
//Creating the Browser Compatible Request Cookie Object
RequestConfig config= RequestConfig.custom().setCookieSpec(CookieSpecs.DEFAULT).build();
// Building the Client Builder
HttpClientBuilder clientBuildert= HttpClients.custom().setConnectionManager(manager)
.setDefaultCookieStore(store)
// Add the ECP Header every request
.addInterceptorFirst(new EcpRequestInterceptor())
// Redirecting the Request
.setRedirectStrategy(new LaxRedirectStrategy())
.setDefaultRequestConfig(config);
client=clientBuildert.setRoutePlanner(new SystemDefaultRoutePlanner(ProxySelector.getDefault())).build();
}
public Envelope accessProtectedPage()
{
Envelope envelope=null;
HttpGet request= new HttpGet(ecp.getProtectedUrl());
try
{
CloseableHttpResponse httpResponse = client.execute(request);
if(httpResponse != null)
{
if(httpResponse.getStatusLine().getStatusCode()==200)
{
HttpEntity entity = httpResponse.getEntity();
envelope=convertRequestToSoap(entity);
if (!envelope.getBody().getUnknownXMLObjects().isEmpty())
{
if(envelope.getBody().getUnknownXMLObjects(AuthnRequest.DEFAULT_ELEMENT_NAME) != null)
{
logger.info("Auethentication Access"+convertSoapToString(envelope));
}
else
{
throw new RuntimeException("Invalid ECP Response");
}
}
else
{
throw new RuntimeException("Invalid ECP Response");
}
EntityUtils.consume(entity);
}
else
{
throw new RuntimeException(httpResponse.getStatusLine().getStatusCode()+" - " +httpResponse.getStatusLine().getReasonPhrase());
}
}
} catch (ClientProtocolException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return envelope;
}
public Envelope sendAuthenticationToIdp(String authRequestXml)
{
logger.info("Authentication Request from ECP to IDP :" + authRequestXml);
Envelope envelope=null;
HttpPost request= new HttpPost(ecp.getIdpUrl());
request.addHeader(HttpHeaders.AUTHORIZATION, "Basic "+ Base64.encodeBytes((ecp.getUserName()+":"+ecp.getPassword()).getBytes()));
try
{
request.setEntity(new StringEntity(authRequestXml));
} catch (UnsupportedEncodingException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
try
{
CloseableHttpResponse httpResponse = client.execute(request);
if(httpResponse != null)
{
if(httpResponse.getStatusLine().getStatusCode()==200)
{
HttpEntity entity = httpResponse.getEntity();
envelope=convertRequestToSoap(entity);
if (!envelope.getBody().getUnknownXMLObjects().isEmpty())
{
if(envelope.getBody().getUnknownXMLObjects(Response.DEFAULT_ELEMENT_NAME) != null)
{
logger.info("Auethentication Access"+convertSoapToString(envelope));
org.opensaml.saml2.core.Response samlResp=(org.opensaml.saml2.core.Response)envelope.getBody().getUnknownXMLObjects(org.opensaml.saml2.core.Response.DEFAULT_ELEMENT_NAME).get(0);
if(samlResp.getStatus().getStatusCode().getValue().equals(StatusCode.SUCCESS_URI))
{
System.out.println("Status :"+samlResp.getStatus().getStatusCode().getValue());
}
else
{
StatusDetail detail= (StatusDetail)samlResp.getStatus().getStatusDetail().getUnknownXMLObjects(StatusDetail.DEFAULT_ELEMENT_NAME).get(0);
throw new RuntimeException("SAML Authentication Failed "+samlResp.getStatus().getStatusCode().getValue() + " - ");
}
}
else
{
throw new RuntimeException("Invalid IDP ECP Response");
}
}
else
{
throw new RuntimeException("Invalid ECP Response");
}
EntityUtils.consume(entity);
}
else
{
// it will throw 403 error due to invalid password or invalid user name or authentication failed
throw new RuntimeException(httpResponse.getStatusLine().getStatusCode()+" - " +httpResponse.getStatusLine().getReasonPhrase());
}
}
} catch (ClientProtocolException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return envelope;
}
public Envelope sendAuthResponseToSp(String authResponseXml,String consumeServiceUrl)
{
Envelope envelope=null;
HttpPost request= new HttpPost(consumeServiceUrl);
request.addHeader(HttpHeaders.CONTENT_TYPE, EcpFlowConstants.PAOS_CONTENT_TYPE);
try
{
request.setEntity(new StringEntity(authResponseXml));
} catch (UnsupportedEncodingException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
try
{
CloseableHttpResponse httpResponse = client.execute(request);
if(httpResponse != null)
{
if(httpResponse.getStatusLine().getStatusCode()==200)
{
HttpEntity entity = httpResponse.getEntity();
entity.writeTo(System.out);
logger.info("Cookies "+store.getCookies());
EntityUtils.consume(entity);
}
else
{
// it will throw 403 error due to invalid password or invalid user name or authentication failed
throw new RuntimeException(httpResponse.getStatusLine().getStatusCode()+" - " +httpResponse.getStatusLine().getReasonPhrase());
}
}
} catch (ClientProtocolException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return envelope;
}
public Envelope buildIdpAuthRequest(Envelope envelope)
{
Envelope newAuthRequest = new EnvelopeBuilder().buildObject();
Body authBody= envelope.getBody();
authBody.detach();
newAuthRequest.setBody(authBody);
return newAuthRequest;
}
public Envelope convertRequestToSoap(HttpEntity entity)
{
Envelope envlope=null;
Document soapDoc;
try
{
soapDoc = pool.parse(entity.getContent());
Unmarshaller unmarshall = Configuration.getUnmarshallerFactory().getUnmarshaller(soapDoc.getDocumentElement());
envlope=(Envelope)unmarshall.unmarshall(soapDoc.getDocumentElement());
} catch (IllegalStateException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (XMLParserException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (UnmarshallingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return envlope;
}
public String convertSoapToString(Envelope envelope)
{
String result=null;
try
{
Marshaller marshaller= Configuration.getMarshallerFactory().getMarshaller(envelope);
Element element= marshaller.marshall(envelope);
Transformer transformer;
transformer = TransformerFactory.newInstance().newTransformer();
Source source = new DOMSource(element);
StringWriter writer= new StringWriter();
StreamResult output = new StreamResult(writer);
transformer.transform(source, output);
result=writer.getBuffer().toString();
} catch (IllegalStateException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (MarshallingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (TransformerConfigurationException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (TransformerFactoryConfigurationError e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (TransformerException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return result;
}
}
package edu.example.ecp.client;
import org.opensaml.common.xml.SAMLConstants;
public interface EcpFlowConstants
{
public String PAOS_CONTENT_TYPE="application/vnd.paos+xml";
public String PAOS_HEADER=SAMLConstants.PAOS_PREFIX.toUpperCase();
}
package edu.example.ecp.client;
import org.opensaml.DefaultBootstrap;
import org.opensaml.saml2.ecp.RelayState;
import org.opensaml.saml2.ecp.Response;
import org.opensaml.ws.soap.soap11.Envelope;
import org.opensaml.ws.soap.soap11.Header;
import org.opensaml.ws.soap.soap11.impl.HeaderBuilder;
import org.opensaml.xml.ConfigurationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class EcpClient {
private static Logger logger= LoggerFactory.getLogger(EcpClient.class);
private static String SECURE_URL="https://shib-sp.example.edu/sample/";
private static String IDP_URL="https://shib-idp.example.edu/idp/profile/SAML2/SOAP/ECP";
private static String USER_NAME="<User Name>";
private static String PASSWORD="<Password>";
public static void main(String[] args)
{
/*
System.setProperty("org.apache.commons.logging.Log","org.apache.commons.logging.impl.SimpleLog");
System.setProperty("org.apache.commons.logging.simplelog.showdatetime","true");
System.setProperty("org.apache.commons.logging.simplelog.log.org.apache.http","DEBUG");
System.setProperty("org.apache.commons.logging.simplelog.log.org.apache.http.wire","DEBUG");
System.setProperty("org.apache.commons.logging.de.tudarmstadt.ukp.shibhttpclient","DEBUG");
*/
Ecp ecp= new Ecp();
ecp.setProtectedUrl(SECURE_URL);
ecp.setIdpUrl(IDP_URL);
ecp.setUserName(USER_NAME);
ecp.setPassword(PASSWORD);
try
{
DefaultBootstrap.bootstrap();
EcpClientFlow clientFlow= new EcpClientFlow(ecp);
Envelope secureResourceResponse = clientFlow.accessProtectedPage();
Envelope authRequest= clientFlow.buildIdpAuthRequest(secureResourceResponse);
Envelope idpAuthResponse = clientFlow.sendAuthenticationToIdp(clientFlow.convertSoapToString(authRequest));
String assertionConsumerUrl=((Response)idpAuthResponse.getHeader().getUnknownXMLObjects(Response.DEFAULT_ELEMENT_NAME).get(0)).getAssertionConsumerServiceURL();
RelayState relayState= (RelayState)secureResourceResponse.getHeader().getUnknownXMLObjects(RelayState.DEFAULT_ELEMENT_NAME).get(0);
relayState.detach();
Header header = new HeaderBuilder().buildObject();
header.getUnknownXMLObjects().clear();
header.getUnknownXMLObjects().add(relayState);
idpAuthResponse.setHeader(header);
clientFlow.sendAuthResponseToSp(clientFlow.convertSoapToString(idpAuthResponse), assertionConsumerUrl);
} catch (ConfigurationException e)
{
logger.error("Ecp Client Error", e);
}
}
}
package edu.example.ecp.client;
import java.io.IOException;
import org.apache.http.HttpException;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpRequest;
import org.apache.http.HttpRequestInterceptor;
import org.apache.http.protocol.HttpContext;
import org.opensaml.common.xml.SAMLConstants;
public class EcpRequestInterceptor implements HttpRequestInterceptor,EcpFlowConstants {
@Override
public void process(HttpRequest request, HttpContext context)
throws HttpException, IOException
{
request.addHeader(HttpHeaders.ACCEPT, PAOS_CONTENT_TYPE);
request.addHeader(PAOS_HEADER,"ver="+SAMLConstants.PAOS_NS+";"+SAMLConstants.SAML20ECP_NS);
}
}
Testing the Code:
Create the Source and Resource Directories
mkdir ecp-client/src/main/java -p
mkdir ecp-client/src/main/resources -p
Copy the Java Source files into ecp-client/src/main/java directory.
Creating Build File
Create a build.gradle File and add the following content in the File and also copy the file to ecp-client directory:
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'application'
repositories{
mavenCentral()
}
dependencies{
compile 'org.opensaml:opensaml:2.6.4','org.apache.httpcomponents:httpclient:4.4',
'org.slf4j:log4j-over-slf4j:1.7.10','org.slf4j:slf4j-simple:1.7.10',
'org.slf4j:slf4j-api:1.7.10'
}
mainClassName='edu.example.ecp.client.EcpClient'
Setting JAVA_HOME, GRADLE_HOME and PATH:
export JAVA_HOME=JavaHOME
export GRADLE_HOME=gradle_home
export PATH=$GRADLE_HOME/bin:$PATH
Setting the Classpath
Run the following command to creating the eclipse project and also setting the classpathgradle cleanEclipse eclipse
Running the Code
execute the gradle run and it will execute the ecp client code and display the output in the command line.
Common Errors:
Problem 1. Service Provider not sending the SOAP response with ECP Header while accessing the secure access page using the http client.Cause: The following headers are not available in the request:
1. Accept: application/vnd.paos+xml
2. PAOS: ver='url:liberty:paos:2003-08';'urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp'
Solution: Add the above headers in the request and SP will send the SOAP request with ECP Request Headers and also Authentication Request in the Soap Body.
Problem 2. The Identity Provider throwing 500 error while sending the authentication request from ECP to IDP
Cause: The following headers are not available in the request:
1. Accept: application/vnd.paos+xml
2. PAOS: ver='url:liberty:paos:2003-08';'urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp'
Solution: Add the above headers in the request and DP will authenticate the user and send the SAML response in the soap body and also ecp response headers.
Problem 3. The Service Provider throwing 500 error while sending the authentication response from ECP to SP
Cause: The following headers are not available in the request:
1. Accept: application/vnd.paos+xml
2. PAOS: ver='url:liberty:paos:2003-08';'urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp'
3. Content-Type: application/vnd.paos+xml
Solution: Add the above headers in the request and SP will accept the SAML response from the IDP issued assertion and It will redirect to requested target page.
Reference:
SAML 2.0 Profiles