Sunday, September 29, 2013

Passive STS Java sample with WSO2 Identity Server

Following explain how to setup and test WSO2 Identity Server's Passive STS using a sample Relying Party (RP) application written in Java. For a .Net based app please refer [1] at WSO2 Library.

The sources of this application can be found at [2].
Application's .war file exist at [3]

WSO2 Identity Server 4.5.0 [4] and Tomcat 6 were used for testing. The application should work for other IS versions as well.

Configuring the Relying Party

In Sample web app's web.xml...

1. for 'idpUrl', give Identiy Server's Passive STS URL.
        i.e. https://localhost:9443/passivests

2.'replyUrl' should be the URL of the web app.
        i.e. http://localhost:8080/PassiveSTSSampleApp

3.'realm' should be an unique identifier for the web app.
        e.g. PassiveSTSSampleApp

4. Make 'displayFullResponse' "true" if you want to see the full RSTR (Request Security Token Response) returned from the IS.

Configuring Identity Server (IS)

1. In IS Management Console, go to 'Security Token Service'.


2. Go to 'Passive STS Configuration' in it.


3. Click 'Add new trusted service'.


4. For 'Service Realm Name' give the same identifier we gave as realm in Web app.

5. Select claims. These will be returned to the RP by the IS.


Execution Flow

1. From a web browser, try to access 'http://localhost:8080/PassiveSTSSampleApp'

2. This request will be intercepted by a Servlet Filter and browser will redirect to IS' Passive STS service.

3. Passive STS prompts the user for credentials.



4. After obtaining the credentials, it will construct the response with the selected claims.

5. Then, Passive STS redirects the browser to the RP.

6. Finally, web browser will display the response received by the IS.


Client Source Code

public class AuthFilter implements Filter {
    
    private String idpUrl;
    private String action; //wa
    private String replyUrl; //wreply
    private String realm; //wtrealm
    private String displayFullResponse;

    public void doFilter(ServletRequest req, ServletResponse res,
            FilterChain chain) throws IOException, ServletException {
        
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;
            
        if(request.getParameter("wresult") == null || request.getParameter("wresult").isEmpty()){
          String redirectUrl =  idpUrl + "?wa=" + action + "&wreply=" + replyUrl + "&wtrealm=" + realm;
          response.sendRedirect(redirectUrl);
          return;
        }
        
        String newLineRemovedStr = request.getParameter("wresult").replaceAll("(\\r|\\n)", "");
        handleResponse(request, newLineRemovedStr);
        
        if("true".equals(displayFullResponse)){
            String htmlSafeStr = escapeHtml(Utils.prettyFormat(newLineRemovedStr,2));
            request.getSession().setAttribute("RSTR", htmlSafeStr);
            request.getSession().setAttribute("displayFullResponse", "true");
        } else {
            request.getSession().setAttribute("displayFullResponse", "false");
        }
        
        chain.doFilter(request, response);
    }
    
    private void handleResponse(HttpServletRequest request, String response){
        OMElement element = null;
        String username = null;
        Map<String, String> claimMap = new HashMap<String, String>();
        
        try {
            element = AXIOMUtil.stringToOM(response);
        } catch (XMLStreamException e) {
            e.printStackTrace();
        }
        
        element = element.getFirstChildWithName(new QName("http://docs.oasis-open.org/ws-sx/ws-trust/200512", "RequestSecurityTokenResponse"));
        element = element.getFirstChildWithName(new QName("http://docs.oasis-open.org/ws-sx/ws-trust/200512", "RequestedSecurityToken"));
        element = element.getFirstChildWithName(new QName("urn:oasis:names:tc:SAML:1.0:assertion", "Assertion"));
        element = element.getFirstChildWithName(new QName("urn:oasis:names:tc:SAML:1.0:assertion", "AttributeStatement"));
        
        if(element != null){
            OMElement subjectElement = element.getFirstChildWithName(new QName("urn:oasis:names:tc:SAML:1.0:assertion", "Subject"));
            
            username = subjectElement.getFirstElement().getText();
            
            Iterator itr = element.getChildrenWithName(new QName("urn:oasis:names:tc:SAML:1.0:assertion", "Attribute"));
            
            while(itr.hasNext()){
                OMElement elem = (OMElement)itr.next();
                String claimURI = ((OMAttribute)elem.getAttribute(new QName("AttributeNamespace"))).getAttributeValue();
                claimMap.put(claimURI, elem.getFirstElement().getText()); 
            }
            
            request.getSession().setAttribute("message", "Response from the Passive STS for User: " + username);
            request.getSession().setAttribute("claimMap", claimMap);
        } else {
            request.getSession().setAttribute("message", "No claims received! Verify RP is registered at Passive STS");
            request.getSession().setAttribute("claimMap", null);
        }
    }

    public void init(FilterConfig fConfig) throws ServletException {
        //Initialize the configurations
        idpUrl = fConfig.getInitParameter("idpUrl");
        action = fConfig.getInitParameter("action");
        replyUrl = fConfig.getInitParameter("replyUrl");
        realm = fConfig.getInitParameter("realm");
        displayFullResponse = fConfig.getInitParameter("displayFullResponse");
    }
}

Ref:

[1] http://wso2.com/library/articles/2011/12/configuring-wso2-identity-server-passive-sts-aspnet-client/
[2] https://svn.wso2.org/repos/wso2/people/dulanja/samples/passive-sts/src/
[3] https://svn.wso2.org/repos/wso2/people/dulanja/samples/passive-sts/bin/
[4] http://wso2.com/products/identity-server/

Active STS vs. Passive STS scenarios

Active STS

The relying party (i.e. client application) has the ability to acquire username and password or some other mean like an x.509 certificate used for authentication.

In the case of username/password, usually it means having a login page.

Then those credentials will be sent over to STS via a web service call for authentication

Passive STS

The relying party does not acquire credentials; STS is responsible of it.

The request from the relying party to STS goes as an HTTP GET.

Security Token Servce concept


Simply - as the name also clearly implies - Security Token Service (STS) is a service which issues tokens regarding users (aka subject/principal).

This is an important part of identity federation, as such, it removes the burden of maintaining identity information across domains.

How does it removes that burden...exactly??

Let's first enter to a world without an STS...

When User-A1 from Domain-A tries to access Service-B of Domain-B, in order to grant access, Service-B should have a way to authenticate (and authorize) User-A1.

To do this, Domain-B should have User-A1 in the userstore. For each User-A1 --> User-An this has to be done.

Now here's the issue...

Maintaining User-Ablahblah details simply to grant access to ServiceB should not be DomainB's business. It's so much additional and unnecessary work. Also DomainB is totally unaware of the new users created in DomainA (unless some sort of identity provisioning exists. burden^2)

Here enters the STS!

Service-B: "If you want to access me, bring me a token from STS-A"

Service-B trusts STS-A of Domain-A, and that's it! Domain-B doesn't maintain any information regarding users of the other domain so it has no clue who the User-A1 is.

So, when that user wants to access Service-B, he should first authenticate with STS-A which resides in his own domain and get a token from it. Then he should present that token to Service-B.

Service-B might requires some attributes (aka claims) of User-A1 in this token for its authorization purpose. This might be the age, email address or any such detail. If the required details are present, ACCESS GRANTED!

voila! User management burden eliminated!

Ref:
[1] http://blog.facilelogin.com/2010/05/ws-trust-with-fresh-banana-service.html

SAML 2.0 message debugging

As methods of debugging SAML requests and responses what I've been using thus far are BurpSuite and usual Java debugging.

The problems with BurpSuite are: it must be set as a browser proxy, and also it cannot be used to decode SAML messages in Redirect binding. It didn't have the ability to deflate (uncompress).  

Obviously, Java debugging is not always a solution since applications can be non-Java based and source code is not always available. And adding to the pain it needs setting up an IDE too.

Recently I came across web based SAML 2.0 Debugger [1] from the SimpleSAMLphp guys, and it's the best solution for SAML message debugging!

It contains both a decoder and an encoder. For decoding we just have to capture the message using a tool like LiveHTTPHeaders for Firefox or Chrome's Developer Tools and give it to the decoder.




The only thing I'm missing in it is the ability to pretty-print the XML. But not a big deal! We can use an online tool like [2] or if Ubuntu based (like me) use Geany editor [3] with the XML pretty-printer add-on installed.

Ref:

[1] https://rnd.feide.no/simplesaml/module.php/saml2debug/debug.php
[2] http://www.softlion.com/webTools/XmlPrettyPrint/default.aspx
[3] http://www.geany.org/

Friday, September 13, 2013

SSO to WSO2 4.2.0 Carbon products via Shibboleth IdP

This post explains the steps needed to Single Sign On (SSO) to WSO2 Carbon based products via Shibboleth IdP.

Environment:
Carbon Product: WSO2 Identity Server 4.5.0 (Should work with any product based on Carbon 4.2.0 platform onwards)
Shibboleth Version: 2.4.0 (Latest at the time of this writing)

Please note that most of the steps are taken from [1] by Asela, which was written to enable SSO in Identity Server 3.2.3 with the help of several patches. Carbon 4.2.0 onwards this scenario can be executed without any such patches.

We will be referring to shibboleth installation directory as "IDP_HOME". And, file system paths and URLs should be changed according to your configurations.

Step 1.
Download Shibboleth IDP from "http://shibboleth.net/" and install it.

Step 2.
Enter following inside "ShibUserPassAuth{}" in IDP_HOME/conf/login.config to connect Shibboleth to the same LDAP IS is using.

    edu.vt.middleware.ldap.jaas.LdapLoginModule required
    ldapUrl="ldap://localhost:10389"
    bindDn="uid=admin,ou=system"
    bindCredential="admin"
    baseDn="ou=Users,dc=wso2,dc=org"
    ssl="false"
    userFilter="uid={0}"
    ;

Step 3.
Add following to 
IDP_HOME/conf/handler.xml
<ph:LoginHandler xsi:type="ph:UsernamePassword"
              jaasConfigurationLocation="file:///home/dulanja/work/apps/shibboleth-2.4.0/conf/login.config">
        <ph:AuthenticationMethod>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</ph:AuthenticationMethod>
    </ph:LoginHandler>
Step 4.
Create new file IDP_HOME/metadata/carbon.xml. And add following to it.
<EntityDescriptor entityID="carbonServer" xmlns="urn:oasis:names:tc:SAML:2.0:metadata">
  <SPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
    <NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</NameIDFormat>
    <AssertionConsumerService index="1" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://localhost:9443/acs" />
    <KeyDescriptor>
      <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
        <ds:X509Data>
          <ds:X509Certificate>
            MIICNTCCAZ6gAwIBAgIES343gjANBgkqhkiG9w0BAQUFADBVMQswCQYDVQQGEwJV
            UzELMAkGA1UECAwCQ0ExFjAUBgNVBAcMDU1vdW50YWluIFZpZXcxDTALBgNVBAoM
            BFdTTzIxEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0xMDAyMTkwNzAyMjZaFw0zNTAy
            MTMwNzAyMjZaMFUxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEWMBQGA1UEBwwN
            TW91bnRhaW4gVmlldzENMAsGA1UECgwEV1NPMjESMBAGA1UEAwwJbG9jYWxob3N0
            MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCUp/oV1vWc8/TkQSiAvTousMzO
            M4asB2iltr2QKozni5aVFu818MpOLZIr8LMnTzWllJvvaA5RAAdpbECb+48FjbBe
            0hseUdN5HpwvnH/DW8ZccGvk53I6Orq7hLCv1ZHtuOCokghz/ATrhyPq+QktMfXn
            RS4HrKGJTzxaCcU7OQIDAQABoxIwEDAOBgNVHQ8BAf8EBAMCBPAwDQYJKoZIhvcN
            AQEFBQADgYEAW5wPR7cr1LAdq+IrR44iQlRG5ITCZXY9hI0PygLP2rHANh+PYfTm
            xbuOnykNGyhM6FjFLbW2uZHQTY1jMrPprjOrmyK5sjJRO4d1DeGHT/YnIjs9JogR
            Kv4XHECwLtIVdAbIdWHEtVZJyMSktcyysFcvuhPQK8Qc/E/Wq8uHSCo=
          </ds:X509Certificate>
        </ds:X509Data>
      </ds:KeyInfo>
    </KeyDescriptor>
  </SPSSODescriptor>
</EntityDescriptor>
X509Certificate above is the default certificate of carbon products.

Step 5.
Add following to IDP_HOME/conf/relying-party.xml under <rp:relyingpartygroup>
<rp:RelyingParty id="carbonServer"
                     provider="https://idp.example.org/idp/shibboleth"
                     defaultSigningCredentialRef="IdPCredential" 
                     defaultAuthenticationMethod="urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport">
        <rp:ProfileConfiguration xsi:type="saml:SAML2SSOProfile" 
                                 signResponses="always" 
                                 signAssertions="never" encryptAssertions="never" 
                                 encryptNameIds="never"/>
    </rp:RelyingParty>
Step 6.
Add following in the same file under <metadata:MetadataProvider> after <metadata:MetadataProvider id="IdPMD">
<metadata:MetadataProvider id="carbonMD" xsi:type="metadata:ResourceBackedMetadataProvider">
            <metadata:MetadataResource xsi:type="resource:FilesystemResource" file="/home/dulanja/work/apps/shibboleth-2.4.0/metadata/carbon.xml"/>
        </metadata:MetadataProvider>
Step 7.
In IDP_HOME/conf/attribute-resolver.xml, comment out <resolver:AttributeDefinition id="transientId">
Add:
<resolver:AttributeDefinition id="principal" xsi:type="PrincipalName" 
            xmlns="urn:mace:shibboleth:2.0:resolver:ad">
        <resolver:AttributeEncoder xsi:type="enc:SAML2StringNameID" 
            nameFormat="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"/> 
    </resolver:AttributeDefinition>
Step 8.
In IDP_HOME/conf/attribute-filter.xml Comment out <afp:AttributeFilterPolicy id="releaseTransientIdToAnyone">
Add:
<afp:AttributeFilterPolicy id="releaseBasicAttributesToAnyone">
        <afp:PolicyRequirementRule xsi:type="basic:ANY"/>
        <afp:AttributeRule attributeID="principal">
                <afp:PermitValueRule xsi:type="basic:ANY" />
        </afp:AttributeRule>
    </afp:AttributeFilterPolicy>
Step 9.
Copy IDP_HOME/war/idp.war to Tomcat 6

Step 10.
Copy IDP_HOME/lib/endorsed directory in to tomcat root directory.

Step 11.
Enable HTTPS in tomcat.

E.g.
<Connector port="8443" protocol="HTTP/1.1" SSLEnabled="true"
               maxThreads="150" scheme="https" secure="true"
               clientAuth="false" sslProtocol="TLS" 
           keystoreFile="/home/dulanja/work/apps/shibboleth-2.4.0/credentials/idp.jks"
           keystorePass="shibboleth" />
Step 12.
Start server and check status of the server by using https://localhost:8443/idp/status

Step 13.
Import IDP_HOME/credentials/idp.crt to wso2is-4.5.0/repository/resources/security/wso2carbon.jks

Step 14.
In wso2is-4.5.0/repository/conf/security/authenticators.xml, under <authenticator name="SAML2SSOAuthenticator">
1. Make disabled="false"
2. Change "IdentityProviderSSOServiceURL" to https://localhost:8443/idp/profile/SAML2/POST/SSO
3. Add new parameter <Parameter name="IdPCertAlias">idp.example.org</Parameter>

Step 15.
Start IS and try to access Management Console. It should redirect to Shibboleth login page. Enter admin/admin.

You should be able to successfully login into Identity Server Management Console.

Now what about Single Logout (SLO)? 

It should be noted that Shibboleth IdP does not fully support Single Logout (SLO) profile [2]. However, Shibboleth 2.4.0 provides a limited feature [3], where it terminates only the SP's session (who sends the Logout request). But it will not send single logout requests to other SPs who have participated in the SSO session.  

i.e. Shibboleth never generates LogoutRequests to SPs.

It provides two mechanisms in this feature, called SAML and Local.

In SAML, SP is expected to send a message of type SAML LogoutRequest to a location like '/idp/profile/SAML2/SLO/Redirect' (this changes according to the binding used). Then Shibboleth will process that message according to the SAML rules and terminate the session.

In Local, SP sends any request to /idp/profile/Logout. No SAML processing will happen. Shibboleth will just terminate the session pointed to by the cookie.

You can learn the detailed behaviour of those two mechanisms by reading [3].

So, how does WSO2 IS 4.5.0 behave in this situation?

SAML mechanism does not work for WSO2 IS 4.5.0, since it always sends the LogoutRequest to the SSO url (i.e. idp/profile/SAML2/POST/SSO).

However, IS can work with Local mechanism. If following is inserted into  wso2is-4.5.0/repository/conf/security/authenticators.xml under "SAML2SSOAuthenticator", IS will terminate its own session and send a request to Shibboleth's Logout path. IdP will then terminate its session with the IS.
<Parameter name="ExternalLogoutPage">https://localhost:8443/idp/profile/Logout</Parameter>

Ref:
[1] http://www.soasecurity.org/2012/05/login-to-wso2carbon-servers-via.html
[2] https://wiki.shibboleth.net/confluence/display/SHIB2/SLOIssues
[3] https://wiki.shibboleth.net/confluence/display/SHIB2/IdPEnableSLO