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

karthik
14 min readApr 2, 2018

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:

  1. Only users belonging to PRODUCT_READ group can invoke read API
  2. Only users belonging to PRODUCT_WRITE group can invoke create / update API
  3. Only users belonging to PRODUCT_DELETE group can invoke delete API
  4. Users belonging to PRODUCT_ADMIN group can invoke all the APIs

Below is the architecture diagram:

  1. User authenticates with ForgeRock AM
  2. AM generates a ssotoken and returns it to the application
  3. Client application invokes the APIs with the ssotoken in the header
  4. API Gateway calls the custom Lambda authorizer with the token and context
  5. Custom Lambda Authorizer makes a policy evaluation request to ForgeRock AM by passing the ssotoken and resource URL
  6. ForgeRock AM evaluates the user’s permission based on LDAP group membership and returns ALLOW or DENY to Custom Lambda Authorizer
  7. Custom Lambda Authorizer returns a ALLOW or DENY policy to API Gateway
  8. Based on ALLOW or DENY, request will get forwarded to the actual Lambda function or a 403 will be returned to the user
  9. Lambda function performs the requested operation on DynamoDB
  10. DynamoDB returns the results to Lambda function
  11. Lambda function will process the results and return the response to API Gateway
  12. If there are no issues with the Lambda function, API Gateway will return a HTTP 200 with response data to the client application.

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

  1. AWS Account with free-tier or paid account
  2. ForgeRock AM environment which can be accessed through internet because custom Lambda authorizer needs to invoke the AM. If you don’t have a public environment, follow this blog to setup a AM environment in AWS using Elastic Beanstalk. For this setup, please use AM 5.5. This can be done even with v12 or v13.5 AM. But, it requires minor code changes.
  3. Knowledge on AWS API Gateway, AWS Lambda, AWS CLI, ForgeRock AM and other AWS services like CloudFormation, SAM templates.
  4. knowledge on Node.js and Java.

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

  1. product_samtemplate.yml : Serverless Application Model template to setup the API Gateway, AWS Lambda services and deploy the source code
  2. products_apidef.yml : Swagger definition file for the API Gateway endpoints. This is referred in the SAM template.
  3. products.js : Node.js Lambda function which performs CRUD operation on DynamoDB
  4. product_api.zip : Deployment package for Node.js Lambda function. If you make any changes in product.js, regenerate this zip.
  5. OpenAMAuthorizer.java : Java Lambda function which makes Authorization request to OpenAM for making RBAC decisions.
  6. OpenAMAuthorizer.zip : This is the Lambda zip package. If you make any changes to OpenAMAuthorizer.java, regenerate the zip using Maven command “mvn clean package -DskipTests” from root folder of this project “OpenAMLambdaAuthorizer”. This should create the zip package in “target” folder.
  • 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
  1. BucketName : Use the same bucket name that was created in previous step.
  2. OpenAMURL : OpenAM URL with context. For ex: http://openam.example.com/auth
  3. PolicyPwd : password for the user “policyuser” which was setup in OpenAM
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

--

--