Client and Subject Authorization in IDM

API designers need to have a solid understanding of the different forms of authorization introduced by OAuth 2.0. This article attempts to clarify the OAuth 2.0 authorization model and apply it to ForgeRock Identity Management (IDM) 7.0.

Traditional authorization models

Traditional authorization logic for applications attempts to answer these questions:

Authentication: Who are you?

Authorization: What are you allowed to do?

This logic is asking a question about “you”; you are the subject of the authorization decision. Every secure application has rules that are used to answer these basic authorization questions for the subject. In the case of IDM, these subject-based authorization rules are described in the Authorization and Access Control chapter in the IDM Security Guide. This aspect of authorization has basically remained the same since it was first implemented in IDM 2.1.

Adding clients into the authorization model

In the 7.0 release of the ForgeRock® Identity Platform, the authorization model for IDM has been expanded to include a new aspect. As described in detail in my article Understanding and Troubleshooting ForgeRock Platform Integration, IDM now is able to operate as an OAuth2 resource server. In addition to being a well-known standard for calling REST APIs, OAuth 2.0 is designed to support a more sophisticated, standards-based authorization model.

If you aren’t very familiar with the high-level design of OAuth 2.0, it is worthwhile to review my OAuth2 Apartment Building article. This will give you the details necessary to compare and contrast OAuth 2.0 with other authorization models.

The key new factor introduced by OAuth 2.0 is the client. The client is an untrusted application which is making API requests on behalf of the subject. Because they are untrusted, client applications have their own authorization rules. These authorization rules are completely distinct from the authorization rules of the subject. Determining whether or not a given request is allowed is done by combining the results of these two types of authorization rules.

An example OAuth 2.0 client that was introduced in the ForgeRock Identity Platform 7.0 release is the platform-enduser UI.

In an OAuth 2.0 context, authorization logic is expanded to answer these questions:

Subject Authentication: Who are you?

Subject Authorization: What are you allowed to do?

Client Authentication: What application is being used?

Client Authorization: What is that application allowed to do on your behalf?

Client authorization rules are implemented with scopes. Scopes are simple labels which correspond to some part of the API that a particular client application would like to use. The scope values will depend on how a particular set of APIs define them. A client application is designed to request whatever scopes it needs from the authorization server. It is the responsibility of the authorization server to decide which scopes to grant to the client. It typically does this based on these two factors:

  • Which scopes have been specifically listed for the client as part of its registration in the authorization server.
  • Whether or not the subject gives their consent to the client to have the requested scope on their behalf.

Other logic may also be employed for granting scopes. See Dynamic OAuth 2.0 Authorization in the ForgeRock Access Management (AM) Authorization Guide for examples on how this could be expanded.

The scopes granted to a client are captured within the access token generated by the authorization server. This access token is included by the client when it makes API calls. The server hosting the APIs (known as the resource server) introspects the access token to see which scopes it has. For any given API request, the server has to verify that the appropriate scopes are included in the token. This “scope check” is how client authorization is enforced. Any request which doesn’t include an access token bearing the appropriate scopes will fail.

After the client authorization passes, the server must still verify that the subject authorization also passes (using much the same logic as is used in a traditional authorization model). The subject is part of the access token (it’s the “sub” value in a standard introspection response), so the server can easily use it to find the appropriate subject authorization rules. It’s important to emphasize that a client application does not have any more access to do something on a subject’s behalf than the subject does on their own.

Subject rules always apply for a given subject, regardless of which scopes a client has obtained. Likewise, scope enforcement will always constrain a client, regardless of what the subject may be able to do on their own.

It is important to note that even client applications which are built and operated by the organization hosting the APIs (sometimes referred to as “first-party” clients) should be constrained by client authorization. It may seem unexpected that such applications should be considered untrusted; after all, they are part of the same team. Why shouldn’t they be trusted? While it’s certainly true that users don’t need to be bothered to grant consent in this scenario, there is still security value in limiting the APIs that your own applications can call. Doing so limits exposure from internal bad actors and external exploits.

IDM scope design and enforcement

Current product support

In the 7.0 release, IDM includes a very basic (but important!) implementation of client authorization via scope checking. The rsFilter option for authentication.json looks something like so:

{
  "rsFilter": {
    "clientId": "idm-client",
    "clientSecret": "idm-client-secret",
    "tokenIntrospectUrl": "http://am.example:8080/openam/oauth2/introspect",

    "scopes": [ "fr:idm:*" ],

    "subjectMapping" : [ "..." ],
    "staticUserMapping" : [ "..." ],
    "anonymousUserMapping" : [ "..." ]
  }
}

This configuration states that every request to IDM APIs must have an access token with all of the listed scopes included. In the case of this example, the one scope required for all requests is fr:idm:*. While this is not a particularly complex scope design, even this minimal scope check serves an important purpose.

Defining a scope that is unique to IDM lets ForgeRock platform administrators indicate which clients may have any kind of interaction with IDM. Only those with this unique scope registered to them can do so. This wouldn’t be the case if IDM only required a generic scope (for example, openid). If that were the case, any client application that obtained an access token with those generic scopes could use it to call IDM on the subject’s behalf; this could easily become a major security risk. Always deploy IDM with a unique, IDM-specific scope required.

Fine-grained scope evaluation through customization

With a little bit of custom script writing, you can add more sophisticated scope checking logic to your IDM 7.0 installation.

A quick note on future product features: it’s expected that IDM will have a more sophisticated scope checking mechanism in a future release. As such, if you decide to implement fine-grained scope checking as described below, there will be some migration effort necessary if you choose to use the future release implementation.

The first change you need to make is within your router.json config. The first filter you see in there by default is for subject authorization (router-authz). As explained above, all you need to do is add another filter before that one which does the client authorization - i.e., check that the scopes match the request. Here’s an example filter entry:

{
    "condition" : {
        "type" : "text/javascript",
        "source" : "context.caller.external === true"
    },
    "onRequest" : {
        "type" : "groovy",
        "file" : "scopeCheck.groovy"
    }
}

And a sample implementation for scopeCheck.groovy that you can easily start with:

// Sample script for client authorization of particular API requests
import static org.forgerock.json.resource.ResourceException.newResourceException
import org.forgerock.http.oauth2.OAuth2Context
import org.forgerock.json.resource.AdviceContext

if (!context.containsContext(OAuth2Context.class)) {
    return;
}

def accessTokenInfo = context.asContext(OAuth2Context.class).getAccessToken().asJsonValue()
def scopes = accessTokenInfo.scope.asString().tokenize(" ")
def requestMethod = request.getRequestType().name().toLowerCase()
def requestResourcePath = request.resourcePath
def requestAction = null

if (requestMethod == "action") {
    requestAction = request.action
}

def requireScope = { scopeToCheck ->
    if (!scopes.inject(false) { anyScopeResult, currentScope ->
        anyScopeResult || currentScope == scopeToCheck
    }) {
        context.asContext(AdviceContext.class).putAdvice("WWW-Authenticate",
            "Bearer scope=\"${scopeToCheck}\",error=\"insufficient_scope\"")
        throw newResourceException(403, "Missing required scope ${scopeToCheck}")
    }
}

/*
  Add your scope checking logic below, using the above variables. For example:

if (requestMethod == "read" && requestResourcePath.startsWith("managed/user/")) {
    requireScope("fr:idm:whatever")
}
*/

Using this script as a starting point, you can add whatever conditional logic applies for mapping scopes to API requests. Just keep in mind that your logic is for clients, not subjects. This script shouldn’t have any reference to the security context; that is for subject authorization. The context that is of primary interest for client authorization is the OAuth2Context;in particular, the scopes that you read from there, as shown.

If you decide to implement finer-grained client authorization, the challenge for you will be deciding how to design your scopes so that they are appropriate for your clients. Scope design is a tricky topic, and there is no one right way to do it. Choosing the right granularity for scopes depends highly on the nature of the clients and the APIs. If the scopes are too granular, using them may become a burden on the client (and possibly on the subject granting consent). If the scopes are too broad, you risk over-exposure. Ultimately, you will need to carefully review your APIs and find meaningful ways to categorize them.

You may be able to get some best-of-both-worlds behavior by designing scopes with wild cards in the name; fr:idm:* may be considered a starting-point for that idea. This kind of pattern-matching logic within scopeCheck.groovy would be up to you to implement, though.

Clients which operate on their own behalf

There is one case that tends to cause a lot of confusion when the topic of scopes and authorization is raised—when clients operate on their own behalf and not on behalf of a separate subject. That’s right - sometimes the client is the subject.

Applications may be allowed to call APIs as themselves. This is sometimes referred to as “machine-to-machine” access. This is possible when clients use the client credentials grant to obtain their access token. When they do this, the sub value in the token introspection response is the client_id. For this to be possible, the client has to be confidential, (meaning, registered with a secret) and it has to have the “client_credentials” grant type enabled for it. Just as with other clients, they also need to be registered with particular scopes; they will only be able to request the scopes they have registered.

In this case, is there any sense to be made of the distinction between client and subject authorization? Are scopes enough on their own? Should scopes even matter? These are all reasonable questions.

I propose that even in the case of a client operating on its own behalf, there is value to maintaining the separate concepts of client and subject authorization.

Client authorization primarily means high-level access to particular APIs. These APIs are protected by the same scope enforcement that every client has to pass, regardless of which type of client they happen to be, and regardless of the subject. This makes the enforcement logic simple and consistent—good attributes for security and auditability.

Likewise, the next filter will need to find the subject and the authorization rules which apply to them, regardless of the client they happen to be using. That same logic can and should be used for subjects which are people and subjects which are applications. It is also easier to build finer-grained authorization rules at this level.

While it may be possible to blur the lines between client and subject authorization in this one case, it’s not going to be in your best interests to do so. Keep the concepts distinct and you’ll be ready to work with all types of clients and subjects.

Supporting client-based access tokens in IDM

How IDM can use access token requests which have the “client_id” for the “sub” value.

Static client-to-subject mapping

As described in the “Static user mapping” section of Understanding and Troubleshooting ForgeRock Platform Integration the default product configuration of authentication.json includes a sample that demonstrates one way to handle client-based access tokens. Review the staticUserMapping block; in particular the way that idm-provisioning has been declared:

"staticUserMapping" : [
    {
        "subject": "idm-provisionsing",
        "localUser": "internal/user/idm-provisioning",
        "roles" : [
            "internal/role/platform-provisioning"
        ]
    }
]

The idm-provisioning subject is exactly the kind of subject that is also a client. It’s designed for the “machine-to-machine” pattern; the AM “Platform Self-Service” nodes operate on one side and the IDM APIs on the other. Internally, AM is initiating a client credentials grant in order to make these calls to IDM. But there is nothing special about the way AM is doing it—any other client can do the same thing. The trick is getting IDM to recognize the client as a valid subject. That’s what the above staticUserMapping block is doing, recognizing the idm-provisioning client as an authenticated “local user”, and assigning it a role so the subject authorization filter can evaluate it.

Dynamic client-to-subject mapping

This is not the only way that IDM can map a client_id to an authenticated “local user”, however. You can also use the subjectMapping option to look for the client_id within a resource collection available on the IDM router (remember, the sub entry of the access token holds the client_id value in this case). The default product configuration specifies subjectMapping like so:

"subjectMapping" : [
  {
    "queryOnResource": "managed/user",
    "propertyMapping": {
      "sub": "_id"
    },
    "userRoles": "authzRoles/*",
    "defaultRoles" : [
      "internal/role/openidm-authorized"
    ]
  }
]

This looks for managed/user records which have matching _id values compared to the sub value read from the access token. This works fine when all of your access tokens are issued to users, but it doesn’t work so well when you also issue them to clients. If you want to support both types of subjects, then you’ll probably want to maintain separate resource types for them; for example, managed/user and managed/client.

Expanding the subjectMapping logic to look for other resource types besides just managed/user is possible with a dynamic queryOnResource value, like so:

"subjectMapping" : [
  {
    "queryOnResource": "managed/{{resourceType}}",
    "propertyMapping": {
        "sub": "_id"
    },
    "userRoles": "authzRoles/*",
    "defaultRoles" : [
        "internal/role/openidm-authorized"
    ]
  }
]

The value for queryOnResource is now a handlebars template, which draws its input from the access token introspection response. Here we can see it’s referring to a custom value called resourceType. This value doesn’t normally exist as part of an access token. To add it, we have to update AM to use an Access Token Modification Script, like so:

if (scopes.contains('fr:idm:*')) {
  if (accessToken.getGrantType() == 'client_credentials') {
    accessToken.setField('resourceType', 'client')
  } else {
    accessToken.setField('resourceType', 'user')
  }
}

This modification means the access token introspection response will have resourceType defined, either with user or client, depending on which grant was used. Here’s an example introspection response with this change included:

{
  "active": true,
  "scope": "fr:idm:*",
  "realm": "/",
  "client_id": "myClientId",
  "user_id": "myClientId",
  "exp": 1601070296,
  "sub": "myClientId",
  "resourceType": "client",
  "iss": "https://am.example.com/am/oauth2"
}

IDM can then look for managed/user or managed/client records having an _id which matches the sub. Whether or not IDM is able to find any matching records is a separate issue. There are different strategies available for IDM to deal with this problem; maintaining the set of managed/client records is essentially the same challenge you would have with maintaining managed/user records. In both cases, the value known to AM when it created the token has to also be known to IDM when it reads it.

The most straightforward way to maintain your managed/client entries is to simply create them via REST with the same _id value as the client_id, like so:

curl -H "Authorization: Bearer x_AdminAccessToken_x" -H "If-None-Match: *" -H "Content-type: application/json" -X PUT \
    --data '{"name": "Client name", "otherdetails": "whatever you want to store for this client"}' \
    https://idm.example.com/openidm/managed/client/$CLIENT_ID

You may also be able to build a REST connector to AM that reads the OAuth Clients from AM’s REST API, and syncs them to managed/client. Unfortunately, there is no pre-built connector for this, so you would need to do this yourself. If you are going to do so, I recommend starting with the ScriptedRESTConnector option from the Groovy Connector Toolkit.

Conclusion

The patterns discussed here in terms of client vs. subject authorization, scope design, client-based tokens, etc. All apply to any OAuth 2.0 API ecosystem. Be sure you apply them correctly, so your APIs are secure and can be easily called by your clients. You can also use the particular examples given for IDM as the basis for securely integrating the ForgeRock Identity Platform alongside your other APIs.

1 Like