search
how tos

Locking Out a User and Delete All of Their Active Tokens in AM

Introduction

Customers often ask, "We have a business security requirement where we want to not only lock a given user out of the system, but also find and delete all SSO tokens they have created using the AM API. How do we do that?"

This article shows you how to do this using the delete_tokens.sh script ;)

In order to achieve this, we'll be making using of the following AM endpoints:

  • ../authenticate: Endpoint for authenticating users.
  • ../users: Endpoint for getting and setting user profile attributes.
  • ../sessions: Endpoint for getting and revoking user sessions.

Note: This article applies to stateful SSO tokens only, and not stateless/client-side ones.

Authenticating the endpoint

An endpoint is used to generate an SSO token following successful traversal of an AM Tree (or Chain). It can take a number of parameters like realm, service (Tree or Chain name), authentication level, etc., to tailor the login experience as required. More on this here and here.

In our example, we'll use the out of the box ldapService using the Zero Page login mechanism, where the user credentials are passed as headers. This is for testing purposes only.

In the script, the authNTarget and authNAdmin functions are used to generate test user sessions and generate an admin session respectively. The authNTarget function has been included to make it easier to generate sample sessions (5 by default), and can be omitted if required. The authNAdmin script is essential. These functions look like this:

authNTarget(){
clear
echo "*********************"
echo "Authenticating $TARGET_USERNAME user $TARGET_AUTHN_ITERATIONS times to generate a set of sample SSO tokens"for (( n=1; n<=$TARGET_AUTHN_ITERATIONS; n++ ))
do
        USER_SSO_TOKEN=`curl -s \
        --request POST \
        --header "$CONTENT_HEADER" \
        --header "$AM_AUTHENTICATE_VERSION" \
        --header "X-OpenAM-Username: $TARGET_USERNAME" \
        --header "X-OpenAM-Password: $TARGET_PASSWORD" \
        --data '' \
        "$AM_USER_AUTHENTICATE"  | jq -r .tokenId`
        echo "SSO Token for user $TARGET_USERNAME is: $USER_SSO_TOKEN"
done
}authNAdmin(){
echo "*********************"
echo "Authenticating $ADMIN_USERNAME user to generate SSO token"
SSO_TOKEN=`curl -s \
--request POST \
--header "$CONTENT_HEADER" \
--header "$AM_AUTHENTICATE_VERSION" \
--header "X-OpenAM-Username: $ADMIN_USERNAME" \
--header "X-OpenAM-Password: $ADMIN_PASSWORD" \
--data '' \
"$AM_AUTHENTICATE"  | jq -r .tokenId`
echo "SSO Token for user $ADMIN_USERNAME is: $SSO_TOKEN"
}

Get the cookie name

It is a best practice to change the default cookie name AM uses to store the tokenId from iplanetDirectoryPro to something unique. The getCookieName function acquires the name of the cookie from the AM serverinfo endpoint. It's then used in subsequent functions. This function looks like this:

getCookieName() {
echo "Getting cookie name"
AM_COOKIENAME=`curl -k "$AM_HOST"/json/serverinfo/\* -s | jq -r .cookieName`
echo "CookieName is: $AM_COOKIENAME"
}

Get the target UID

In some customer environments, the UID does not always match the subject defined in the token. To ensure the correct subject is set to find, delete and lock a given user when the getUIDTargetUser function is used. This function looks like this:

#In some environments subject is different to UID. The function gets the subject from the token which is definitive.
getUIDTargetUser() {
echo "*********************"
echo "Getting subject from the token for user $TARGET_USERNAME"
SUBJECT=`curl -s \
--request POST \
--header "$CONTENT_HEADER" \
--header "Cache-Control: no-cache" \
--header "$AM_COOKIENAME: $SSO_TOKEN" \
--header "$AM_SESSIONS_VERSION" \
--data '{ "tokenId": "'$USER_SSO_TOKEN'" }' \
$AM_GETSESSION_INFO | jq -r .username`
echo "Subject ID is: $SUBJECT"
if [ $REALM == "root" ]; then
        AM_LIST_SESSIONS=$AM_HOST/json/realms/$REALM/sessions?_queryFilter=username%20eq%20%22$SUBJECT%22%20and%20realm%20eq%20%22%2F%22
else
        AM_LIST_SESSIONS=$AM_HOST/json/realms/$REALM/sessions?_queryFilter=username%20eq%20%22$SUBJECT%22%20and%20realm%20eq%20%22%2F$REALM%22
fi
AM_USER_ENDPOINT=$AM_HOST/json/realms/$REALM/users/$SUBJECT
}

The users endpoint

Customers also ask how to lock out a user to prevent them from generating a new session in the future. IDM can be used for this, but in this blog, we'll use the AM user's endpoint. More on this endpoint here.

Using the admin token ($SSO_TOKEN) generated from the authNAdmin function, we can set the User Lockout Attribute (default is inetUserStatus) to Inactive to disable the user by using a HTTP PUT operation. This function looks like this:

setUserInactive(){
echo "*********************"
echo "Disabling $SUBJECT user by setting $LOCK_ATTRIBUTE to Inactive:"
curl -s \
--request PUT \
--header "$AM_COOKIENAME: $SSO_TOKEN" \
--header "$CONTENT_HEADER" \
--header "$AM_USER_VERSION" \
--header "If-Match: *" \
--data '{ "'$LOCK_ATTRIBUTE'": "Inactive" }' \
$AM_USER_ENDPOINT | jq -r .inetUserStatus
}

Note this function is commented out by default as manual interaction. It's required that you set the user back to active again through REST or the UI. Uncomment in the functions section at the bottom of the script if required.

The sessions endpoint

This endpoint can be used to list all active sessions for a given user using a HTTP GET with a queryFilter parameter, and to also revoke sessions using the logoutByHandle action. More on this endpoint here.

The getActiveSessions function lists all active sessions for a user. If active sessions are found, through the magic of sed, the output is manipulated, and the ACTIVE_SESSIONS variable set for later consumption by the deleteActiveSession function. The number of active sessions is also printed. This function looks like this:

getActiveSessions(){
echo "*********************"
echo "Getting active SSO sessions for user: $SUBJECT"
ACTIVE_LIST=`curl -s \
--request GET \
--header "$CONTENT_HEADER" \
--header "Cache-Control: no-cache" \
--header "$AM_COOKIENAME: $SSO_TOKEN" \
--header "$AM_SESSIONS_VERSION" \
$AM_LIST_SESSIONS | jq '.result[].sessionHandle'`
if [[ -n "${ACTIVE_LIST/[ ]*\n/}" ]]; then
 ACTIVE_SESSIONS=`echo $ACTIVE_LIST | sed -r 's/[[:space:]]+/,/g'`
 echo "Number of active sessions found for user: $SUBJECT: `echo $ACTIVE_LIST | wc -w`"
 echo $ACTIVE_LIST | jq .
 deleteActiveSession
else
 echo "No active sessions found"
fi
}

The deleteActiveSession function deletes active sessions and is called from within the getActiveSessions function. This endpoint is targeted with a HTTP POST operation with the sessions defined as an array in the POST body in a property named sessionHandles, which is based on the value of the ACTIVE_SESSIONS variable. The function looks like this:

deleteActiveSession(){
echo "*********************"
echo "Deleting all active SSO sessions for user: $SUBJECT"
curl -s \
--request POST \
--header "$CONTENT_HEADER" \
--header "Cache-Control: no-cache" \
--header "$AM_COOKIENAME: $SSO_TOKEN" \
--header "$AM_SESSIONS_VERSION" \
--data '{
    "sessionHandles": [
    '$ACTIVE_SESSIONS'
    ]
}' \
$AM_DELETE_SESSIONS | jq .
}

Putting it all together 

The delete_tokens.sh script illustrates how all of the above operations can be combined to meet the requirements of locking a given user out of the system, and deleting all of their active sessions.

To execute the script, run ./find_delete_tokens_lock_user.sh. This will:

  1. Check for the presence of the jq tool, which is essential for manipulating the AM JSON responses.
  2. Generate 5 SSO tokens for the darinder user (omit/comment out the function if sample sessions are not required).
  3. Generate an admin SSO session for the amAdmin user.
  4. Get the SSO cookie name from the environment.
  5. Get the subject identifier from the user (Darinder) sso token.
  6. (Optional) Disable the user specified in the execution parameter for the script (darinder) by setting the inetUserStatus to Inactive.
  7. Find all valid session for user darinder in a given realm (test by default).
  8. Delete all sessions for user darinder in a given realm (test by default).
  9. Find all valid sessions again; zero sessions should be returned indicating success.

Here's an example output:

fr-01-how-to-lockout-a-user-and-delete-all-their-active-tokens.png
Example output. Environment specific detail omitted

Conclusions and further reading

So there you have it, an API-driven approach to disabling a given user and deleting all of their active sessions. This article only covers SSO sessions. "What about OAuth2 tokens?" I hear you say.

Well, the AM ../oauth2/token/revoke endpoint can be used to provide a specification-compliant API to revoke individual OAuth2 tokens (described here). There's also the new and shiny extension to the AM specific users endpoint, ../users/<user>/oauth2/applications, which can be used to find and delete all tokens issued to a given user filtered by OAuth2 client, described here.

Thanks for reading :)

Script is below, and is also hosted on Stash here:

#!/bin/bash<< ////Written by Darinder S. Shokar - ForgeRock Customer SuccessAccompanying blog post - https://medium.com/@darinder.shokar/how-to-lockout-a-user-and-delete-all-their-active-tokens-in-forgerock-am-b3fb2fe7ea92The sample code described herein is provided on an "as is" basis, without warranty of any kind, to the fullest extent permitted by law. ForgeRock does not warrant or guarantee the individual success developers may have in implementing the sample code on their development platforms or in production configurations.ForgeRock does not warrant, guarantee or make any representations regarding the use, results of use, accuracy, timeliness or completeness of any data or information relating to the sample script. ForgeRock disclaims all warranties, expressed or implied, and in particular, disclaims all warranties of merchantability, and warranties related to the script/code, or any service or software related thereto.ForgeRock shall not be liable for any direct, indirect or consequential damages or costs of any type arising out of any action taken by you or others related to the sample script/code.NOTE THIS SCRIPT WILL NOT WORKS WHERE STATELESS/CLIENTSIDE SSO TOKENS ARE IN USENOTE the setUserInactive is commented out. Uncomment if required.////# Parameters. Modify as appropriate:
REALM=alpha
AM_HOST=FQDN_of_AM_with_context #E.g. https://openam.example.com/am
ADMIN_USERNAME=XXXXXX
ADMIN_PASSWORD=XXXXXX
TARGET_USERNAME=demo
TARGET_PASSWORD=Ch4ng31t
TARGET_AUTHN_ITERATIONS='5'
LOCK_ATTRIBUTE=inetUserStatusAM_AUTHENTICATE=$AM_HOST/json/realms/ROOT/authenticate
AM_USER_AUTHENTICATE=$AM_HOST/json/realms/$REALM/authenticate
AM_GETSESSION_INFO=$AM_HOST/json/realms/$REALM/sessions/?_action=getSessionInfo
AM_DELETE_SESSIONS=$AM_HOST/json/realms/$REALM/sessions/?_action=logoutByHandle
CONTENT_HEADER='Content-Type: application/json'
AM_AUTHENTICATE_VERSION='Accept-API-Version: resource=2.0, protocol=1.0'
AM_USER_VERSION='Accept-API-Version: protocol=2.1,resource=3.0'
#Each version of AM has a different API version (v4 for AM 7.0) for the Sessions endpoint. Use the API Explorer to set as appropriate. 
AM_SESSIONS_VERSION='Accept-API-Version: resource=4.0, protocol=1.0'jqCheck(){
hash jq &> /dev/null
if [ $? -eq 1 ]; then
 echo >&2 "The jq Command-line JSON processor is not installed on the system. Please install and re-run."
 exit 1
fi
}authNTarget(){
clear
echo "*********************"
echo "Authenticating $TARGET_USERNAME user $TARGET_AUTHN_ITERATIONS times to generate a set of sample SSO tokens"for (( n=1; n<=$TARGET_AUTHN_ITERATIONS; n++ ))
do
        USER_SSO_TOKEN=`curl -s \
        --request POST \
        --header "$CONTENT_HEADER" \
        --header "$AM_AUTHENTICATE_VERSION" \
        --header "X-OpenAM-Username: $TARGET_USERNAME" \
        --header "X-OpenAM-Password: $TARGET_PASSWORD" \
        --data '' \
        "$AM_USER_AUTHENTICATE"  | jq -r .tokenId`
        echo "SSO Token for user $TARGET_USERNAME is: $USER_SSO_TOKEN"
done
}authNAdmin(){
echo "*********************"
echo "Authenticating $ADMIN_USERNAME user to generate SSO token"
SSO_TOKEN=`curl -s \
--request POST \
--header "$CONTENT_HEADER" \
--header "$AM_AUTHENTICATE_VERSION" \
--header "X-OpenAM-Username: $ADMIN_USERNAME" \
--header "X-OpenAM-Password: $ADMIN_PASSWORD" \
--data '' \
"$AM_AUTHENTICATE"  | jq -r .tokenId`
echo "SSO Token for user $ADMIN_USERNAME is: $SSO_TOKEN"
}getCookieName() {
        echo "Getting cookie name"
        AM_COOKIENAME=`curl -k "$AM_HOST"/json/serverinfo/\* -s | jq -r .cookieName`
        echo "CookieName is: $AM_COOKIENAME"
}#In some environments subject is different to UID. The function gets the subject from the token which is definitive.
getUIDTargetUser() {
echo "*********************"
echo "Getting subject from the token for user $TARGET_USERNAME"
SUBJECT=`curl -s \
--request POST \
--header "$CONTENT_HEADER" \
--header "Cache-Control: no-cache" \
--header "$AM_COOKIENAME: $SSO_TOKEN" \
--header "$AM_SESSIONS_VERSION" \
--data '{ "tokenId": "'$USER_SSO_TOKEN'" }' \
$AM_GETSESSION_INFO | jq -r .username`
echo "Subject ID is: $SUBJECT"
if [ $REALM == "root" ]; then
        AM_LIST_SESSIONS=$AM_HOST/json/realms/$REALM/sessions?_queryFilter=username%20eq%20%22$SUBJECT%22%20and%20realm%20eq%20%22%2F%22
else
        AM_LIST_SESSIONS=$AM_HOST/json/realms/$REALM/sessions?_queryFilter=username%20eq%20%22$SUBJECT%22%20and%20realm%20eq%20%22%2F$REALM%22
fi
AM_USER_ENDPOINT=$AM_HOST/json/realms/$REALM/users/$SUBJECT
}setUserInactive(){
echo "*********************"
echo "Disabling $SUBJECT user by setting $LOCK_ATTRIBUTE to Inactive:"
curl -s \
--request PUT \
--header "$AM_COOKIENAME: $SSO_TOKEN" \
--header "$CONTENT_HEADER" \
--header "$AM_USER_VERSION" \
--header "If-Match: *" \
--data '{ "'$LOCK_ATTRIBUTE'": "Inactive" }' \
$AM_USER_ENDPOINT | jq -r .inetUserStatus
}getActiveSessions(){
echo "*********************"
echo "Getting active SSO sessions for user: $SUBJECT"
ACTIVE_LIST=`curl -s \
--request GET \
--header "$CONTENT_HEADER" \
--header "Cache-Control: no-cache" \
--header "$AM_COOKIENAME: $SSO_TOKEN" \
--header "$AM_SESSIONS_VERSION" \
$AM_LIST_SESSIONS | jq '.result[].sessionHandle'`
if [[ -n "${ACTIVE_LIST/[ ]*\n/}" ]]; then
 ACTIVE_SESSIONS=`echo $ACTIVE_LIST | sed -r 's/[[:space:]]+/,/g'`
 echo "Number of active sessions found for user: $SUBJECT: `echo $ACTIVE_LIST | wc -w`"
 echo $ACTIVE_LIST | jq .
 deleteActiveSession
else
 echo "No active sessions found"
fi
}deleteActiveSession(){
echo "*********************"
echo "Deleting all active SSO sessions for user: $SUBJECT"
curl -s \
--request POST \
--header "$CONTENT_HEADER" \
--header "Cache-Control: no-cache" \
--header "$AM_COOKIENAME: $SSO_TOKEN" \
--header "$AM_SESSIONS_VERSION" \
--data '{
    "sessionHandles": [
    '$ACTIVE_SESSIONS'
    ]
}' \
$AM_DELETE_SESSIONS | jq .
}#Functions
jqCheck
authNTarget
authNAdmin
getCookieName
getUIDTargetUser
# setUserInactive # Note need to manually re-enable/set to Active before executing again
getActiveSessions