Security Global Forum

Security Global Forum

Our mission is to provide clients with an online user community of industry peers and IBM experts, to exchange tips and tricks, best practices, and product knowledge. We hope the information you find here helps you maximize the value of your IBM Security solutions.

 View Only

Using Custom Access Tokens in the ISAM OAuth Server

By Shane Weeden posted Mon July 25, 2016 12:00 AM

  

ISAM has provided a general-purpose OAuth 2.0 server since version 8 of the ISAM appliance (and earlier than that in Tivoli Federated Identity Manager). In that OAuth server implementation, access tokens are generated as random-value strings (pass-by-reference), and are completely opaque to both clients and resource servers. Resource servers must call the ISAM STS to perform access token validation. Over the course of the last year or so, several customers have asked me about the possibility of using their own format for access tokens. This is typically to allow custom resource server implementations to perform local validation of access tokens rather than having to call back to the ISAM STS. Naturally this comes with the limitation that access tokens are non-revocable for their lifetime, unless the resource server has another bespoke revocation capability. This is usually a reasonable limitation anyway because access tokens are configured to be short-lived, and for performance reasons resource servers cache the validity results of access tokens even in the pass-by-reference model.

 

In this article I will show you how to incorporate your own access token format into an ISAM OAuth 2.0 API Protection definition. I will also show you how, for ISAM 9+ where the STS is available, you can still leverage built-in ISAM OAuth resource server enforcement points (e.g. WebSEAL) to accept your custom-format access tokens.

 

If you are not already familiar with the ISAM OAuth 2.0 server and it’s related configuration, I recommend working through the OAuth sections of the ISAM for Mobile Demonstration Cookbook.

 

The rest of this article assumes you have a working ISAM Appliance with an OAuth API Protection Definition configured and working, and at least one oauth-protected resource, with that resource protected with WebSEAL using either oauth-auth or the OAuth EAS.

 

Solution Overview

Part 1 – Issuing custom access tokens

A client obtaining an access token does so via the /authorize endpoint (for the implicit grant type flow) or the /token endpoint (for all other grant type flows).

In both cases, ISAM will invoke both the pre-token and post-token mapping rules configured for an API Protection definition, as shown in this diagram:

The pre and post token mapping rules, which operate on a simple STSUniversalUser object to manage attributes and values, allow you to hook into the OAuth processing flow for a variety of purposes. One common purpose is to provide your own validation for resource owner credentials in the pre-token mapping rule. For the purposes of issuing your own custom-format access token, the hook point will be the post-token mapping rule. Details of how to do this are provided later.

Part 2 – Validating custom access tokens at WebSEAL

WebSEAL offers two types of OAuth-related resource server implementations – these are OAuth External Authorization Service (EAS), and the oauth-auth authentication mechanism (allowing you to use an OAuth token to establish and maintain a WebSEAL session). My colleague Philip Nye has written about these capabilities extensively in his ISAM blog. In both cases, WebSEAL will call the ISAM STS to validate access tokens, and optionally cache results. The “contract” between WebSEAL and the STS is the same as for Datapower, or your own custom enforcement point that needs to validate access tokens at ISAM. This contract was documented back in TFIM 6.2.2 days, and is still relevant for OAuth 2.0 and ISAM 8 and ISAM 9:

http://www.ibm.com/support/knowledgecenter/SSZSXU_6.2.2.7/com.ibm.tivoli.fim.doc_6227/config/reference/oauthsts.html

When a client passes an access token to a resource protected by WebSEAL, WebSEAL will create an STSUU with information about the request, including the access token, and call the STS for validation. WebSEAL will then optionally (by configuration) cache the validation results. What is interesting about this process is how WebSEAL formats the request to the STS, in particular the WS-Trust elements for Issuer, AppliesTo, RequestType, etc. An example request is shown as part of the documentation referenced above.

Of particular interest to our purposes is the AppliesTo address of the RequestSecurityToken (RST) request. The value for this parameter comes from the WebSEAL configuration file, in particular the setting:

[oauth]
default-fed-id = https://localhost/sps/oauth/oauth20

By changing this setting, we can make WebSEAL use a different AppliesTo address. Then, for ISAM 9+,  we can can configure a custom STS trust chain with a matching AppliesTo address. So long as we honour the same STSUU in/out contract that WebSEAL is expecting to get back (documented in the link above), we can perform whatever validation we like for the presented access token in a javascript mapping rule in this custom STS chain. Architecturally, the solution looks like this:

 

Instructions for configuring the STS chain and an example custom Javascript mapping rule that works with the sample custom format access token used in this post appear below.

Updating the post-token mapping rule to issue custom access tokens

The first and most important decisions we need to make is what will be the format and content of our custom access token. There are no real standards in this area, but what is important is that if we want to do “local validation”, the token must be self-encapsulated and secured, and should contain at least the following attributes:

  • the resource owner username
  • scope(s) associated with the access token, if any
  • the expiry time of the access token

Additionally, I would recommend you also include:

  • client_id of the client the token was issued to

The reason I’ve identified those specific pieces of metadata as being encapsulated into the access token is that these attributes are required to satisfy the WebSEAL to STS validation contract mentioned earlier.

Having decided what attributes to include in the access token, we now need to decide on a format. Probably the most obvious choice is a signed JWT. There are a variety of JWT Javascript libraries in the public domain, and I have definitely used at least one with success in Javascript mapping rules in ISAM, including implementing the solution architecture described in this article. To reproduce all that code in this article though is impractical, so instead I will tell you *how* I did it with JWT later, and you can try that yourself if you wish. For the purposes of demonstrating the process of issuing and validation of custom tokens in this article, we are just going to abstract away the actual token string and represent it here as “CUSTOM_TOKEN_STRING”, and assume it securely self-encapsulates the attributes described above.

In the post-token mapping rule, you need to determine if an access_token is being returned in the current request, and simply “replace” it’s value with your custom-generated token string. Here’s sample code that you can append to the end of the example post-token mapping rule that is automatically configured as part of a new API protection definition in ISAM:

 


/******* CUSTOM TOKEN EXAMPLE - ADD TO POST-TOKEN MAPPING RULE ********/
/*
 * A function to build your custom access token - implement this 
 * however you wish, however it is recommended you encapsulate the 
 * following attributes in your token:
 *
 * username (get this from OAuthMappingExtUtils.getAssociation(state_id, "username"). This is established below.)
 * client_id (get from stsuu)
 * scope - (optional, get from stsuu)
 * expiry - (seconds since epoch. Generate from expires_in from stsuu, which is in seconds from now)
 */
function generateCustomAccessTokenString() {
    var username = OAuthMappingExtUtils.getAssociation(state_id, "username");
    var client_id = stsuu.getContextAttributes().getAttributeValueByName("client_id");
    var scopeStringArray = stsuu.getContextAttributes().getAttributeValuesByName("scope");
    // calculate expiry
    var expiresInSeconds = parseInt(stsuu.getContextAttributes().getAttributeValueByName("expires_in"));
    var now = new Date();
    var expiry = Math.floor((now.getTime() + (1 * expiresInSeconds * 1000)) / 1000);
    // Build your own token string with at least the information above
    // this example just uses a canned string for demonstrating the solution
    return "CUSTOM_ACCESS_TOKEN";
}
// "normalize" setting of the username associated attribute from state_id for all grant type flows
var request_type = stsuu.getContextAttributes().getAttributeValueByName("request_type");
var grant_type = stsuu.getContextAttributes().getAttributeValueByName("grant_type");
var state_id = stsuu.getContextAttributes().getAttributeValueByName("state_id");
// how we obtain the username is based on the request_type and grant_type 
var username = null;
if (request_type != null && request_type.equals("authorization")) {
    // browser user-present flow (implicit, azncode). The username should be in an oauth request attribute.
    username = stsuu.getContextAttributes().getAttributeValueByNameAndType(
        "username", "urn:ibm:names:ITFIM:oauth:request");
} else if (request_type != null && request_type.equals("access_token")) {
    // request to token endpoint
    if (grant_type != null && grant_type.equals("password")) {
        // ropc flow - it comes in a post param
        username = stsuu.getContextAttributes().getAttributeValueByNameAndType(
            "username", "urn:ibm:names:ITFIM:oauth:body:param");
    } else if (grant_type != null && grant_type.equals("client_credentials")) {
        // for client credentials, set the username to the client id
        username = stsuu.getContextAttributes().getAttributeValueByNameAndType(
            "client_id", "urn:ibm:names:ITFIM:oauth:body:param");
    } 
}
// if we got a username, associate it with state_id
if (username != null) {
 OAuthMappingExtUtils.associate(state_id, "username", username);
}
// determine if we have an access token to replace in the current response
// if we do, delete the existing access token from the database, and replace
// the value in the response with our custom token
var access_token_id = null;
// access_token_id
temp_attr = stsuu.getContextAttributes().getAttributeValuesByNameAndType(
    "access_token_id", "urn:ibm:names:ITFIM:oauth:response:metadata");
if (temp_attr != null && temp_attr.length > 0) {
    access_token_id = temp_attr[0];
}
// only proceed if there is an access token to replace
if (access_token_id != null) {
    /*
     * Generate a new custom access token format from what we know.
     * For example you might call another STS chain to get a JWT, or similar.
     * 
     * Validation of custom access tokens for this example is done in oauth_customat_validate.js
     */
    var customATString = generateCustomAccessTokenString();
    // delete existing access token from database because no client will ever receive it
    OAuthMappingExtUtils.deleteToken(access_token_id);
 
    // replace the existing access token that's going to be sent to the client with our custom access token
    stsuu.getContextAttributes().removeAttributeByNameAndType(
        "access_token", "urn:ibm:names:ITFIM:oauth:response:attribute");
    var a = new com.tivoli.am.fim.trustserver.sts.uuser.Attribute(
        "access_token", 
        "urn:ibm:names:ITFIM:oauth:response:attribute", 
        customATString);
stsuu.getContextAttributes().setAttribute(a);
}

 

In the API protection definition, called MyTestAPIDefnition on my server I’ve created a client, with the following attributes:

client_id: DemoOAuthClientID
client_secret: DemoOAuthClientSecret

I also have an ISAM user configured:

Username: testuser
Password: passw0rd

By issuing the following curl command, I am able to request a new access token using the resource owner password credentials flow:

curl -k -d "grant_type=password&client_id=DemoOAuthClientID&client_secret=DemoOAuthClientSecret&scope=myscope1 myscope2&username=testuser&password=passw0rd" https://www.myidp.ibm.com/isam/sps/oauth/oauth20/token

The formatted response looks like:

{
 "access_token": "

CUSTOM_ACCESS_TOKEN

",
 "expires_in": 3599,
 "token_type": "bearer",
 "refresh_token": "S3us3rWy3fopA5OZirUrHR0KaQxRzYkVdP5Ywxk0",
 "scope": "myscope2 myscope1"
}

Notice the access token is now “CUSTOM_ACCESS_TOKEN”. Obviously you need to replace the implementation of generateCustomAccessTokenString() with your formatted access token encapsulating the attributes indicated in the comments of that method.

Let’s now look at how we configure WebSEAL and the STS to validate custom access tokens.

Validating Custom Access Tokens

Validation of custom access tokens is really all about creating a custom STS chain to validate your access_token in Javascript, then modifying WebSEAL to invoke that custom chain.

Another colleague of mine, Leo Farrell, documented a VERY similar process for validating tokens that came from a different authorization server (in this case an OIDC authorization server) in a developerworks article that is quite a good reference for the steps I am doing here.

Creating an STS chain to validate custom access tokens

Create a custom STS template and chain (there are plenty of references for how to do this, such as the ISAM Federation Cookbook) of the format:

  • STSUU(validate)
  • JavascriptMap (map)
  • STSUU(issue)

Use the following lookup parameters:

Issuer:  urn:ibm:ITFIM:oauth20:token:bearer    (this is a constant, WebSEAL will always use this value)
AppliesTo: http://appliesto/custom_access_token    (this is any URI - we will use the same value in WebSEAL's config file later)

 

The javascript mapping rule needs to validate your custom access token, and honour the STSUU contract with WebSEAL described in the related documentation referenced earlier. For this example, we will use the following Javascript:


importPackage(Packages.com.tivoli.am.fim.trustserver.sts.utilities);
importPackage(Packages.com.tivoli.am.fim.trustserver.sts.uuser);
IDMappingExtUtils.traceString("oauth_customat_validate mapping rule called with stsuu: " + stsuu.toString());
//Method for converting JavaScript array into Java array.
function jsArrayToJavaArray(jsArray) {
 var javaArray = java.lang.reflect.Array.newInstance(java.lang.String, jsArray.length);
 for (var i = 0; i < javaArray.length; i++) {
 javaArray[i] = jsArray[i];
 }
return javaArray;
}
// returns a UTC String representing now plus secondsTillExpiry
function getExpiryStr(secondsTillExpiry) {
 var now = new Date();
 var later = new Date(now.getTime() + (1000 * secondsTillExpiry));
 return IDMappingExtUtils.getTimeStringUTC(later.getFullYear(),
 later.getMonth(),
 later.getDate(),
 later.getHours(),
 later.getMinutes(),
 later.getSeconds());
}
var access_token = ''+stsuu.getContextAttributes().getAttributeValueByName("access_token");
var authorized = "TRUE";
// we know access_token is CUSTOM_ACCESS_TOKEN in this example, so that's what we'll validate.
// you should replace this with your own validaton
if (access_token != "CUSTOM_ACCESS_TOKEN") {
 authorized = "FALSE";
}
// some constants
//Types:
var t_attribute = "urn:ibm:names:ITFIM:oauth:response:attribute";
var t_decision = "urn:ibm:names:ITFIM:oauth:response:decision";
// unpack your access token, verify the expiry time and populate the following attributes
// we use canned values for this example because our access token doesn't really
// encapsulate them
var CANNED_USERNAME="testuser";
var CANNED_CLIENT_ID="DemoOAuthClientID";
var CANNED_SCOPE=["myscope1", "myscope2"];
var CANNED_EXPIRY=3600; // one hour
stsuu.addAttribute(new Attribute("authorized", t_decision, authorized));
// only need other attributes if authorized
if (authorized == "TRUE") {
 stsuu.addAttribute(new Attribute("username", t_attribute, CANNED_USERNAME));
 stsuu.addAttribute(new Attribute("oauth_token_client_id", t_attribute, CANNED_CLIENT_ID));
 stsuu.addAttribute(new Attribute("scope", t_attribute, jsArrayToJavaArray(CANNED_SCOPE)));
 stsuu.addAttribute(new Attribute("expires", t_attribute, getExpiryStr(CANNED_EXPIRY)));
}

 

Configuring WebSEAL to use the custom chain for access_token validation

Modify the WebSEAL configuration file supporting your OAuth 2.0 API Protection Definition to change this setting:

[oauth]
default-fed-id = 

http://appliesto/custom_access_token

Note that this value is the same as the AppliesTo address we specified for the custom STS chain above.

 

Testing Custom Access Token Validate

At this stage I would recommend turning on runtime trace for

com.tivoli.am.fim.trustserver.sts.utilities.*=all

Monitor the runtime trace.log in the ISAM appliance, and run the following curl command against any URL resource you have protected with your API protection definition (or if you have oauth-auth enabled, any protected resource):

curl -k -H "Authorization: Bearer CUSTOM_ACCESS_TOKEN" https://www.myidp.ibm.com/your_oauth_protected_resource

All going well, you should get back your protected resource.

Try a negative test by passing any other string other than CUSTOM_ACCESS_TOKEN as the Bearer token. You should not get back your protected resource. The actual response you get will differ depending on whether you are using the OAuth EAS for enforcement or oauth-auth, but so long as you don’t get the intended protected resource, that’s a valid test.

 

How I created and validated JWTs in Javascript mapping rules

Here’s a cheat-sheet for how I got JWTs issued and validated purely in Javascript mapping rules in OAuth and the STS.

First, I started by looking at the jsrsasign project on github. In particular I went to the DEMOS link, and looked at the RSA signing example.

If you view/source that page, you can find a consolidated set of minimized source for all required libraries at http://kjur.github.io/jsrsasign/jsrsasign-latest-all-min.js.

I was able to completely copy/paste the content of that jsrsasign-latest-all-min.js source into an ISAM Javascript mapping rule. Note that this source includes license references, which you would need to adhere to if using this code.

There is one trick to using this code in the ISAM environment. The code as currently written assumes you are running in a browser Javascript engine because in a couple of places it makes reference to some global variables that do not exist in the ISAM Javascript runtime environment. You can work around this issue by inserting the following Javascript just BEFORE all of the content from jsrsasign-latest-all-min.js in your ISAM Javascript mapping rule:

/* 
* The following two variables are declared to work around assumptions in included libraries that think we are running in a browser
*/
var navigator = { "appName": null, "appVersion": null };
var window = { "crypto": null };

 

You can take a look at the documentation for the KJUR Javascript library here: http://kjur.github.io/jsrsasign/api/

It’s relatively straight forward to create and validate both RS-* and HS-* signed JWT’s with this library, and use them as your custom access tokens with the solution pattern described in this article.

 

Summary

In this article you have seen how you can inject your own custom access token format into the response from the ISAM OAuth 2.0 authorization server. You have also seen how you can configure WebSEAL to point to a custom STS chain for access token validation. Hopefully these tips are useful for your own OAuth projects.

Finally, and I can’t say too much right now, expect to see another article in the not-too-distant future with more information on using JWTs in ISAM.

0 comments
27 views

Permalink