March 29, 2020
As out-of-the-box π¦, the WSO2 API Manager supports Basic Auth and Digest Auth Protocol protected APIs and Backend services to integrate through. But, what if we have an OAuth2 Protected service and want to integrate and expose the service through API Manager? π€π€π€
In this medium, we will be going through a couple of custom mediation sequence and custom mediator implementations to fulfill our requirements on integrating OAuth2 protected APIs and Backends.
WSO2 Products, to be more specific the underlying Carbon Kernal platform provides extension points to plug and play custom sequences and mediation implementations to achieve customize requirements. We will be going through a custom mediation sequence and a custom mediator implementation today to achieve our requirements.
We can make use of the Synapse engine and the Synapse mediation sequences in WSO2 API Manager to achieve our ultimate requirement. Given below is a sample mediation in-sequence developed to call the respective OAuth2 token endpoint and use the generated Bearer Token to access the Backend.
Please note that the below-given Synapse mediation sequence is a sample and it needs some tweaks to make it work with other customized use-cases
<?xml version="1.0" encoding="UTF-8"?> | |
<sequence name="oauth2-sequence" xmlns="http://ws.apache.org/ns/synapse"> | |
<!-- token generation to the oauth server's token endpoint --> | |
<!-- add the base64 encoded credentials --> | |
<property name="client-authorization-header" scope="default" type="STRING" value="MDZsZ3BTMnh0enRhOXBsaXZGUzliMnk4aEZFYTpmdE4yWTdLcnE2SWRsenBmZ1RuTVU1bkxjUFFh" /> | |
<property name="request-body" expression="json-eval($)" scope="default" type="STRING" /> | |
<property name="resource" expression="get-property('axis2', 'REST_URL_POSTFIX')" scope="default" type="STRING" /> | |
<!-- creating a request payload for client_credentials --> | |
<payloadFactory media-type="xml"> | |
<format> | |
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"> | |
<soapenv:Body> | |
<root xmlns=""> | |
<grant_type>client_credentials</grant_type> | |
</root> | |
</soapenv:Body> | |
</soapenv:Envelope> | |
</format> | |
<args></args> | |
</payloadFactory> | |
<!-- set related headers to call the token endpoint --> | |
<header name="Authorization" expression="fn:concat('Basic ', get-property('client-authorization-header'))" scope="transport" /> | |
<header name="Content-Type" value="application/x-www-form-urlencoded" scope="transport" /> | |
<property name="messageType" value="application/x-www-form-urlencoded" scope="axis2" type="STRING" /> | |
<property name="REST_URL_POSTFIX" value="" scope="axis2" type="STRING" /> | |
<!-- change the token endpoint --> | |
<call blocking="true"> | |
<endpoint> | |
<http method="POST" uri-template="https://localhost:9443/oauth2/token" /> | |
</endpoint> | |
</call> | |
<!-- append the acquired access token and make the call to the backend service --> | |
<property name="bearer-token" expression="json-eval($.access_token)" scope="default" type="STRING" /> | |
<property name="REST_URL_POSTFIX" expression="get-property('resource')" scope="axis2" type="STRING" /> | |
<header name="Authorization" expression="fn:concat('Bearer ', get-property('bearer-token'))" scope="transport" /> | |
<payloadFactory media-type="json"> | |
<format>$1</format> | |
<args> | |
<arg evaluator="xml" expression="get-property('request-body')" /> | |
</args> | |
</payloadFactory> | |
</sequence> |
Now you have given us a sample mediation sequence to call the token endpoint and then to invoke the actual endpoint. But wait β, I noticed that the mediation is going to perform a Token call each and every time, and that is going to be an extra call for the API Manager.
Hmmm β¦ ππ€
Yes true, as per the sample mediation, yes, it will perform Token call each and every time. Well, we can minimize that by storing the acquired access token and the validity period in a temporary location (probably a Registry location) and make use of it.
Given-below is an enhanced sample mediation sequence to store and retrieve the access token and the generated time from a custom Registry location and based on the validity to perform the Token call. π
If you are willing to continue with the enhanced mediation sequence, then it is required to create the necessary Registry resource in the API Manager server prior to the usage for a flawless experience.
I will be creating the following two Registry resources in a custom Governance Registry location
access_token
: To store the acquired Access Tokengenerated_time
: To store the Generated time of the Access tokenYou can create the necessary Registry collections and resources by navigating to the Carbon Management Console > Browse > Registry > _system > governance and add a registry collection named oauth_endpoint
.
After successfully creating the Registry collection (oauth_endpoint
), create the above-mentioned two registry resources with the following content
access_token
: Leave the content as emptygenerated_time
: 0 (this is because we will be referring to this as Long type)<?xml version="1.0" encoding="UTF-8"?> | |
<sequence name="oauth2-enhanced-sequence" xmlns="http://ws.apache.org/ns/synapse"> | |
<!-- retreiving the access token and the generated time if exists in the registry location --> | |
<property name="stored-token" expression="get-property('registry', 'gov:/oauth_endpoint/access_token')" scope="default" type="STRING" /> | |
<property name="generated-time" expression="get-property('registry', 'gov:/oauth_endpoint/generated_time')" scope="default" type="LONG" /> | |
<!-- token generation to the oauth server's token endpoint --> | |
<!-- add the base64 encoded credentials --> | |
<property name="client-authorization-header" scope="default" type="STRING" value="MDZsZ3BTMnh0enRhOXBsaXZGUzliMnk4aEZFYTpmdE4yWTdLcnE2SWRsenBmZ1RuTVU1bkxjUFFh" /> | |
<property name="request-body" expression="json-eval($)" scope="default" type="STRING" /> | |
<property name="resource" expression="get-property('axis2', 'REST_URL_POSTFIX')" scope="default" type="STRING" /> | |
<!-- enhanced to perform subtract function --> | |
<property name="CURRENT_SYSTEM_TIME" expression="get-property('SYSTEM_TIME')" type="STRING" /> | |
<property name="token_generated_time" expression="get-property('generated-time')" type="STRING" /> | |
<!-- filter and based on the validity condition make the token call --> | |
<filter xpath="get-property('CURRENT_SYSTEM_TIME') - get-property('token_generated_time') > 3600000 or get-property('stored-token') = ''"> | |
<then> | |
<!-- creating a request payload for client_credentials --> | |
<payloadFactory media-type="xml"> | |
<format> | |
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"> | |
<soapenv:Body> | |
<root xmlns=""> | |
<grant_type>client_credentials</grant_type> | |
</root> | |
</soapenv:Body> | |
</soapenv:Envelope> | |
</format> | |
<args></args> | |
</payloadFactory> | |
<!-- set related headers to call the token endpoint --> | |
<header name="Authorization" expression="fn:concat('Basic ', get-property('client-authorization-header'))" scope="transport" /> | |
<header name="Content-Type" value="application/x-www-form-urlencoded" scope="transport" /> | |
<property name="messageType" value="application/x-www-form-urlencoded" scope="axis2" type="STRING" /> | |
<property name="REST_URL_POSTFIX" value="" scope="axis2" type="STRING" /> | |
<!-- change the token endpoint --> | |
<call blocking="true"> | |
<endpoint> | |
<http method="POST" uri-template="https://localhost:9443/oauth2/token" /> | |
</endpoint> | |
</call> | |
<!-- extract the access token from the response --> | |
<property name="bearer-token" expression="json-eval($.access_token)" scope="default" type="STRING" /> | |
<!-- store the generated token and the time --> | |
<property name="gov:/oauth_endpoint/access_token" expression="get-property('bearer-token')" scope="registry" type="STRING" /> | |
<property name="gov:/oauth_endpoint/generated_time" expression="get-property('SYSTEM_TIME')" scope="registry" type="LONG" /> | |
<!-- append the acquired access token and make the call to the backend service --> | |
<property name="REST_URL_POSTFIX" expression="get-property('resource')" scope="axis2" type="STRING" /> | |
<header name="Authorization" expression="fn:concat('Bearer ', get-property('bearer-token'))" scope="transport" /> | |
<payloadFactory media-type="json"> | |
<format>$1</format> | |
<args> | |
<arg evaluator="xml" expression="get-property('request-body')" /> | |
</args> | |
</payloadFactory> | |
</then> | |
<else> | |
<!-- the stored access token is still active, so we will be using that to invoke the endpoint --> | |
<header expression="fn:concat('Bearer ', get-property('stored-token'))" name="Authorization" scope="transport" /> | |
</else> | |
</filter> | |
</sequence> |
π© Note:
The above-given mediation sequence needs access to the Registry DB to store and retrieve the access token related data. Therefore, if you are using a Distributed deployment (separate Gateway node), we do not recommend this approach as it is required to configure the GW nodes with the Registry DB which can impact the performance.
In this section, we will be looking into a custom class mediator implementation developed especially to cater to the requirement on integrating OAuth2 Protected endpoint with the WSO2 API Manager.
You can find the complete implementation and the source of the OAuth Mediator (class mediator) in GitHub.
WSO2 OAuth Mediator
Clone the project and build it by executing the following command from the root directory of the project
mvn clean package
Copy and place the built JAR Artifact inside the <APIM>/repository/components/lib
directory. Then create a JSON file named wso2-oauth-mediator.json
with the following structure and configure the Authorization serverβs Token endpoint and the credentials
[{"id": "EP1","tokenApiUrl": "https://localhost:9443/oauth2/token","apiKey": "1234567890","apiSecret": "0987654321","username": "admin","password": "admin","grantType": "client_credentials","scope": "default","tokenRefreshInterval": 20}]
Place the wso2-oauth-mediator.json
inside the <APIM>/repository/conf
directory and restart the server to take effect on the changes.
π After a successful restart, create an in-sequence for the protected API and define the class mediator inside the sequence to execute and acquire the token
<sequence xmlns="http://ws.apache.org/ns/synapse" name="oauth-sequence"><class name = "org.wso2.apim.mediators.oauth.OAuthMediator"><property name="endpointId" value="EP1" /></class></sequence>
We have now successfully slid through the approaches on integrating OAuth2 Protected endpoint with the WSO2 API Manager π
Happy Stacking !!! π€ βοΈ
WSO2 OAuth Mediator