Dynamic endpoint API based on custom claims (tit for tat)
Use case
In a modern environment when you building APIM it is a common request to use some kind of business logic to allow a certain user to have access to different resources based on some attributes that they have. That could be a role that the user has or some access token. So basic flow goes like this
User request for resource goes through Gateway where Policy decision point (PDP) will be contacted and if is authenticated request will be passed down the stream to Resource and response will be returned to the user. In some real-life scenario, Resource endpoint could be a static address for API, DataService, Web Service, etc. In other cases, the endpoint can be dynamically chosen and point to a different endpoint.
Scenario
- User token will be sent to API Gateway.
- On Gateway level token will be parsed to get information about the current user.
- User information will be sent to PDP and authenticated response will be sent back
- The flow will be continued and response will be returned
In my scenario, the request will use JWT token, I will use WSO2 APIM and it’s Gateway, for Policy decision point will use DataService, a part of WSO2EI.
Define Policy Decision Point
First I created a MySQL table called URL in POLICY_DB.
CREATE TABLE `URL` (
`ID` int(11) NOT NULL,
`EXT_ID` int(11) DEFAULT NULL,
`URL` varchar(250) COLLATE latin1_general_ci DEFAULT NULL,
PRIMARY KEY (`ID`),
UNIQUE KEY `EXT_ID_UNIQUE` (`EXT_ID`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_general_ci;
Idea is to expose this table as a web service. In param, I will send an EXT_ID and retrieve an URL.
In WSO2 Enterprise Integrator, click on Service -> Data Service -> Generate to start Wizard
Add Data service Name and click Next. In my case, I named it simple PDP.
In Datasources section click on Add New Datasource and populate field as :
Datasource Id: MySQLDB ( you can put any unique name)
Datasource Type: RDBMS (select from drop down menu)
Database Engine: MySQL (select from drop down menu)
Driver Class: com.mysql.jdbc.Driver
(NOTE: by default, there is no MySQL driver in [WSO2_EI_HOME]/lib folder. Download MySQL jdbc driver and save it and restart WSO2EI)
URL: jdbc:mysql://localhost:3306/POLICY_DB?useSSL=false
(MySql instance runs on the default port, change it accordingly)
User name:sqladmin
Password:xxxxx
Test connection and continue by clicking on Next. In Queries section click on Add New Query
Query ID: getURL
Datasource: MySQLDB (choose it from drop down menu)
SQL: select URL from URL where EXT_ID = :ext_id
Click on Generate Input Mapping and Generate Response to get two new sections. Leave default values. In Result section, I changed Group by element to Entry and Row name to Data.
Click on Save and Next to get to SOAP Operation section.
Click on Next to skip it (it is not necessary and for simplicity I want to have only REST Resource)
Click on Add New Resource
Resource Path: GetURL
Resource Method: GET
Query ID: getURL
(select value from drop drown list. It will automatically pull Query Parameter Name)
Click on Save and Finish.
Newly created service named PDP will be visible in Service List.
By default, service is published on port 8080 but in my case, the whole EI run with offset 7 my server is running on port 8087.
By clicking on the service name (PDP) in the list will pop up the window where you can edit the service by a wizard or in XML format, try the service out, get WSDL definition and even the Axis2 Clinet.
To test you can use:
- Try this service option
Populate request with some value from DataBase table and click on Send button to get response
- Postman
Define Gateway (WSO2 APIM)
Add claim (optional)
By default, JWT token will pick all profile information but if you want you can add more info by choosing one from claim list. I wanted to use http://wso2.org/claims/externalid claim but you can choose any that fits your needs.
In WSO2 APIM carbon portal choose Claims -> Add
Claim URL: https://wso2.org/claims/externalid
Display Name: External User ID
Description: Unique ID of the user used in external systems
Mapped Attribute(s): externalId
Display Order : 0
Supported by Default: true
When you click on Update button new claim will be visible under wso2.org/claim list
The implication of this that this filed will be available under the user profile.
Enable JWT generation
To authenticate end users, the API Manager passes attributes of the API invoker to the backend API implementation using JWT. In most production deployments, service calls go through the API Manager or a proxy service.
If you enable JWT generation in the API Manager, each API request will carry a JWT to the back-end service in X-JWT-Assertion (by default) HTTP header.
When the request goes through the API manager, the JWT is appended as a transport header to the outgoing message. The back-end service fetches the JWT and retrieves the required information about the user, application, or token.
To enable it <APIM_HOME>/repository/conf/api-manager.xml file uncomment <EnableJWTGeneration> property and set the value to true to enable JWT.
Create Class mediator for JWT
By default, APIM doesn’t have the mechanism to parse JWT that comes in request but we can write one and call in sequence.
(special thanks here goes to Naduni Pamudika, I customized her code to fit my needs)
Code is available on GitHub
https://github.com/lpastor74/wso2
In code, parsing the JWT was done to extract externalid (custom claim that I added in the user profile) but you can extract any info and make it exposable.
mvn clean install
command over the project will create two jar files :
- org-wso2-apim-jwt-decode-mediator-1.0-SNAPSHOT.jar
- wso2-jwt-decode-mediator-1.0-SNAPSHOT.jar
Copy wso2-jwt-decode-mediator-1.0-SNAPSHOT.jar in <APIM_HOME>/repository/components/lib folder and restart the APIM.
Create API and subscribe to it
Let’s create API that will be our point of interaction. We will use APIM publisher to create and APIM store to subscribe user.
Publisher part
Open API Publisher, by default it uses 9443 so in the browser call https://localhost:9443/publisher/ and login with default admin/admin credentials.
Note: 9443 is default port if you set up some offset you need to change port accordingly.
Click on ADD NEW API -> Design a NEW REST API -> Start Creating
Setup the first tab with the following :
Name: Dynamic
Context: ep
Version: 0.0.1
In API definition
URL pattern: info
Check GET and click on add
Click on Next: Implement button to save it and move to next tab
In Implement tab for Endpoint Type choose Dynamic Endpoint from drop down menu.
Select Enable Message Mediation checkbox and In Flow upload the following xml file
<sequence xmlns=”http://ws.apache.org/ns/synapse" name=”custom_in_flow”>
<log level=”full”>
<property name=”::IN_MESSAGE::” expression=”$trp:X-JWT-Assertion”/>
<property name=”IN_Authorization” expression=”$trp:Authorization”/>
</log>
<! — call class mediator and pass X-JWT-Assertion →
<class name=”org.wso2.apim.JwtDecodeMediator”>
<property name=”JWT_HEADER” expression=”$trp:X-JWT-Assertion”/>
</class>
<! — get externalID custom claim from JWT →
<log level=”custom”>
<property name=”externalID” expression=”get-property(‘externalID’)”/>
</log>
<! — call PDP (mysql DataService) with required attribute (externalID) →
<property name=”uri.var.temp” expression=”$ctx:externalID”/>
<call blocking=”true”>
<endpoint>
<http method=”get” uri-template=”https://localhost:8250/services/PDP/GetURL?ext_id={uri.var.temp}"/>
</endpoint>
</call>
<! — saved return value from PDP (url value) →
<enrich>
<source clone=”true” xmlns:n0=”http://ws.wso2.org/dataservice" xpath=”$body/n0:Entry/n0:Data/n0:URL”/>
<target property=”toURL” type=”property”/>
</enrich>
<log level=”custom”>
<property name=”..:: info — remote URL ::..” expression=”$ctx:toURL”/>
</log>
<! — remove postfix that is passed from API call →
<property name=”REST_URL_POSTFIX” scope=”axis2" action=”remove”/>
<! — change the target of call by seting ‘To’ header value →
<header name=”To” expression=”$ctx:toURL”/>
<! — change the end point addess for analitics purpose →
<property expression=”get-property(‘To’)” name=”ENDPOINT_ADDRESS” />
</sequence>
Click on Next:Manage to save it and move to next tab
In Throttling Settings section select at least one option (I selected Unlimited).
Click on Save and Published to get confirmation that you successfully published your API
You can click on Go to API Store or type https://localhost:9443/store in the browser. You can browser publicly visible API but to subscribe to one, you will need to login (use admin/admin)
Store part
By default, Default application is already created in store. You can create one by click on Add Application button
Name: customApp
Per Token Quota: 10PerMin
(I chose the 10 requests per minute to show throttling mechanism in action)
Once created, you can generate clientID and client secret for your application.
Subscribe app to API
Choose
Note: by default access token validity period is 3600 sec. If you put -1 the token will be valid forever (this is not suggested for production environment)
Select API and in right corner select your application and Tiers and click on Subscribe.
Once you Subscribe you will get a message.
In API console you can even try calling by click on Try It out or you call it on Postman.
Based on Token in API gateway JWT token is parsed and based on externalID flow will be redirected to different backend URL
Both users will call the same URL (ep/0.0.1/info) but user1 with Ext_ID 1974 will get pizza menu while user3 with Ext_ID 1950 will get a list of capitals and countries.
I hope this article was helpful in understanding how to use dynamic endpoint in WSO2 API manager. If there are any questions or if you run into any problems while trying this out, please post them in the comments section.
I will try and answer all the questions as soon as possible. If you think this article is helpful don’t forget to clap for it, that will make it easier for others to find it :).