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.

Authorization for API Security
Authorization for 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 to true - 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, and Doctor 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/