Wednesday 13 July 2016

Fun with OpenAM13 Authz Policies over REST - the ‘Claims’ parameter of the ‘Subject’

Consider this API for a moment:
https://backstage.forgerock.com/#!/docs/openam/13/dev-guide#rest-api-authz-policy-decision-concrete

In particular consider the ‘subject’ field to be passed to the endpoint.

Using the ssoToken as the ‘subject’ is fairly easy to understand. You have full access to the subject’s datastore properties in a policy condition, including things like group membership.
But what do the other ‘subject’ options mean?
The documentation isn’t particularly expansive on how to use the ‘jwt’ and ‘claims’ subject parameters that are available in the REST interface.

This article will focus on the ‘claims’ parameter.

Firstly, it’s worth noting that these options are *not* mutually exclusive. You can specify multiple subject parameters and they will be combined. We’ll come back to this point later in this article.

Let’s look at the ‘claims’ parameter in detail.
This needs to be specified as an object map. e.g.
{"key1":"value1","key2":"value2",…}

The critical thing that the docs omit is that you must include a ‘sub’ key. Without this you will receive an ‘Invalid value subject’ error.
The value of the ‘sub’ key seems largely irrelevant in that it doesn’t automatically hydrate this value into a corresponding subject in the datastore. It is, however, a valid ‘claim’ that can be used in the policy. For example:

Of course, the ‘sub’ claim value could be empty.  All that’s important is that it is included as a key.  

So, with a policy defined like this:

You could specify a policy evaluation request like this:
curl --request POST --header "iPlanetDirectoryPro: AQIC5…*” --header "Content-Type: application/json" --data '{"resources":["customers"],"application":"api","subject":{"claims":{"sub":"","iss":"http://as.uma.com:8080/openam/oauth2/ScopeAz"}}}' http://as.uma.com:8080/openam/json/ScopeAz/policies?_action=evaluate

And you would receive a successful policy evaluation response.

Note that the iPlanetDirectoryPro header must be an ssoToken of a user with permissions to execute this REST API call.  In all these samples I’m using the ssoToken of amadmin here.

I might want to augment this policy so that only members of a given a datastore group would be considered.  I might think this is as easy as ensuring I include the correct ‘sub’ value of a user in the datastore that has a group membership, and updating the policy ‘subjects’ tab to include the group membership.  For example:


Then issue (note the ‘demo’ value for the ‘sub’ key):
curl --request POST --header "iPlanetDirectoryPro: AQIC5…*” --header "Content-Type: application/json" --data '{"resources":["customers"],"application":"api","subject":{"claims":{"sub":"demo","iss":"http://as.uma.com:8080/openam/oauth2/ScopeAz"}}}' http://as.uma.com:8080/openam/json/ScopeAz/policies?_action=evaluate

But, as stated before, the ‘sub’ value is not hydrated to a datastore subject so the policy cannot know about the group memberships.

To prove this, and also to show that multiple ‘subjects’ are combined by the policy evaluation we can get an ssoToken for the ‘demo’ user using the ‘authenticate’ api in OpenAM.  Then issue something like:
curl --request POST --header "iPlanetDirectoryPro: AQIC5..*" --header "Content-Type: application/json" --data '{"resources":["customers"],"application":"api","subject":{"ssoToken":"AQIC5..*","claims":{"sub":"demo","iss":"http://as.uma.com:8080/openam/oauth2/ScopeAz"}}}' http://as.uma.com:8080/openam/json/ScopeAz/policies?_action=evaluate

And you will get a successful response.

Note that I have two separate ssoTokens here.  One is the iPlanetDirectoryPro token which corresponds to a user with permissions to execute the REST API call (I’m using amadmin here).  The other, used in the ssoToken parameter, is that of the ‘demo’ user.

If I was to execute the same call, but remove the ‘claims’ parameter, e.g.
curl --request POST --header "iPlanetDirectoryPro: AQIC5..*" --header "Content-Type: application/json" --data '{"resources":["customers"],"application":"api","subject":{"ssoToken":"AQIC5..*"}}' http://as.uma.com:8080/openam/json/ScopeAz/policies?_action=evaluate
then the policy would return false.

This proves that both the ‘ssoToken’ and ‘claims’ parameters are being considered together as part of the policy evaluation of the ‘subjects’ tab.  i.e. the group membership is being evaluated based on the ssoToken (which is a representation of the user in the datastore) as well as the ‘iss’ claim value passed by the REST API call.

So can I use the ‘claims’ parameter *only* (i.e. without an ssoToken) in order to evaluate a policy based on group membership where the ‘sub’ claim is assumed to be a user in the datastore?

Well, yes!
Let me return my ‘subjects’ tab to the original:


Now let’s consider the ‘environments’ tab.


Maybe we could try an LDAP Filter condition?  Well, not exactly.  The LDAP Filter condition implicitly adds the uid of the user in the datastore to the query by relying on the contents of the ‘subject’ ssoToken.  And, as we now know, the ‘sub’ claim is not rehydrated to a datastore subject/ssoToken so the implicit addition of the uid means the query fails top return a result.

But, what if we look at the ‘Scripted Authorisation Condition’?
One thing we do have available in a scripted condition is a ‘username’ property.  Now this *is* populated from the ‘sub’ claim.
(This also makes it very clear that the ‘identity’ object, which is a reference to the subject in the datastore, is *only* available if the ssoToken is used).

So, with the ‘username’ available perhaps we can write a script that connects to a directory (the datastore, for arguments sake) and reads properties, such as group membership for that subject?
It turns out that, yes, we can!

Head to the ‘Scripts’ page and create a ‘Policy Condition’ script like this:




The script would be something like this:

import org.forgerock.openam.ldap.LDAPUtils
import org.forgerock.util.Options
import org.forgerock.opendj.ldap.Connection
import org.forgerock.opendj.ldap.LDAPConnectionFactory
import org.forgerock.opendj.ldap.SearchScope

//default to false
authorized=false

//log the username, which will be the 'subject' as defined by the claims.sub param
logger.message('username' + username)

//set up the ldap server connection params
ldapServer='localhost'
ldapPort=50389
ldapBindUser='cn=Directory Manager'
ldapBindPassword='password'

//connect to ldap server
LDAPUtils ldapUtils = new LDAPUtils()
Options options = new Options()
LDAPConnectionFactory factory = ldapUtils.createFailoverConnectionFactory(ldapServer, ldapPort, ldapBindUser, ldapBindPassword, options.defaultOptions())
Connection connection = factory.getConnection();

//define the filter
groupFilter = '(isMemberOf=cn=api_customer,ou=groups,dc=openam,dc=forgerock,dc=org)'
userFilter = '(uid='+username+')'
filter = '(&'+userFilter+groupFilter+')'
baseDN = 'ou=people,dc=openam,dc=forgerock,dc=org'

//issue the search
try {
  result = connection.searchSingleEntry(baseDN, SearchScope.SUBORDINATES, filter, 'uid')
  logger.message('result:' + result.getAttribute('uid').toString())
  //success!
  authorized=true
} catch (any) {
    logger.message('Assume filter returned no results, so assume that user is not a member of the group')
}


You will want to ensure that the groupFilter variable matches your environment.
You may also need to change the connection details.
You may also want to improve the error handling, or use a different method for querying the ldap.

Note the ‘imports’ lines at the top of the script.  Some of these classes are not on the script module ‘whitelist’ for Policy Condition scripts.  In order for the script to run, they need to be added to the whitelist.
So, head to Configuration -> Global -> Scripting -> Policy condition -> Engine Configuration
and ensure the imported class names are permitted by the whitelist patterns.

Now, head back to the ‘Environments’ tab of the Policy and configure it like this:


Now let’s assume you have two users in your datastore: Alice and Bob.
Let’s also assume you have a group called ‘api_customer’.
Now Bob is made a member of the group, but Alice is not.

So, issuing:
curl --request POST --header "iPlanetDirectoryPro: AQIC5..*" --header "Content-Type: application/json" --data '{"resources":["customers"],"application":"api","subject":{"claims":{"sub":"alice","iss":"http://as.uma.com:8080/openam/oauth2/ScopeAz"}}}' http://as.uma.com:8080/openam/json/ScopeAz/policies?_action=evaluate
will return failure, i.e.:
[
 {
 "advices":{},
 "ttl":9223372036854775807,
 "resource":"customers",
 "actions":{},
 "attributes":{}
 }
]

Whereas:
curl --request POST --header "iPlanetDirectoryPro: AQIC5..*” --header "Content-Type: application/json" --data '{"resources":["customers"],"application":"api","subject":{"claims":{"sub":"bob","iss":"http://as.uma.com:8080/openam/oauth2/ScopeAz"}}}' http://as.uma.com:8080/openam/json/ScopeAz/policies?_action=evaluate
will return success, i.e.:

[
  {
    "advices":{},
    "ttl":9223372036854775807,
    "resource":"customers",
    "actions":{
      "permit":true
    },
    "attributes":{
      "hello":["world"]
    }
  }
]

Note that in my policy, I have an action of ‘permit’ defined, and also Static Response Response attributes of ‘hello:world’.

Don’t try to use ‘Subject Attributes’ because these depend on the SSOToken referencing the datastore identity.  You could, however, update the script to return attributes returned from the call to the directory if you wanted to emulate this functionality.  See: https://backstage.forgerock.com/#!/docs/openam/13/dev-guide#scripting-api-authz-response


No comments:

Post a Comment