Token Exchange and Delegation using the ForgeRock Identity Microservices

In 2017 ForgeRock introduced an Early Access program (aka beta) for the ForgeRock Identity Microservices. In summary the capabilities offered include token issuance using the OAuth2 client credentials grant, token validations of OAuth2/OIDC tokens (and even ssotokens) and token exchange based on the draft OAuth2 token exchange spec. I wrote a press release here and an introductory community blog post here.

In this article we will discuss how to implement delegation as per the OAuth2 Token Exchange  draft specification. An overview of the process is worth discussing at this point.

We basically first want to grab hold of an OAuth2 token for our client to use in an Authorization header when calling the Token Exchange service. Next, we need our Authorization Server such as ForgeRock Access Management in our example, to issue our friendly neighbors Alice and Bob their respective OIDC id_tokens. Note that Alice wants to delegate authority to Bob. While we shall show the REST calls to acquire those, the details of setting up the OAuth2 Provider will be left out. We shall however discuss the custom OIDC Claims Script for setting up the delegation framework for authenticated users, as well as the Policy that sets up allowed actors who may be chosen for delegation.

But first things first, so we start with acquiring a bearer authentication token for our client who will make calls into the Token Exchange service. This can be accomplished by having the Authentication microservice issue an OAuth2 access token with the exchange scope using the client-credentials grant_type. For the example we shall just use a JSON backend as our client repository, although the Authentication microservice supports using Cassandra, MongoDB, any LDAP v3 directory including ForgeRock Directory Services.

The repository holds the following data for the client:

{
 "clientId" : "client",
 "clientSecret" : "client",
 "scopes" : [ "exchange", "introspect" ],
 "attributes" : {}
 }

The authentication request is:

POST /service/access_token?grant_type=client_credentials HTTP/1.1
Host: localhost:18080
Content-Type: multipart/form-data
Authorization: Basic ****
Cache-Control: no-cache

After client authenticates, the Authentication microservice issues a token that when decoded looks like this:

{
 "sub": "client",
 "scp": [
     "exchange",
     "introspect"
 ],
 "auditTrackingId": "237cf708-1bde-4800-9bcd-5db5b5f1ca12",
 "iss": "https://fr.tokenissuer.example.com",
 "tokenName": "access_token",
 "token_type": "Bearer",
 "authGrantId": "a55f2bad-cda5-459e-8620-3e826bad9095",
 "aud": "client",
 "nbf": 1523058711,
 "scope": [
     "exchange",
     "introspect"
 ],
 "auth_time": 1523058711,
 "exp": 1523062311,
 "iat": 1523058711,
 "expires_in": 3600,
 "jti": "39b72a84-112d-46f3-a7cb-84ce7513e5bc",
 "cid": "client"
}

Bob, the actor, authenticates with AM and receives id_token:

{
 "at_hash": "Hgjw0D49EfYM6dmB9_K6kg",
 "sub": "user.1",
 "auditTrackingId": "079fda18-c96f-4c78-90f2-fc9be0545b32-111930",
 "iss": "http://localhost:8080/openam/oauth2",
 "tokenName": "id_token",
 "aud": "oidcclient",
 "azp": "oidcclient",
 "auth_time": 1523058714,
 "realm": "/",
 "may_act": {},
 "exp": 1523062314,
 "tokenType": "JWTToken",
 "iat": 1523058714
}

Alice, the user also authenticates with AM and receives a special id_token:

{
 "at_hash": "nT_tDxXhcee7zZHdwladcQ",
 "sub": "user.0",
 "auditTrackingId": "079fda18-c96f-4c78-90f2-fc9be0545b32-111989",
 "iss": "http://localhost:8080/openam/oauth2",
 "tokenName": "id_token",
 "aud": "myuserclient1",
 "azp": "myuserclient1",
 "auth_time": 1523058716,
 "realm": "/",
 "may_act": {
     "sub": "Bob"
 },
 "exp": 1523062316,
 "tokenType": "JWTToken",
 "iat": 1523058716
}

Why does Alice receive a may_act claim but Bob does not? This has to do with Alice’s group membership in DS. The OIDC Claims script attached to the OAuth2 Provider in AM checks for membership to this group, and if found retrieves a value for the assigned “actor” or delegate. In this case Alice’s has designed actor status to Bob (via some out of band method, such as on a user portal, for example). The script retrieves the actor and sets a special claim with its value: may_act. The claims script is shown here in part:

def isAdmin = identity.getMemberships(IdType.GROUP)
.inject(false) { found, group ->
found ||
group.getName() == “policyEval”
}

//…search for actor.. not shown

def mayact = [:]
if (isAdmin) {
mayact.put(“sub”,actor)
}

Next, we must define a Policy in AM. This policy could be based on OAuth2 scopes or URI, whatever you choose. I chose to setup an OAuth2 Scopes policy but whatever scope you choose must match the resource you pass into the TE service. Rest of the policy is pretty standard, for example, here is a summary of the Subject tab of the policy:

{
  "type": "JwtClaim",
  "claimName": "iss",
  "claimValue": "http://localhost:8080/openam/oauth2"
}

I am checking for an acceptable issuer, and thats it. This could be replaced with a subject condition for Authenticated users as well although the client would then have to send in Alice’s “ssotoken” as the subject_token instead of her id_token when invoking the TE service.

This also implies that delegation is “limited” to the case where you are using a JWT for Alice, preferably an OIDC id_token. We cannot do delegation without a JWT because the TE depends on the presence of a “may_act” claim in Alice’s subject_token. When using only an ssotoken or an access_token for Alice we cannot pass in this claim to the TE service. Hopefully thats clear as mud now (laughter).

Anyway.. back to the policy setup in AM. Keep in mind that the TE service expects certain response attributes: “aud” and “scp” are mandatory. “allowedActors” is optional but needed for our discussion of course. It contains a list of allowed actors and this list could be computed based on who the subject is. One subject attribute must also be returned. A simple example of response attributes could be:

aud: images.example.com
scp: read,write
allowedActors: Bob
allowedActors: HelpDeskAdministrator
allowedActors: James

The subject must also be returned from the policy and one way to do that is to parse the id_token and return the sub claim as a response attribute. A scripted policy condition attached to the Policy Environment extracts the sub claim and returns it in the response.

This policy script is attached as an environment condition as follows:

The extracted sub claim is then included as a response attribute.

At this point we are ready to setup the Token Exchange microservice to invoke the Policy REST endpoint in AM for resource-match, allowed actions and subject evaluation. Keep in mind that the TE service supports pluggable policy backends – a really powerful feature – using which one could have specific “pods” of microservices running local JSON-backed policies, other “pods” could have Open Policy Agent (OPA)-powered regex policies and yet another set of “pods” could have the TE service calling out to AM’s policy engine for evaluation.

The setup for when the TE is configured for using AM as the token exchange policy backend  looks like this:

{
 // Credentials for an OpenAM "subject" account with permission to access the OpenAM policy endpoint
 "authSubjectId" : "&{EXCHANGE_OPENAM_AUTH_SUBJECT_ID|service-account}",
 "authSubjectPassword" : "&{EXCHANGE_OPENAM_AUTH_SUBJECT_PASSWORD|****}",

// URL for the OpenAM authentication endpoint, without query parameters
 "authUrl" : "&{EXCHANGE_OPENAM_AUTH_URL|http://localhost:8080/openam/json/authenticate}",

// URL for the OpenAM policy-endpoint, without query parameters
 "policyUrl" : "&{EXCHANGE_OPENAM_POLICY_URL|http://localhost:8080/openam/json/realms/root/policies}",

// OpenAM Policy-Set ID
 "policySetId" : "&{EXCHANGE_OPENAM_POLICY_SET_ID|resource_policies}",

// OpenAM policy-endpoint response-attribute field-name containing "audience" values
 "audienceAttribute" : "&{EXCHANGE_OPENAM_POLICY_AUDIENCE_ATTR|aud}",

// OpenAM policy-endpoint response-attribute field-name containing "scope" values
 "scopeAttribute" : "&{EXCHANGE_OPENAM_POLICY_SCOPE_ATTR|scp}",

// OpenAM policy-endpoint response-attribute field-name containing the token's "subject" value
 "subjectAttribute" : "&{EXCHANGE_OPENAM_POLICY_SUBJECT_ATTR|uid}",

// OpenAM policy-endpoint response-attribute field-name containing the allowedActors
 "allowedActorsAttribute" : "&{EXCHANGE_OPENAM_POLICY_ALLOWED_ACTORS_ATTR|may_act}",

// (optional) OpenAM policy-endpoint response-attribute field-name containing "expires-in-seconds" values
 "expiresInSecondsAttribute" : "&{EXCHANGE_OPENAM_POLICY_EXPIRES_IN_SEC_ATTR|}",

// Set to "true" to copy additional OpenAM policy-endpoint response-attributes into generated token claims
 "copyAdditionalAttributes" : "&{EXCHANGE_OPENAM_POLICY_COPY_ADDITIONAL_ATTR|true}"
}

The TE services checks the response attributes shown above in the policy evaluation result in order to setup claims in the signed JWT it returns to the client.

The body of a sample REST Policy call from the TE service to an OAuth2 scope policy defined in AM looks like as follows:

{
 "subject" : {
     "jwt" : "{{AM_ALICE_ID_TOKEN}}"
 },
 "application" : "resource_policies",
 "resources" : [ "delegate-scope" ]
}

Policy response returned to the TE service:

[
 {
 "resource": "delegate-scope",
 "actions": {
     "GRANT": true
 },
 "attributes": {
     "aud": [
         "images.example.com"
     ],
     "scp": [
         "read"
     ],
     "uid": [
         "Alice"
     ],
     "allowedActors": [
          "Bob"
     ]
 },
 "advices": {},
 "ttl": 9223372036854775807
 }
]

Again, whatever resource type you choose for the policy in AM, it must match the resource you pass into the TE service as shown above.

We have come far and if you are still with me, then it is time to invoke the TE service and get back a signed and exchanged JWT with the correct “act” claim to indicate delegation was successful. The decoded JWT claims set of the issued token is shown below. The new JWT is issued by the Token Exchange service and intended for consumption by a system entity known by the logical name “images.example.com” any time before its expiration. The subject “sub” of the JWT is the same as the subject of the subject_token used to make the request.

{
 "sub": "Alice",
 "aud": "images.example.com",
 "scp": [
     "read",
     "write"
 ],
 "act": {
     "sub": "Bob"
 },
 "iss": "https://fr.tokenexchange.example.com",
 "exp": 1523087727,
 "iat": 1523084127
}

The decoded claims set shows that the actor “act” of the JWT is the same as the subject of the “actor_token” used to make the request. This indicates delegation and identifies Bob as the current actor to whom authority has been delegated to act on behalf of Alice.

Impersonation Authentication module for OpenAM

Introduction

Support for impersonation is useful in the enterprise use cases where designated administrators are required to act on behalf of a user in certain scenarios. By impersonating another user an administrator, if authorized to do so, gains access to a restricted view of the user’s profile in the system. This is helpful in situations involving password reset, request-based access and profile updates. However, the design of such a system must call for controls that actively restrict access to the user’s entitlements at the outset. This can be achieved using step-up authentication for gaining access to private user data, and also by using the OpenAM policy engine for performing advanced resource-based decisioning.

Configuration

An OpenAM custom authentication module was written to enable impersonation support. The module requires input of the username of the end-user being impersonated and the administrator credentials. After submitting the username and password, the admin account is authenticated first and then it is also authorized to complete the impersonation request using REST calls to a specified OpenAM Policy endpoint. This policy can be either local or external as we shall examine further. The impersonated user is also validated for being in active status in the system. If all is okay, the administrator is permitted to impersonate and OpenAM creates a session for the impersonated user. The module can be configured using the following gauges to complete the described functions correctly:

  1. Setup the resource-set you want to check policy for. This resource set its nothing but a special URL that invokes policy evaluation for impersonation
  2. The authentication realm you want the administrator to authenticate in. The authentication module allows for realm-specific authentication
  3. The OpenAM server where the policy resides, the realm where the policy resides, and the policy-set name. The policy does not need to be local and can be on a remote policy host
  4. Check whether you want the administrator to be a member of a local group as well, in addition to the external policy authorization.
A step by step account of the workings of the module follows.

Development

Configuration read from Module Instance

options -> {iplanet-am-auth-check-group-membership=[True], iplanet-am-auth-impersonation-hash-enabled=[true], iplanet-am-auth-authentication-realm=[authn], iplanet-am-auth-impersonation-auth-level=[1], iplanet-am-auth-resource-set=[http://openam:8080/openam/index.html], moduleInstanceName=impersonate, iplanet-am-auth-impersonation-id=[Enter the user id to impersonate?], iplanet-am-auth-impersonation-group-name=[impersonation], iplanet-am-auth-openam-server=[http://openam:8080/openam], iplanet-am-auth-policy-realm=[impersonation], iplanet-am-auth-policy-set-name=[impersonation]}

Authorize the administrator locally

In our test scenario, the ‘user.0’ is really an administrative user that has been granted membership to the group named ‘impersonation’, as configured in the module (see above).

We build an AMIdentity object for the group and validate membership.

[AMIdentity object: id=impersonation,ou=group,o=impersonation,ou=services,dc=openam,dc=forgerock,dc=org]
value of attribute: uid=user.0,ou=People,dc=forgerock,dc=com
userName to check: user.0
match found! admin: user.0 allowed to impersonate user: user.1

Authorize the Administrator

Get the ssotoken for the admin who is trying to impersonate via a policy call, and authenticate the user to the realm specified in the config

json/authn/authenticate response-> {"tokenId":"AQIC5wM2LY4Sfcxokjvdayf3ig0oDuQITXRTWT9B_3hq72A.*AAJTSQACMDEAAlNLABI1ODk0Nzg1NTEyNDUzNzcxNDI.*","successUrl":"/openam/console"}
tokenId-> AQIC5wM2LY4Sfcxokjvdayf3ig0oDuQITXRTWT9B_3hq72A.*AAJTSQACMDEAAlNLABI1ODk0Nzg1NTEyNDUzNzcxNDI.*

 

Build the 2nd policy rest call, and use the resource set, openam server, policy set and policy container from the configuration passed to the module.

stringentity-> {"resources": ["http://openam:8080/openam/index.html"],"application":"impersonation", "subject": {"ssoToken":"AQIC5wM2LY4Sfcxokjvdayf3ig0oDuQITXRTWT9B_3hq72A.*AAJTSQACMDEAAlNLABI1ODk0Nzg1NTEyNDUzNzcxNDI.*"}}
json/impersonation/policies?_action=evaluate response-> [{"advices":{},"actions":{"POST":true,"PATCH":true,"GET":true,"DELETE":true,"OPTIONS":true,"PUT":true,"HEAD":true},"resource":"http://openam:8080/openam/index.html","attributes":{"uid":["user.0"],"cn":["Javed Shah"],"roleName":["timeBoundAdmin"]}}]

Custom response attributes can be passed back to the module for further evaluation if needed. For example, a statically defined roleName=timeBoundAdmin could be used to further restrict this impersonation request within the time window specified in the ‘timeBoundAdmin’ control. This example is only given to seed the imagination, the module currently does not restrict the impersonation session using a time window, but this is possible to do.

Parse the JSON response from Policy Evaluation

jsonarray-> {"resource":"http://openam:8080/openam/index.html","attributes":{"uid":["user.0"],"cn":["Javed Shah"],"roleName":["timeBoundAdmin"]},"advices":{},"actions":{"POST":true,"PATCH":true,"GET":true,"DELETE":true,"OPTIONS":true,"HEAD":true,"PUT":true}}
If the ACTION set returned for GET/POST is TRUE, the admin is permitted to impersonate. This could be extended to include other actions as necessary. Finally, destroy the admin session, now that it is not needed anymore and return the impersonated user as the Principal for constructing an OpenAM session.

Demo

Our short demo begins with the administrator being asked for the username of the user they want to impersonate.
Next, the module asks for the admin credentials.
If the administrator is unable to authenticate, or does not belong to the local group, or fails external policy evaluation, the following error screen is shown.
If all checks pass, the adminsitrator is granted the user’s session and logs into OpenAM.

Source

This article was first published on the OpenAM Wiki Confluence site: Impersonation in OpenAM