OAuth2ResourceServerFilter

Validates a request containing an OAuth 2.0 access_token. The filter expects an OAuth 2.0 token from the HTTP Authorization header of the request, such as the following example header, where the OAuth 2.0 access_token is 1fc…​ec9:

Authorization: Bearer 1fc...ec9

The filter performs the following tasks:

  • Extracts the access_token from the request header.

  • Uses the configured access_token resolver to resolve the access_token against an authorization server, and validate the token claims.

  • Checks that the token has the scopes required by the filter configuration.

  • Injects the access_token info into the OAuth2Context.

The following errors can occur during access_token validation:

Error Response from the filter to the user-agent

Combination of the filter configuration and access_token result in an invalid request to the authorization server.

HTTP 400 Bad Request

There is no access_token in the request header.

HTTP 401 Unauthorized WWW-Authenticate: Bearer realm="IG"

The access_token isn’t valid, for example, because it has expired.

HTTP 401 Unauthorized

The access_token doesn’t have all of the scopes required in the OAuth2ResourceServerFilter configuration.

HTTP 403 Forbidden

Usage

{
  "name": string,
  "type": "OAuth2ResourceServerFilter",
  "config": {
    "accessTokenResolver": AccessTokenResolver reference,
    "cache": object,
    "executor": executor,
    "requireHttps": configuration expression<boolean>,
    "realm": string,
    "scopes": [ runtime expression<string>, ... ] or object
  }
}

An alternative value for type is OAuth2RSFilter.

Properties

"accessTokenResolver": AccessTokenResolver reference, required

Resolves an access_token against an authorization server. Configure one of the following access_token resolvers:

To decorate an AccessTokenResolver, add the decoration at the accessTokenResolver level. The following example uses the default timer decorator to record the time that a TokenIntrospectionAccessTokenResolver takes to process a request:

{
  "accessTokenResolver": {
    "type": "TokenIntrospectionAccessTokenResolver",
    "config": {
      ...
    },
  "timer": true
  }
}
"cache": object, optional

Configuration of caching for OAuth 2.0 access_tokens. By default, access_tokens are not cached. For an alternative way of caching of OAuth 2.0 access_tokens, configure CacheAccessTokenResolver.

When an access_token is cached, IG can reuse the token information without repeatedly asking the authorization server to verify the access_token. When caching is disabled, IG must ask the authorization server to verify the access_token for each request.

(From AM 6.5.3.) When an access_token is revoked on AM, the CacheAccessTokenResolver can delete the token from the cache when both of the following conditions are true:

  • The notification property of AmService is enabled.

  • The delegate AccessTokenResolver provides the token metadata required to update the cache.

    When a refresh_token is revoked on AM, all associated access_tokens are automatically and immediately revoked.

    "cache": {
      "enabled": configuration expression<boolean>,
      "defaultTimeout": configuration expression<duration>,
      "maxTimeout": configuration expression<duration>,
      "amService": AmService reference,
      "onNotificationDisconnection": configuration expression<enumeration>
    }
    enabled: configuration expression<boolean>, optional

    Enable or disable caching.

    Default: false

    defaultTimeout: configuration expression<duration>, optional

    The duration for which to cache an OAuth 2.0 access_token if it doesn’t provide a valid expiry value.

    If an access_token provides an expiry value that falls before the current time plus the maxTimeout, IG uses the token expiry value.

    The following example caches access_tokens for these times:

    • One hour, if the access_token doesn’t provide a valid expiry value.

    • The duration specified by the token expiry value, when the token expiry value is shorter than one day.

    • One day, when the token expiry value is longer than one day.

      "cache": {
        "enabled": true,
        "defaultTimeout": "1 hour",
        "maxTimeout": "1 day"
      }

      Default: 1 minute

    maxTimeout: configuration expression<duration>, optional

    The maximum duration for which to cache OAuth 2.0 access_tokens.

    If an access_token provides an expiry value that falls after the current time plus the maxTimeout, IG uses the maxTimeout.

    The duration cannot be zero or unlimited.

    "amService": AmService reference, required

    (From AM 6.5.3.) The AmService to use for the WebSocket notification service. To evict revoked access_tokens from the cache, enable the notifications property of AmService.

    See also AmService.

onNotificationDisconnection: configuration expression<enumeration>, optional

The strategy to manage the cache when the WebSocket notification service is disconnected, and IG receives no notifications for AM events. If the cache is not cleared it can become outdated, and IG can allow requests on revoked sessions or tokens.

Cached entries that expire naturally while the notification service is disconnected are removed from the cache.

Use one of the following values:

  • NEVER_CLEAR

    • When the notification service is disconnected:

      • Continue to use the existing cache.

      • Deny access for requests that are not cached, but do not update the cache with these requests.

    • When the notification service is reconnected:

      • Continue to use the existing cache.

      • Query AM for incoming requests that are not found in the cache, and update the cache with these requests.

  • CLEAR_ON_DISCONNECT

    • When the notification service is disconnected:

      • Clear the cache.

      • Deny access to all requests, but do not update the cache with these requests.

    • When the notification service is reconnected:

      • Query AM for all requests that are not found in the cache. (Because the cache was cleared, the cache is empty after reconnection.)

      • Update the cache with these requests.

  • CLEAR_ON_RECONNECT

    • When the notification service is disconnected:

      • Continue to use the existing cache.

      • Deny access for requests that are not cached, but do not update the cache with these requests.

    • When the notification service is reconnected:

      • Query AM for all requests that are not found in the cache. (Because the cache was cleared, the cache is empty after reconnection.)

      • Update the cache with these requests.

Default: CLEAR_ON_DISCONNECT

"executor": executor, optional

An executor service to schedule the execution of tasks, such as the eviction of entries in the access_token cache.

Default: ScheduledExecutorService

"requireHttps": configuration expression<boolean>, optional

Whether to require that original target URI of the request (originalUri in UriRouterContext) uses the HTTPS scheme.

If the request received by the web container is not using HTTPS, the request is rejected.

Default: true.

"realm": string, optional

HTTP authentication realm to include in the WWW-Authenticate response header field when returning an HTTP 401 Unauthorized status to a user-agent that need to authenticate.

Default: IG

"scopes": array of runtime expression<string> or ScriptableResourceAccess object, required

A list of one of more scopes that the OAuth 2.0 access_token must have.

array of runtime expression<string>, required if ScriptableResourceAccess is not used

A string, array of strings, runtime expression<string>, or array of runtime expression<string> to represent one or more scopes; mutually exclusive with ScriptableResourceAccess.

ScriptableResourceAccess, required if "array of runtime expression<string>" is not used

A script that produces a list of one or more required scopes; mutually exclusive with "array of runtime expression<string>".

The script evaluates each request dynamically and returns the scopes that request needs to access the protected resource. The script must return a Promise<Set, ResponseException> or a Set<String>.

For information about the properties of ScriptableResourceAccess, see Scripts. For an example of how to use this property, see the examples section on this page.

{
  "name": string,
  "type": "ScriptableResourceAccess",
  "config": {
    "type": string,
    "file": expression,                     // Use either "file"
    "source": string or array of strings,   // or "source", but not both.
    "args": object,
    "clientHandler": Handler reference
  }
}

Examples

For examples using OAuth2ResourceServerFilter, see Act As an OAuth 2.0 Resource Server.

Defining Required Scopes by Using a Script

The following example uses a ScriptableResourceAccess object to define the scopes required in a request:

  • If the request path is /rs-tokeninfo, only the scopes mail is required.

  • If the request path is /rs-tokeninfo/employee, the scopes mail and employeenumber are required.

  1. Set up and test the example in Validate Access_Tokens Through the Introspection Endpoint.

  2. Add the following route to IG:

    • Linux

    • Windows

    $HOME/.openig/routes/rs-dynamicscope.json
    appdata\OpenIG\rs-dynamicscope.json
    {
      "name": "rs-dynamicscope",
      "baseURI": "http://app.example.com:8081",
      "condition": "${matches(request.uri.path, '^/rs-dynamicscope')}",
      "heap": [
        {
          "name": "SystemAndEnvSecretStore-1",
          "type": "SystemAndEnvSecretStore"
        },
        {
          "name": "AmService-1",
          "type": "AmService",
          "config": {
            "agent": {
              "username": "ig_agent",
              "passwordSecretId": "agent.secret.id"
            },
            "secretsProvider": "SystemAndEnvSecretStore-1",
            "url": "http://openam.example.com:8088/openam/",
            "version": "7.1"
          }
        }
      ],
      "handler": {
        "type": "Chain",
        "config": {
          "filters": [
            {
              "name": "OAuth2ResourceServerFilter-1",
              "type": "OAuth2ResourceServerFilter",
              "config": {
                "scopes": {
                  "name": "myscript",
                  "type": "ScriptableResourceAccess",
                  "config": {
                    "type": "application/x-groovy",
                    "source": [
                      "// Minimal set of required scopes",
                      "def scopes = [ 'mail' ] as Set",
                      "if (request.uri.path =~ /employee$/) {",
                      "  // Require another scope to access this resource",
                      "  scopes += 'employeenumber'",
                      "}",
                      "return scopes"
                    ]
                  }
                },
                "requireHttps": false,
                "realm": "OpenIG",
                "accessTokenResolver": {
                  "name": "token-resolver-1",
                  "type": "TokenIntrospectionAccessTokenResolver",
                  "config": {
                    "amService": "AmService-1",
                    "providerHandler": {
                      "type": "Chain",
                      "config": {
                        "filters": [
                          {
                            "type": "HttpBasicAuthenticationClientFilter",
                            "config": {
                              "username": "ig_agent",
                              "passwordSecretId": "agent.secret.id",
                              "secretsProvider": "SystemAndEnvSecretStore-1"
                            }
                          }
                        ],
                        "handler": "ForgeRockClientHandler"
                      }
                    }
                  }
                }
              }
            }
          ],
          "handler": {
            "type": "StaticResponseHandler",
            "config": {
              "status": 200,
              "headers": {
                "Content-Type": [ "text/html" ]
              },
              "entity": "<html><body><h2>Decoded access_token: ${contexts.oauth2.accessToken.info}</h2></body></html>"
            }
          }
        }
      }
    }
  3. Test the setup with the mail scope only:

    1. In a terminal, use a curl command to retrieve an access_token:

      $ mytoken=$(curl -s \
      --user "client-application:password" \
      --data "grant_type=password&username=demo&password=Ch4ng31t&scope=mail" \
      http://openam.example.com:8088/openam/oauth2/access_token | jq -r ".access_token"
    2. Confirm that the access_token is returned for the /rs-dynamicscope path:

      $ curl -v http://openig.example.com:8080/rs-dynamicscope --header "Authorization: Bearer ${mytoken}"
      
      {
        active = true,
        scope = mail,
        client_id = client-application,
        user_id = demo,
        token_type = Bearer,
        exp = 158...907,
        sub = demo,
        iss = http://openam.example.com:8088/openam/oauth2, ...
        ...
      }
      • Confirm that the access_token is not returned for the /rs-dynamicscope/employee path:

        $ curl -v http://openig.example.com:8080/rs-dynamicscope/employee --header "Authorization: Bearer ${mytoken}"
  4. Test the setup with the scopes mail and employeenumber:

    1. In a terminal window, use a curl command similar to the following to retrieve an access_token:

      $ mytoken=$(curl -s \
      --user "client-application:password" \
      --data "grant_type=password&username=demo&password=Ch4ng31t&scope=mail%20employeenumber" \
      http://openam.example.com:8088/openam/oauth2/access_token | jq -r ".access_token")
    2. Confirm that the access_token is returned for the /rs-dynamicscope/employee path:

      $ curl -v http://openig.example.com:8080/rs-dynamicscope/employee --header "Authorization: Bearer ${mytoken}"