How to integrate ForgeRock ID Cloud with AWS Lambda and AWS API Gateway

Dennis Andrade
20 min readMar 27, 2023

The use of AWS Lambda is becoming a very popular use case among companies because it’s easy to run code without managing a server; it’s usually cheaper as this service is pay-as-you-go so you don’t have to pay for minutes not used; It scales automatically in case of a sudden spike in usage, etc.

One of the biggest challenges companies face is how to protect their APIs using their current Identity and Access management solution. The AWS API Gateway offers a few options to secure the APIs. We will be exploring the authorizer option. The authorizer is a Lambda function that controls access to the APIs and ForgeRock ID Cloud (IDC) provides the authorization module by returning an advice whether access should be allowed or denied.

In this article I will walk you through how to integrate IDC as the Authorization server so Lambda Authorizer can make the decision whether to allow or deny access to the resource.

WHAT’S THE STORY?

A user is browsing through an application. It could be an iOS, Android or a web application.

The user is requested to authenticate with IDC so the application can identify the user and get an ID Token.

The user accesses a page in the application where the application needs to make an API call to access a resource and display it to the user.

We need to make sure this user is allowed to access the resource.

Below is the flow diagram of this use case

We will have the following players in this walk through:

  1. User: The user navigating through the application
  2. Application: We will be using postman to simulate what the application would be doing
  3. IDC: ForgeRock ID Cloud as the OIDC provider and OAuth2 authorization server
  4. AWS API Gateway: API endpoint the application will try to access
  5. AWS LAMBDA Authorizer: The Policy Enforcement Point (PEP)

PREREQUISITES

  1. AWS account with privileges to create an API Gateway, LAMBDA authorizer, roles, etc. A full AWS admin would be preferable to avoid authorization problems.
  2. A FR ID Cloud Sandbox tenant

SETTING UP AWS

  1. Create the API Gateway endpoint

The first step in this setup is to create the AWS API Gateway endpoint. This is the endpoint our application will be calling and displaying if the user actually has access to it.

Login you your AWS admin console and go to the API Gateway page and click on “Create API”:

Click on “build”under “Rest API”:

On the “Create a new API page” enter an “API name” and change the “Endpoint Type” to “regional” and click on “create API”:

Now we need to create a resource in this new API. Click on actions -> New Resource:

Give it a resource name. In my example I will name it “payments” and the resource path will be auto populated but you can change it if you want. I will leave it as it is. As you can imagine as someone queries our APIs, they will have to add a /payments to the end of it and it will forward it to this path. Click on “create resource”

Next we need to define a method. Let’s create just a simple GET method for this API. Click on Actions -> create method:

Then click on GET from the drop down and click on the check mark:

Now we need to provide some details on what we want to connect it to. We do want to use the Lambda function but we don’t have one just yet. So, let’s duplicate the tab so we can go ahead and create this lambda function.

Once the AWS console tab is duplicated, go to the lambda page:

Click on the “Create function” button and enter a function name and choose python 3.7 as the runtime and click on create function.

Once in the lambda function page, scroll down to the code and let’s create a simple Hello World lambda function. Change the code to this and click on deploy:

def lambda_handler(event, context):
print (event)
return “Hello World!!!”

Now, let’s close this tab and go back to the GET method screen from the API gateway. Refresh the page so the new lambda function displays on the screen when you start typing it. Under “Lambda Function” start typing the function name and it should display the function you just created. Click on it and save it and click OK and the screen that will pop up:

We can go ahead and test this API endpoint by clicking on the test button. Keep in mind that this API endpoint is not public yet. There is no public URL we can call from the internet. We will set this up next. But let’s test it first to make sure it’s returning what we expect it to return.

After clicking on the test button, you can pass a query string or a header but for this test we don’t need to do anything else, just click on the test button again and we should see the result with the response body as “Hello World!!!” as expected.

When we click that test button we are invoking that lambda function and this is what you are seeing here.

Next, let’s make this API public so we can call it from the public internet. Click on the resource you created “Payments”. Then Actions -> Deploy API.

In the Deploy API screen we need to specify a stage which we don’t have yet. Let’s create one, click on “[New Stage]” and type a new stage name and click on deploy:

We got an URL we can call from the public internet: https://u9u3fo6om5.execute-api.us-east-1.amazonaws.com/Test

Open another tab and navigate to https://u9u3fo6om5.execute-api.us-east-1.amazonaws.com/Test/payments

You should see the “Hello World!!!” message in the browser window. Great!

Well… Not really. Right now there is nothing stopping anyone from calling this API endpoint so we want to add an authorizer to protect it in such a way where we need to pass an ID token, then we can verify this token in ForgeRock IDC and return an advice to allow or deny access.

2. Creating the Authorizer

On the same API Gateway endpoint page we have created on step one, click on the “Authorizers” link in the menu on the left:

Click on the “Create a New Authorizer” button and enter the following:

  • Name: SampleApiAuthorizer
  • Type: Lambda
  • Lambda Function: Need to create one. Will get to it next.
  • Lambda Event Payload: Request
  • Identity Sources: Authorization
  • Authorization Caching: Disabled

But before we click on the create button, we will need to create the function so we can specify on this page.

Again, let’s duplicate this tab and navigate to the lambda page. Click on “Create Function” and enter a function name and set the runtime as Python 3.7 and click on “Create function”:

On the lambda function screen, enter the following code:

import requests
import json
def lambda_handler(event, context):
# 1
print (‘********* The event is: ***********’)
print (event)

# 2
auth= ‘Deny’

# 3
body = { “subject”:{“ssoToken”:event[‘headers’][‘Authorization’] }, “resources”:[“https://u9u3fo6om5.execute-api.us-east-1.amazonaws.com/Test/payments"], “application”:”DemoPolicySet”, }
url = ‘https://<Tenant URL>/am/json/realms/root/realms/alpha/policies?_action=evaluate’
headers = {“Content-Type”:”application/json”, “Accept-API-Version”:”protocol=1.0,resource=2.0", “Cookie”:”amlbcookie=01; dace7de536887a6=” + event[‘authorizationToken’]}
r = requests.post(url, headers=headers, json=body)

jsonResponse = r.json()
print (‘******* REST API response ***********’)
print (jsonResponse)

# 4
data = jsonResponse[0]
# 5
if not bool(data[“advices”]):
if bool(data[“actions”]) and data[“actions”][“GET”] == True:
auth = ‘Allow’
# 6
authResponse = { "principalId": "abc123", "policyDocument": { "Version": "2012–10–17", "Statement": [{"Action": "execute-api:Invoke", "Resource": ["arn:aws:execute-api:us-east-1:2459…430:89mjga8n52/*/*"], "Effect": auth}] }}
return authResponse

#1 — Log the event

#2 — Set the variable to deny access by default

#3 — Call IDC to verify the token and authorization. Make sure to include in the body, the policy set name under “application” and the resource you are trying to access under “resource”

#4 — jsonResponse is a list with one json item in it. We will get this item which will become a dictionary with the json

#5 — We will check the advice that is returned from FR IDC, in this example, if the user is allowed to do a get on the specified resource. If so, we will set the auth variable to “allow”

#6 — Build the response. Make sure the resource section reflects your configuration. The first part, where it starts with 2459, is the AWS account number. The 89mjga8n52 is the apiID. The format should be the following:

“arn:aws:execute-api:{regionId}:{accountId}:{apiId}/{stage}/{httpVerb}/[{resource}/[{child-resources}]]”

Ok. Back to the create new API authorizer screen, enter the new Lambda function we created and click on create. Then grant and create.

Now we need to tell the API resource that we want to use this authorizer to protect the endpoint.

Click on the “resources” link on the left menu and then click on the specific API method, GET in my example. Then click on “Method request”, under Authorization, click on the pencil icon and choose the authorizer you just created. If the authorizer is not displaying, try refreshing the page. Click on the checkmark and deploy the API.

The AWS server should be all set. Let’s move on top of the ForgeRock ID Cloud configuration.

SETTING UP FORGEROCK ID CLOUD

  1. Create the OAuth2 client

Login to your IDC tenant and open the Access Management native console by going to “Native Consoles” -> “Access Management”. A New tab will open with the native console. Navigate to Applications -> OAuth 2.0 -> Clients -> Add client

In the new client window enter the following information and click on create:

Once in the OAuth 20.0 client settings window, go to the advanced tab and change the Token Endpoint Authentication Method to client_secret_post and “implied consent” to enabled.

2. Add the OAuth2 client to the OAuth2 Provider configuration

After creating the OAuth2 client, we need to authorize this client as an OIDC SSO client in the OAUt2 Provider.

Open the Native Access Management console and go to Services -> OAuth2 Provider -> Advanced OpenID Connect and add “democlient” to the “Authorized OIDC SSO Clients” field.

We are allowing the client to use an ID Token as the SSO Token.

3. Create two users

One user will be part of the group allowed to access the protected resource and the second user will not be part of that group. We will be able to test both scenarios with those two users.

In the platform UI, click on Manage -> identities -> Alpha Realm — Users and click on the button New Alpha realm — user and enter the following information for each user:

User 1:

  • Username: Demo_Allow
  • First Name: Demo
  • Last Name: Allow
  • Password: Garbage123!

User 2:

  • Username: Demo_Deny
  • First Name: Demo
  • Last Name: Deny
  • Password: Garbage123!

4. Create a group if one does not exist yet

In this example we will be using only one group but you may have as many groups as you wish.

Go to the platform UI and click on Identities -> Manage. You should see the Alpha Realm — Groups. Click on + New Alpha realm — group and enter the group name: Demo. Click Next and Save. Then add user Demo_Allow to this group.

If the Alpha realm — Groups is not displaying in your tenant, you might need to enable it by following the instructions on this document: https://backstage.forgerock.com/docs/idcloud-idm/latest/objects-guide/groups.html#_enable_groups

5. Create a policy

Back to the Native Access Management console, go to Authorization -> Policy Sets and click on + New Policy Set. Enter the following information:

  • Id: DemoPolicySet
  • Name: Demos Policy Set
  • Resource Types: URL

Once in the new policy set, let’s create a new policy by clicking on + Add a Policy and entering the following information:

  • Name: DemoPolicy
  • Resource Type: URL
  • Resources: *://*:*/Test/payments

Click on create. Go to the actions tab -> add an action -> GET. Set it to allow and save the changes.

Click on the subjects tab and click on the pencil on the subject condition “Never Match” to change it. In Type select “Users & Groups” and under groups type Demo. Click on the checkmark button and then Save Changes.

This is how the final policy should look like:

We are all set to test it out.

DEMO THE CONFIGURATION

I will post the flow diagram here again so it’s easier to follow the step by step.

Postman will be playing the role of the Application since we do not have an actual application for this demo. In the next steps, I will post the curl commands and the response from each command.

Let’s give it a try with the Demo_Allow user. This user is a member of the Demo group, so it should be allowed to access the resource:

Step 1: Authenticate to ID Cloud

curl — location — request POST ‘https://<Tenant URL>/am/json/alpha/authenticate?authIndexType=service&authIndexValue=PasswordGrant’ \
— header ‘X-OpenAM-Username: Demo_Allow \
— header ‘X-OpenAM-Password: Gargabge123!’ \
— header ‘Content-Type: application/json’ \
— header ‘Accept-API-Version: resource=2.1’

Response:

{
“tokenId”: “PgorAqLecgsJbwyKwcWkdelgvnw.*AAJTSQACMDIAAlNLABw1RzJnOHIvdVdQUmk5NUFiVG9TTElQMGtNVGs9AAR0eXBlAANDVFMAAlMxAAIwMQ..*”,
“successUrl”: “/enduser/?realm=/alpha”,
“realm”: “/alpha”
}

Step 2: Return OIDC Token

curl — location — request POST ‘https://<Tenant URL>/am/oauth2/alpha/authorize’ \
— header ‘Cookie: dace7de536887a6=PgorAqLecgsJbwyKwcWkdelgvnw.*AAJTSQACMDIAAlNLABw1RzJnOHIvdVdQUmk5NUFiVG9TTElQMGtNVGs9AAR0eXBlAANDVFMAAlMxAAIwMQ..*; amlbcookie=01; dace7de536887a6=y_-Gtv7_oW43r0tL00K5V52CnTc.*AAJTSQACMDIAAlNLABwwRFB6MGQvZVU1NUMzcVd4azBacUorZUd1bnc9AAR0eXBlAANDVFMAAlMxAAIwMQ..*’ \
— header ‘Content-Type: application/x-www-form-urlencoded’ \
— data-urlencode ‘scope=profile email openid’ \
— data-urlencode ‘response_type=code’ \
— data-urlencode ‘client_id=democlient’ \
— data-urlencode ‘redirect_uri=http://example.com:80/callback' \
— data-urlencode ‘decision=allow’ \
— data-urlencode ‘csrf=PgorAqLecgsJbwyKwcWkdelgvnw.*AAJTSQACMDIAAlNLABw1RzJnOHIvdVdQUmk5NUFiVG9TTElQMGtNVGs9AAR0eXBlAANDVFMAAlMxAAIwMQ..*’ \
— data-urlencode ‘state=abc123’ \
— data-urlencode ‘service=PasswordGrant’

Response on this steps should be the Auth Code:

 iyGRLugraqqZ8qF0dFzkrofhdpE

Step 2a: Exchange the Auth Code to an ID Token:

curl — location — request POST ‘https://<Tenant URL>/am/oauth2/alpha/access_token’ \
— header ‘Content-Type: application/x-www-form-urlencoded’ \
— header ‘Cookie: amlbcookie=01; dace7de536887a6=y_-Gtv7_oW43r0tL00K5V52CnTc.*AAJTSQACMDIAAlNLABwwRFB6MGQvZVU1NUMzcVd4azBacUorZUd1bnc9AAR0eXBlAANDVFMAAlMxAAIwMQ..*’ \
— data-urlencode ‘grant_type=authorization_code’ \
— data-urlencode ‘code=iyGRLugraqqZ8qF0dFzkrofhdpE’ \
— data-urlencode ‘client_id=democlient’ \
— data-urlencode ‘client_secret=Garbage123!’ \
— data-urlencode ‘redirect_uri=http://example.com:80/callback'

Response:

{
“access_token”: “eyJ0eXAiOiJKV1QiLCJhbGciOiJI...F90aW1lIjoxNjc4ODI5MzQwLCJyZWFsbSI6Ii9hbHBoYSIsImV4cCI6MTY3ODgzMzI2OSwiaWF0IjoxNjc4ODI5NjY5LCJleHBpcmVzX2luIjozNjAwLCJqdGkiOiJKY0RjQ0Jzb1VrZVJUN0diMVQ3Rkg5NzRvZzAifQ.mGwUOS_n1TNJ72jG0372lG4KvAejyfMNK7GXL5t5q6o”,
“scope”: “openid profile email”,
“id_token”: “eyJ0eXAiOiJKV1QiLCJraWQiO...cmVhbG0iOiIvYWxwaGEiLCJleHAiOjE2Nzg4MzMyNjksInRva2VuVHlwZSI6IkpXVFRva2VuIiwiaWF0IjoxNjc4ODI5NjY5LCJmYW1pbHlfbmFtZSI6IlVzZXIyIiwiZW1haWwiOiJ0dXNlcjJAZXhhbXBsZS5jb20ifQ.H7BNCu-uw93UBsZj40-pOKLsmY8og1lrXsHSLw6tis0LaZCBrOozn6yHhuR1Xgf2UNs7UBXXsEytlNTOH53IG95SvfMovpwPErLMNpptKQvBwbKSsf9XRZOE8XPbAN1GHwziHfg1LBWgWXVXUvL8uh5M5r2lDB720EHqxN1B6IJD6oHTKnXQ7zK22afMtaD9ut9XYHEG7kDDI4h2_Do56XM5_mzMh5imVXLHHrhZCgPAysUS6pokRCpOaAYVDrSvVVcyLZX0pN88GPmAyQ3KNoWwWXaQ5L6I9iwFnnE4cW3cg26K_sIw0vhWGNX_cgqaDOTGAB3W5cU7llCGqgYnDA”,
“token_type”: “Bearer”,
“expires_in”: 3599
}

Step 3: Request access to a protected service

curl — location — request GET ‘https://89mjga8n52.execute-api.us-east-1.amazonaws.com/Test/payments' \
— header ‘Authorization: eyJ0eXAiOiJKV1QiLCJraWQiOiJaZXlhMnJUdU...zUUhrSmFpUSIsImF6cCI6ImRlbW9jbGllbnQiLCJhdXRoX3RpbWUiOjE2Nzg4MzIyMjQsIm5hbWUiOiJEZW1vIEFsbG93IiwicmVhbG0iOiIvYWxwaGEiLCJleHAiOjE2Nzg4MzU4NDUsInRva2VuVHlwZSI6IkpXVFRva2VuIiwiaWF0IjoxNjc4ODMyMjQ1LCJmYW1pbHlfbmFtZSI6IkFsbG93In0.foTB_UzGlw-4AzwCCcThle3unHyM81_X-SGaXV9-OcCawvkMbzNXz8x2EFVk8JMwye0i7DtcWrbbUOC6JcZQrnXm99uybKpc-CauC7wAos_mByZ8tQhM-k6MZ_Y4sePUfhKxnZbcI1HqY5UIi78EhUzYRmEkymHGVSvkzO30BRN46a63BU3JvxdxIIflBmXGGucckCvWdJ5tS1z_o-p2I9WBR3DdelNyHfL3oOKCOXS5s38HUXoWVcU3CiqYXKUMXD8_lgcqIGKXDD0H90Zws067t0xhlINAv-nE2SWq73rHLuJI80XUUTFRGFoGL1qtz-5SKFYmV3Qi278z_wZ6rQ’

Steps 4, 5, 6 and 7 are all done within AWS Lambda and IDC automatically.

Response (Step 8):

“Hello World!!!”

SUCCESS!!

Now, if we do the same testing with the Demo_Deny user, we should get a different message when we try to access the resource. All you have to do is to change the username from step 1. I won’t go through all the steps again to test Demo_Deny but the response is the following:

{
“Message”: “User is not authorized to access this resource with an explicit deny”
}

THE PROBLEM

This method works as you can see but the above method is not the most elegant one because it does bring some problems.

If you look at the flow diagram above, we have the Lambda Authorizer, which is sitting on top of the API Gateway, in between the application and the resource. This is done to protect that resource from attacks; Well, at least increase its security. The question here is how does the API Gateway know who the user is?

In the example above we used the API Gateway as pretty much a proxy, where the API Gateway received the ID Token and passed it blindly to IDC, the authorization server. As you can see, it works but here is the concern.

Let’s start by defining what the Audience claim is in the token. According to RFC 7519:

“ The “aud” (audience) claim identifies the recipients that the JWT is intended for. Each principal intended to process the JWT MUST identify itself with a value in the audience claim. If the principal processing the claim does not identify itself with a value in the “aud” claim when this claim is present, then the JWT MUST be rejected. In the general case, the “aud” value is an array of case-sensitive strings, each containing a StringOrURI value. In the special case when the JWT has one audience, the “aud” value MAY be a single case-sensitive string containing a StringOrURI value. The interpretation of audience values is generally application specific.”

The audience claim represents the application to which the token was issued. This token should only be used by the application listed in the audience claim. In the example above, the token was issued for use by the application and the API Gateway is acting as a proxy blindly trusting a token that was not intended for it.

What does that all mean? Well, it means that if the resource is directly accessible by the application, the API Gateway could be bypassed because the same token used on the API Gateway could be used directly on the resource, therefore opening up a potential vulnerability.

THE SOLUTION: TOKEN EXCHANGE

What we need to do to resolve this problem is to create its own credentials to the API Gateway using the client credentials OAuth2 flow. Now, the API resource will only trust and accept requests coming from the API Gateway and deny everything else. Done! Well… Not really.

We fixed the problem where the same ID Token could be used directly on the resource because now, the resource will only accept requests from the API Gateway since it has its own token. But how do we know who the user is AND if this user has access to the resource? Remember we would be making the requests to the resource using the API Gateway’s Access Token. That’s where the delegation from a Token Exchange flow comes handy and in a very elegant way.

We will make a request to IDC with both tokens, the subject token, which is the original token to be exchanged, and the actor token, which is the identity that can act on behalf of another identity. The result is a brand new token very similar to the subject token but with the may_act claim with the party allowed to act on behalf of the user. This is exactly what we need to resolve those potential vulnerabilities.

I know this could be a little overwhelming but let’s get to the implementation part of it and hopefully it will clear things up.

Below is the new flow diagram with the token exchange:

TOKEN EXCHANGE IMPLEMENTATION IN FORGEROCK IDC

  1. Create a new May Act Script

IDC provides a script of the type OAuth2 May Act to add the may_act claim to the token. We will leverage that for delegation in the token exchange.

In the platform UI, navigate to Script -> Auth Script -> + New Script and pick the “OAuth2 May Act” template. Click Next. Enter the following information:

  • Name: Demo May Act Script
  • Script:
(function () {
var frJava = JavaImporter(
org.forgerock.json.JsonValue
);
var mayAct = frJava.JsonValue.json(frJava.JsonValue.object())
mayAct.put(‘client_id’, [‘TokenExchangeService’])
mayAct.put(‘sub’,’AWSAPIGatewayClient’)
token.setMayAct(mayAct)
}());

2. Create an OAuth 2.0 Client for the AWS API Gateway

Step four of the diagram above is to request an Access Token to the API Gateway, so we will need to create a new OAuth 2.0 Client for it.

In the native AM console, navigate to Applications -> OAuth 2.0 -> Clients. Click on + add Client and enter the following information and click on the create button:

  • Client ID: AWSAPIGatewayClient
  • Client secret: <secret>
  • Default Scope: apigateway

In the client screen, navigate to the advanced tab -> Grant Types, add “Client Credentials” and remove “Authorization Code”. Also, change the “Token Endpoint Authentication Method” to “client_secret_post”.

3. Configure the tenant for the Token Exchange flow

In the AM Native console, navigate to Services > OAuth2 Provider > Advanced tab, in the Grant Types field, add the Token Exchange type if it is not already configured and save it.

4. Create an OAuth 2.0 Client for the Token Exchange Service

Step six of the diagram above is to do the token exchange, so we will need to create a new OAuth 2.0 Client for it.

In the native AM console, navigate to Applications -> OAuth 2.0 -> Clients. Click on + add Client and enter the following information and click on the create button:

  • Client ID: TokenExchangeService
  • Client secret: <secret>
  • Default Scope: profile email

In the client screen, navigate to the advanced tab -> Grant Types, add “Token Exchange” and remove “Authorization Code”. Also, change the “Token Endpoint Authentication Method” to “client_secret_post”.

5. Modify the democlient OAuth 2.0 client

We need to add the may_act claim to the original ID_Token the user first got from the server. In this example we named the OAuth2 client “democlient”. In the “OAuth2 Provider Overrides”, enable “Enable OAuth2 Provider Overrides” and add the may_act script created in step 1 to both “OAuth2 Access Token May Act Script” and “OIDC ID Token May Act Script” and save it.

TOKEN EXCHANGE IMPLEMENTATION IN AWS

  1. Modify the Lambda Authorizer code with the new Token Exchange flow in it.

Modify the AWS Lambda Authorizer code. Replace the code in the Lambda function with the following:

import requests
import json

def lambda_handler(event, context):
# Log the event
print (‘********* The event is: ***********’)
print (event)

# 1
auth= ‘Deny’

# 2
bodyClientCredentials = {“grant_type”:”client_credentials”, “client_id”:”AWSAPIGatewayClient”,”client_secret”:”Garbage123!”}
urlClientCredentials = ‘https://<tenant URL>/am/oauth2/alpha/access_token’
headersClientCredentials = {“Content-Type”: “application/x-www-form-urlencoded”}
r = requests.post(urlClientCredentials, headers=headersClientCredentials, data=bodyClientCredentials)

jsonResponse = r.json()

# 3
actorToken = jsonResponse["access_token"]
subjectToken = event['headers']['Authorization']
resource = event['headers']['X-Forwarded-Proto'] + "://" + event['requestContext']['domainName'] + event['requestContext']['path']

# 4
bodyTokenExchange = {"grant_type":"urn:ietf:params:oauth:grant-type:token-exchange", "client_id":"TokenExchangeService","client_secret":"Garbage123!", "subject_token":subjectToken, "subject_token_type":"urn:ietf:params:oauth:token-type:id_token", "requested_token_type":"urn:ietf:params:oauth:token-type:id_token", "actor_token": actorToken, "actor_token_type":"urn:ietf:params:oauth:token-type:access_token"}
urlTokenExchange = 'https://<tenant URL>/am/oauth2/alpha/access_token'
headersTokenExchange = {"Content-Type": "application/x-www-form-urlencoded" }
rTokenExchange = requests.post(urlTokenExchange, headers=headersTokenExchange, data=bodyTokenExchange)

jsonResponseTokenExchange = rTokenExchange.json()

# 5
authToken = jsonResponseTokenExchange["access_token"]
COOKIE_NAME = "dace7de536887a6"

# 6
bodyAuthorize = { "subject":{"ssoToken":authToken }, "resources":[resource], "application":"DemoPolicySet", }
urlAuthorize = 'https://<tenant URL>/am/json/realms/root/realms/alpha/policies?_action=evaluate'
headersAuthorize = {"Content-Type":"application/json", "Accept-API-Version":"protocol=1.0,resource=2.0", "Cookie":"amlbcookie=01;" + COOKIE_NAME + "=" + authToken}
rAuthorize = requests.post(urlAuthorize, headers=headersAuthorize, json=bodyAuthorize)

jsonResponseAuthorize = rAuthorize.json()

# 7
data = jsonResponseAuthorize[0]
# 8
if not bool(data["advices"]):
if bool(data["actions"]) and data["actions"]["GET"] == True:
auth = 'Allow'

# 9
authResponse = { "principalId": "abc123", "policyDocument": { "Version": "2012–10–17", "Statement": [{"Action": "execute-api:Invoke", "Resource": ["arn:aws:execute-api:us-east-1:245940442430:u9u3fo6om5/*/*"], "Effect": auth}] }}
return authResponse

There is a lot going on here, so let’s break it down.

  1. Set the variable auth to deny the access by default
  2. Request an access token for the API Gateway. This will be the actor token
  3. We are setting the actor token to the one just issued from step 2 and the subject token to the user’s ID Token sent in the request to access the API. We are also setting the resource the user is trying to access by picking it up from the request itself.
  4. Request the exchanged Token from ForgeRock IDC by sending in the subject and actor tokens and requesting an ID Token.
  5. We are setting the exchanged ID Token to the authToken variable and also setting a constant for the Tenant’s cookie name.
  6. We are making a call to IDC to verify the token and authorization
  7. The json response is a list with one json item in it. We will get this item which will become a dictionary with the json so we can parse it
  8. We are parsing the json response, which is the FR IDC advice whether the user has access to the resource or not and set the auth variable accordingly.
  9. Build the AWS response with the access result from the policy

TESTING THE FINAL SOLUTION

As I have mentioned before, postman will be playing the role of the Application since we do not have an actual application for this demo. In the next steps, I will post the curl commands and the response from each command.

Let’s give it a try with the Demo_Allow user. This user is a member of the Demo group, so it should be allowed to access the resource:

Step 1: Authenticate to ID Cloud

curl — location — request POST ‘https://<Tenant URL>/am/json/alpha/authenticate?authIndexType=service&authIndexValue=PasswordGrant’ \
— header ‘X-OpenAM-Username: Demo_Allow \
— header ‘X-OpenAM-Password: Gargabge123!’ \
— header ‘Content-Type: application/json’ \
— header ‘Accept-API-Version: resource=2.1’

Response:

{
“tokenId”: “ICKww0OtUyexTgVMjlr6S0wF0qg.*AAJTSQACMDIAAlNLABx3WTJ5VE1OZUE0ZUZUWEFoVi9jUUhFUFYyYnM9AAR0eXBlAANDVFMAAlMxAAIwMQ..*”,
“successUrl”: “/enduser/?realm=/alpha”,
“realm”: “/alpha”
}

Step 2: Return OIDC Token

curl — location — request POST ‘https://<tenant-name>/am/oauth2/alpha/authorize' \
— header ‘Cookie: dace7de536887a6=ICKww0OtUyexTgVMjlr6S0wF0qg.*AAJTSQACMDIAAlNLABx3WTJ5VE1OZUE0ZUZUWEFoVi9jUUhFUFYyYnM9AAR0eXBlAANDVFMAAlMxAAIwMQ..*; amlbcookie=01; dace7de536887a6=-3SD3Iz78Cg2zChg1seJ6EOXGSg.*AAJTSQACMDIAAlNLABxBemJZMnVOYUh3bStiSS9BUzlRZGlIQnFjWU09AAR0eXBlAANDVFMAAlMxAAIwMQ..*’ \
— header ‘Content-Type: application/x-www-form-urlencoded’ \
— data-urlencode ‘scope=profile email openid’ \
— data-urlencode ‘response_type=code’ \
— data-urlencode ‘client_id=democlient’ \
— data-urlencode ‘redirect_uri=http://example.com:80/callback' \
— data-urlencode ‘decision=allow’ \
— data-urlencode ‘csrf=ICKww0OtUyexTgVMjlr6S0wF0qg.*AAJTSQACMDIAAlNLABx3WTJ5VE1OZUE0ZUZUWEFoVi9jUUhFUFYyYnM9AAR0eXBlAANDVFMAAlMxAAIwMQ..*’ \
— data-urlencode ‘state=abc123’ \
— data-urlencode ‘service=PasswordGrant’

Response on this steps should be the Auth Code:

UvuPSGLC5yazd97jvKA3UWNqdds

Step 2a: Exchange the Auth Code to an ID Token:

curl — location — request POST ‘https://<tenant-name>/am/oauth2/alpha/access_token' \
— header ‘Content-Type: application/x-www-form-urlencoded’ \
— header ‘Cookie: amlbcookie=01; dace7de536887a6=ICKww0OtUyexTgVMjlr6S0wF0qg.*AAJTSQACMDIAAlNLABx3WTJ5VE1OZUE0ZUZUWEFoVi9jUUhFUFYyYnM9AAR0eXBlAANDVFMAAlMxAAIwMQ..*’ \
— data-urlencode ‘grant_type=authorization_code’ \
— data-urlencode ‘code=UvuPSGLC5yazd97jvKA3UWNqdds’ \
— data-urlencode ‘client_id=democlient’ \
— data-urlencode ‘client_secret=Garbage123!’ \
— data-urlencode ‘redirect_uri=http://example.com:80/callback'

Response:

{
“access_token”: “eyJ0eXAiOiJKV1QiLCJhbGci...Y3OTA3MTUxOSwiaWF0IjoxNjc5MDY3OTE5LCJleHBpcmVzX2luIjozNjAwLCJqdGkiOiJZa3RTUnIyNmpvLWtqSEZmSmd4R3pwUXF0b2MiLCJtYXlfYWN0Ijp7ImNsaWVudF9pZCI6WyJUb2tlbkV4Y2hhbmdlU2VydmljZSJdLCJzdWIiOiJBV1NBUElHYXRld2F5Q2xpZW50In19.hm1x7odrs0hVuHKbYT5WLzVUe6euuhyMmVPPykxilms”,
“scope”: “openid profile email”,
“id_token”: “eyJ0eXAiOiJKV1QiLCJraWQiO...UUhrSmFpUSIsImF6cCI6ImRlbW9jbGllbnQiLCJhdXRoX3RpbWUiOjE2NzkwNjc1MjEsIm5hbWUiOiJEZW1vIEFsbG93IiwicmVhbG0iOiIvYWxwaGEiLCJtYXlfYWN0Ijp7ImNsaWVudF9pZCI6WyJUb2tlbkV4Y2hhbmdlU2VydmljZSJdLCJzdWIiOiJBV1NBUElHYXRld2F5Q2xpZW50In0sImV4cCI6MTY3OTA3MTUxOSwidG9rZW5UeXBlIjoiSldUVG9rZW4iLCJpYXQiOjE2NzkwNjc5MTksImZhbWlseV9uYW1lIjoiQWxsb3ciLCJlbWFpbCI6ImRlbW9fYWxsb3dAZXhhbXBsZS5jb20ifQ.SqGNo-1JS22KywbBg37r6bqfjHDFg8cCrW569l9NGfIYTs6pgNKl33W8tUenrxOU331Z311MtUmqdvp2iW0vvPA2t4Ld8ZK4qqHZchhXT2N8wO1vKepwDnVvI9tt7rcHJcrqLBygRU6qWAQSpBVoCIhPimu-OaD2cvhd5oM9c6QP6AA7B7kulo3xC_vidgexvJdvStmXLd9Lh1HgQp73mGP6et8FJkbteoKOLM4Tx2YvToA5Gxw_SWhAbYSRgdNjgAy4QxMnHknsvD-SePc_JDqm3cAlTbrINayjqCxvNTnI1csWuTnqrq-TkzIMZNpO_PNjGfkTNV7ZnnIrFgbLXw”,
“token_type”: “Bearer”,
“expires_in”: 3599
}

Step 3: Request access to a protected service

curl — location — request GET ‘https://u9u3fo6om5.execute-api.us-east-1.amazonaws.com/Test/payments' \
— header ‘Authorization: eyJ0eXAiOiJKV1QiLC...9rZW4iLCJpYXQiOjE2NzkwNjc5MTksImZhbWlseV9uYW1lIjoiQWxsb3ciLCJlbWFpbCI6ImRlbW9fYWxsb3dAZXhhbXBsZS5jb20ifQ.SqGNo-1JS22KywbBg37r6bqfjHDFg8cCrW569l9NGfIYTs6pgNKl33W8tUenrxOU331Z311MtUmqdvp2iW0vvPA2t4Ld8ZK4qqHZchhXT2N8wO1vKepwDnVvI9tt7rcHJcrqLBygRU6qWAQSpBVoCIhPimu-OaD2cvhd5oM9c6QP6AA7B7kulo3xC_vidgexvJdvStmXLd9Lh1HgQp73mGP6et8FJkbteoKOLM4Tx2YvToA5Gxw_SWhAbYSRgdNjgAy4QxMnHknsvD-SePc_JDqm3cAlTbrINayjqCxvNTnI1csWuTnqrq-TkzIMZNpO_PNjGfkTNV7ZnnIrFgbLXw’

Steps 4, 5, 6, 7, 8 and 9 are all done within AWS Lambda and IDC automatically.

Response (Step 10):

“Hello World!!!”

SUCCESS!!

Now, if we do the same testing with the Demo_Deny user, we should get a different message when we try to access the resource. All you have to do is to change the username from step 1. I won’t go through all the steps again to test Demo_Deny but the response is the following:

{
“Message”: “User is not authorized to access this resource with an explicit deny”
}

Note that the token we use for the authorization policy check in IDC is the exchanged token and not the subject token. The exchanged token has an audience claim set to “TokenExchangeService” which is the client that issued the exchanged token. It has a sub claim set to the user’s id (Demo_Allow) and it has an act claim set to AWSAPIGatewayClient, which tell us that delegation has happened and that AWSAPIGatewayClient is the party that can act on behalf of the user.

The subject token has an audience set to democlient, which is the client that issues user tokens. And it has the may_act claim set to client_id TokenExchangeService and sub AWSAPIGatewayClient. The client_id is the client allowed to exchange the token and the sub is the party allowed to act on the behalf of the subject of the token, Demo_Allow in this case.

I know, I know. My brain was going into a shock trying to understand the flow so, I will try to make it a little easier to understand in the table below with the three clients we are using in this flow:

That’s it! You are done!

--

--