[WSO2 ESB] Securing a REST End-point using OAuth 2.0

When you deploy an API into WSO2 ESB, there might be cases where you want to secure it. If you use WSO2 API Manager to  front the API s deployed at ESB, you can go ahead with the security provided by API Manager.

If you do not use WSO2 API Manager to front the APIs, at ESB level we can secure them.

You can achieve this task with the following steps.

1. Create the custom handler to validate the Bearer token.
2. Create API element in the ESB and pointing the rest endpoint that you have
3. Include created handler to the created API element.
4. Go to IS and create the OAuth2.0 application and get the Access token form IS
5. Invoke the API with the valid access token.

Following diagram depicts the high level message flow.


Let's look at how this can be done step by step.

Creating a Custom Handler 

You can download the project from here

You need to extends AbstractHandler and implements ManagedLifecycle as follows. Here I’m reading some parameters from the axis2.xml as well.


package org.wso2.handler;

import org.apache.axis2.AxisFault;
import org.apache.axis2.client.Options;
import org.apache.axis2.client.ServiceClient;
import org.apache.axis2.context.ConfigurationContext;
import org.apache.axis2.context.ConfigurationContextFactory;
import org.apache.axis2.transport.http.HTTPConstants;
import org.apache.axis2.transport.http.HttpTransportProperties;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.HttpHeaders;
import org.apache.synapse.core.axis2.Axis2MessageContext;
import org.wso2.carbon.identity.oauth2.stub.OAuth2TokenValidationServiceStub;
import org.wso2.carbon.identity.oauth2.stub.dto.OAuth2TokenValidationRequestDTO;
import org.apache.synapse.ManagedLifecycle;
import org.apache.synapse.MessageContext;
import org.apache.synapse.core.SynapseEnvironment;
import org.apache.synapse.rest.AbstractHandler;
import org.wso2.carbon.identity.oauth2.stub.dto.OAuth2TokenValidationRequestDTO_OAuth2AccessToken;

import java.util.Map;

public class SimpleOAuthHandler extends AbstractHandler implements ManagedLifecycle {

    private static final String CONSUMER_KEY_HEADER = "Bearer";
    private static final String OAUTH_HEADER_SPLITTER = ",";
    private static final String CONSUMER_KEY_SEGMENT_DELIMITER = " ";
    private static final String OAUTH_TOKEN_VALIDATOR_SERVICE = "oauth2TokenValidationService";
    private static final String IDP_LOGIN_USERNAME = "identityServerUserName";
    private static final String IDP_LOGIN_PASSWORD = "identityServerPw";
    private ConfigurationContext configContext;
    private static final Log log = LogFactory.getLog(SimpleOAuthHandler.class);

    @Override
    public boolean handleRequest(MessageContext msgCtx) {
        if (this.getConfigContext() == null) {
            log.error("Configuration Context is null");
            return false;
        }
        try{
            //Read parameters from axis2.xml
            String identityServerUrl =
                    msgCtx.getConfiguration().getAxisConfiguration().getParameter(
                            OAUTH_TOKEN_VALIDATOR_SERVICE).getValue().toString();
            String username =
                    msgCtx.getConfiguration().getAxisConfiguration().getParameter(
                            IDP_LOGIN_USERNAME).getValue().toString();
            String password =
                    msgCtx.getConfiguration().getAxisConfiguration().getParameter(
                            IDP_LOGIN_PASSWORD).getValue().toString();
            OAuth2TokenValidationServiceStub stub =
                    new OAuth2TokenValidationServiceStub(this.getConfigContext(), identityServerUrl);
            ServiceClient client = stub._getServiceClient();
            Options options = client.getOptions();
            HttpTransportProperties.Authenticator authenticator = new HttpTransportProperties.Authenticator();
            authenticator.setUsername(username);
            authenticator.setPassword(password);
            authenticator.setPreemptiveAuthentication(true);
            options.setProperty(HTTPConstants.AUTHENTICATE, authenticator);
            client.setOptions(options);
            OAuth2TokenValidationRequestDTO dto = this.createOAuthValidatorDTO(msgCtx);
            return stub.validate(dto).getValid();
        }catch(Exception e){
            log.error("Error occurred while processing the message", e);
            return false;
        }
    }
    private OAuth2TokenValidationRequestDTO createOAuthValidatorDTO(MessageContext msgCtx) {
        OAuth2TokenValidationRequestDTO dto = new OAuth2TokenValidationRequestDTO();
        Map headers = (Map) ((Axis2MessageContext) msgCtx).getAxis2MessageContext().
                getProperty(org.apache.axis2.context.MessageContext.TRANSPORT_HEADERS);
        String apiKey = null;
        if (headers != null) {
            apiKey = extractCustomerKeyFromAuthHeader(headers);
        }
        OAuth2TokenValidationRequestDTO_OAuth2AccessToken token =
                new OAuth2TokenValidationRequestDTO_OAuth2AccessToken();
        token.setTokenType("bearer");
        token.setIdentifier(apiKey);
        dto.setAccessToken(token);
        return dto;
    }
    private String extractCustomerKeyFromAuthHeader(Map headersMap) {
        //From 1.0.7 version of this component onwards remove the OAuth authorization header from
        // the message is configurable. So we dont need to remove headers at this point.
        String authHeader = (String) headersMap.get(HttpHeaders.AUTHORIZATION);
        if (authHeader == null) {
            return null;
        }
        if (authHeader.startsWith("OAuth ") || authHeader.startsWith("oauth ")) {
            authHeader = authHeader.substring(authHeader.indexOf("o"));
        }
        String[] headers = authHeader.split(OAUTH_HEADER_SPLITTER);
        if (headers != null) {
            for (String header : headers) {
                String[] elements = header.split(CONSUMER_KEY_SEGMENT_DELIMITER);
                if (elements != null && elements.length > 1) {
                    boolean isConsumerKeyHeaderAvailable = false;
                    for (String element : elements) {
                        if (!"".equals(element.trim())) {
                            if (CONSUMER_KEY_HEADER.equals(element.trim())) {
                                isConsumerKeyHeaderAvailable = true;
                            } else if (isConsumerKeyHeaderAvailable) {
                                return removeLeadingAndTrailing(element.trim());
                            }
                        }
                    }
                }
            }
        }
        return null;
    }
    private String removeLeadingAndTrailing(String base) {
        String result = base;
        if (base.startsWith("\"") || base.endsWith("\"")) {
            result = base.replace("\"", "");
        }
        return result.trim();
    }
    @Override
    public boolean handleResponse(MessageContext messageContext) {
        return true;
    }
    @Override
    public void init(SynapseEnvironment synapseEnvironment) {
        try {
            this.configContext =
                    ConfigurationContextFactory.createConfigurationContextFromFileSystem(null, null);
        } catch (AxisFault axisFault) {
            log.error("Error occurred while initializing Configuration Context", axisFault);
        }
    }
    @Override
    public void destroy() {
        this.configContext = null;
    }
    private ConfigurationContext getConfigContext() {
        return configContext;
    }
}


You can make this a maven project and bundle into a .jar file (see pom.xml for dependancies).


Deploying the handler into  ESB


To deploy the built handler into ESB, copy the above jar file into [ESB_HOME]/repository/components/lib directory. 


Edit Axis2.xml file to read properties

We are reading 3 configurations from axis2.xml file. 

  1. Server url of  oauth2TokenValidationService in WSO2 Identity Server. Format is [host Name of Identity Server] : [https port]/services/OAuth2TokenValidationService
  2. Username to use when connecting to IS.
  3. Password to use when connecting to IS. 
Navigate to [ESB_HOME]/repository/conf/axis2/axis2.xml and place following at the top.

<!-- OAuth2 Token Validation Service -->
<parameter name="oauth2TokenValidationService">https://localhost:9444/services/OAuth2TokenValidationService</parameter>
<!-- Server credentials -->
<parameter name="identityServerUserName">admin</parameter>
<parameter name="identityServerPw">admin</parameter>


Here oauth2TokenValidationService is a service running in Identity Server. Now let us look at how to create this service inside WSO2 Identity Server.



Preparing WSO2 Identity Server


Download WSO2 IS server from here and extract it to a preferred location. If you are going to run ESB and Identity servers on the same machine, we need to offset ports used by Identity server. To do that go to [IS_HOME]/repository/conf/carbon.xml file and edit it specifying a port offset of 1.

<Ports>

        <!-- Ports offset. This entry will set the value of the ports defined below to
         the define value + Offset.
         e.g. Offset=2 and HTTPS port=9443 will set the effective HTTPS port to 9445
         -->
        <Offset>1</Offset>


Note that this is why we defined 9444 as the port under axis2.xml

Start Identity Server by [IS_HOME]/bin sh wso2server.sh command and point the browser to the Management Console (https://localhost:9444/carbon). Log in using admin/admin default credentials.

Under "Service Providers" click on add.


Provide a name and click on register. Now, under "Inbound Authentication Configuration" section add "OAuth/OpenID Connect Configuration" .  Identity Server will generate a OAuth Client Key and a secret





Defining an API in ESB and securing

Deploy a JAX-RS web service 


Now, we need to create an API  in ESB. Before that we need some service to call by that API. We can use JAX-RS web service hosted here. Build the .war file and deploy it into WSO2 Application Server or to Apache Tomcat to host it. 

This service is used to receive guest details by a guest name. 

Test the deployed service 


We can invoke and test the service using following GET url (assume WSO2 AS is started with port offset =2) 

curl -v -X GET http://127.0.0.1:9765/GuestDetailsService_1.0.0/services/guest_details_service/
guestDetailService/guestInfo/tom



Make an API using above service


Create an API in ESB with following content. 

<api xmlns="http://ws.apache.org/ns/synapse" name="guestDetailNew" context="/guestDetailNew">
   <resource methods="GET" uri-template="/{id}">
      <inSequence>
         <property name="NO_ENTITY_BODY" scope="axis2" action="remove"></property>
         <log>
            <property name="STATUS" value="GUEST_DETAIL_NEW_API_HIT"></property>
         </log>
         <send>
            <endpoint>
               <http method="get" uri-template="http://127.0.0.1:9765/GuestDetailsService_1.0.0/services/guest_details_service/guestDetailService/guestInfo/{uri.var.id}"></http>
            </endpoint>
         </send>
      </inSequence>
      <outSequence>
         <send></send>
      </outSequence>
   </resource>
</api>


Now we can test this API using following curl command


curl -v -X GET http://127.0.0.1:8280/guestDetailNew/tom

Securing the API


Update the API pointing to the handler as below

<api xmlns="http://ws.apache.org/ns/synapse" name="guestDetailNew" context="/guestDetailNew">
   <resource methods="GET" uri-template="/{id}">
      <inSequence>
         <property name="NO_ENTITY_BODY" scope="axis2" action="remove"></property>
         <log>
            <property name="STATUS" value="GUEST_DETAIL_NEW_API_HIT"></property>
         </log>
         <send>
            <endpoint>
               <http method="get" uri-template="http://127.0.0.1:9765/GuestDetailsService_1.0.0/services/guest_details_service/guestDetailService/guestInfo/{uri.var.id}"></http>
            </endpoint>
         </send>
      </inSequence>
      <outSequence>
         <send></send>
      </outSequence>
   </resource>
   <handlers>
      <handler class="org.wso2.handler.SimpleOAuthHandler"/>
   </handlers>
</api>                   


Testing the Secured API


1. First get the access token. Use generated client key and secret by Identity Server above.

curl -v -k -X POST --user ksf_LQIQu4Zh3tNX0f_1CH9FwBAa:oi1o71BwefGSbIM8cw2dld3WVVIa -H "Content-Type: application/x-www-form-urlencoded;charset=UTF-8" -d 'grant_type=password&username=admin&password=admin' https://localhost:9444/oauth2/token



2. Invoke the created API using the access token granted.


curl -v -X GET -H "Authorization: Bearer 51c7b6dc3e98d7ce979e2f30a80ba" http://localhost:8280/guestDetailNew/tom




Please note following:

  1. You can use refresh token to renew the token. Token time out can be configured at Identity Server. 
  2. This is based on official documentation here. Please read it carefully for enhanced use cases. 





Hasitha Hiranya

2 comments:

  1. Hi Hasitha,
    I am making Secured API using OAuth2.
    while hitting API,got exception like Caused by: javax.net.ssl.SSLPeerUnverifiedException: peer not authenticated.
    So Can you please help me to fix this issue?

    ReplyDelete

Instagram