Authorization for OAuth2 driven API Security
Posted September 6, 2021 by Jatinder Singh ‐ 8 min read
Implements RBAC access control using Policy-based Authorization to protect OAuth2 driven API Security.

This blog post continues my excursion on the API Security subject and where we left off in my previous blog post. So to recap, the last blog provided a primer on OAuth 2.0 and OIDC and demoed how to employ OAuth 2.0 to secure access to API endpoints. This blog post will build on that foundation by introducing new technical security requirements for our MaDOC API and understanding and learning how to implement those requirements within the ForgeRock Platform.
A quick summary of what we will uncover:
Security Requirements
While we did a fine job in the previous post by implementing basic security requirements, our security team has proudly requested that we extend our security posture by introducing granular level access control to state “who” can access “what” endpoints.
But how do we implement such a requirement? Easy Peasy Lemon Squeezy.
Since each IG filter in our implementation already validates the provided access token for a given scope, we can easily control API endpoint access using OAuth2 scopes. Okay, that makes things a bit easier, but this is an authorization problem, as you may have already realized. And to successfully implement the requirements, we have to answer two questions - what Access Control to use? and how to enforce Authorization?
We will use RBAC as our access control model and use Policy-based Authorization coupled with IG to enforce scope-specific access. And if you need a primer on Policy-based Authorization, I have covered the topic in detail in my earlier blog post. I highly recommend reading and going through the exercise and companion source code.
So, let’s go through the high-level steps before deep-diving into the Postman-based demo:
- All steps from the previous post;
- OAuth2 Provider Service sets the
Use Policy Engine for Scope decisions
property totrue
- This essentially allows the Policy-based Authorization to kick-in and enforce policies around the available scopes; - Create Authorization Policies for each scope label;
- Create LDAP groups to implement the RBAC access control model;
P.S. All source-code including AM, IG, Directory Data, and Postman collection, are available in our Github repository. Please see the Github section below for details.
Demo
To make it easy for my audience to follow along, I will use my Postman collection for the demo. The folder that applies to this post in the Postman collection is called authz-oauth2-driven-api-security
. And as always, the folder is divided into Set-Up
and Demo
sub-folders. The below sections will utilize requests from each of these two folders.
Configure OAuth2 Services & Clients
As the first steps, we have to set up the OAuth2 services, Clients, and Policies within AM. Let’s run through steps 1 to 7 in the Set-Up
folder.
Step1: Service Account Login
Request
curl --location --request POST 'https://identity1.sqoopdata.local:17143/openam/json/realms/root/authenticate' \
--header 'Accept-API-Version: resource=2.1' \
--header 'X-OpenAM-Username: amadmin' \
--header 'X-OpenAM-Password: changeit'
Response
Note: Response truncated. Please download our Postman collection for complete details.
{
"tokenId": "EmR0wem5e4298pEmVSiPVsqKRFk.*AAJTSQACMDIAAlNLABxPM095RnRyVjdiU1V2cTBsZWFWT283Q2VJZlE9AAR0eXBlAANDVFMAAlMxAAIwMQ..*",
"successUrl": "/openam/console",
"realm": "/"
}
Step2: Create OAuth2 Service Provider
Request
Note: Request truncated. Please download our Postman collection for complete details.
curl --location --request POST 'https://identity1.sqoopdata.local:17143/openam/json/realms/emr/realm-config/services/oauth-oidc?_action=create' \
--header 'iplanetDirectoryPro: {{adminSSOToken}}' \
--header 'Content-Type: application/json' \
--header 'Accept-API-Version: resource=1.0' \
--header 'Cookie: amlbcookie=01; iPlanetDirectoryPro=EmR0wem5e4298pEmVSiPVsqKRFk.*AAJTSQACMDIAAlNLABxPM095RnRyVjdiU1V2cTBsZWFWT283Q2VJZlE9AAR0eXBlAANDVFMAAlMxAAIwMQ..*' \
--data-raw '{
"advancedOAuth2Config": {
"responseTypeClasses": [
"code|org.forgerock.oauth2.core.AuthorizationCodeResponseTypeHandler",
"device_code|org.forgerock.oauth2.core.TokenResponseTypeHandler",
"token|org.forgerock.oauth2.core.TokenResponseTypeHandler",
"id_token|org.forgerock.openidconnect.IdTokenResponseTypeHandler"
...
...
...
}'
Response
Note: Response truncated. Please download our Postman collection for complete details.
{
"_id": "",
"_rev": "1580707338",
"advancedOAuth2Config": {
"responseTypeClasses": [
"code|org.forgerock.oauth2.core.AuthorizationCodeResponseTypeHandler",
"id_token|org.forgerock.openidconnect.IdTokenResponseTypeHandler",
"device_code|org.forgerock.oauth2.core.TokenResponseTypeHandler",
"token|org.forgerock.oauth2.core.TokenResponseTypeHandler"
],
"grantTypes": [
"authorization_code",
"password"
],
...
...
}
Step3: Create Client Application (CA)
Request
Note: Request truncated. Please download our Postman collection for complete details.
curl --location --request PUT 'https://identity1.sqoopdata.local:17143/openam/json/realms/emr/realm-config/agents/OAuth2Client/client-application' \
--header 'Content-Type: application/json' \
--header 'X-Requested-With: ForgeRock Collection' \
--header 'Cookie: amlbcookie=01; iPlanetDirectoryPro=EmR0wem5e4298pEmVSiPVsqKRFk.*AAJTSQACMDIAAlNLABxPM095RnRyVjdiU1V2cTBsZWFWT283Q2VJZlE9AAR0eXBlAANDVFMAAlMxAAIwMQ..*' \
--data-raw '{
"coreOAuth2ClientConfig": {
"userpassword": "password",
"scopes": {
"inherited": false,
"value": [
"users",
"appointments",
"healthrecords"
]
...
...
...
}'
Response
Note: Response truncated. Please download our Postman collection for complete details.
{
"_id": "client-application",
"_rev": "1376747112",
"coreOAuth2ClientConfig": {
"userpassword": null,
"loopbackInterfaceRedirection": {
"inherited": false,
"value": false
},
...
...
}
Step4: Create Resource Server (RS)
Request
Note: Request truncated. Please download our Postman collection for complete details.
curl --location --request PUT 'https://identity1.sqoopdata.local:17143/openam/json/realms/emr/realm-config/agents/OAuth2Client/resource-server' \
--header 'Content-Type: application/json' \
--header 'X-Requested-With: ForgeRock Collection' \
--header 'Cookie: amlbcookie=01; iPlanetDirectoryPro=EmR0wem5e4298pEmVSiPVsqKRFk.*AAJTSQACMDIAAlNLABxPM095RnRyVjdiU1V2cTBsZWFWT283Q2VJZlE9AAR0eXBlAANDVFMAAlMxAAIwMQ..*' \
--data-raw '{
"coreOAuth2ClientConfig": {
"userpassword": "password",
"scopes": {
"inherited": false,
"value": [
"am-introspect-all-tokens"
]
},
...
...
}'
Response
Note: Response truncated. Please download our Postman collection for complete details.
{
"_id": "resource-server",
"_rev": "-1497047801",
"coreOAuth2ClientConfig": {
"userpassword": null,
"loopbackInterfaceRedirection": {
"inherited": false,
"value": false
},
...
...
}
Step5: Create Policy for Users Scope
Request
Note: Request truncated. Please download our Postman collection for complete details.
curl --location --request POST 'https://identity1.sqoopdata.local:17143/openam/json/realms/root/realms/emr/policies?_action=create' \
--header 'Content-Type: application/json' \
--header 'Accept-API-Version: resource=1.0' \
--header 'Cookie: amlbcookie=01; iPlanetDirectoryPro=EmR0wem5e4298pEmVSiPVsqKRFk.*AAJTSQACMDIAAlNLABxPM095RnRyVjdiU1V2cTBsZWFWT283Q2VJZlE9AAR0eXBlAANDVFMAAlMxAAIwMQ..*' \
--data-raw '{
"name": "UserMgmtPolicies",
"active": true,
"description": "Policy determines who can perform user management functionality. E.g. patient registration",
"applicationName": "oauth2Scopes",
...
}'
Response
Note: Response truncated. Please download our Postman collection for complete details.
{
"_id": "UserMgmtPolicies",
"_rev": "1630615064416",
"name": "UserMgmtPolicies",
"active": true,
"description": "Policy determines who can perform user management functionality. E.g. patient registration",
"resources": [
"users"
],
"applicationName": "oauth2Scopes",
...
}
Step6: Create Policy for Appointments Scope
Request
Note: Request truncated. Please download our Postman collection for complete details.
curl --location --request POST 'https://identity1.sqoopdata.local:17143/openam/json/realms/root/realms/emr/policies?_action=create' \
--header 'Content-Type: application/json' \
--header 'Accept-API-Version: resource=1.0' \
--header 'Cookie: amlbcookie=01; iPlanetDirectoryPro=EmR0wem5e4298pEmVSiPVsqKRFk.*AAJTSQACMDIAAlNLABxPM095RnRyVjdiU1V2cTBsZWFWT283Q2VJZlE9AAR0eXBlAANDVFMAAlMxAAIwMQ..*' \
--data-raw '{
"name": "AppointmentPolicies",
...
}'
Response
Note: Response truncated. Please download our Postman collection for complete details.
{
"_id": "AppointmentPolicies",
"_rev": "1630615105815",
"name": "AppointmentPolicies",
"active": true,
"description": "Policy determines who can manage appointments functionality",
"resources": [
"appointments"
],
...
}
Step7: Create Policy for HealthRecords Scope
Request
Note: Request truncated. Please download our Postman collection for complete details.
curl --location --request POST 'https://identity1.sqoopdata.local:17143/openam/json/realms/root/realms/emr/policies?_action=create' \
--header 'Content-Type: application/json' \
--header 'Accept-API-Version: resource=1.0' \
--header 'Cookie: amlbcookie=01; iPlanetDirectoryPro=EmR0wem5e4298pEmVSiPVsqKRFk.*AAJTSQACMDIAAlNLABxPM095RnRyVjdiU1V2cTBsZWFWT283Q2VJZlE9AAR0eXBlAANDVFMAAlMxAAIwMQ..*' \
--data-raw '{
"name": "HealthRecordPolicies",
...
}'
Response
Note: Response truncated. Please download our Postman collection for complete details.
{
"_id": "HealthRecordPolicies",
"_rev": "1630615117205",
"name": "HealthRecordPolicies",
"active": true,
"description": "Policy determines who can manage health records functionality",
"resources": [
"healthrecords"
],
...
}
Let’s Test
With the prerequisite OAuth2 infrastructure set up, we are all set to run through the demo steps. The Demo
folder has a total of three steps from 8 - 10. Steps 8 and 9 have variances and can vary based on the test case. Let’s define our test cases:
Admin
,Patient
, andDoctor
can access/appointments
endpoint.- Only
Doctor
should be able to access/healthrecords
endpoint;
NOTE I will only test the Patient
test case and leave the rest for my audience to try using the companion source code. If you have any questions, please feel free to comment below.
Test Case: Patient should be able to access the Appointments endpoint.
**Request: Step8.2 **
Authenticate as Patient
to initiate the OAuth2 flow.
curl --location --request POST 'https://identity1.sqoopdata.local:17143/openam/json/realms/root/realms/emr/authenticate' \
--header 'Content-Type: application/json' \
--header 'X-OpenAM-Username: patient' \
--header 'X-OpenAM-Password: bXm*.0.5{DU!' \
--header 'Accept-API-Version: resource=2.0, protocol=1.0' \
--data-raw '{}'
Response
{
"tokenId": "ktJbwnKvYVX7A5m2VHN8rdAvaiY.*AAJTSQACMDIAAlNLABxEazhKbHZCMHMza2dhdDcyeGZTMy9rbkpOeHM9AAR0eXBlAANDVFMAAlMxAAIwMQ..*",
"successUrl": "/openam/console",
"realm": "/emr"
}
Request: Step9.2
Invoke the authorization process by sending a request to the /authorize
endpoint.
curl --location --request POST 'https://identity1.sqoopdata.local:17143/openam/oauth2/realms/root/realms/emr/authorize' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--header 'Cookie: amlbcookie=01; iPlanetDirectoryPro=ktJbwnKvYVX7A5m2VHN8rdAvaiY.*AAJTSQACMDIAAlNLABxEazhKbHZCMHMza2dhdDcyeGZTMy9rbkpOeHM9AAR0eXBlAANDVFMAAlMxAAIwMQ..*' \
--data-urlencode 'scope=appointments' \
--data-urlencode 'response_type=code' \
--data-urlencode 'client_id=client-application' \
--data-urlencode 'csrf=ktJbwnKvYVX7A5m2VHN8rdAvaiY.*AAJTSQACMDIAAlNLABxEazhKbHZCMHMza2dhdDcyeGZTMy9rbkpOeHM9AAR0eXBlAANDVFMAAlMxAAIwMQ..*' \
--data-urlencode 'redirect_uri=https://ig1.sqoopdata.local:17193/oauth2' \
--data-urlencode 'state=abc123' \
--data-urlencode 'decision=allow'
Response
AS issues a 302 response with an authorization code.
https://ig1.sqoopdata.local:17193/oauth2?code=6LlckM-G9x0X3TyrGAfYNtDwdnY&iss=https%3A%2F%2Fidentity1.sqoopdata.local%3A17143%2Fopenam%2Foauth2%2Frealms%2Froot%2Frealms%2Femr&state=abc123&client_id=client-application
Request: Step10
Get the access token using the authorization code obtained in the earlier request.
curl --location --request POST 'https://identity1.sqoopdata.local:17143/openam/oauth2/realms/root/realms/emr/access_token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=authorization_code' \
--data-urlencode 'code=6LlckM-G9x0X3TyrGAfYNtDwdnY' \
--data-urlencode 'client_id=client-application' \
--data-urlencode 'client_secret=password' \
--data-urlencode 'redirect_uri=https://ig1.sqoopdata.local:17193/oauth2'
Response
{
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIodXNyIXBhdGllbnQpIiwiY3RzIjoiT0FVVEgyX1NUQVRFTEVTU19HUkFOVCIsImF1dGhfbGV2ZWwiOjAsImF1ZGl0VHJhY2tpbmdJZCI6IjQ0ZmJlNjkwLWFmMzUtNDE4Zi04MTYyLWY1YTQxNGEzNjIyMy03NzEiLCJzdWJuYW1lIjoicGF0aWVudCIsImlzcyI6Imh0dHBzOi8vaWRlbnRpdHkxLnNxb29wZGF0YS5sb2NhbDoxNzE0My9vcGVuYW0vb2F1dGgyL3JlYWxtcy9yb290L3JlYWxtcy9lbXIiLCJ0b2tlbk5hbWUiOiJhY2Nlc3NfdG9rZW4iLCJ0b2tlbl90eXBlIjoiQmVhcmVyIiwiYXV0aEdyYW50SWQiOiI1Y1lKWXh5Z0RzZ0lqRmFVczVrbWx5NkVuWjQiLCJhdWQiOiJjbGllbnQtYXBwbGljYXRpb24iLCJuYmYiOjE2MzEwMjcxNzEsImdyYW50X3R5cGUiOiJhdXRob3JpemF0aW9uX2NvZGUiLCJzY29wZSI6WyJhcHBvaW50bWVudHMiXSwiYXV0aF90aW1lIjoxNjMxMDI3MTY0LCJyZWFsbSI6Ii9lbXIiLCJleHAiOjE2MzEwMzA3NzEsImlhdCI6MTYzMTAyNzE3MSwiZXhwaXJlc19pbiI6MzYwMCwianRpIjoiaWNtNUk4ODZSa2IyaVJ0alhrUlRMaV9yWWI4In0.1tB7iO4NTMcIi9WKcj5QE0Nyono2y_5nyl8G5wYN5l0",
"scope": "appointments",
"token_type": "Bearer",
"expires_in": 3599
}
Request: Get Appointments
Now that we have the access token from the AS server, let’s attempt to access the /appointments
endpoint to test if Patient
has access.
curl --location --request GET 'https://ig1.sqoopdata.local:17193/appointments?username=patient' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIodXNyIXBhdGllbnQpIiwiY3RzIjoiT0FVVEgyX1NUQVRFTEVTU19HUkFOVCIsImF1dGhfbGV2ZWwiOjAsImF1ZGl0VHJhY2tpbmdJZCI6IjQ0ZmJlNjkwLWFmMzUtNDE4Zi04MTYyLWY1YTQxNGEzNjIyMy03NzEiLCJzdWJuYW1lIjoicGF0aWVudCIsImlzcyI6Imh0dHBzOi8vaWRlbnRpdHkxLnNxb29wZGF0YS5sb2NhbDoxNzE0My9vcGVuYW0vb2F1dGgyL3JlYWxtcy9yb290L3JlYWxtcy9lbXIiLCJ0b2tlbk5hbWUiOiJhY2Nlc3NfdG9rZW4iLCJ0b2tlbl90eXBlIjoiQmVhcmVyIiwiYXV0aEdyYW50SWQiOiI1Y1lKWXh5Z0RzZ0lqRmFVczVrbWx5NkVuWjQiLCJhdWQiOiJjbGllbnQtYXBwbGljYXRpb24iLCJuYmYiOjE2MzEwMjcxNzEsImdyYW50X3R5cGUiOiJhdXRob3JpemF0aW9uX2NvZGUiLCJzY29wZSI6WyJhcHBvaW50bWVudHMiXSwiYXV0aF90aW1lIjoxNjMxMDI3MTY0LCJyZWFsbSI6Ii9lbXIiLCJleHAiOjE2MzEwMzA3NzEsImlhdCI6MTYzMTAyNzE3MSwiZXhwaXJlc19pbiI6MzYwMCwianRpIjoiaWNtNUk4ODZSa2IyaVJ0alhrUlRMaV9yWWI4In0.1tB7iO4NTMcIi9WKcj5QE0Nyono2y_5nyl8G5wYN5l0' \
--header 'Cookie: amlbcookie=01; iPlanetDirectoryPro=KnTqDw0dP6wWCHFptt5evU_LRPc.*AAJTSQACMDIAAlNLABwxVUhWQXBKSVhmTDc5bFFJM0pKdXZqd0hqVTg9AAR0eXBlAANDVFMAAlMxAAIwMQ..*'
Response
Viola! We get a successful response, and the system returns a list of appointments for this patient.
[
{
"apptId": 3,
"startTime": "2021-09-08T17:40:21.362898Z",
"endTime": "2021-09-08T17:50:21.362898Z",
"patient": "patient",
"createdBy": "admin",
"created": "2021-09-07T15:02:06.920302Z"
}
]
Request: Get Health Records
Let’s check if the Patient
has access to the /healthrecords
endpoint.
curl --location --request GET 'https://ig1.sqoopdata.local:17193/healthrecords?username=patient' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIodXNyIXBhdGllbnQpIiwiY3RzIjoiT0FVVEgyX1NUQVRFTEVTU19HUkFOVCIsImF1dGhfbGV2ZWwiOjAsImF1ZGl0VHJhY2tpbmdJZCI6IjQ0ZmJlNjkwLWFmMzUtNDE4Zi04MTYyLWY1YTQxNGEzNjIyMy03NzEiLCJzdWJuYW1lIjoicGF0aWVudCIsImlzcyI6Imh0dHBzOi8vaWRlbnRpdHkxLnNxb29wZGF0YS5sb2NhbDoxNzE0My9vcGVuYW0vb2F1dGgyL3JlYWxtcy9yb290L3JlYWxtcy9lbXIiLCJ0b2tlbk5hbWUiOiJhY2Nlc3NfdG9rZW4iLCJ0b2tlbl90eXBlIjoiQmVhcmVyIiwiYXV0aEdyYW50SWQiOiI1Y1lKWXh5Z0RzZ0lqRmFVczVrbWx5NkVuWjQiLCJhdWQiOiJjbGllbnQtYXBwbGljYXRpb24iLCJuYmYiOjE2MzEwMjcxNzEsImdyYW50X3R5cGUiOiJhdXRob3JpemF0aW9uX2NvZGUiLCJzY29wZSI6WyJhcHBvaW50bWVudHMiXSwiYXV0aF90aW1lIjoxNjMxMDI3MTY0LCJyZWFsbSI6Ii9lbXIiLCJleHAiOjE2MzEwMzA3NzEsImlhdCI6MTYzMTAyNzE3MSwiZXhwaXJlc19pbiI6MzYwMCwianRpIjoiaWNtNUk4ODZSa2IyaVJ0alhrUlRMaV9yWWI4In0.1tB7iO4NTMcIi9WKcj5QE0Nyono2y_5nyl8G5wYN5l0' \
--header 'Cookie: amlbcookie=01; iPlanetDirectoryPro=KnTqDw0dP6wWCHFptt5evU_LRPc.*AAJTSQACMDIAAlNLABwxVUhWQXBKSVhmTDc5bFFJM0pKdXZqd0hqVTg9AAR0eXBlAANDVFMAAlMxAAIwMQ..*'
Response
Yay! The access is denied as instructed via our policy.
WWW-Authenticate: Bearer realm="OpenIG",error_description="The request requires higher privileges than provided by the access token.",scope="healthrecords",error="insufficient_scope"
Status: 403 Forbidden
Github
The companion source code for this blog post includes the following:
You can find the source code at our repository hosted at Github here.
You can find the source code for the MaDOC API at our Github here.
Summary
In this blog post, we learned about Authorization for OAuth 2.0 driven API security. In addition, the post introduced using easy-to-follow Postman collection on how easy it is to configure Policy-based authorization to drive access control for any API. Finally, we built on my earlier post’s foundation and successfully built access control around the Appointments
, Users
, and HealthRecords
endpoints.
For any further queries, feel free to post your comments; we are happy to help!
Meanwhile…
I highly recommend going through the companion source code for various examples and source code snippets.
Keep ForgeRocking and Keep Learning!
References
OAuth2 Guide https://backstage.forgerock.com/docs/am/7.1/oauth2-guide/
Authorization Guide https://backstage.forgerock.com/docs/am/7.1/authorization-guide/