We have an environment where multiple websites are configured to use OIDC authentication (authorization code flow) to an IBM ISAM acting as the Idp (Identity Provider).
All these websites expect different scopes in their tokens (eg. access tokens and id tokens). Of course, the user can also use multiple devices (browsers) to access the sites.

The IDP thus can hand out a number of different tokens for a single user.

We also created a custom “Remember Me” function ; that relies on an Oauth access token stored in a cookie (more on that some other time).

So a user effectively has single sign on between all the websites now : every time a new access token is requested (eg. with a different scope); the user is redirected to the idp. But because of the “Remember Me” cookie, the user is logged on automatically to the idp. The idp then hands out the new access token.

The challenge in this case now, is to implement a Logout function, that not only invalidates the current access token in use for that particular website; but also all other active tokens (access tokens, refresh tokens; …).

Infomap

We want to terminate ALL active tokens (from all different applications (client_id), but also from all logged in devices - logout everywhere.

To remove all tokens for a particular (logged in) user; there are methods in the API:

OAuthMappingExtUtils.deleteAllTokensForUser(username);  

That would do what we need. Note that this removes the Oauth tokens that exist for the user across ALL devices the user is using.

WebSeal’s Single-signoff-uri

Our initial idea was that we could use WebSeal’s single-signoff-uri parameter. This is intended as a mechanism to log the user out of any backend applications when the WebSeal session is terminated.
There are four different mechanisms that can terminate a WebSEAL session:

  1. User request by accessing pkmslogout.
  2. Session timeout.
  3. EAI session termination command.
  4. Session terminate command from the pdadmin tool.

When the WebSeal session terminates, it sends (GET) requests to the url’s you configure here; including cookies and headers from the incoming request.
By default (after you’ve configure the OIDC and API Connect features); there’s an entry already for Oauth:

single-signoff-uri = /mga/sps/oauth/oauth20/logout  

So adding a single-signoff-uri that points to our custom infomap; would be triggered when the WebSeal session terminates …
The problem with this configuration in our scenario; is that it would mean that every time you are logged out of the IdP, it would trigger the logout mechanism . We only want to trigger it when the user clicks “Logout” !

https://www.ibm.com/support/knowledgecenter/SSPREK_9.0.6/com.ibm.isam.doc/wrp_config/concept/con_single_signoff_overvw.html

Logout everywhere

Infomap setup

The infomap can obviously also be called directly by the enduser.

By using the api endpoint, we can furthermore do the calls in an Ajax call (json returned). Note that this requires a CORS configuration in most cases.

So assuming my ISAM that acts as the IDP is on https://idp.tombosmans.eu , the call to the infomap could look like this . Note the difference in the uri (apiauthsvc vs. authsvc).

API Call (return JSON)

https://idp.tombosmans.eu/mga/sps/**api**authsvc?PolicyId=urn:ibm:security:authentication:asf:federatedlogout

Browser

https://idp.tombosmans.eu/mga/sps/authsvc?PolicyId=urn:ibm:security:authentication:asf:federatedlogout

For the API call to succeed; you must add an Accept Header to your GET call with the value “application/json” (see here , for instance : https://philipnye.com/2015/10/02/isam-lmi-rest-api-http-405-method-not-allowed-error/ )

You need to have the necessary template files in place; you need

/authsvc/authenticator/federatedlogout/logout.html  
/authsvc/authenticator/federatedlogout/logout.json  

The html file is returned for the Browser call, the json file when you use the API call

I’ve used this excellent blog entry from my colleague Shane Weeden as an example and a source for code : https://www.ibm.com/blogs/sweeden/implementing-isam-credential-viewer-infomap/

Infomap code

Now this code does 2 things : it calls the deleteAllTokensForUser function to remove all tokens for the logged in user, and then it performs a logout by using a REST call to the pdadmin utility , to run the command

server task terminate all_sessions  

This code depends on a Server Connection to exist ; named “ISAM Runtime REST” , that should point to your LMI; with the userid and password for an admin user.
The details to run the pdadmin command are hardcoded in this example:

What is missing from this code, is the possibility to pass a redirect URL. This would make sense in the “Browser” version, less for the API version of the infomap.

The infomap code :

importClass(Packages.com.tivoli.am.fim.trustserver.sts.utilities.IDMappingExtUtils);  
importClass(Packages.com.tivoli.am.fim.trustserver.sts.utilities.OAuthMappingExtUtils);  
importClass(Packages.com.tivoli.am.fim.base64.BASE64Utility);  
importClass(Packages.com.ibm.security.access.server_connections.ServerConnectionFactory);  
importClass(Packages.com.ibm.security.access.httpclient.HttpClient);  
importClass(Packages.com.ibm.security.access.httpclient.HttpResponse);  
importClass(Packages.com.ibm.security.access.httpclient.Headers);  
  
/**  
* Utility to get the username from the the Infomap context  
*/  
function getInfomapUsername() {  
      // get username from already authenticated user  
      var result = context.get(Scope.REQUEST,  
                       "urn:ibm:security:asf:request:token:attribute", "username");  
      IDMappingExtUtils.traceString("[FEDLOGOUT] : username from existing token: " + result);  
  
      // if not there, try getting from session (e.g. UsernamePassword module)  
      if (result == null) {  
              result = context.get(Scope.SESSION,  
                               "urn:ibm:security:asf:response:token:attributes", "username");  
              IDMappingExtUtils.traceString("[FEDLOGOUT] : username from session: " + result);  
  
      }  
      return result;  
}  
  
/**  
* Utility to html encode a string  
*/  
function htmlEncode(str) {  
       return String(str).replace(/&/g, '&').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');  
}  
/*  
*  perform pdadmin session deletion  
*/  
function deleteAllSessions(username) {  
      IDMappingExtUtils.traceString("[FEDLOGOUT] Entering deleteAllSessions()");  
                
      // Get the Web Server Connection Details for ISAM Runtime  
      servername = "ISAM Runtime REST";  
      var ws1 = ServerConnectionFactory.getWebConnectionByName(servername);  
      if (ws1 == null) {  
              IDMappingExtUtils.traceString("[FEDLOGOUT] Could not find the server data for " + servername);  
              next = "abort";  
              return;  
      }  
       
      var restURL = ws1.getUrl()+"/isam/pdadmin";  
      var adminUser = ws1.getUser();  
      var adminPwd = ws1.getPasswd();  
      IDMappingExtUtils.traceString("[FEDLOGOUT] url : "+restURL+" adminUser " + adminUser + " password: "+ adminPwd );  
      var headers = new Headers();  
      headers.addHeader("Content-Type", "application/json");  
      headers.addHeader("Accept", "application/json"); var respbody = '{"admin_id":"**sec_master**", "admin_pwd":"**<sec_master_password>**", "commands":"server task **<websealinstance>** terminate all_sessions ' + username + '"}'; var hr = HttpClient.httpPost(restURL, headers, respbody, null, adminUser, adminPwd, null, null);  
      if(hr != null) {  
              var rc = hr.getCode();  
              IDMappingExtUtils.traceString("[FEDLOGOUT] got a response code: " + rc);  
              var body = hr.getBody();  
              if (rc == 200) {  
                       if (body != null) {  
                               IDMappingExtUtils.traceString("[FEDLOGOUT] got a response body: " + body);  
                       } else {  
                               IDMappingExtUtils.traceString("[FEDLOGOUT] body of response from pdadmin is null?");  
                       }  
              } else {  
                       IDMappingExtUtils.traceString("[FEDLOGOUT] HTTP response code from pdadmin is " + rc);  
              }  
      } else {  
              IDMappingExtUtils.traceString("[FEDLOGOUT] HTTP post to pdadmin failed");  
      }  
      return true;          
}  
  
// infomap that returns a page indicating if you are authenticated and who you are  
var username = getInfomapUsername();  
// We must have a logged in session, otherwise we cannot logout ....  
// this is a bit annoying and I'm not sure how to make sure  
if ( username != null ) {  
var tokens_for_user = OAuthMappingExtUtils.getAllTokensForUser(username);  
      var tokens = [];  
      for (i = 0; i < tokens_for_user.length; i++) {  
                       var a = tokens_for_user[i];  
                       tokens.push(a.getClientId() + "---" + a.getId() + "---"+a.getType();  
                       IDMappingExtUtils.traceString("[FEDLOGOUT] : Active Token ID " + a.getId() + ", ClientID " + a.getClientId() + " getType " + a.getType() );  
      }  
       
      OAuthMappingExtUtils.deleteAllTokensForUser(username);  
      IDMappingExtUtils.traceString("[FEDLOGOUT] : Deleted all tokens for : " + username );  
       
      /*  
      * Now invalidate the session for the idp  
      */  
      IDMappingExtUtils.traceString("[FEDLOGOUT] : Trying to logout : " + username );  
      deleteAllSessions(username);  
       
      /*  
      * Now return the page ... the page should actually never be shown ... logout.json should exist as well  
      */  
      page.setValue("/authsvc/authenticator/federatedlogout/logout.html");  
      macros.put("@AUTHENTICATED@", ''+(username != null));  
      macros.put("@USERNAME@", (username != null ? htmlEncode(username) : ""));  
      macros.put("@TOKENS@", tokens.join(";");  
      // we never actually perform a login with this infomap  
      success.setValue(false);  
} else {  
      IDMappingExtUtils.traceString("[FEDLOGOUT] : Anonymous session; no logout performed");  
      page.setValue("/authsvc/authenticator/federatedlogout/needloginfirst.html");  
      success.setValue(false);  
}