Generic Consentor Plugin

Generic Consentor Plugin

tutorials

Consentors are plugins that allow additional processing after the user-consent has been gathered before any token is issued. Such processing typically includes but is not limited to validations, information retrieval, and user interaction.

There are two types of Consentors:

  • Generic Consentors (also referred to as Consentors)
  • Signing Consentors

Signing Consentors are a specialization of Generic Consentors, used when the goal is just to sign the consented information.

For more information about Consentors, check out the Curity Identity Server Documentation.

This article describes how to create a Generic Consentor plugin from scratch, access attributes, parse JSON data from an external service (API) during execution, and interact with the user before issuing a token.

In detail, the plugin will parse a prefix scope (transactionId), use the value to query an API for the order related to the transaction, and display the total_price of the order before issuing the token.

Prerequisites

Maven Archetype

Maven Archetypes are essentially template projects. Curity Maven Archetypes are available for different Curity plugin types to quickly generate a skeleton of a given plugin. Use Curity Maven Archetype to create the skeleton for the Consentor plugin.

mvn -B archetype:generate \
      -DarchetypeArtifactId=identityserver.plugins.archetypes.consentor \
      -DarchetypeGroupId=io.curity \
      -DarchetypeVersion=2.2.0  \
      -DartifactId=identityserver.plugins.consentor.example \
      -DgroupId=com.example.curity \
      -DpluginName=Example \
      -Dversion=1.0.0-SNAPSHOT

In general, a Curity Identity Server plugin consists of an implementation class, an interface for the configuration, and a description of its characteristics. All of which have been created by Maven:

  • ExampleConsentor
  • ExampleConsentorConfig
  • ExampleConsentorDescriptor

Descriptor

During startup, the Curity Identity Server will scan for implementations of the subtypes of PluginDescriptor to load all the plugins. A Plugin Descriptor informs the Curity Identity Server of a plugin’s characteristics, such as the implementation type, the name of the implementing class, and the type of configuration.

Curity Identity Server offers different extensibility mechanisms. Each of them implements a different subtype of PluginDescriptor. The plugin in this example is a Generic Consentor plugin; thus, the subtype is ConsentorPluginDescriptor.

Maven already created the descriptor and generated the required methods. However, the implementation type Maven created is called example, which is too generic and may lead to conflicts. Therefore, the return value of the method getImplementationType() was changed in the code below.

public final class ExampleConsentorDescriptor implements ConsentorPluginDescriptor<ExampleConsentorConfig>
{
    ...
    @Override
    public String getPluginImplementationType()
    {
        return "price-consent";
    }    
}

Configuration

All plugins must provide an interface that extends Configuration representing the runtime configuration of the plugin. Maven prepared the interface ExampleConsentorConfig, and its use is fairly simple.

Configuration parameters are represented by get-methods (or is-methods) in the configuration interface. These are also referred to as members. Curity Identity Server takes care of the rendering. In this example, the plugin supports a configurable scope prefix.

public interface ExampleConsentorConfig extends Configuration 
{
    @Description("The prefix of the scope to be used in the query of the API")
    String getScopePrefix();
}

The type of the member is not limited to simple data types and can also be Interfaces. For example, Curity Identity Server SDK includes WebServiceClient. Using a member of this type in the configuration allows you to configure API access details in one single line of code. These details include hostname, port, path, the scheme (i.e., HTTP or HTTPS), and the HTTP client that specifies the HTTP authentication.

The API will return JSON data that must be parsed. There is another Interface in the SDK called Json for that.

public interface ExampleConsentorConfig extends Configuration 
{  
    ...
    
    WebServiceClient getWebServiceClient();
    Json getJsonService();
}

When the user closes the screen, the Example Consentor will be completed. Therefore, the system must use SessionManager to keep track of the state.

public interface ExampleConsentorConfig extends Configuration 
{
    ...

    SessionManager getSessionManager();
}

The Curity Identity Server will render some configuration parameters. Others, namely the JSON wrapper and SessionManager, will not be visible in the Admin UI. However, visible or not, all members in the configuration Interface can be accessed in code.

Plugin Execution

The constructor of the Consentor will take the configuration as a parameter, and all the configurations, i.e., the members of the configuration Interface, will be available.

The central part of a Consentor is the apply method called when the Consentor is executed. It returns either a result of type success, unsuccessful, or pending when there are different types of pending results. This example uses a pending result that requires user interaction.

When the Consentor has completed successfully, it will return the consent data, i.e., the data that the user consented to.

In this example, the Consentor will first get the transactionId from the list of scopes requested by the client. The value of the transactionId is used to query the API for user-friendly data related to the transaction.

public ConsentorResult apply(ConsentAttributes consentAttributes, String consentId)
{ 
    ...
            
    //Get transaction id from scopes
    //Requested scopes are part of the consent attributes
    Optional<String> transactionId = getTransactionId(consentAttributes);
        
    //Use transaction id and query API for more data
    //<scheme>://<hostname>:<port><path>?transactionId=<value from prefix scope>
    ...
    Map<String, Collection<String>> queryParameters = new HashMap<>();
    queryParameters.put("transactionId", Collections.singleton(transactionId.get()));

    HttpResponse apiResponse = _webServiceClient.withQueries(
        queryParameters)
        .request()
        .get()
        .response();
} 

The response includes a JSON array with a single object representing the order. The order object has a JSON value with the key total_price. To access the value, the string in the response needs to be parsed as JSON.

public ConsentorResult apply(ConsentAttributes consentAttributes, String consentId)
{
    ...
        
    //Parse response as JSON
    //   [{"id": 111, ..., "total_price": 198, "transactionId": 123}]
    String jsonResponse = apiResponse.body(HttpResponse.asString());
    List<?> jsonList = _jsonService.fromJsonArray(jsonResponse);
    Optional<Map> order = jsonList.stream().map(Map.class::cast).findFirst();
    ...

    //Read price from Json object and put the value in the sessionManager
    Optional<Integer> price = Optional.of((Integer) order.get().get("total_price"));
    ...
}

The values are stored in the sessionManager to be accessed later when handling the user prompt. In this case, the session attributes are bound to the very exact consent transaction. This is because the Example Consentor will have to handle several consent transactions within the same session and thus must distinguish between the attributes from one flow and the other to avoid conflicts.

public ConsentorResult apply(ConsentAttributes consentAttributes, String consentId)
{
    ...
    _sessionManager.put(Attribute.of(AttributeName.of(consentId + "price"), price.get()));
    _sessionManager.put(Attribute.of(AttributeName.of(consentId + "currency"),"SEK"));
    ...

    //Prompt user
    return ConsentorResult.pendingWithPromptUserCompletion();
}

In this example, the Example Consentor is considered completed when the user closed the screen. It will otherwise require the user to confirm the price of the order. The sessionManager is used to track the status.

@Override
public ConsentorResult apply(ConsentAttributes consentAttributes, String consentId)
{
    // check if user approved: _sessionManager.get(APPROVED) == true
    if (isCompleted(consentId)) {
        // save space and clean sessionManager from attributes related to this consent transaction.
        cleanSessionManager(consentId)
        return ConsentorResult.success(
            //some attributes that should be included in the consent...
            Attribute.of("subject", consentAttributes.getAuthenticationAttributes().getSubject()),
            ...
        );
    } else {
        ...
        
        //Prompt user
        return ConsentorResult.pendingWithPromptUserCompletion();
    }
}

User interaction

When the Example Connector returns with ConsentorResult.pendingWithPromptUserCompletion(), the request will be routed to the index handler. Maven did not prepare any files for that, and a handler that implements the Interface ConsentorCompletionRequestHandler must be created manually. It is possible to inject specific interfaces in the constructor of a ConsentorCompletionRequestHandler. The constructor of the ExampleConsentorHandler takes the configuration and intermediate consent state to access the sessionManager and consentId.

public ExampleConsentorHandler(ExampleConsentorConfig config, IntermediateConsentState intermediateConsentState) {
    this._sessionManager = config.getSessionManager();
    this._consentId = intermediateConsentState.getTransactionId();
}

The handler in this example is pretty simple since it will only read the variables price and currency for the current consent flow from the sessionManager and update the variables for the template that Curity Identity Server will render.

public class ExampleConsentorHandler implements ConsentorCompletionRequestHandler<Request> 
{
    ...

    @Override
    public Request preProcess(Request request, Response response) {
        Map<String, Object> templateVariables = new HashMap<>();
        templateVariables.put("_price", _sessionManager.get(_consentId + "price").getValue());
        templateVariables.put("_currency", _sessionManager.get(_consentId + "currency").getValue());
        response.setResponseModel(ResponseModel.templateResponseModel(templateVariables, "index")
                , Response.ResponseModelScope.ANY);
        return request;
    }
    ...
    
}

The same handler will update the sessionManager when the user clicks the button which submits a form using POST. The handler includes a post method for handling this POST request.

public class ExampleConsentorHandler implements ConsentorCompletionRequestHandler<Request> 
{
    ...

    @Override
    public Optional<ConsentorCompletionResult> post(Request request, Response response) {
        _sessionManager.put(Attribute.of(AttributeName.of(
                _consentId + ExampleConsentor.APPROVED), true));

        return Optional.of(ConsentorCompletionResult.complete());
    }
    ...
}

The handler must be registered so that Curity Identity Server knows which handler to use when processing user interaction requests. This is done in the class ExampleConsentorDescriptor by overwriting the method getConsentorRequestHandlerTypes().

A handler is mapped to a path. In this example, the path is index. This means that ExampleConsentorHandler will take care of requests routed to index, as the user prompt will be.

public final class ExampleConsentorDescriptor implements ConsentorPluginDescriptor<ExampleConsentorConfig>
{
    ...

    @Override
    public Map<String, Class<? extends ConsentorCompletionRequestHandler<?>>> getConsentorRequestHandlerTypes() {
        Map<String, Class<? extends  ConsentorCompletionRequestHandler<?>>> exampleRequestHandlerTypes = new HashMap<>();
        exampleRequestHandlerTypes.put("index", ExampleConsentorHandler.class);
        return exampleRequestHandlerTypes;
    }
    ...
}

As stated above, the ExampleConsentorHandler will update the response model for the template. There is not such a template for index yet. A new file called index.vm must be created in the directory templates/consentor/price-consent where price-consent is the implementation type of the Example Consentor.

The template makes use of the response model and prints out the price. When the form is submitted, the request gets posted to the ExampleConsentorHandler that, in turn, will update the sessionManager. At the end, the ExampleConsentor will complete the flow by returning success.

#set ($page_symbol = $page_symbol_authenticate_desktop)

#define ($_body)

<form method="post">
    <input type="hidden" name="state" value="$_state" />
    <p>Please approve.</p>
    <p>Do you want to order for $!_price $!_currency?</p>
    <button id="submit-button" type="submit" class="button button-fullwidth $!button_color mt2">OK</button>
</form>
#end

#parse("layouts/default")

Conclusion

The extensibility mechanisms of the Curity Identity Server allow highly customized workflows. Consentors are just another powerful tool that perform additional processing before issuing a token. Using request handlers flows with pending results, with or without user interaction, helps to meet even complex compliance requirements.

Together with several Interfaces shipped with the Curity Identity Server SDK, Maven Archetypes enable developers to quickly get the building blocks in place and focus on the actual logic of the plugin.

Check out the code example for the Example Generic Consentor.

Keep up with our latest articles and how-tos RSS feeds.