Sunday, July 24, 2016

WSO2 IDP for Spring SAML Service Provider

Today we are going to see how to add WSO2 Identity Server as an IDP to a Spring application. We are going to do this using the Spring SAML sample provided here. You can download one of the releases of Spring SAML sample from here. I have tested this with 1.0.2.RELEASE.

Sample Application Configurations

Create SAML metadata file like below (wso2.xml). You have to change the entityID, samlsso url and X509Certificate according to your installation of the Identity Server. Below values is the default. You can use this blog post to find the X509Certificate of your Identity Server deployment.

<?xml version="1.0" encoding="UTF-8"?>
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" entityID="localhost" validUntil="2026-05-16T21:51:14.927Z">
   <md:IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
      <md:KeyDescriptor use="signing">
         <ds:KeyInfo>
            <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>
      </md:KeyDescriptor>
      <md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://localhost:9443/samlsso"/>
      <md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://localhost:9443/samlsso"/>
      <md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</md:NameIDFormat>
      <md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://localhost:9443/samlsso"/>
      <md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://localhost:9443/samlsso"/>
   </md:IDPSSODescriptor>
</md:EntityDescriptor>

Copy the created wso2.xml metadata file to <SAMPLE_HOME>/sample/src/main/resources/metadata directory. Now you need to refer this metadata file from the application. To do this you need to open <SAMPLE_HOME>/sample/src/main/webapp/WEB-INF/securityContext.xml file and find the bean with id "metadata". In the list add below to include Identity Server as an identity provider.


<bean class="org.springframework.security.saml.metadata.ExtendedMetadataDelegate">
    <constructor-arg>
        <bean class="org.opensaml.saml2.metadata.provider.ResourceBackedMetadataProvider">
            <constructor-arg>
                <bean class="java.util.Timer"/>
            </constructor-arg>
            <constructor-arg>
                <bean class="org.opensaml.util.resource.ClasspathResource">
                    <constructor-arg value="/metadata/wso2.xml"/>
                </bean>
            </constructor-arg>
            <property name="parserPool" ref="parserPool"/>
        </bean>
    </constructor-arg>
    <constructor-arg>
        <bean class="org.springframework.security.saml.metadata.ExtendedMetadata">
        </bean>
    </constructor-arg>
</bean>

Identity Server Configurations

Create a service provider. Go to Inbound Authentication Configuration and SAML2 Web SSO Configuration. Add the below configurations.
  • Issuer: http://localhost:8080/spring-security-saml2-sample/saml/metadata
  • Assertion Consumer URLs: http://localhost:8080/spring-security-saml2-sample/saml/SSO
  • Enable Response Signing.
  • Enable Single Logout.
  • SLO Response URL: http://localhost:8080/spring-security-saml2-sample/saml/SingleLogout
  • Enable Attribute Profile.
  • Include Attributes in the Response Always.
It will look like below.


Working with the sample.

Go to <SAMPLE_HOME>/sample from the terminal and run below command to build the project.
                 mvn package
Now run the below command to start the tomcat server and start the application.
                 mvn tomcat7:run

Go to the below url.
                 http://localhost:8080/spring-security-saml2-sample
You will be redirected to below page which is the index page of the sample application.


You can see the localhost as an IDP here. Its because the entityID of the metadata file we created is localhost. From the list select localhost and click "Start single sign-on" button. Now you will be redirected to the login page of the Identity Server.


Insert the username and password in the Identity Server and click Sign In. You will be redirected back to the sample application. Now you are logged in to the application and you can see the details of the authenticated user.


Wednesday, May 4, 2016

Adding custom admin service to run LDAP search filter from WSO2 Identity Server

From this post I'm going to explain how to create a custom admin service WSO2 Identity Server. For this I am using WSO2 Identity Server 5.0.0 with Service Pack 1 installed and going to create a admin service to run a LDAP search filter and get the results from the ReadWriteLDAPUserStoreManager.

First you need to created a maven project. Use the bellow pom file.

<?xml version="1.0" encoding="utf-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">

    <modelVersion>4.0.0</modelVersion>
    <groupId>org.wso2.carbon</groupId>
    <artifactId>custom.admin.service</artifactId>
    <version>4.2.3</version>
    <packaging>bundle</packaging>
    <name>WSO2 Carbon - OAuth</name>
    <description>A custom wso2 products or solution</description>
    <url>http://wso2.org</url>

    <dependencies>
        <dependency>
            <groupId>org.wso2.carbon</groupId>
            <artifactId>org.wso2.carbon.logging</artifactId>
            <version>4.2.0</version>
        </dependency>
        <dependency>
            <groupId>org.wso2.carbon</groupId>
            <artifactId>org.wso2.carbon.core</artifactId>
            <version>4.2.0</version>
        </dependency>
        <dependency>
            <groupId>org.wso2.carbon</groupId>
            <artifactId>org.wso2.carbon.user.core</artifactId>
            <version>4.2.0</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.felix</groupId>
                <artifactId>maven-scr-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.apache.felix</groupId>
                <artifactId>maven-bundle-plugin</artifactId>
                <extensions>true</extensions>
                <configuration>
                    <instructions>
                        <Bundle-SymbolicName>
                            ${project.artifactId}
                        </Bundle-SymbolicName>
                        <Private-Package>
                            custom.admin.service.internal
                        </Private-Package>
                        <Import-Package>
                            org.apache.commons.logging.*; version="1.0.4",
                            *;resolution:=optional
                        </Import-Package>
                        <Export-Package>
                            custom.admin.service,
                        </Export-Package>
                        <Embed-Dependency>
                        </Embed-Dependency>
                        <DynamicImport-Package>*</DynamicImport-Package>
                    </instructions>
                </configuration>
            </plugin>
        </plugins>
    </build>

    <repositories>
        <repository>
            <id>wso2-nexus</id>
            <name>WSO2 internal Repository</name>
            <url>http://maven.wso2.org/nexus/content/groups/wso2-public/</url>
            <releases>
                <enabled>true</enabled>
                <updatePolicy>daily</updatePolicy>
                <checksumPolicy>ignore</checksumPolicy>
            </releases>
        </repository>
    </repositories>
</project>

You need to write a custom userstore manager to be able to run you LDAP search filter. This can easily done by extending ReadWriteLDAPUserStoreManager. Our new userstore manager is CustomUserStoreManager. We need to add the bellow constructor to our custom userstore manager.


public CustomUserStoreManager(org.wso2.carbon.user.api.RealmConfiguration realmConfig,
                                  Map<String, Object> properties,
                                  ClaimManager claimManager,
                                  ProfileConfigurationManager profileManager,
                                  UserRealm realm, Integer tenantId)
            throws UserStoreException {

        super(realmConfig, properties, claimManager, profileManager, realm, tenantId);

}

Then we need to add a method to run the search filter on LDAP.

public String[] runSearchFilter(String filter) throws UserStoreException {

    List<String> values = new ArrayList<String>();
    String userPropertyName = realmConfig
            .getUserStoreProperty(LDAPConstants.USER_NAME_ATTRIBUTE);

    String searchFilter = filter;

    DirContext dirContext = this.connectionSource.getContext();
    NamingEnumeration<?> answer = null;
    NamingEnumeration<?> attrs = null;
    try {
        answer = this.searchForUser(searchFilter, new String[] { userPropertyName }, dirContext);
        while (answer.hasMoreElements()) {
            SearchResult sr = (SearchResult) answer.next();
            Attributes attributes = sr.getAttributes();
            if (attributes != null) {
                Attribute attribute = attributes.get(userPropertyName);
                if (attribute != null) {
                    StringBuffer attrBuffer = new StringBuffer();
                    for (attrs = attribute.getAll(); attrs.hasMore();) {
                        String attr = (String) attrs.next();
                        if (attr != null && attr.trim().length() > 0) {
                            attrBuffer.append(attr + ",");
                        }
                    }
                    String propertyValue = attrBuffer.toString();
                    // Length needs to be more than one for a valid
                    // attribute, since we
                    // attach ",".
                    if (propertyValue != null && propertyValue.trim().length() > 1) {
                        propertyValue = propertyValue.substring(0, propertyValue.length() - 1);
                        values.add(propertyValue);
                    }
                }
            }
        }

    } catch (NamingException e) {
        throw new UserStoreException(e.getMessage(), e);
    } finally {
        // close the naming enumeration and free up resources
        JNDIUtil.closeNamingEnumeration(attrs);
        JNDIUtil.closeNamingEnumeration(answer);
        // close directory context
        JNDIUtil.closeContext(dirContext);
    }
    return values.toArray(new String[values.size()]);
}

Now you need to register the newly created userstore manager as a service. For this you need to create private package and add service component class. This private package need to be added to pom file as done above. Bellow is a sample code snippet.


import custom.admin.service.CustomUserStoreManager;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.osgi.service.component.ComponentContext;
import org.wso2.carbon.user.api.UserStoreManager;

/**
 * @scr.component name="custom.user.store.manager.dscomponent" immediate=true
 */
public class CustomUserStoreMgtDSComponent {
    private static Log log = LogFactory.getLog(CustomUserStoreMgtDSComponent.class);

    protected void activate(ComponentContext ctxt) {

        CustomUserStoreManager customUserStoreManager = new CustomUserStoreManager();
        ctxt.getBundleContext().registerService(UserStoreManager.class.getName(), customUserStoreManager, null);
        log.info("CustomUserStoreManager bundle activated successfully..");
    }

    protected void deactivate(ComponentContext ctxt) {
        if (log.isDebugEnabled()) {
            log.debug("Custom User Store Manager is deactivated ");
        }
    }
}

Now we need to create the admin service class. It retrieves the primary userstore manager for the super tenant and cast it to our new custom userstore manager and invoke our new method.

import org.wso2.carbon.context.PrivilegedCarbonContext;
import org.wso2.carbon.core.AbstractAdmin;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.wso2.carbon.user.api.UserRealm;
import org.wso2.carbon.user.core.common.AbstractUserStoreManager;
import org.wso2.carbon.user.core.service.RealmService;

public class CustomAdminService extends AbstractAdmin {

    protected Log log = LogFactory.getLog(CustomAdminService.class);

    public String[] filterUsers(String filter) throws Exception {
        //get super tenant context and get realm service which is an osgi service
        RealmService realmService = (RealmService)
                PrivilegedCarbonContext.getThreadLocalCarbonContext().getOSGiService(RealmService.class);
        if (realmService == null) {
            String error = "Can not obtain carbon realm service..";
            throw new Exception(error);
        }

        int tenantId = -1234;
        UserRealm userRealm = realmService.getTenantUserRealm(tenantId);
        if (userRealm == null || !(userRealm instanceof org.wso2.carbon.user.core.UserRealm)) {
            String error = "Can not obtain user realm for tenant carbon.super.";
            throw new Exception(error);
        }
        AbstractUserStoreManager userStoreManager = (AbstractUserStoreManager) userRealm.getUserStoreManager();
        if (userStoreManager instanceof CustomUserStoreManager) {
            return ((CustomUserStoreManager)userStoreManager).runSearchFilter(filter);
        }else {
            throw new Exception("Operation not supported");
        }
    }
}

Now we need to create services.xml. It should be in <project home>/src/main/resources/META-INF/ directory. Bellow is a sample services.xml file. This file is used to map the service name to the service class, add permission to the service and etc.


<serviceGroup>
 <service name="CustomAdminService" scope="transportsession">
     <transports>
            <transport>https</transport>
        </transports>
  <schema schemaNamespace="http://org.apache.axis2/xsd" elementFormDefaultQualified="true" />
  <description>OAuth administration related functionality.</description>
  <parameter name="ServiceClass">custom.admin.service.CustomAdminService</parameter>
  <parameter name="AuthorizationAction" locked="false">/permission/admin/manage</parameter>
        <parameter name="adminService" locked="true">true</parameter>
    </service>
 <parameter name="hiddenService" locked="true">true</parameter>
</serviceGroup>

Now our custom userstore manager is ready. Build the project using maven and add it to the <CARBON_HOME>/repository/components/dropins/ directory.

Finally you need to change the userstore manager class name in user-mgt.xml file. So we need to change bellow line in user-mgt.xml

<UserStoreManager class="org.wso2.carbon.user.core.ldap.ReadWriteLDAPUserStoreManager">

to

<UserStoreManager class="custom.admin.service.CustomUserStoreManager">

Now our custom component is ready. To view the wsdl of the service we need to change HideAdminServiceWSDLs in carbon.xml to true. Finally start the Identity Server and go to the bellow url to view the wsdl of the service.

https://localhost:9443/services/CustomAdminService?wsdl

You can invoke the admin service using SOAP client. Bellow is a sample SOAP request for the service. We need admin privileges to invoke this service.

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsd="http://org.apache.axis2/xsd">
   <soapenv:Header/>
   <soapenv:Body>
      <xsd:filterUsers>
         <!--Optional:-->
         <xsd:filter>(uid=user102)</xsd:filter>
      </xsd:filterUsers>
   </soapenv:Body>
</soapenv:Envelope>

You can pass any LDAP search filter to this service and it will return all the users that matches the search filter.

Source code for the service and userstore manager is available at https://github.com/madurangasiriwardena/custom-admin-service