Implementing OIDC back-channel logout on ISAM/ISVA
Author(s): László Czap | Created: 16 December 2021 | Last modified: 16 December 2021
Tested on: -
Table of contents
- OIDC back-channel logout
- Registering the logout URI for the clients
- Upon token request, mark the client as a logout target
- Create an STS chain that produces the logout JWT token
- Create an endpoint that triggers the logout flow
- Create a template page for the InfoMap
- Create a mapping rule for InfoMap
- Create an authentication mechanism
- Create an authentication policy
- (Optional) Apply an HTTP transformation to make the URL look nicer
- Extend OP metadata indicating that back-channel logout is supported
- (Optional) Register the logout trigger endpoint as a logout URI on the reverse proxy to make it automatic on session termination
OIDC back-channel logout↑
The back-channel logout flow is an extension to the OIDC specification (here: https://openid.net/specs/openid-connect-backchannel-1_0.html). This extension adds the single-logout functionality to the protocol suite. The protocol flow consists of sending a JWT format logout token from the OP to all affected RPs, which is a back-channel communication between two providers. This feature is not natively implemented in ISAM/ISVA, but we can add it using the tools we have in the system.
This write-up is about implementing the OP side flow, i.e. sending the logout tokens, not the processing of a received logout token.
At a high level we need the following main components:
- Register the logout URI for the clients
- Upon token request, mark the client as a logout target
- Create an STS chain that produces the logout JWT token
- Create an endpoint that triggers the logout flow (sending the logout token to clients that use a token)
- (Optional) Apply an HTTP transformation to make the URL look nicer
- Extend OP metadata indicating that back-channel logout is supported
- (Optional) Register the logout trigger endpoint as a logout URI on the reverse proxy to make it automatic on session termination
Registering the logout URI for the clients↑
On the client configuration form we have no option to set the logout URI, but there is the option to add 'Extension Properties', which is perfectly suitable for the purpose.
We need two attributes:
backchannel_logout_uri
: where the logout token is consumedbackchannel_logout_session_required
: is an indication whether a session identifier is needed in the logout token. This is optional, if not there we'll assumefalse
.
We choose to use JSON format to make our life easier. Our 'Extension Properties' for our test client look like:
{
"backchannel_logout_uri": "https://192.168.11.216/testclientLogout",
"backchannel_logout_session_required": true
}
Upon token request, mark the client as a logout target↑
We choose to mark the client for logout when the call to the authorize
endpoint is done. If there is a registered logout URI for the client, we associate the clientid with the authorization grant such that later we can retrieve it. We also choose to index the saved ids by the session index of the authenticated user. By this, we can selectively terminate RP sessions that are created in the given OP session.
Note 1: Using the authorize
endpoint works for the most common authorization code grant. For other grant types a slight modification might be needed.
Note 2: The use of the authorize
endpoint does not always imply issuing a login token. It is possible to do the same upon use of the token
endpoint. The only difference is that the user session is not available on that call. To be able to selectively terminate sessions, one would need to save the session index attribute on the authorize
request paired with the authorization code and then retrieve it on the token
enpoint based on the authorization code and save the clientid together with the session index.
The following code snippet is added to the post-token generation mapping rule to do the job:
var is_oidc_scope = false;
temp_attr = stsuu.getContextAttributes().getAttributeByName("scope");
if (temp_attr != null) {
for( var scope in temp_attr.getValues()) {
if (temp_attr.getValues()[scope] == "openid") {
is_oidc_scope = true;
break;
}
}
}
if (request_type == 'authorization' && is_oidc_scope){
client_ext_data = JSON.parse(oauth_client.getExtendedData());
client_logout_uri = client_ext_data["backchannel_logout_uri"];
//here we check if a logout url is registered. If so, we save the client id to be able to call back on logout.
IDMappingExtUtils.traceString("Cheking for logout uris...");
if (client_logout_uri != null) {
IDMappingExtUtils.traceString("Saving client ID for logout: "+oauth_client.getClientId());
save_clientid_for_logout(oauth_client.getClientId());
}
}
function save_clientid_for_logout(client_id){
var session_ix = ""+stsuu.getAttributeContainer().getAttributeValueByNameAndType("tagvalue_session_index","urn:ibm:names:ITFIM:5.1:accessmanager");
var logoutClients = ""+OAuthMappingExtUtils.getAssociation(state_id,"logoutClients");
IDMappingExtUtils.traceString("logoutclient: "+logoutClients);
if (logoutClients =! "null"){
logoutClients = JSON.parse(logoutClients);
if (logoutClients[session_ix] == null) {
logoutClients[session_ix] = [];
}
}else {
logoutClients = JSON.parse('{"'+session_ix+'":[]}');
}
logoutClients[session_ix].push(""+client_id);
IDMappingExtUtils.traceString("logoutclient: "+JSON.stringify(logoutClients));
OAuthMappingExtUtils.associate(state_id, "urn:ibm:am:logoutclients", JSON.stringify(logoutClients));
}
Create an STS chain that produces the logout JWT token↑
We will use this chain from the script STS client. The simplest option is to use the STSUU format as input and simply issue a JWT. We do not need a mapping, we will populate the STSUU object with the required content before calling the chain. The chain template has two modules:
- Default STSUU, Mode: Validate
- Default Jwt Module, Mode: Issue
We name our template as OIDC Logout Token
.
Next, we create a chain instance:
- Name: OIDC_Logout
- Template: OIDC Logout Token
- Request Type: Issue
- Lookup Type: Traditional WS-Trust Elements
- Applies to Address: oidclogout
- Issuer Address: oidclogout
- Token Type: Any
- JWT Signing / Signature algorithm: RS256
- JWT Signing / Certificate Database: rt_profile_keys
- JWT Signing / Certificate Label: server
Note 1: Singing the token is mandatory by specification. All the other JWT attributes will be set from script.
Note 2: In a production system use a dedicated key for token signing.
Create an endpoint that triggers the logout flow↑
For this purpose we are going to use a custom InfoMap authentication module. Of course, this will not perform any kind of authentication, but we get script control with access to user session attributes.
Note: The specification does not tell anything about how to trigger the logout. We find it natural to publish an HTTP GET endpoint to initiate the flow.
Create a template page for the InfoMap↑
We need a page to be displayed when the trigger URL is called. We simply created an empty html file in the template pages folder C/OIDC/OIDCBackLogout.html
.
Create a mapping rule for InfoMap↑
The rule logic of the InfoMap connects everything together and executes the actual login flow. It reads the saved logout client ids for the session from the authorization grants of the user, retrieves the registered logout URIs from the client definitions and constructs a logout JWT token for each using the STS chain. It then posts the token using the script HTTP client to each relevant RP.
We created the file named OIDCBackLogout
of type InfoMap. Here is the content:
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.com.ibm.security.access.httpclient.HttpClient);
importClass(Packages.com.ibm.security.access.httpclient.HttpResponse);
importClass(Packages.com.ibm.security.access.httpclient.Headers);
importClass(Packages.com.ibm.security.access.httpclient.Parameters);
importClass(Packages.com.tivoli.am.fim.fedmgr2.trust.util.LocalSTSClient);
importClass(Packages.java.util.ArrayList);
importClass(Packages.java.util.HashMap);
var username = context.get(Scope.SESSION, "urn:ibm:security:asf:response:token:attributes", "username");
var session_ix = context.get(Scope.SESSION, "urn:ibm:security:asf:response:token:attributes", "tagvalue_session_index");
IDMappingExtUtils.traceString("Logout user:" + username);
IDMappingExtUtils.traceString("Session ix user:" + session_ix);
var grants = OAuthMappingExtUtils.getGrants(username);
var logoutClientList = [];
for (i=0;i<grants.length;i++){
var stateId = grants[i].getStateId();
logoutclients = JSON.parse(OAuthMappingExtUtils.getAssociation(stateId,"urn:ibm:am:logoutclients"));
if (logoutclients != null){
var sessionClients = logoutclients[session_ix];
if (sessionClients != null){
for (j = 0; j < sessionClients.length; j++) {
logoutClientList.push(sessionClients[j]);
}
}
}
}
for (i=0; i< logoutClientList.length; i++){
var clientId = logoutClientList[i];
var client = OAuthMappingExtUtils.getClient(clientId);
var ext_data = JSON.parse(client.getExtendedData());
var logoutURI = ext_data.dynamic_client["backchannel_logout_uri"];
var includeSID = ext_data.dynamic_client["backchannel_logout_session_required"] == "true" ? true:false;
var sid = includeSID ? session_ix : null;
IDMappingExtUtils.traceString("CLIENT ID: " + clientId);
IDMappingExtUtils.traceString("LOGOUT URI: " + logoutURI);
do_logout(username,clientId,logoutURI,sid)
}
function do_logout(username,clientId,uri,sid){
var stsuu = new STSUniversalUser();
var client = OAuthMappingExtUtils.getClient(clientId);
var definitionId = client.getDefinitionID();
var definition = OAuthMappingExtUtils.getDefinitionByID(definitionId);
var oidc = definition.getOidc();
var issuer = ""+oidc.getIss();
add_claim(stsuu,"iss",issuer);
//We choose to always add 'sub'. It is not mandatory when using 'sid'...
add_claim(stsuu,"sub",username);
add_claim(stsuu,"aud",clientId);
var now = new Date();
var now_epoch = now.getTime();
now_epoch_sec = Math.floor(now_epoch/1000);
var iat = String(now_epoch_sec);
add_claim(stsuu,"iat",iat);
var jti = OAuthMappingExtUtils.generateRandomString(4);
add_claim(stsuu,"jti",jti);
add_claim(stsuu,"events",'{"http://schemas.openid.net/event/backchannel-logout": {} }');
if (sid != null){
add_claim(stsuu,"sid",sid);
}
var stsRes = LocalSTSClient.doRequest("http://schemas.xmlsoap.org/ws/2005/02/trust/Issue", "oidclogout", "oidclogout", stsuu.toXML().getDocumentElement(), null);
if (stsRes.token) {
logoutJWT = stsRes.token.getTextContent();
IDMappingExtUtils.traceString("LOGOUT JWT: " + logoutJWT);
call_logout_endpoint(uri,logoutJWT);
} else {
IDMappingExtUtils.traceString("Error getting token from STS: " + stsRes.errorMessage);
//We choose not to throw an exception to give a chance for the rest of logout tokens to be sent.
}
}
function add_claim(stsuu,name,value){
var attr = new com.tivoli.am.fim.trustserver.sts.uuser.Attribute(name, "urn:ibm:jwt:claim", value);
stsuu.addAttribute(attr);
}
function call_logout_endpoint(uri,jwt){
headers = new Packages.com.ibm.security.access.httpclient.Headers();
IDMappingExtUtils.traceString("LOGOUT URI: " + uri);
headers.addHeader("Content-Type","application/x-www-form-urlencoded");
parameters = new Packages.com.ibm.security.access.httpclient.Parameters();
parameters.addParameter("logout_token",jwt);
http_reponse = com.ibm.security.access.httpclient.HttpClient.httpPost(uri, headers, parameters,"httpClient",null,null,null,null);
}
page.setValue("/OIDC/OIDCBackLogout.html");
success.endPolicyWithoutCredential();
Create an authentication mechanism↑
We need to create a new InfoMap authentication mechanism using the mapping rule above. We use the attributes as follows. Note that we don't need a template page.
Create an authentication policy↑
To be able to trigger the mechanism, we need an authentication policy. Our new authentication policy simply looks like:
Having this, we have a logout URL (our AAC junction is /demo
): /demo/sps/apiauthsvc?PolicyId=urn:ibm:security:authentication:asf:oidc:bclogout
(Optional) Apply an HTTP transformation to make the URL look nicer↑
The logout URL we created is not very friendly. There is no standard endpoint, but we want that it is good looking.
With omitting step-by-step detalis on how to create an HTTP transformation, we create a transformation that simply rewrites the URL (boilerplate parts omitted):
<xsl:template match="//HTTPRequest/RequestLine/URI"> <URI>/demo/sps/apiauthsvc?PolicyId=urn:ibm:security:authentication:asf:oidc:bclogout</URI>
</xsl:template>
We then apply this transformation on URL /demo/testOIDC/logout
which will serve as our logout trigger URL.
Extend OP metadata indicating that back-channel logout is supported↑
Edit the template file C/oauth20/metadata.json
and add the following 3 lines:
"backchannel_logout_trigger_uri": "@POC_PREFIX@@OAUTH_DEFINITION@/logout",
"backchannel_logout_supported":<%var supported = true;templateContext.response.body.write(supported);%>,
"backchannel_logout_session_supported":<%var supported = true;templateContext.response.body.write(supported);%>
Note 1: The backchannel_logout_trigger_uri
is not part of the specification, but we find it useful.
Note 2: In our case @POC_PREFIX@@OAUTH_DEFINITION@
is equal to /demo/testOIDC
. The value you set for backchannel_logout_trigger_uri
must match the logout trigger URL as defined by the HTTP transformation or the authentication policy address.
(Optional) Register the logout trigger endpoint as a logout URI on the reverse proxy to make it automatic on session termination↑
In your reverse proxy configuration you can use the single-signoff-uri
attribute in the [acnt-mgt]
stanza to automate triggering the single logout when terminating the session. In certain cases this comes handy, in other cases it causes headache. This is not part of any OIDC specification.
[acnt-mgt]
single-signoff-uri = /demo/sps/apiauthsvc?PolicyId=urn:ibm:security:authentication:asf:oidc:bclogout
The configuration described here is tested on ISVA 10.0.0.