API Token via Http Lookup in Adapter Module
Introduction
Token based authentication is prominent everywhere on the web nowadays. With most every web company using an API, tokens are the best way to handle authentication for multiple users.
In this blog I want to share a simple and generic adapter module implementation about how to obtain such a token and store it in the dynamic configuration for further use.
Additionally there’s an option to cache the token for subsequent calls. This is useful if you have loads of requests and you want to save some traffic or if the number of tokens (logins) that can be obtained per hour/day is limited.
We’re using this module in several REST and SOAP scenarios to request an Auth-Token that is needed in the HTTP Header of a HTTP Request. The caching function allows reusing a token within multiple XI-Messages, so the login is only done once in a specified time frame.
Implementation
In all our scenarios we had to put an Auth-Token in the HTTP Header of the request. HTTP-Header modification is possible in HTTP_AAE, REST and SOAP (AXIS) adapter. In every adapter you can access values from the Dynamic Configuration and put it into the HTTP Header. (examples are listed below)
In his very good blog post “Setting Dynamic Configuration Attributes Using Custom Adapter Module in Advanced Adapter Engine of PI/PO“, Vadim Klimov is describing how to modify the dynamic configuration of an XIMessage. The provided classes to access the dynamic configuration can be extended and this is what we do here. So before you continue reading, it’s a good idea to read Vadims post first. 😉
The following picture highlights all classes that have been added to the project.
- DynamicConfigurationProviderHttpLookup: Implements DynamicConfigurationProvider and contains the main logic. The following extract shows the idea of what is behind:
if (storageEnabled) { [...] parameterValue = keyValueStore.get(storageKey); [...] } if(parameterValue == null){ [...] HttpRequest request = new HttpRequest(parameters); HttpResponse response = HttpClient.doRequest(request, parameters); parameterValue = response.getResponse(parameters); [...] if (storageEnabled) { [...] keyValueStore.add(storageKey, parameterValue, storageExpirationTime); [...] } } [...] DynamicConfigurationAttribute dcAttribute = new DynamicConfigurationAttribute(parameterNamespace, parameterName, parameterValue); if (dcAttribute.isDynamicConfigurationAttributeComplete()) { dcAttributes.add(dcAttribute); } [...]
- KeyValueStore: Wrapper class for the MessageIDMapper class. Provides add, get and remove method. The MessageIDMapper is used to persist data in PI Tables for a specified time. If you want to have a look at the raw data, open a SQL Browser an go to table SAPJ2EE.XI_AF_SVC_ID_MAP.
- …util.http Package: Contains Helper Classes for HTTP-Requests.
Usage
Module Processing Sequence
- Number: <before the module you want to use the token, usually somewhere at the beginning>
- Module Name: Custom_AF_Modules/AddDynamicConfigurationBean
- Type: Local Enterprise Bean
- Module Key: <up to you> (e.g. dclookup)
Module Configuration
Parameter name | Description |
---|---|
*class | DynamicConfigurationProviderHttpLookup |
*dc.attribute.name | The name of the attribute in the Dynamic Configuration where you want to save the value received by the lookup. |
*dc.attribute.namespace | The namespace of the attribute in the Dynamic Configuration where you want to save the value received by the lookup. |
*dc.http.request.url | The URL that should be called (http://… , https://…). |
dc.http.request.proxyHost | If you need to route the call via a proxy, enter the Host-name of the Proxy-server here. |
dc.http.request.proxyPort | If you need to route the call via a proxy, enter the Port of the Proxy-server here. |
dc.http.request.method | HTTP Method: Only GET or POST are implemented yet. Default is POST. |
dc.http.request.header.<custom> |
If you need to add specific HTTP Header you can add them here. The <custom>-Tag has to be replaced by a custom name. As a value you can enter what you need, make sure to keep the following syntax: <httpHeaderKey>: <httpHeaderValue> e.g. If you need to add a SOAPaction you can do something like: ParameterName: dc.http.request.header.MySoapAction ParameterValue: SOAPAction: A_Custom_SOAP_ACTION |
dc.http.request.postdata | POST-Data is only available when you do a HTTP-Post. Enter the payload that you want to sent in the request. |
dc.http.request.connectiontimeout | HTTP connection timeout in ms, default is 60000s. |
dc.http.request.readtimeout | HTTP read timeout in ms, default is 60000s. |
dc.http.response.valuesource | It may happen, that the HTTP Response returns more information than you want to store in the dynamic configuration. This parameter accepts “Reqex” or “XPath“. If set, the selected operation is applied on the HTTP-Response. |
dc.http.response.valuesource.xpath | If dc.http.response.valuesource=XPath, enter the Xpath of the value you want to have. |
dc.http.response.valuesource.regex | If dc.http.response.valuesource=Regex, enter the Regex of the value you want to have. |
dc.http.response.stringformat | It may also happen, that you need to format the token somehow before further use. This is just a simple String.format() operation on the http/xpath or regex result. [String.format(<your value>, value)] As an idea, you can enter something like “Bearer %s“. |
dc.keyValueStore.enabled | As already mentioned above, the module is able to store the received token for subsequent calls. This is enabled by setting the parameter to true. Default is false. |
dc.keyValueStore.key | This parameter is optional and by default initialized with the Channel-ID. It is possible to make several channels use the same storage by using the parameter. This is useful if the same token can be used in several receiver channels. E.g. If you have multiple receiver channels pointing to the same host and all can share the same token. |
dc.keyValueStore.expirationTime | Enter a value in minutes. Default is 1 minute. |
dc.keyValueStore.clear | By setting this to “true“, it will disable the KeyValueStore and removes the saved token. This is useful, if you need to remove a saved token for any reason. |
(* mandatory)
Http header manipulation
Once a token is received, it can be used wherever the Dynamic Configuration is accessible. The following examples show, how to add an Auth-Token in the HTTP Header of a request.
HTTP Adapter
- In Tab “Advanced” check “Set Adapter Specific Message Properties” and “HTTP Header Fields”
- e.g. in Field 1 (HeaderFieldOne): AuthKey
- In the Modul Configuration (Custom_AF_Modules/AddDynamicConfigurationBean)
- dc.attribute.name= HeaderFieldOne (… HeaderFieldSix)
- dc.attribute.namespace = http://sap.com/xi/XI/System/HTTP_AAE
- …
SOAP Adapter (AXIS Mode)
- Have a look at the XI30DynamicConfigurationHandler for accessing the Dynamic Configuration in the SOAP Adapter.
- The Note: “1039369 – FAQ XI Axis Adapter” shows some examples as well (search for ASMA)
REST Adapter
- In Tab “REST URL” use Pattern Variable Replacement
- Value Source: AdapterSpecific Key
- Pattern Element Name: AuthKey
- Adapter Specific Attribute: CustomAttribute
- Attribute Name: AuthKey
- In Tab “HTTP Headers” add a new row:
- Header Name: e.g. Authorization
- Value Pattern: {AuthKey}
- In the Modul Configuration (Custom_AF_Modules/AddDynamicConfigurationBean)
- dc.attribute.name= AuthKey
- dc.attribute.namespace= http://sap.com/xi/XI/System/REST
- …
Source
You can find the sources in my fork of Vadim’s Git-Repository here:
https://github.com/MartinBuselmeier/sap-xpi-adapter-module-add-dynamic-configuration
Et voilà! A generic module to request tokens with caching option.
Inheriting from DynamicConfigurationProviderHttpLookup
As you see, there are a lot of parameters to fill. Sometimes it makes sense to create a more specific implementation that fits to a special software. This makes it easier for PI Admins to maintain the Module Configuration in the channels. The following code is extracted from DynamicConfigurationProviderDemoSoftware class.
It shows how to extend the class DynamicConfigurationProviderHttpLookup and sets additional properties that are not specified by a module property by keeping the possibility to overwrite them if needed.
public class DynamicConfigurationProviderDemoSoftware extends
DynamicConfigurationProviderHttpLookup {
private static final String PARAMETER_USERNAME = "DemoSoftware.username";
private static final String PARAMETER_PASSWORD = "DemoSoftware.password";
private static final String PARAMETER_URL = "DemoSoftware.url";
@Override
public List<DynamicConfigurationAttribute> execute(Message message,
Map<String, String> parameters)
throws DynamicConfigurationProviderException {
String username = "";
String password = "";
String url = "";
for (Map.Entry<String, String> parameter : parameters.entrySet()) {
if(parameter.getKey().equals(PARAMETER_URL)) {
url = parameter.getValue();
} else if (parameter.getKey().equals(PARAMETER_USERNAME)) {
username = parameter.getValue();
} else if (parameter.getKey().equals(PARAMETER_PASSWORD)) {
password = parameter.getValue();
} else {
}
}
String postdata = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
+ "<soapenv:Envelope xmlns:soapenv=\"http://schemas.xmlsoap.org/soap/envelope/\">"
+ "<soapenv:Header/>"
+ "<soapenv:Body>"
+ "<ns:Login>"
+ "<ns:userName>"
+ username
+ "</ns:userName>"
+ "<ns:password>"
+ password
+ "</ns:password>"
+ "</ns:Login>"
+ "</soapenv:Body>" + "</soapenv:Envelope>";
// Set default values if not already set by Module Parameter
if (!parameters.containsKey("http.request.url"))
parameters.put("http.request.url", url);
if (!parameters.containsKey("http.request.postdata"))
parameters.put("http.request.postdata", postdata);
if (!parameters.containsKey("http.request.header.soapAction"))
parameters.put("http.request.header.soapAction","SOAPAction: \"Login\"");
[...]
return super.execute(message, parameters);
}
}
The next picture shows the module configuration for the DynamicConfigurationProviderDemoSoftware class.
That’s it 🙂
Hi Martin,
Thank you for sharing this development with the community - very impressive and well-considered work!
Together with exploring feasibility of manipulation of dynamic attributes of the message from within the adapter module sequence execution, I was involved in a project, were we finally used that development in a practical way, given the requirement of using OAuth 2.0 authentication mechanisms when sending requests from PO to an external system (originally applying this enhancement to HTTP_AAE adapter, later on switching to REST adapter), which is another use case of setting additional HTTP header with dynamically determined header value. I've documented the approach here - but, I shall admit, the design and implementation is intended for low/medium volume scenarios with no involvement of any additional persistence layer on PO side, hence there is no OAuth key persistence in PO, and every processed message triggers acquisition of the access token without using token refreshment / invalidation techniques.
Furthermore, I can evidence recent enhancements and improvements introduced in functionality of the REST adapter - for example, support of some OAuth 2.0 grant types for PI/PO release 7.5 (refer to SAP Note 2405166), so we may get some features from this area, that we currently have to develop as custom functions (like within custom adapter modules), being a part of SAP standard in the future. Having written so, I shall say that flexibility of your implementation definitely allows its wider application that goes beyond specific authentication framework / mechanism usage.
Regards,
Vadim
I have never added a module to PO 7.40. Can explain how to deploy your module or point me to an example of how to do it?
All help is appreciated.
David
Hi,
I know it's pretty late since this blog was written, but I face an issue when I try to use this module in PI system. Could someone please advise whats wrong here?.
I deployed the module using NWDS to my 7.4 system.
My scenario is File -> PI -> REST.
I have configured module tab as mentioned in this blog, but i received below mentioned error when tested.
Exception caught by adapter framework: Cannot cast class com.sun.proxy.$Proxy3840 to interface com.sap.aii.af.lib.mp.module.ModuleLocal (found matching interface com.sap.aii.af.lib.mp.module.ModuleLocal loaded by sap.com/tokenEAR@com.sap.engine.boot.loader.ResourceMultiParentClassLoader@4328c55c@alive, but needed loader library:com.sap.aii.af.lib@com.sap.engine.boot.loader.ResourceMultiParentClassLoader@394642e5@alive)
Many thanks in advance.
Regards,
Kumaran
Hi Kumaran,
i've haven't seen this before. Can you try to run Vadims module withe the example he provided in the linked post?
Do you get the same Error?
Maybe you can also try to debug the Module remotely as described here: https://blogs.sap.com/2014/01/25/debugging-java-applications-java-mapping-java-proxies-etc-on-sap-pipo/
Dear Martin,
we got one business requirement where we need to call the JSON API in our ESS portal.
we need to call the API using the authorization token generated run time dynamically.
ADD API https://benefits-qa.apigee.net/v1/members
Update a member API end point in QA: https://benefits-qa.apigee.net/v1/members/{memberId}
i am new to This topic.
so kindly can u help me asap with the complete solution step by step.
Regards,
Manjunath
Hello Martin Buselmeier,
How this Message ID mapper works if there are mutliple interfaces which requires refresh token.
Does it create new tokens for each interface which is configured with Module Module(Custom_AF_Modules/AddDynamicConfigurationBean)?
In my case I have 2 OAUth Calls, I'm using SAP PO 7.5 SP 13. First call to generate a token and second call to refresh new tokens(with first call token as HTTP header parameter).
Rajesh, it's described above, read next to: dc.keyValueStore.key
Hi Martin,
Great blog and solution! For me, the biggest take away is the learning about the existence of the KeyValueStore! However, you mention this one more on a side note as it’s not the core purpose of the module obviously. I wonder how you got to know about it and if we can rely on it to stay in future updates, give that it seems to be an undocumented (only internally used) feature (or am I wrong?).
For sure it is great to know that we herewith have the possibility to persist data in a relatively easy way on PI/PO, without having to resort to custom services running independently from PI/PO.
Philippe
Hi Philippe!
Actualy, MessageIDMapper class is rather well documented in PI Java Docs. As I could understand, the main aim of that class is to store the mappings between PI message IDs and any external message ID for corellation purposes. So, I guess, we can stay assured enough about using the class in our PI developments.
Regards, Evgeniy.
Hi Evgeniy,
You’re right, the API documentation includes it, but as you say only to store message ID mappings. Anyway, it seems we can “abuse” it to store other data safely. It’s really a great idea!
Thanks,
Philippe Addor
Yes, we use it from time to time for storing the data. It's worth mentioning that column length for both message ids is limited to 128 symbols.
Regards, Evgeniy.
Hi Martin,
I tried to clone the git project in NWDS but it is not extracting the Project, can you please help me what I am missing here.
Git hub link: https://github.com/MartinBuselmeier/sap-xpi-adapter-module-add-dynamic-configuration.git