Integrate AWS API Gateway, Lambda & ForgeRock OpenAM Role-based access control (RBAC) Authorization

Amazon API Gateway is a fully managed service that makes it easy for developers to create, publish, maintain, monitor, and secure APIs at any scale.

AWS Lambda lets you run code without provisioning or managing servers. With Lambda, you can run code for virtually any type of application or backend service — all with zero administration.

ForgeRock OpenAM provides a service called access management, which manages access to resources, such as a web page, an application, or web service, available over the network. AM centralizes access control by handling both authentication and authorization.

AWS Lambda is being used by many companies to perform complex functionalities and exposed as-a-service using AWS API Gateway. One of the main challenges is to secure these APIs using the organization’s Identity & Access Management application. API Gateway provides multiple options to secure the APIs and one of the option is to use a custom authorizer. Custom authorizer is a Lambda function that can control access to the API endpoints. ForgeRock AM provides Authorization framework where policies can be defined to control access to a resource.

In this blog, we are going to integrate API Gateway custom authorizer with ForgeRock AM’s RBAC Authorization.

Sample Use Case :

API Gateway exposes APIs for performing CRUD operations on DynamoDB through Lambda functions. For ex: creating, updating, deleting product details in DynamoDB. Let us assume that this organization uses ForgeRock AM to secure their web applications which makes API calls to AWS. When the applications are using these APIs, API Gateway should use the AM’s ssotoken to authorize the API calls. Below are the authorization requirements:

Below is the architecture diagram:

In order to setup this application, there are few prerequisites:

ForgeRock AM Configuration

First step is to configure policies, groups and users in OpenAM. Please follow the below steps to configure AM:

  • Login to OpenAM console with amadmin credentials and create a new realm called products
  • Go to products realm and click “Subjects”
  • By default, a demo user will be there. Now, let us create few groups as per the requirement. Select “Group” tab.
  • Click “New” and create a new group called “PRODUCT_READ”
  • Similarly, create PRODUCT_ADMIN, PRODUCT_DELETE and PRODUCT_WRITE groups.
  • Create one more group called “POLICY_USERS”. Now, you should see 5 groups.
  • Select “Privileges” tab in navigation bar and select the “POLICY_USERS” group link. That should open the privileges page for this group.
  • Select “REST calls for policy evaluation” checkbox and save the changes.
  • Again, select “Subjects” tab and click “New” user button. We are going to create multiple users and assign to different groups.
  • Let us create a “policyuser”. This is like a service account which will be used for invoking OpenAM’s policy evaluation REST API.
  • After creating the user, again select “policyuser” link from “Subjects” tab. In the user profile page, select “Group” tab and assign this user to “POLICY_USERS” group. Save the changes and go “Back to Subjects” tab.
  • Create another user “productread” and assign it to “PRODUCT_READ” group. Similarly, create “productwrite” user and assign it to “PRODUCT_WRITE” group, create “productdelete” user and assign it to “PRODUCT_DELETE” group, create “productadmin” user and assign it to “PRODUCT_ADMIN” group. You should see the below list of users:
  • Select “policies” tab in navigation bar.
  • Select “Authorization > Resource Types” and create “New Resource Type”
  • Name it as “PRODUCT_RS”. Add a new URL pattern “ https://*.execute-api.*.amazonaws.com/product/*?action=*” and add 3 actions — GET, PUT and DELETE. Click “Create” to create this resource type. This is the URL pattern for AWS API Gateway. Refer this link for more details.
  • Select “Authorization > Policy Sets” and click “Add a policy set”
  • Name it as PRODUCT_PS and select Resource Types as “PRODUCT_RS” from drop-down.
  • From policy set screen, select “Add a Policy”.
  • Name the policy as GET_PRODUCT_PY, select Resource Type : “PRODUCT_RS”, select the URL pattern from “Resources” and update the value action=GET. Click “Add” button to add the resource and “Create” button to create the policy.
  • Select “Actions” tab for GET_PRODUCT_PY policy and select “GET” from “Add an Action” drop-down. Then “Save Changes”
  • Select “Subjects” tab for GET_PRODUCT_PY policy. Change “Type” to “Authenticated Users” and “Add a Subject Condition” of Type “Users & Groups” with “Group Subjects” set to PRODUCT_READ. Then “Save Changes”
  • Select “Response Attributes” tab and add “uid” in “SUBJECT ATTRIBUTES”. Then “Save Changes”
  • Select “Summary” tab and you should see the below screen. This policy basically says that only authenticated users belonging to “PRODUCT_READ” group can perform “GET” operation on the product API.
  • Similarly, add 2 more policies “PUT_PRODUCT_PY” and “DELETE_PRODUCT_PY” mapped to different groups “PRODUCT_WRITE” and “PRODUCT_DELETE”.
  • Create one more policy for PRODUCT_ADMIN_PY. For this policy, add all three actions “GET”, “PUT”, “DELETE” and map it to “PRODUCT_ADMIN” group. As per this policy, any user part of PRODUCT_ADMIN can perform all the operations on product.
  • Test AM authentication using curl command for one of the user created during OpenAM configuration. Replace <AM URL> and <password> accordingly. You should get a tokenId if it works fine.
curl -X POST \
http://<AM URL>/auth/json/realms/root/realms/products/authenticate \
-H 'content-type: application/json' \
-H 'x-openam-password: <password>' \
-H 'x-openam-username: productread'
{"tokenId":"Ay7tDxn--P91M24icGf0Sd8RtVM.*AAJTSQACMDEAAlNLABx2VDVnUFRxV28yNFl5MEQ3QkRVSEh6ZlJuY3M9AAJTMQAA*","successUrl":"/auth/console","realm":"/products"}

AWS Environment Setup

  • Create a new bucket in AWS, preferably in US East (N. Virginia) region.
  • Upload ProductLambda/deployment/product_api.zip to the bucket
  • Upload ProductLambda/ products_apidef.yml to the bucket.
  • Upload OpenAMLambdaAuthorizer/deployment/OpenAMAuthorizer.zip to the bucket. You should see 3 files in the bucket:
  • Setup AWS CLI in your local OS. Follow this link.
  • To test the setup, execute the below command and check if you are seeing all the files that were uploaded.
aws s3 ls s3://<bucket name>
2018-04-01 15:04:36 8551389 OpenAMAuthorizer.zip
2018-04-01 15:00:16 824 product_api.zip
2018-04-01 15:02:31 1588 products_apidef.yml
  • Go to the local source code directory and change directory to ProductLambda folder. product_samtemplate.yml is the SAM template for setting up API Gateway and AWS Lambda. It is IaC (Infrastructure as Code) for serverless applications. For more details, refer AWS Serverless Application Model (AWS SAM).
  • Execute the below command to generate the cloudformation template. This should generate product-serverless-output.yml file. Replace <bucket_name> accordingly.
aws cloudformation package --template-file product_samtemplate.yml --output-template-file product-serverless-output.yml --s3-bucket <bucket_name>
  • Execute the below command to setup API Gateway, AWS Lambda functions using CloudFormation. Replace the following values in paramter
aws cloudformation deploy --template-file product-serverless-output.yml --stack-name ProductLambdaAPI --parameter-overrides BucketName=<bucket_name> OpenAMURL=<OPENAM_URL> PolicyPwd=<password> PolicyUser=policyuser --capabilities CAPABILITY_IAM
  • After executing the above command, login to AWS console and navigate to CloudFormation service. You should see a new stack “ProductLambdaAPI” and monitor the status of this deployment.
  • If it is successful, navigate to API Gateway service and check whether it created a new endpoint for “ProductLambdaAPI”.
  • Navigate to AWS Lambda and you should see 4 Lambda functions deployed — 3 functions for GET, PUT, DELETE for Product operations on DynamoDB and another Lambda function called OpenAM Authorizer. You can view the details of each function by clicking the link.

Testing

  • Note down the ApiUrl value from “Outputs” tab in CloudFormation template. Another way to get this URL is to navigate to “Api Gateway Service > ProductLambdaAPI > Stages > Dev”. You will see a value called “Invoke URL” which should be used for invoking these APIs.
  • First step is to authenticate against OpenAM using “productwrite” user and get a SSOToken. Use the below curl command. Replace the <OPENAM_URL> and <password> value accordingly. You should get a valid tokenId.
curl -X POST \
<OPENAM_URL>/json/realms/root/realms/products/authenticate \
-H 'content-type: application/json' \
-H 'x-openam-password: <password>' \
-H 'x-openam-username: productwrite'
{"tokenId":"N3uPxuKAa-W-O6-EKJ0IOg4RZxE.*AAJTSQACMDEAAlNLABxWMXNONUtjSmJJTzBxdXovaWZLVmNTVGp0MjQ9AAJTMQAA*","successUrl":"/auth/console","realm":"/products"}
  • Next step is to create a product using PUT request. When this PUT request is made, API Gateway’s Lambda Authorizer makes a call to OpenAM to evaluate the policy for the SSOToken passed in Authorization header. Replace <API Gateway Invoke URL> with the value taken in first step. Note the /product/1 in the URL. That is the actual endpoint which got created as part of deployment process.
curl -X PUT \
<API Gateway Invoke URL>/product/1 \
-H 'Authorization: jrLO1DM2c9fxQXcRfsOauuNo0x4.*AAJTSQACMDEAAlNLABxVTzZqMVRWK2lKOTJoc08vRnpuVTQ1VjNFeEE9AAJTMQAA*' \
-H 'Content-Type: application/json' \
-d '{
"id" : 1,
"name" : "iphone",
"seller" : "apple"
}'
  • Navigate to AWS DynamoDB service and check if this record got created. You can try creating multiple products by invoking the URL with /product/2, passing “id” as 2 and so on.
  • Try to retrieve the product details using GET request with same SSOToken. You will get a HTTP 403 response. This is because “GET_PRODUCT_PY” policy in OpenAM allows only users belonging to PRODUCT_READ group to perform GET operation. Only “productread” user was added in that group.
curl -X GET \
<API Gateway Invoke URL>/product/1 \
-H 'Authorization: jrLO1DM2c9fxQXcRfsOauuNo0x4.*AAJTSQACMDEAAlNLABxVTzZqMVRWK2lKOTJoc08vRnpuVTQ1VjNFeEE9AAJTMQAA*' \
-H 'Content-Type: application/json'
{"Message":"User is not authorized to access this resource with an explicit deny"}
  • Now, authenticate using “productread” user
curl -X POST \
<OPENAM_URL>/json/realms/root/realms/products/authenticate \
-H 'content-type: application/json' \
-H 'x-openam-password: <password>' \
-H 'x-openam-username: productread'
{"tokenId":"cwk3gG-ejekPZkJRjT68t9lwzIk.*AAJTSQACMDEAAlNLABxNZ2l1Q1VPVll1bFJkRURLeEw2ZEUyOWgyRE09AAJTMQAA*","successUrl":"/auth/console","realm":"/products"}
  • Use “productread” user’s token to perform GET operation. You should see the json response with the product details.
curl -X GET \
<API Gateway Invoke URL>/product/1 \
-H 'Authorization: cwk3gG-ejekPZkJRjT68t9lwzIk.*AAJTSQACMDEAAlNLABxNZ2l1Q1VPVll1bFJkRURLeEw2ZEUyOWgyRE09AAJTMQAA*' \
-H 'Content-Type: application/json'
{
"id" : 1,
"name" : "iphone",
"seller" : "apple"
}
  • Try DELETE request with the “productread” user’s SSOToken. It will fail with HTTP 403 because as per “DELETE_PRODUCT_PY” OpenAM policy, only users belonging to PRODUCT_DELETE can perform DELETE operation.
curl -X DELETE \
<API Gateway Invoke URL>/product/1 \
-H 'Authorization: cwk3gG-ejekPZkJRjT68t9lwzIk.*AAJTSQACMDEAAlNLABxNZ2l1Q1VPVll1bFJkRURLeEw2ZEUyOWgyRE09AAJTMQAA*' \
-H 'Content-Type: application/json'
{"Message":"User is not authorized to access this resource with an explicit deny"}
  • Now, authenticate using “productdelete” user and get the token value.
  • Try DELETE request again with “productdelete” user’s token.
curl -X DELETE \
<API Gateway Invoke URL>/product/1 \
-H 'Authorization: ghAtrj7kMDSMgNIpNt5dqMwIOrs.*AAJTSQACMDEAAlNLABx0UzhGaDRFLzlmRzdHQ2hsajBaakJqdFFRUnM9AAJTMQAA*' \
-H 'Content-Type: application/json'
  • Try GET operation again using “productread” user’s token and you will get a “ITEM NOT FOUND” response with HTTP 404.
curl -X GET \
<API Gateway Invoke URL>/product/1 \
-H 'Authorization: cwk3gG-ejekPZkJRjT68t9lwzIk.*AAJTSQACMDEAAlNLABxNZ2l1Q1VPVll1bFJkRURLeEw2ZEUyOWgyRE09AAJTMQAA*' \
-H 'Content-Type: application/json'
ITEM NOT FOUND
  • Now, lets try the PUT, GET and DELETE with “productadmin” user’s SSOToken. As per “PRODUCT_ADMIN_PY” OpenAM policy, any user belonging to “PRODUCT_ADMIN” group can perform all the operations.
curl -X POST \
<OPENAM_URL>/json/realms/root/realms/products/authenticate \
-H 'content-type: application/json' \
-H 'x-openam-password: <password>' \
-H 'x-openam-username: productadmin'
{"tokenId":"3nUbYApVfir-Oduj-8AGjQk2-qs.*AAJTSQACMDEAAlNLABx6ZVJQZ2puMm5uallVTFZ3VXIweW9aQm96ME09AAJTMQAA*","successUrl":"/auth/console","realm":"/products"}-----------------------------------------------------------------
curl -X PUT \
<API Gateway Invoke URL>/product/1 \
-H 'Authorization: 3nUbYApVfir-Oduj-8AGjQk2-qs.*AAJTSQACMDEAAlNLABx6ZVJQZ2puMm5uallVTFZ3VXIweW9aQm96ME09AAJTMQAA*' \
-H 'Content-Type: application/json' \
-d '{
"id" : 1,
"name" : "iphone",
"seller" : "apple"
}'
-----------------------------------------------------------------
curl -X GET \
<API Gateway Invoke URL>/product/1 \
-H 'Authorization: 3nUbYApVfir-Oduj-8AGjQk2-qs.*AAJTSQACMDEAAlNLABx6ZVJQZ2puMm5uallVTFZ3VXIweW9aQm96ME09AAJTMQAA*' \
-H 'Content-Type: application/json'
{
"id" : 1,
"name" : "iphone",
"seller" : "apple"
}
-----------------------------------------------------------------
curl -X DELETE \
<API Gateway Invoke URL>/product/1 \
-H 'Authorization: 3nUbYApVfir-Oduj-8AGjQk2-qs.*AAJTSQACMDEAAlNLABx6ZVJQZ2puMm5uallVTFZ3VXIweW9aQm96ME09AAJTMQAA*' \
-H 'Content-Type: application/json'
-----------------------------------------------------------------
curl -X GET \
<API Gateway Invoke URL>/product/1 \
-H 'Authorization: 3nUbYApVfir-Oduj-8AGjQk2-qs.*AAJTSQACMDEAAlNLABx6ZVJQZ2puMm5uallVTFZ3VXIweW9aQm96ME09AAJTMQAA*' \
-H 'Content-Type: application/json'
ITEM NOT FOUND
  • This concludes the testing. You can play around with the policies and see how the API permissions change dynamically. Let us slightly modify the policies to allow users belonging to “PRODUCT_WRITE” group to perform GET operation also.
  • Navigate to OpenAM > Realms > products > Authorization > Policy Sets > PRODUCT_PS > GET_PRODUCT_PY. Select “Subjects” tab and add a Logical OR condition for “Users & Groups” type. Now, this policy allows both “PRODUCT_READ” and “PRODUCT_WRITE” group to perform GET operation.
  • Now, authenticate using “productwrite” user and use that SSOToken in Authorization header to perform a “PUT” followed by a “GET” for the same product ID. It should work because “GET_PRODUCT_PY” policy is mapped to both “PRODUCT_READ” and “PRODUCT_WRITE” groups.
  • This is the greatest advantage of using external policies. Without modifying any configuration on AWS API Gateway, the permissions of the users invoking these policies can be dynamically modified using Role Based Access Control (RBAC)

Additional Tips

  • In AWS API Gateway custom Authorizer setup, “Authorization Caching” is not enabled. This can be enabled with a TTL value to improve the Authorizer performance. This was disabled purposefully for this POC so that we can change the OpenAM policies and immediately see the results on API Gateway side. Otherwise, we have to wait till the TTL (seconds) before Authorizer evaluates the OpenAM policy again. Whether to enable or not depends on the Organization’s business requirements, security guidelines and other factors like the operations that these APIs perform. May be a PUT / DELETE operation might be more costlier than a GET operation and hence it requires a policy evaluation for every request.
  • For the OpenAM Authorizer Lambda function, there are 3 “Environment Variables” — OpenAM URL, POLICY_EVAL_UNAME and POLICY_EVAL_PWD. These are not encrypted for POC purpose. But for production deployments, best practice is to encrypt these values using KMS key and pass those values to Lambda function. In Lamda function, these values can be decrypted using SDK functions.

Troubleshooting Tips

  • This entire setup is based on OpenAM 5.5. In case you are using previous versions like v12.x, v13.x, all the API calls in OpenAMAuthorizer.java needs to be modified accordingly and recompiled. Also, the curl requests for /json/authenticate is slightly different for previous versions.
  • curl commands provided in Testing section works fine with Windows. It might fail in Mac / Linux Terminal. If it fails, most probably it is due to some formatting issues.
  • If there are any issues in invoking the APIs, navigate to CloudWatch service and analyse the logs for “OpenAMAuthorizer” Lambda function. If additional logs needs to be added, modify “OpenAMAuthorizer.java” and rebuild the deployment zip package. Messages to CloudWatch can be logged using below API:
context.getLogger().log("Message to be logged");
  • If you are getting 403 error repeatedly, you can manually invoke the OpenAM Authorization APIs to check if policies are setup properly. To manually evaluate the policies, use the below curl commands:
Authenticate using policyuser:curl -X POST \
<OPENAM_URL>/json/realms/root/realms/products/authenticate \
-H 'content-type: application/json' \
-H 'x-openam-password: <password>' \
-H 'x-openam-username: policyuser'
{"tokenId":"3nUbYApVfir-Oduj-8AGjQk2-qs.*AAJTSQACMDEAAlNLABx6ZVJQZ2puMm5uallVTFZ3VXIweW9aQm96ME09AAJTMQAA*","successUrl":"/auth/console","realm":"/products"}-------------------------------------------------------------Authenticate using productwrite or some other user depending on GET, PUT or DELETE requests:curl -X POST \
<OPENAM_URL>/json/realms/root/realms/products/authenticate \
-H 'content-type: application/json' \
-H 'x-openam-password: <password>' \
-H 'x-openam-username: productwrite'
{"tokenId":"3nUbYApVfir-Oduj-8AGjQk2-qs.*AAJTSQACMDEAAlNLABx6ZVJQZ2puMm5uallVTFZ3VXIweW9aQm96ME09AAJTMQAA*","successUrl":"/auth/console","realm":"/products"}
-------------------------------------------------------------
Evaluate policy by passing policyuser SSOToken in iPlanetDirectoryPro header and productwrite SSOToken in "subject" json attribute in body. If the policies are configured properly, response should include actions and attributes values. curl -X POST \
http://openamserver1.us-east-1.elasticbeanstalk.com/auth/json/realms/root/realms/products/policies?_action=evaluate \
-H 'iPlanetDirectoryPro:SJdHct6MieN4Ms1Bn7rL7yuxPoc.*AAJTSQACMDEAAlNLABxsaEFxa3BlNHlQTk01MXZHUDRSZTB5N2RXdzQ9AAJTMQAA*' \
-H 'Content-Type: application/json' \
-d '{
"application": "PRODUCT_PS",
"subject": {
"ssoToken": "bVIF88S7raSSPgVH7lcHQEecyfk.*AAJTSQACMDEAAlNLABxPWlVjTEV5d2RKYUdLZms0NFd6aU1aTDNadjg9AAJTMQAA*"
},
"resources": [
"https://*.execute-api.*.amazonaws.com/product/1?action=GET"
]
}'
[{"advices":{},"ttl":9223372036854775807,"resource":"https://*.execute-api.*.amazonaws.com/product/1?action=PUT","actions":{"PUT":true},"attributes":{"uid":["productwrite"]}}]

Thank you for reading this article. We have successfully setup AWS API Gateway with Lambda functions and added a security filter using custom OpenAM Authorizer.

Please feel free to leave your questions or suggestions in the comment.

Other blogs related to AWS and OpenAM:

https://medium.com/@awskarthik82/federation-between-aws-cognito-forgerock-openam-using-openid-connect-oidc-saml-d0bf316a2ed2

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store