6 Creating Custom HTTP Processors

This chapter explains how you can create custom HTTP processors for Oracle Communications Services Gatekeeper (OCSG).

Creating a Custom HTTP Processor

To write custom processors that perform authentication, assertion or other security related tasks that need to execute in the WebLogic security provider, the OCSG security provider has been refactored to make it lightweight for developing new authentication or assertion mechanisms.

The framework also allows the API to subscribe the available HTTP processors with the values for the configuration parameters. Each API can subscribe to more than one processor, and the same processor also can subscribe to multiple different configuration values.

There are two types of processors: out-of-the box and custom. There are also two scopes: global and API.

Currently, the out-of-the-box processors are:

  • ShieldHttpProcessor (global): used to block IP addresses that have been blocked by the threat protection framework.

  • AnonymousHttpProcessor: used for anonymous APIs

  • AppKeyHttpProcessor: used for validating APPKeys

  • BasicAuthHttpProcessor: handles basic authorization header

  • CORSProcessor: handles CORS requests, always present unless API is anonymous

  • OauthProcessor: handles Bearer and MAC OAuth tokens (only OCSG generated tokens)

  • SoapHttpProcessor: handles usernameToken in WSSE Header

Deploying and Undeploying Custom HTTP Processors

Out-of-the-box HTTP processors do not need to register or unregister, while custom HTTP processors must.

Custom HTTP processor must extend the ApplicationLifecycleListener in its EAR. And the ear must be deployed to AT (cluster) in OCSG ENV.

The sequence invocations are repeated on each AT node as Figure 6-1 illustrates:

Figure 6-1 HTTP Processor Deployment/Undeployment Sequence

Surrounding text describes Figure 6-1 .

HTTP Processor Runtime Architecture

As Figure 6-2 shows, all of the HTTP processor instances are contained in the OCSG Application HTTP Provider:

Figure 6-2 HTTP Processor Runtime Architecture

Surrounding text describes Figure 6-2 .

In runtime, the processors are invoked from HttpTrafficLoginModule. The custom and out-of-the-box chains are invoked in that order. Both chains produce a set of principals that are merged. The custom chain is created based on a URL that maps to an API that might or might not have custom HTTP processors attached to it. The out-of-the-boxchain is created based on the API auth setting (NONE, TEXT, OAUTH, APPKEY).

When a request comes to the OCSG Application HTTP provider it is processed by four chains in the following order:

  1. Out-of-the-box Global processors

  2. Custom global processors

  3. Custom per-API processors

  4. Out-of-the-box per API processors

Each chain can be broken if a processor return true in isDone. If that happens, the next chain is executed.

If a processor throws a runtime exception, the next processor in the chain is executed.

If a processor throws HttpProcessorViolationException, all execution is aborted and the overall login or assertion fails.

Custom Processor EDRs

Each processor's execution is monitored by a tracker, and when all processors have executed, the tracker logs the execution to EDR map.

Each processor's execution has one of the following results:

  • Success (no exception)

  • Exception (if a runtime exception is thrown)

  • Deny (if HttpProcessorViolationException is thrown)

If a login or assertion fails for any reason, the DAF error servlet fires an EDR.

Note:

This means that an unsuccessful login for other services will not generate this EDR. The reason is that even if the HTTP provider fails, OAM or SMPP could still be successful - so the provider itself cannot decide whether the EDR should be logged.

A login or assertion failure occurs when a security constraint is not met; that is, when the principal TrafficUser is not returned from any of the processors.

Tip:

In an EDR, you can track the number of principals each processor adds to the subject, and if all add 0 principals, you can be sure that a failed login occurred.

Example EDRs

Example 6-1 shows a sample EDR for the success outcome.

Example 6-1 Success

[04-21 03:52:20:DEBUG EdrInternalPublisher.java] *** EDR:
ServiceName = null
ContainerTransactionId = null
Method = null
Position = before
ServiceProviderId = partner
TransactionId = 57a211e0-032b-4730-a722-8935da86c9d0_IDX_1
State = ENTER_AT
Class = oracle.ocsg.daf.trafficlogger.SingleTierTrafficLogger
ApplicationId = weather
Processors = "seq=0, name=ShieldHttpProcessor, status=Success, new_principals=0", "seq=1, name=JsonTokenProcessor, status=Exception, code=NullPointerException:null", "seq=2, name=AppKeyHttpProcessor, status=Success, new_principals=2", "seq=3, name=CORSProcessor, status=Success, new_principals=0"
TsBeAT = 1492782740932
HttpMethod = POST
Timestamp = 1492782740932
Direction = south
Source = method
URL = /ECHOServer/1/echo
ServiceProviderGroup = gold
AppInstanceId = appkey_Password1
ServerName = Server1
ReqMsgSize = 17

Example 6-2 shows a sample EDR for the exception outcome.

Example 6-2 Exception

[04-21 03:09:19:DEBUG EdrInternalPublisher.java] *** EDR:
ServiceName = null
ContainerTransactionId = null
Method = null
Source = null
Position = after
AccessUrl = http://10.88.42.23:8001/ECHOServer/1
https://10.88.42.23:8002/ECHOServer/1
TransactionId = 5d8a0a62-a9bb-4306-84dd-e2e3770564eb
Class = com.bea.wlcp.wlng.security.providers.authentication.account.http.HttpTrafficLoginModule
Processors = "seq=0, name=ShieldHttpProcessor, status=Success, new_principals=0", "seq=1, name=JsonTokenProcessor, status=Exception, code=NullPointerException:null", "seq=2, name=AppKeyHttpProcessor, status=Success, new_principals=0", "seq=3, name=CORSProcessor, status=Success, new_principals=0"
Timestamp = 1492780149789
ServerName = Server1
ApiId = ECHOServer

Example 6-3 shows a sample EDR for the deny outcome.

Example 6-3 Deny

[04-21 03:06:30:DEBUG EdrInternalPublisher.java] *** EDR:
ServiceName = null
ContainerTransactionId = null
DenyCode = 39
Position = after
AccessUrl = http://10.88.42.23:8001/ECHOServer/1
https://10.88.42.23:8002/ECHOServer/1
Method = POST
TransactionId = 86a3e481-4313-42e7-b1dc-f3b3e67a63a0
Class = ErrorServlet
Processors = "seq=0, name=ShieldHttpProcessor, status=Success, new_principals=0", "seq=1, name=JsonTokenProcessor, status=Deny, code=39"
Timestamp = 1492779990517
URL = /ECHOServer/1/echo/
Source = Exception
ServerName = Server1
ApiId = ECHOServer

Implementing a Custom HTTP Processor

The framework provides an abstract class AbstractHttpProcessor, which you should use to implement customized HTTP processors.

Note:

There is an HttpProcessor interface available but you should never implement this interface directly; always extend AbstractHttpProcessor.

The HttpProcessor interface has some default methods, such as update(T), destroy(), getConfiguration(). The general type T in the HttpProcessor is the configuration class of the processor implementation.

To Implement a Custom HTTP Processor:

  1. You must use HttpProcessorAnnotation on the class level to specify the name for end-users, the configuration class name and the description of the processor.

  2. The implementation must extend weblogic.application.ApplicationLifecycleListener

    Invoke CustomHttpProcessorsRepository.INSTANCE.registerProcessor(Class) to register itself in the preStart(ApplicationLifecyclevEent) method

    Invoke CustomHttpProcessorsRepository.INSTANCE.deRegisterProcessor(String) to register itself in the postStop(ApplicationLifecycleEvent) method

  3. The configuration for the processor is optional.

  4. The processor class implementation must invoke the constructor without parameters because in the framework, processor instances are initiated as Class.forInstance().

  5. The processor class must extend AbstractHTTPProcessor and implement

    Set<Principal> process(HttpServletRequest httpReq) throws HttpProcessorViolationException

  6. Oracle strongly recommends that you implement the package as a WebLogic application (EAR file).

Example: A Symmetric Key Encrypted JSON Token

Figure 6-3 provides an overview use case to illustrate the concepts of custom HTTP processors. This is the overall flow:

  1. Develop the custom processor EAR.

  2. Deploy the custom processor EAR.

  3. Come up with a header name (example: x-json-token) and encryption key (example: 1y7M3...) and share it with a partner offline.

  4. Subscribe the processor to an API.

  5. The partner application invokes the API.

Figure 6-3 Example HTTP Processor Architecture

Surrounding text describes Figure 6-3 .

OCSG includes a custom processor that is a deployable EAR file that has a life cycle class defined in weblogic-application.xml, shown in Example 6-4.

Example 6-4 weblogic-application.xml

<?xml version="1.0" encoding="UTF-8"?>
<weblogic-application
 xmlns="http://xmlns.oracle.com/weblogic/weblogic-application"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://xmlns.oracle.com/weblogic/weblogic-application http://xmlns.oracle.com/weblogic/weblogic-application/1.4/weblogic-application.xsd">
   <listener>
     <listener-class>
       oracle.ocsg.security.ProcessorLifecycleListener
     </listener-class>
   </listener>
</weblogic-application>

The listener class is responsible for registering, and deregistering all custom HttpProcessors that you want to want to add to OCSG.

Example 6-5 ProcessorLifecycleListener

/**
* Listener class for HTTP processor to register.
*/
public class ProcessorLifecycleListener extends ApplicationLifecycleListener {
  List<Class<? extends HttpProcessor>> processorClasses;
 
  public ProcessorLifecycleListener() {
    processorClasses = new ArrayList<>();
    processorClasses.add(JsonTokenProcessor.class);
  }
 
  /**
  * Register the Processor(s)
  */
  public void preStart(ApplicationLifecycleEvent evt) {
    registerProcessors(processorClasses);
  }
 
  /**
  * Unregister the Processor(s)
  */
  public void postStop(ApplicationLifecycleEvent evt) {
    deRegisterProcessors(processorClasses);
  }
 
  void registerProcessors(List<Class<? extends HttpProcessor>> processorClasses) {
    List<Class<? extends HttpProcessor>> registeredProcessors = new
      ArrayList<Class<? extends HttpProcessor>>();
    for (Class<? extends HttpProcessor> processorClass : processorClasses) {
      try {
        CustomHttpProcessorsRepository.INSTANCE.registerProcessor(processorClass);
        registeredProcessors.add(processorClass);
 
      } catch (Exception e) {
        // If we have exception, let's rollback all processors we already
                // registered before throwing exception. 
        deRegisterProcessors(registeredProcessors);
        throw new RuntimeException(e);
      }
    }
  }
 
  void deRegisterProcessors(List<Class<? extends HttpProcessor>> processorClasses)
  {
    for (Class<? extends HttpProcessor> registeredProcessor : processorClasses) {
      try {
        CustomHttpProcessorsRepository.INSTANCE.deRegisterProcessor
                                             (registeredProcessor.getName());
      } catch (Exception ex) {
        ex.printStackTrace();
        //keep going
      }
    }
  }
}

Note:

At this stage they are not associated with any API and will not be in the execution path until this association is made.

To enable a HttpProcessor for a given API you must PUT the processors plus configuration to that API using the Portal REST interface:

Example 6-6 Processor Configuration REST PUT Payload

{ 
 "SubscribeProcessorsRequest":{ 
   "processors":[ 
     { 
      "name":"JsonTokenProcessor",
      "configuration":"{\"key\":\"1y7M3cO(86m0R5FwC>(N61lRwiTQYu\",\"header\":
        \"x-json-token\"}"
    }
   ]
 }
}

The configuration above maps to the configuration class JsonTokenProcessorConfig, shown in Example 6-7:

Example 6-7 JsonTokenProcessorConfig

public class JsonTokenProcessorConfig implements HttpProcessorConfig {
  private String key;
  private String header;
 
  public String getKey() {
    return key;
  }
 
  public void setKey(String key) {
    this.key = key;
  }
 
  public String getHeader() {
    return header;
  }
 
  public void setHeader(String header) {
    this.header = header;
  }
}

The next request to the ECHOServer API invokes JsonTokenprocessor which looks like Example 6-8:

Example 6-8 JsonTokenProcessor

@HttpProcessorAnnotation(name = "JsonTokenProcessor", configBean =
    JsonTokenProcessorConfig.class)
  public class JsonTokenProcessor extends
     AbstractHttpProcessor<JsonTokenProcessorConfig> {
    private JsonTokenProcessorConfig configuration;
 
    @Override
    public Set<Principal> process(HttpServletRequest httpServletRequest) throws
          HttpProcessorExecutionException {
      String clearTextToken = getTokenFromRequest(httpServletRequest);
      JsonReader reader = Json.createReader(new StringReader(clearTextToken));
      JsonObject token = reader.readObject();
      Set<Principal> principals =
             assertApplication(token.getString("application"));
      if (!principals.isEmpty()) {
        //We have found an application, let's add scope to application principle
        for (Principal principal: principals) {
          if (principal instanceof ApplicationInstance) {
            principals.remove(principal);
            principal = new JsonTokenApplication((ApplicationInstance)principal,
                token.getString("scope"));
            principals.add(principal);
            break;
        }
      }
    }
    return principals;
  }
  private String getTokenFromRequest (HttpServletRequest httpServletRequest) {
    return AES.getInstance(getConfiguration().getKey()).
      decrypt(httpServletRequest.getHeader(getConfiguration().getHeader()));
  }
  @Override
  public JsonTokenProcessorConfig getConfiguration() {
    return configuration;
  }
 
  @Override
  public void init(JsonTokenProcessorConfig configuration) throws
      HttpProcessorConfigException {
    this.configuration = configuration;
  }
}

Optional Custom HTTP Processor Configuration

The framework provides an optional interface for the configuration of the processors. The HttpProcessorConfig interface is used to mark that its subclass is the configuration type of the processors. Example 6-9 illustrates:

Example 6-9 HttpProcessorConfig

/* Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved. */
package oracle.ocsg.security.provider;
 
import java.io.Serializable;
 
/**
 * Interface of a HTTP processor configuration, its implementation must be a
 * mean type.
 */
public interface HttpProcessorConfig extends Serializable {
}

A custom HTTP processor configuration class must meet the following criteria:

  1. The implementation must implement HttpProcessorConfig.

  2. The configuration for the processor is optional.

  3. The configuration class must invoke the constructor without parameters.

  4. Plain Old Java Object (POJO) style is strongly recommended.

  5. It can be exchanged with JSON strings.

Example 6-10 shows a sample custom processor configuration.

Example 6-10 Example Custom Processor Configuration

package oracle.ocsg.security;
 
import oracle.ocsg.security.provider.HttpProcessorConfig;
 
/**
 * Sample HTTP processor configuration.
 */
public class SampleHttpProcessorConfig implements HttpProcessorConfig {
  /**
   * generated serial version UID.
   */
  private static final long serialVersionUID = 8390799967874999370L;
  private long intervals;
  private int maxTryTimes;
  /**
   * Constructor without parameters MUST be there.
   */
  public SampleHttpProcessorConfig() {
  }
  public SampleHttpProcessorConfig(final long intervals, final int maxTryTimes) {
    this.intervals = intervals;
    this.maxTryTimes = maxTryTimes;
  }
  public long getIntervals() {
    return intervals;
  }
  public void setIntervals(long intervals) {
    this.intervals = intervals;
  }
  public int getMaxTryTimes() {
    return maxTryTimes;
  }
  public void setMaxTryTimes(int maxTryTimes) {
    this.maxTryTimes = maxTryTimes;
  }
}

Custom HTTP Processor MBean

The custom HTTP processor MBean is oracle.ocsg.security.provider.processor.management.ProcessorConfigurationMBean. Example 6-11 shows the MBean operations that are exposed:

Example 6-11 Exposed MBean Operations

/**
 * List all the HTTP processor subscriptions for all the APIs.
 * <p><strong>Scope</strong>: Cluster</p>
 *
 * @return The HTTP processor subscriptions on all the APIs.
 * @throws ManagementException When it fails to get the subscriptions.
 */
Map<String, String> listProcessorSubscriptions() throws ManagementException;
 
/**
 * Gets the JSON string of the HTTP processor subscription for the specified API context path.
 * <p><strong>Scope</strong>: Cluster</p>
 *
 * @param contextPath The API context path.
 * @return The JSON content of the API JSON subscription.
 * @throws ManagementException When it fails to get the subscription.
 */
String getProcessorSubscription(String contextPath) throws ManagementException;