[Article] Exposing WSO2 Identity Server Admin Services the REST Way

  • By Gayan Gunawardana
  • 24 Oct, 2016

This article will show you how to transform existing SOAP-based admin services into REST services in WSO2 Identity Server.

Table of contents


Introduction

All WSO2 products expose their management functionality as admin services, which are SOAP services. WSO2 Identity Server contains many admin services some of which are frequently used to create end user portals and dashboards. UserInformationRecoveryService, UserIdentityManagementAdminService, UserAccountAssociationService, UserProfileMgtService, UserRegistrationAdminService are some of these admin services. End application developers hesitate when consuming SOAP services and prefer to use REST services. This article will discuss how to expose WSO2 Identity Server admin services as REST services to make the lives of developers easier.


Background and motivation

A couple of decades ago there were a lot of heterogeneous solutions for specific business requirements such as accounting, finance, sales, human resources, content management and more. These systems were usually not integrated, and if they were it was a costly and complicated task.

Suppose you wanted to integrate your enterprise’s HR system with its finance system. Both software systems were providing by different vendors. You will need to understand the APIs exposed by both system and write custom code to communicate between both parties. However, because of heterogeneous protocols, if one system needs a small change, a lot of work has to done to make it. The main solution to this integration problem was to create a service layer over each application by using a standard communication mechanism. Simple Object Access Protocol (SOAP) was the first protocol with a standardized web service interface. SOAP sends and receives XML messages in a specific format. These messages can be sent across different transports such as HTTP, FT and SMTP, with HTTP being the most common transport for SOAP. SOAP played a major role in system integration because of its well-defined message structure.

Later, people understood that the because of the complexity of SOAP messages it consumes a lot of resources such as memory and processing power when converting XML to a Java object with Java Architecture for XML Binding (JAXB). This doesn’t make a big difference when processing a small number of messages, but when processing relatively large number of requests, the JVM needs to have a sufficient amount of memory.

This is when developers switched to REST, which isn’t a protocol but an architectural style. In REST architecture, everything s a resource and you can use standard HTTP verbs (GET, PUT, POST, DELETE) to perform operations on the resources. Due to its simplicity it became very popular in enterprises. But you still may need SOAP for certain specific use cases such as transactional operations in banking applications. This section was written not to compare REST and SOAP but to provide background information as to why WSO2 Identity Server uses SOAP services.


Admin services exposed by WSO2 Identity Server

The WSO2 Carbon platform was made with the concept of separating the backend and frontend. The back end contains a SOAP web services interface. Follow the steps given here1 to get a list of available admin services as shown in figure 1 below.

Figure 1

You can also browse admin service WSDLs if necessary2.


Expose a SOAP-based admin service as a REST API

WSO2 Carbon 4.0 introduced a significant architectural change to the platform by moving Tomcat into the OSGi environment. As a result, third party web applications can now access any carbon libs that are in the plugins directory. Native CXF support was also added to WSO2 Carbon 4.0.

By taking advantage of the above mentioned changes, you can now expose OSGi-level functionality as REST APIs via JAX-RS web applications.

An OSGi service is a java object instance, registered into an OSGi framework with a set of properties. Any java object can be registered as a service, but typically it implements a well-known interface3.

Figure 2 below will explain the high-level architecture and message flow of the REST API interface. Basically, requests with the context path “account-recovery” go to the account-recovery JAX-RS service.

Figure 2

In order to expose a SOAP-based admin service as a REST API there are two main tasks that need to be done.

  1. Expose an OSGi service by consuming the admin service
  2. Develop a JAX-RS-based web application to consume the OSGi service

In WSO2 Identity Server some admin service capabilities are already exposed as OSGi services and some are not.

For example, OAuth2Service admin service was already exposed as an OSGI service with OAuth2ServiceComponent. In that case, you can consume the existing OSGi service inside the JAX-RS web application without developing a new one. But OAuthAdminService was not exposed as an OSGi service so you have to develop a new OSGI service by wrapping up OAuthAdminService. If an admin service is already exposed as an OSGI service, don’t do the first step.


Password recovery with notification

We will use the “UserInformationRecoveryService” to explain the use case. UserInformationRecoveryService caters to functionality such as password recovery with email notification, challenge question and user self registration. We will now go through an end-to-end use case of password recovery with email notification.

  1. getCaptcha() -­ Generates a captcha.
  2. verifyUser() -­ Validates the captcha answer and username and returns a new key.
  3. sendRecoveryNotification() -­ Sends an email notification with a confirmation code to the user. You need to provide the key from previous call.
  4. getCaptcha() ­- Generates a captcha when the user clicks on the URL.
  5. verifyConfirmationCode() -­ Validates the captcha answer and confirmation code. This returns a key.
  6. updatePassword -­ Updates the password in the system. Need to provide the key from previous call and new password. It returns the status of the update (true or false).

Expose “UserInformationRecoveryService” functionalities as an OSGi service

The complete source can be found here8.

public class UserManager {

private static UserManager userManager = new UserManager();
private UserInformationRecoveryService userInformationRecoveryService = new UserInformationRecoveryService();

  	 public static UserManager getInstance() {
       		return userManager;
   	 }

   	public CaptchaInfoBean getCaptcha() throws IdentityMgtServiceException {
       		return userInformationRecoveryService.getCaptcha();
   	}

public VerificationBean verifyUser(String username, CaptchaInfoBean captchaInfoBean, String tenantDomain) throws IdentityMgtServiceException, UserStoreException {
      		 try {
           			Utils.startTenantFlow(tenantDomain);
           		return userInformationRecoveryService.verifyUser(username, captchaInfoBean);
      		 } finally {
           			Utils.endTenantFlow();
       		}
   	}
}

/**
* @scr.component name="password.recovery.rest.api.core.component" immediate="true"
* @scr.reference name="user.realmservice.default"
* interface="org.wso2.carbon.user.core.service.RealmService"
* cardinality="1..1" policy="dynamic" bind="setRealmService"
* unbind="unsetRealmService"
*/

public class PasswordRecoveryServiceComponent {

private static final Log LOGGER
           = LogFactory.getLog(PasswordRecoveryServiceComponent.class);
   	private static RealmService realmService;

   	public static RealmService getRealmService() {
       		return realmService;
   	}

   	protected void setRealmService(RealmService realmService) {
       		this.realmService = realmService;
  	 }

   	protected void activate(ComponentContext context) {
       		BundleContext bundleContext = context.getBundleContext();
       		bundleContext.registerService(UserManager.class.getName(), new UserManager(),     null);
       		LOGGER.info("Password Recovery ServiceComponent bundle is activated");
  	 }

   	protected void deactivate(ComponentContext context) {
       		LOGGER.info("Password Recovery ServiceComponent bundle is deactivated");
  	 }

   	protected void unsetRealmService(RealmService realmService) {
       		setRealmService(null);
  	 }
}

Develop a JAX-RS-based web application to consume the OSGi service

Any carbon libs is now be accessible inside CXF web application. This means that the web application has access to PrivilegedCarbonContext from which we can get an instance of the OSGi service. You can find more information about PrivilageCarbonContext here7.

In this example Utils.getUserManager() provides an instance of the UserManager OSGi service.

public static UserManager getUserManager() {
   return (UserManager) PrivilegedCarbonContext.getThreadLocalCarbonContext()
           .getOSGiService(UserManager.class);
}

@Path("/recover")
@Consumes({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
@Produces(MediaType.APPLICATION_JSON)
public class UserRecoveryResource extends AbstractResource {
   	private static final Log LOG = LogFactory.getLog(UserRecoveryResource.class);
   	UserManager userManager;

   	public UserRecoveryResource() throws UserCoreRestException {
       		userManager = Utils.getUserManager();
   	}

   	@GET
   	@Path("captcha")
   	public Response getCaptcha(@HeaderParam(Constants.AUTHORIZATION_HEADER) String authorization)  throws IdentityMgtServiceException {

       	CaptchaInfoBean captchaInfoBean;
       	try {
           		captchaInfoBean = userManager.getCaptcha();
       	} catch (Exception e) {
          		return handleResponse(ResponseStatus.FAILED, "Error while generating captcha");
      	}
       return Response.ok(captchaInfoBean).build();
   }
}

Try it from REST client.

  1. getCaptcha()

    URL: https://localhost:9443/password-recovery/rest/recover/captcha
    HTTP Verb: GET
    Response:

    {"imagePath":"registry/resource/_system/config/repository/components/org.wso2.carbon.captcha-images/1e9216df-16e1-4477-930a-d2f3fceadd7b.jpg","secretKey":"1e9216df-16e1-4477-930a-d2f3fceadd7b","userAnswer":null}
    


  2. verifyUser()

    URL: https://localhost:9443/password-recovery/rest/recover/verify-user
    HTTP Verb: PUT
    Request:

    {
    	"username": "gayan",
    	"userDomain": "PRIMARY",
    	"tenantId": -1234,
    	"captcha": {
        	"imagePath": "registry/resource/_system/config/repository/components/org.wso2.carbon.captcha-images/1e9216df-16e1-4477-930a-d2f3fceadd7b.jpg",
        	"secretKey": "1e9216df-16e1-4477-930a-d2f3fceadd7b",
        	"userAnswer": "ffmmn"
    	}
    }
    
    Response:
    {"userId":"gayan","key":"871d5129-6ace-4969-bcd7-b32e2825435a","verified":true}
    


  3. sendRecoveryNotification()

    URL: https://localhost:9443/password-recovery/rest/recover/send-notification
    HTTP Verb: PUT
    Request:

    {
    	"username": "gayan",
    	"userDomain": "PRIMARY",
    	"tenantId": -1234,
    	"key": "871d5129-6ace-4969-bcd7-b32e2825435a",
    	"notificationType": "email"
    }
    
    Response:
    {"verified":true}
    

    From the email message find below content and retrieve secret code (53ed8605-c076-419c-a80c-ef3e2b941c76) which is necessary to invoke the next API.

    53ed8605-c076-419c-a80c-ef3e2b941c76&userstoredomain=PRIMARY&username=gayan&tenantdomain=carbon.super
    

  4. getCaptcha()

    URL: https://localhost:9443/password-recovery/rest/recover/captcha
    HTTP Verb: GET
    Response:

    {"imagePath":"registry/resource/_system/config/repository/components/org.wso2.carbon.captcha-images/1e9216df-16e1-4477-930a-d2f3fceadd7b.jpg","secretKey":"1e9216df-16e1-4477-930a-d2f3fceadd7b","userAnswer":null}
    


  5. verifyConfirmationCode()

    URL: https://localhost:9443/password-recovery/rest/recover/verify-code
    HTTP Verb: PUT
    Request:

    {
    "username": "gayan",
    	"tenantId": -1234,
    	"userDomain": "PRIMARY",
    	"code": "53ed8605-c076-419c-a80c-ef3e2b941c76",
    	"captcha": {
        	"imagePath": "registry/resource/_system/config/repository/components/org.wso2.carbon.captcha-images/3d81a774-80cb-473c-bf3b-a8a9f825deb9.jpg",
        	"secretKey": "3d81a774-80cb-473c-bf3b-a8a9f825deb9",
        	"userAnswer": "3nyZd3"
    	}
    }
    
    Response:
    {"userId":"gayan","key":"9c1c67e6-7204-41d1-b2cc-9f1961626e8d","verified":true}
    


  6. updatePassword

    URL: https://localhost:9443/password-recovery/rest/recover/update-password
    HTTP Verb: PUT
    Request:

    {
    	"username": "gayan",
    	"userDomain": "PRIMARY",
    	"tenantId": -1234,
    	"newPassword": "abcd1234",
    	"confirmationCode": "9c1c67e6-7204-41d1-b2cc-9f1961626e8d"
    }
    
    Response:
    {"verified":true}
    

You can find the full source code here8.


How to secure your REST API

There are two different ways to secure a server side application:

  1. Cookie-based authentication
  2. Token-based authentication

Cookie-based authentication is the oldest way of securing stateful client server web applications. When it comes to REST APIs, cookie-based authentication doesn’t work well because of the stateless behavior of REST APIs. Cross-domain / Cross-origin resource sharing (CORS) support, support for stateless behavior, mobile friendliness and the ability to avoid Cross-Site Request Forgery (CSRF) are some advantages of token-based authentication over cookie-based authentication for REST APIs. Below you can find different types of token-based authentication methods.


Basic authentication

One of the primary methods for REST API authentication is Basic Authentication (BA). BA is just an HTTP header with the username and password encoded in bas644.

  • Authorization: Basic base64Encode(admin:admin)
  • Authorization: Basic YWRtaW46YWRtaW4NCg==

If you authenticate the REST API with basic authentication over a non-Secure Sockets Layer (SSL) connection, the man in the middle can decode the username and password from the authorization header. You must use HTTPS (SSL/TSL) instead of an HTTP connection.

Basic authentication is simple to implement but has a few disadvantages including:

  • SSL is relatively slower to run than basic HTTP so this might cause clients to run slowly.
  • The server cannot force clients to use SSL so developers who are not using SSL cause a security risk.

In summary, if you have trusted clients, or if you can make sure they use SSL, BA is a good choice.


OAuth 2.0

Nowadays OAuth is the de facto mechanism to secure your REST APIs. OAuth enables a client application to access certain resources (APIs) without giving the end user’s credentials to them. In other words, it leverages access delegation. You can find more information from here5 and here6.


Certificate-based authentication

Certificate-based authentication provides several advantages over traditional password-based authentication. The main difference is password-based authentication relies on secrets defined and managed by the end user but certificate-based authentication utilizes secrets issued and managed by the certificate issuer or authority.

The following example will use Basic Authentication to secure REST APIs. But it has the flexibility to add any different authentication handler such as OAuth or certificate-based authentication handler.


Secure with WSO2 API Manager

One of the most common integrations of WSO2 Identity Server is as a key manager for WSO2 API Manager. WSO2 Identity Server handles security aspects of the API manager deployment such as access tokens issuing, user authentication, etc. In such an environment you can use WSO2 API manager to secure your REST API. It is not mandatory to have a key manager deployment, even stand alone API manager instance you can use to secure your REST API.


Conclusion

The next generation Carbon 5 provides a lightweight microservices framework with JAX-RS annotations. Soon, all back-end services will be REST services with a JWT-based security mechanism, eliminating the need to consume SOAP services. Until all WSO2 products are migrated to Carbon 5 (by the end of 2017 Q1), you can use this technique to expose SOAP admin services as REST services.


References

About Author

  • Gayan Gunawardana
  • WSO2