IG 2023.11

Secure the OAuth 2.0 access token endpoint

This section uses a GrantSwapJwtAssertionOAuth2ClientFilter to transform requests for OAuth 2.0 access tokens into secure JWT bearer grant type requests. It propagates the transformed requests to Identity Cloud to obtain an access token.

Use GrantSwapJwtAssertionOAuth2ClientFilter to increase the security of less-secure grant-type requests, such as Client credentials grant requests or Resource owner password credentials grant requests.

The GrantSwapJwtAssertionOAuth2ClientFilter obtains access tokens from the /oauth2/access_token endpoint. To prevent unwanted or malicious access to the endpoint, make sure only a well-defined set of clients can use this filter.

Consider the following options to secure access to the GrantSwapJwtAssertionOAuth2ClientFilter:

  • Deploy IG on a trusted network.

  • Use mutual TLS (mTLS) and X.509 certificates for authentication between clients and IG. For more information, refer to OAuth 2.0 Mutual TLS Client Authentication and Certificate Bound Access Tokens.

  • Configure an AllowOnlyFilter in front of the GrantSwapJwtAssertionOAuth2ClientFilter to control access within a route.

  • Define restrictive Route conditions to allow access only for expected grant-type requests. For example, define a route condition that requires a specific client ID, grant-type, or scope.

  • Configure a ScriptableFilter in front of the GrantSwapJwtAssertionOAuth2ClientFilter to validate requests.

The following figure shows the flow of information for a grant swap:

GrantSwapJwtAssertionOAuth2ClientFilter

Before you start, prepare Identity Cloud, IG, and the sample application as described in Example installation for this guide.

  1. Set up Identity Cloud:

    1. Log in to the Identity Cloud admin UI as an administrator.

    2. Create a service account with the following values, as described in Create a new service account:

      • Name: myServiceAccount

      • Scopes: fr:idm:* All Identity Management APIs

        The service account ID is displayed and you are prompted to download the private key. The public key is held in Identity Cloud.

        For more information, refer to Service accounts.

    3. Make a note of the service account ID and download the private key to your secrets directory.

    4. Rename the key to match the regex format [a-zA-Z0-9]+(\.[a-zA-Z0-9]+)*. For example, rename myServiceAccount_privateKey.jwk to privateKey.jwk.

  2. Set up IG:

    1. Add the following route to IG:

      • Linux

      • Windows

      $HOME/.openig/config/routes/grant-swap.json
      appdata\OpenIG\config\routes\grant-swap.json
      {
        "name" : "grant-swap",
        "properties": {
          "idcInstanceUrl": "https://myTenant.forgeblocks.com",
          "issuer": "service-account-id",
          "secretsDir": "path-to-secrets",
          "privateKeyFilename": "privateKey.jwk"
        },
        "condition" : "#{find(request.uri.path, '^/am/oauth2/access_token') && request.entity.form['grant_type'][0] == 'client_credentials'}",
        "baseURI" : "&{idcInstanceUrl}:443/",
        "heap" : [ {
          "name": "JwkPropertyFormat-01",
          "type": "JwkPropertyFormat"
        },
          {
            "name": "FileSystemSecretStore-01",
            "type": "FileSystemSecretStore",
            "config": {
              "format": "JwkPropertyFormat-01",
              "directory": "&{secretsDir}",
              "mappings": [ {
                "secretId": "&{privateKeyFilename}",
                "format": "JwkPropertyFormat-01"
              }
              ]
            }
          }
        ],
        "handler" : {
          "type" : "Chain",
          "capture" : "all",
          "config" : {
            "filters" : [
              {
                "name" : "GrantSwapJwtAssertionOAuth2ClientFilter-01",
                "description": "access /access_token endpoint with jwt-bearer-profile",
                "type" : "GrantSwapJwtAssertionOAuth2ClientFilter",
                "capture" : "all",
                "config" : {
                  "clientId" : "service-account",
                  "assertion" : {
                    "issuer" : "&{issuer}",
                    "audience" : "&{idcInstanceUrl}/am/oauth2/access_token",
                    "subject" : "&{issuer}",
                    "expiryTime": "2 minutes"
                  },
                  "signature": {
                    "secretId": "&{privateKeyFilename}",
                    "includeKeyId": false
                  },
                  "secretsProvider": "FileSystemSecretStore-01",
                  "scopes" : {
                    "type": "RequestFormResourceAccess"
                  }
                }
              }
            ],
            "handler" : "ForgeRockClientHandler"
          }
        }
      }
    2. In the route, replace the values for the following properties with your values:

      • idcInstanceUrl: The root URL of your Identity Cloud.

      • issuer: The ID of the service account created in Identity Cloud

      • secretsDir: The directory containing the downloaded private key

      • privateKeyFilename: The filename of the downloaded private key

    3. Notice the following features of the route:

      • The condition intercepts only client_credentials grant-type requests on the path /am/oauth2/access_token. A more secure condition can be set on the client ID.

      • Requests are rebased to the Identity Cloud URL.

      • A FileSystemSecretStore loads the private-key JWK used to sign the JWT.

      • The GrantSwapJwtAssertionOAuth2ClientFilter:

        • Requires the core JWT claims issuer, subject, audience, and expiryTime.

        • Uses RequestFormResourceAccess to extract scopes from the inbound request for inclusion in the JWT-assertion grant-type request propagated to AM.

        • Signs the JWT with the JWK provided by the service account.

      • The GrantSwapJwtAssertionOAuth2ClientFilter clientId refers to the OAuth 2.0 client ID created by AM. The value must be service-account.

    4. Add the following route to IG to return a standard OAuth 2.0 error response if the request fails the route condition:

      • Linux

      • Windows

      $HOME/.openig/config/routes/zz-returns-invalid-request.json
      appdata\OpenIG\config\routes\zz-returns-invalid-request.json
      {
        "name" : "zz-returns-invalid-request",
        "handler" : {
          "type" : "StaticResponseHandler",
          "capture" : "all",
          "config" : {
            "status": 400,
            "headers": {"Content-Type": ["application/json; charset=UTF-8"]},
            "entity": "{\"error\": \"Invalid_request\", \"error_description\": \"Invalid request\"}"
          }
        }
      }
  3. Test the setup by accessing the route with a curl command similar to this:

    $ curl --location \
        --request POST 'http://ig.example.com:8080/am/oauth2/access_token' \
        --header 'Content-Type: application/x-www-form-urlencoded' \
        --data-urlencode 'client_id=myServiceAccount' \
        --data-urlencode 'grant_type=client_credentials' \
        --data-urlencode 'scope=fr:idm:*'
    
    
    {"access_token":"eyJ...","scope":"fr:idm:*","token_type":"Bearer","expires_in":899}

The command makes a client_credentials grant-type request on the path /am/oauth2/access_token, supplying the client ID and scopes. IG transforms the request into a JWT-assertion grant-type request and propagates it to Identity Cloud.

Because the service account in Identity Cloud supports the requested scope, the GrantSwapJwtAssertionOAuth2ClientFilter returns an access token.

Copyright © 2010-2023 ForgeRock, all rights reserved.