Setting Custom OAuth Tokens in ISAM
Contents
- Overview
- API Definition Mapping Rules
- Setting A Custom ID Token
- Setting A Custom Access Token
- Additional Information/Links
Overview
In the early days, to authenticate and authorize users, many developers had to look to proprietary approaches when integrating with external identity providers. Luckily this didn't stick around as SAML (Security Assertion Markup Language), an open XML based standard message type was introduced. This later resulted in OAuth and OAuth 2.0, a more RESTful approach using JSON objects. This then evolved into the pista resistance that is OpenID Connect (OIDC) - which exists on top of OAuth 2.0.
With OIDC, a trusted identity provider is used to prove that you are who you say you are. This is achieved through the exchange of signed JWTs (JSON Web Tokens), which leaves the protection of your identity to the OP (OpenID Provider) and not the RP (Relying Party).
There are three types of tokens that are used in an OIDC flow. They are the:
id_token
Contains information about an authentication event and claims about the authenticated user.access_token
Used to call protected API's, e.g./userinfo
refresh_token
Used to generate additional access tokens.
ISAM automatically creates these for us, which means we don't have to worry to much about them, but what happens if we want to customize the values of these tokens?
Well...
API Definition Mapping Rules
In ISAM, the OAuth flow passes over the API Definition Mapping Rules. For every OAuth API definition, there are two mapping rules that automatically get created:
- Pre-Token Mapping Rule
This mapping rule in ISAM is triggered before token or attribute validation. This means we can validate credentials, such as username or password, and use it for a variety of other mechanisms. - Post-Token Mapping Rule
This mapping rule is triggered after the tokens have been generated or exchanged with the RP. Here we can access user attributes stored against the OAuth token.
Setting A Custom ID Token
In our OAUTH PreTokenGeneration.js
we can add the following section of code to create a custom id_token
which will be returned to the RP on the /oauth20/authorize
call.
importPackage(Packages.com.tivoli.am.fim.trustserver.sts);
importPackage(Packages.com.tivoli.am.fim.trustserver.sts.oauth20);
importPackage(Packages.com.tivoli.am.fim.trustserver.sts.uuser);
importPackage(Packages.com.ibm.security.access.user);
importClass(Packages.com.tivoli.am.fim.trustserver.sts.utilities.IDMappingExtUtils);
importClass(Packages.com.tivoli.am.fim.trustserver.sts.utilities.OAuthMappingExtUtils);
importClass(Packages.java.util.ArrayList);
importClass(Packages.java.util.HashMap);
importClass(Packages.java.lang.System);
const SESSION_LIFETIME = 3600; // 60 minutes.
const NOT_BEFORE = 60; // 1 minute.
if (request_type == "access_token") {
user = getAttributeValue("oidc_username", "urn:ibm:names:ITFIM:5.1:accessmanager", ATTRIBUTE, "");
now = new Date();
claims = {};
claims.auth_time = Math.floor(now.getTime() / 1000);
claims.exp = SESSION_LIFETIME; // Optional: Will override value set in API Definition.
claims.nbf = Math.floor(now.getTime() / 1000 - NOT_BEFORE);
claims.sub = "" + user.toUpperCase();
claims.jti = "" + java.util.UUID.randomUUID();
stsuu.addContextAttribute(new com.tivoli.am.fim.trustserver.sts.uuser.Attribute("claim_json", "urn:ibm:oidc10:jwt:create", JSON.stringify(claims)));
// Overwrite `sub` with the `user_id`.
stsuu.addAttribute(com.tivoli.am.fim.trustserver.sts.uuser.Attribute("sub", "urn:ibm:jwt:claim", "" + claims.sub));
}
If we need to add a custom X.509 certificate thumbprint to the token header, you can do this by adding the following line.
const X5T_KEY = "X2maFm3VYlFcJHY71G7nTaYiZaOm";
// Add X.509 Fingerprint (SHA-1) to token.
stsuu.addContextAttribute(new com.tivoli.am.fim.trustserver.sts.uuser.Attribute("x5t", "urn:ibm:JWT:header:claim", X5T_KEY));
Setting A Custom Access Token
To return a custom access_token
to the RP, similar to how we we set a custom id_token
, we want to add the below into our OAUTH PreTokenGeneration.js
mapping rule.
if (request_type == "access_token") {
accessToken = "" + java.util.UUID.randomUUID();
stsuu.addContextAttribute(new com.tivoli.am.fim.trustserver.sts.uuser.Attribute("urn:ibm:ITFIM:oauth20:custom:token:access_token", "urn:ibm:ITFIM:oauth20:custom:token", accessToken));
stsuu.addContextAttribute(new com.tivoli.am.fim.trustserver.sts.uuser.Attribute("urn:ibm:ITFIM:oauth20:custom:token:access_token", "urn:ibm:ITFIM:oauth20:custom:token:persistent", "true"));
}
But what if we want to return a JWT as the access token instead?
To do this, you have to first configure a STS chain. Once done, we use this chain to issue our token.
This means, instead of returning an opaque token, we now return a JWT which we can later be used to validate API calls.
importClass(Packages.com.tivoli.am.fim.fedmgr2.trust.util.LocalSTSClient);
const ACCESS_TOKEN_LIFETIME = 1800; // 30 minutes.
const NOT_BEFORE = 60; // 1 minute.
if (request_type == "access_token") {
user = getAttributeValue("oidc_username", "urn:ibm:names:ITFIM:5.1:accessmanager", ATTRIBUTE, "");
let RS256 = {}
RS256.keystore = "jwtKeyStore"; // SSL Certificate Database.
RS256.key = "jwtKey"; // Signing Certificate.
now = new Date();
claims = {};
claims.iat = Math.floor(now.getTime() / 1000);
claims.exp = Math.floor((now.getTime() + (1 * ACCESS_TOKEN_LIFETIME * 1000)) / 1000);
claims.nbf = Math.floor(now.getTime() / 1000 - NOT_BEFORE);
claims.sub = "" + user.toUpperCase();
claims.jti = "" + java.util.UUID.randomUUID();
accessToken = token.issue(claims, RS256);
stsuu.addContextAttribute(new com.tivoli.am.fim.trustserver.sts.uuser.Attribute("urn:ibm:ITFIM:oauth20:custom:token:access_token", "urn:ibm:ITFIM:oauth20:custom:token", accessToken));
stsuu.addContextAttribute(new com.tivoli.am.fim.trustserver.sts.uuser.Attribute("urn:ibm:ITFIM:oauth20:custom:token:access_token", "urn:ibm:ITFIM:oauth20:custom:token:persistent", "true"));
}
token = {};
token.issue = function (claims, RS256) {
tokenStsuu = new STSUniversalUser();
// Define signing certificate for JWT.
tokenStsuu.addContextAttribute(new com.tivoli.am.fim.trustserver.sts.uuser.Attribute("signing.db", "", "" + RS256.keystore));
tokenStsuu.addContextAttribute(new com.tivoli.am.fim.trustserver.sts.uuser.Attribute("signing.cert", "", "" + RS256.key));
tokenStsuu.addContextAttribute(new com.tivoli.am.fim.trustserver.sts.uuser.Attribute("signing.alg", "", "RS256"));
// Add claims to STSUU.
tokenStsuu.addContextAttribute(new com.tivoli.am.fim.trustserver.sts.uuser.Attribute("claim_json", "", "" + JSON.stringify(claims)));
// Issue JWT token.
stsToken = token.callSts(tokenStsuu.toXML().getDocumentElement(), "urn:jwt:issue");
if (stsToken.token) {
return stsToken.token.getTextContent();
} else {
IDMappingExtUtils.traceString("Error getting token from STS: " + stsToken.errorMessage);
}
}
token.callSts = function (baseToken, identifier) {
requestType = "http://schemas.xmlsoap.org/ws/2005/02/trust/Issue";
tokenResult = LocalSTSClient.doRequest(requestType, identifier, identifier, baseToken, null);
return tokenResult;
}
Additional Information/Links
Here are some useful links you can checkout for additional information.