Developer Tutorial: Part II: A SAML SSO Test flow with cURL and SSOCheck API

Part II of the tutorial will build on the BASH script which we developed in Part I. Up to now the script was able to run a SAML SSO flow with cURL against the SSOCircle IDP (IDP) and the Fedlet Test SAML service provider (SP). We will now add functionality to call the SSOCheck API which modifies the SAML messages in such a way that the newly created messages can be used to test SAML conformity and security of the SP. We also add a loop in order to run almost 100 SAML Tests.

For an introduction to the tutorial visit here. Part I created a SAML test script with cURL.

At the end of Part II our script should run the steps:

  1. CLIENT accesses the SP (Initiate the SSO flow – this is typically a “sign in” button or simply a request to a protected page)
  2. SP replies with a SAML AuthnRequest message (typically a 302 redirect to the IDP)
  3. CLIENT follows the redirect and sends the SAML AuthnRequest message to IDP
  4. If user is not already authenticated with the IDP
    • a. IDP redirects to the IDP login page
    • b. CLIENT submits credentials vial the login form to IDP
    • c. IDP authenticates the user and redirects the user to continue with SSO
  5. IDP generates the SAML Response with the Assertion and sends it back to the CLIENT (typically this is a HTML page with an auto-posted form)
  6. CLIENT intercepts the auto-posting and sends the SAML Response message to the API
  7. API modifies the SAML Response message to a SAML TEST message and returns the message to CLIENT
  8. CLIENT continues posting the SAML Response message to the SP
  9. SP receives the SAML Response message and authenticates the user based on the Assertion (if SSO is successful the SP typically replies with a “logged in” page or the protected content)
  10. CLIENT receives the SP response and evaluates if the SSO flow was successful (user is logged in)

The steps in bold describe the functionality that will be added to the script. Please note: In order to use the SSOCheck API some initialization steps must be processed (e.g. get an OAuth access token which authorizes access to the API). These steps are not listed above, but they are mandatory and we need to add them to the script.

We will follow the same procedure as in Part I: Develop building blocks of new functionality and include these blocks into a growing script which can be run. At the end of this tutorial the script should accomplish the steps listed above.

Let’s start with the API initialization stuff – it is just s.th. that has to be done before you can use the API. Like cleaning the house before the party can start …

If you want to read about the details of the SSOCheck API, you might review the manual.

Before you can run tests and use the Test Execution API, you need to

  • a. Retrieve an access token which authorizes the API access
    -> You need your SSOCircle user credentials and query the Authorization API to get an access token
  • b. Get a test plan definition which specifies the tests that should be run
    -> You will get this form the Configuration API

The first building block a.: “Retrieve the access token”:


# SSOCheck API URLs
API_AUTHZ="https://idp.ssocircle.com/sso/ttapi/2.0/authz/0/authn"

# function that calls the SSOCheck Authz API
function apiAuthz {
>&2 echo "apiAuthz"
local url="$1"
local response=$($CURL --data-urlencode grant_type=password --data-urlencode username=${USERNAME}@ssocircle --data-urlencode password=${PASSWORD} --data-urlencode scope=SAML ${url})
if [[ $response =~ \"access_token\": ]]
then
>&2 echo "Got access token"
local token=$(echo "$response" | cut -d '"' -f4 | tr -d '\\')
echo "$token"
fi
}

#API INIT
# Get an access token
ACCESS_TOKEN=$(apiAuthz ${API_AUTHZ})
>&2 echo "Token is ${ACCESS_TOKEN}"

We have introduced a new variable for the URL of the Authorization API. The parameters USERNAME and PASSWORD will be the same as the credentials of the user we used to log in in the test case. As in Part I we will move some functionality in functions to get more modularity.

Goals achieved: Step a. With the function we are able to retrieve an access token which can then be used to query other APIs.

That said, we use the access token and query the Configuration API to get a Test Plan – as mentioned in b). The building block for the task looks like:


API_CFG="https://idp.ssocircle.com/sso/ttapi/2.0/cfg/0/authn"

# function that calls the SSOCheck Config API
function apiConfig {
>&2 echo "apiConfig"
local url="$1"
local token="$2"
local response=$($CURL --header "Authorization: Bearer ${token}" ${url})
if [[ $response =~ \"plan\": ]]
then
>&2 echo "Got test plan"
local plan=$(echo "$response" | cut -d '"' -f4 )
local step=$(echo "$response" | cut -d '"' -f7 | tr -d ':}' )
echo PLAN="$plan"\;STEP1="${step}"
fi
}

# Get Test Plan if not supplied as an argument
if [ -z $PLAN ] || [ -z $STEP1 ]
then
eval $(apiConfig ${API_CFG} ${ACCESS_TOKEN})
fi
>&2 echo "Plan is: " $PLAN
>&2 echo "STEP1 is: " $STEP1
STEP=$STEP1

You may have noticed the function needs the Configuration API URL and the Access Token as a parameter. The first parameter introduces the new variable API_CFG. The function will return the plan itself and the starting point of a test plan run. We have also added an additional “if” statement that allows to supply the plan and start step as an argument to the script. This might be useful when running partial tests.

Goals achieved: Step b. With the function we are able to retrieve a Test Plan and know the first step that we should execute.

At this point it does not make sense to put these two building blocks into the full script as the API initialization steps do not really change the way the script runs unless we will query the SSOCheck Execution API. If you want to include the blocks into the script, feel free to do that. It might help to find syntax and errors at an early stage.

The next building block tries to achieve step 6 and step 7 and actually takes the SAML message returned by the IDP, submit this message to the Execution API which in turn will transform the message and construct a test payload out of it. The script will take the returned payload and POST it to the SP – as if nothing happened.

To achieve these tasks, we need to modify the script so that it does not immediately POSTs the SAML message to the SP. Instead we need a code snippet with a function to query the Execution API and insert the function call right after the SAML response was returned from the IDP:


API_EXEC="https://idp.ssocircle.com/sso/ttapi/2.0/nxt/STEP/authn"

# function that calls the SSOCheck Execution API
function apiExec {
>&2 echo "apiExec"
local url="$1"
local token="$2"
local payload="$3"
local plan="$4"

local json="{\"ttin\":\""$payload"\",\"plan\":\""$plan"\"}"

local response=$($CURL --header "Authorization: Bearer ${token}" -d ${json} ${url})
if [[ $response =~ \"next\": ]]
then
>&2 echo "Got exec response"
local plan=$(echo "$response" | cut -d '"' -f4 )
local next=$(echo "$response" | cut -d '"' -f7 | tr -d ':' | tr -d ',')
local rule=$(echo "$response" | cut -d '"' -f9 | tr -d ':' | tr -d ',')
local saml=$(echo "$response" | cut -d '"' -f12 )
echo PLAN="$plan"\;STEP="${next}"\;RULE="${rule}"\;SAML="${saml}"
else
>&2 echo "Error receiving response from API: $response"
echo "unset STEP"
fi
}

# QUERY the Test Execution API and get the test payload
API_URL="${API_EXEC/STEP/${STEP}}"
TEST=$STEP
eval $(apiExec ${API_URL} ${ACCESS_TOKEN} "${SAML}" "${PLAN}")
if [ -z $STEP ]
then
echo "Error calling API - exit"
exit
fi

The function is called with four parameters whereas only the Execution API URL is new to us. The API URL itself is constructed dynamically by concatenation of a constant and the test step that should be executed.

The API call returns the test payload, the next step, a modified plan and something we haven’t yet seen: A test rule. The test rule determines how the outcome of the whole SAML SSO flow should be: For example, a rule might say that as a result of the test the SSO process should fail (for example if the signature is missing). You can find details on rules in the manual. We will use the rule in the next building block. But first review what we achieved up to now:

Goals achieved: Step 6 and 7. The SAML message is now transformed into a test payload which is POSTed to the SP in step 8.
We are basically done with the script. We will now add an evaluation of the actual result (SSO successful or failed) against the expected result defined by the rule.


# Check whether the SSO result was expected for that test case
# means: test result matches the rule
echo "$SUCCESS versus expected result $RULE"
# handle rules 0 1 11 2
echo -n "Test[${TEST}] Result: "
if [ $SUCCESS == $RULE ] || [ "1$SUCCESS" == $RULE ]
then
echo "OK"
elif [ $RULE == "2" ]
then
echo "PARTIAL"
elif [ $RULE == "10" ] || [ $RULE == "11" ]
then
echo "WARNING"
else
echo "ERROR"
fi

The code snippet will just output OK, ERROR, etc.
To make the script run we are now going to include the blocks developed in this part of the tutorial into the result of Part I.

It is also necessary to modify some parts of the existing script. We need to add additional HTTP headers to instruct the IDP that the request is a SSOCheck test. For that we add the following Headers to the cURL request which initiates the test flow or does a login to SSOCircle.

X-ttool-on: TRUE
X-ttool-step: $STEP
X-ttool-plan: $PLAN

The function onloadSAML() needs to pick up new test plans returned by the IDP. For that we add the code snippet:


local plan=$(echo "$in" | grep "X-ttool-plan:" | tr -d '\r' | cut -d ' ' -f2)
if [ ! -z $plan ]
then
PLAN=${plan}
>&2 echo "received a new plan"
fi

We will also add a loop that runs the test case repeatedly with modified test payloads.

For a quick start just copy the script below into your editor. Insert username and password of your SSOCircle user and run the script from the command line: # ./ssocheck.sh

IMPORTANT: How to check the script behaves well? Test 1 in the Test Plan is always a positive test. The SAML response is not modified and the login should succeed. If this is not the case, the flow is not properly implemented.

TIP: If everything runs fine, use the command:


# ./ssocheck.sh 2>&1 | grep Result

to reduce the output message.


#!/bin/bash

USERNAME=<YOUR SSOCIRCLE USER>
PASSWORD=<YOUR SSOCIRCLE USER PASSWORD>

#Variable definitions
CURL="curl -L -c cookies -b cookies -s -o -"
INITURL="https://fedlet.idpee.com/sp/saml2/jsp/fedletSSOInit.jsp?metaAlias=/sp&idpEntityID=http%3A%2F%2Fidp.ssocircle.com"
LOGINURL="https://idp.ssocircle.com/sso/UI/Login"
LOGOUTURL="https://fedlet.idpee.com/sp/localLO.jsp"

# SSOCheck API URLs
API_AUTHZ="https://idp.ssocircle.com/sso/ttapi/2.0/authz/0/authn"
API_CFG="https://idp.ssocircle.com/sso/ttapi/2.0/cfg/0/authn"
API_EXEC="https://idp.ssocircle.com/sso/ttapi/2.0/nxt/STEP/authn"

#functions

# recognize the login page through simply pattern match for the page title and submit credentials
function doLogin
{
>&2 echo "doLogin"
local in="$1"
if [[ $in =~ "SSOCircle Identity Provider (Login)" ]]
then
>&2 echo "Login page detected"
local in="$1"
local goto=$(echo "$in" | grep -m 1 name=\"goto\" | cut -d '"' -f6)
local response=$($CURL --header "X-ttool-on: TRUE" --header "X-ttool-step: $STEP" --header "X-ttool-plan: $PLAN" --data-urlencode IDToken1="${USERNAME}" --data-urlencode IDToken2="${PASSWORD}" --data-urlencode goto="${goto}" ${LOGINURL})
>&2 echo "Logging in to IDP"
echo "$response"
else
>&2 echo "Already logged in to IDP"
echo "$in"
fi
}

# recognizes a page with body on load and
# retrieves the parameters from the form
function onloadSAML
{
>&2 echo "onloadSAML"
local in="$1"
if [[ $in =~ "body onload=" ]]
then
>&2 echo "Autopost detected"
local plan=$(echo "$in" | grep "X-ttool-plan:" | tr -d '\r' | cut -d ' ' -f2)
if [ ! -z $plan ]
then
PLAN=${plan}
>&2 echo "received a new plan"
fi
ACTION_URL=$(echo "$in" | grep "action=" | cut -d '"' -f4 | recode html)
SAML_RESPONSE=$(echo "$in" | grep SAMLResponse | cut -d '"' -f6)
RELAY_STATE=$(echo "$in" | grep RelayState | cut -d '"' -f6 )
fi
}

# function that actually posts the SAML to the Service Provider
function postSAML
{
>&2 echo "postSAML"
local encodeopt="$1"
local url="$2"
local saml="$3"
local relay="$4"
local response=$($CURL --${encodeopt} SAMLResponse="${saml}" --data-urlencode RelayState="${relay}" ${url})
echo "$response"
}
# function checks whether login to the service provider was successful
# and returns 0 on success and 1 if not
function loginSuccess
{
>&2 echo "loginSuccess"
local in="$1"
if [[ $in =~ "Single Sign-On successful" ]]
then
# success
echo 0
else
echo 1
fi
}

# function that calls the SSOCheck Authz API
function apiAuthz {
>&2 echo "apiAuthz"
local url="$1"
local response=$($CURL --data-urlencode grant_type=password --data-urlencode username=${USERNAME}@ssocircle --data-urlencode password=${PASSWORD} --data-urlencode scope=SAML ${url})
if [[ $response =~ \"access_token\": ]]
then
>&2 echo "Got access token"
local token=$(echo "$response" | cut -d '"' -f4 | tr -d '\\')
echo "$token"
fi
}

# function that calls the SSOCheck Config API
function apiConfig {
>&2 echo "apiConfig"
local url="$1"
local token="$2"
local response=$($CURL --header "Authorization: Bearer ${token}" ${url})
if [[ $response =~ \"plan\": ]]
then
>&2 echo "Got test plan"
local plan=$(echo "$response" | cut -d '"' -f4 )
local step=$(echo "$response" | cut -d '"' -f7 | tr -d ':}' )
echo PLAN="$plan"\;STEP1="${step}"
fi
}

# function that calls the SSOCheck Execution API
function apiExec {
>&2 echo "apiExec"
local url="$1"
local token="$2"
local payload="$3"
local plan="$4"

local json="{\"ttin\":\""$payload"\",\"plan\":\""$plan"\"}"

local response=$($CURL --header "Authorization: Bearer ${token}" -d ${json} ${url})
if [[ $response =~ \"next\": ]]
then
>&2 echo "Got exec response"
local plan=$(echo "$response" | cut -d '"' -f4 )
local next=$(echo "$response" | cut -d '"' -f7 | tr -d ':' | tr -d ',')
local rule=$(echo "$response" | cut -d '"' -f9 | tr -d ':' | tr -d ',')
local saml=$(echo "$response" | cut -d '"' -f12 )
echo PLAN="$plan"\;STEP="${next}"\;RULE="${rule}"\;SAML="${saml}"
else
>&2 echo "Error receiving response from API: $response"
echo "unset STEP"
fi
}

####################################
# MAIN ROUTINE
####################################

# check user and password are set
if [ "$USERNAME" == '<YOUR SSOCIRCLE USER>' ]
then
echo "Username not set - exit"
exit 1
fi

#Read optional arguments
PLAN=$1
STEP1=$2

#API INIT
# Get an access token
ACCESS_TOKEN=$(apiAuthz ${API_AUTHZ})
>&2 echo "Token is ${ACCESS_TOKEN}"

# Get Test Plan if not supplied as an argument
if [ -z $PLAN ] || [ -z $STEP1 ]
then
eval $(apiConfig ${API_CFG} ${ACCESS_TOKEN})
fi
>&2 echo "Plan is: " $PLAN
>&2 echo "STEP1 is: " $STEP1
STEP=$STEP1

# START TEST CASE in a LOOP

while [ $STEP != -1 ]
do

# Run use case: The SAML SSO flow
OUTPUT=$($CURL -i --header "X-ttool-on: TRUE" --header "X-ttool-step: $STEP" --header "X-ttool-plan: $PLAN" ${INITURL})

# check if we need to login to the IDP
>&2 echo "Checking Login"
OUTPUT=$(doLogin "$OUTPUT")

# Evaluate the SAML autoposting page
>&2 echo "Autoposting"
onloadSAML "$OUTPUT"

# decode the parameters
SAML="$(echo -n $SAML_RESPONSE | recode html | tr -d '\r\n')"
RELAY="$(echo -n $RELAY_STATE | recode html)"

# some error handling
if [ -z "$SAML" ]
then
echo "Error: No SAML payload received"
exit
fi

# QUERY the Test Execution API and get the test payload
API_URL="${API_EXEC/STEP/${STEP}}"
TEST=$STEP
eval $(apiExec ${API_URL} ${ACCESS_TOKEN} "${SAML}" "${PLAN}")
if [ -z $STEP ]
then
echo "Error calling API - exit"
exit
fi

# Send the message to the service provider
>&2 echo "Submitting the response"
OUTPUT=$(postSAML data $ACTION_URL "$SAML" "$RELAY")

# Check the output for successful SSO
SUCCESS=$(loginSuccess "$OUTPUT")

# Check whether the SSO result was expected for that test case
# means: test result matches the rule
echo "$SUCCESS versus expected result $RULE"
# handle rules 0 1 11 2
echo -n "Test[${TEST}] Result: "
if [ $SUCCESS == $RULE ] || [ "1$SUCCESS" == $RULE ]
then
echo "OK"
elif [ $RULE == "2" ]
then
echo "PARTIAL"
elif [ $RULE == "10" ] || [ $RULE == "11" ]
then
echo "WARNING"
else
echo "ERROR"
fi

# DO a SP local Logout
OUTPUT=$($CURL ${LOGOUTURL})

# Rerun the test with a new payload
>&2 echo "Continuing with next step $STEP"
done

You might notice that we silently introduced the processing of arguments, if they are specified at the command line.

TIP: If you only want to run specific tests. Generate a test plan manually at https://idp.ssocircle.com/sso/hos/PlanConfig.jsp Choose the tests that you want to run and submit the selection. Down at the bottom of the page you can find a plan definition string. Don’t wonder, it is an internal format, so it looks cryptic. Just copy and paste it to the command line: e.g.:

./ssocheck.sh <plan string> 1 (first step to start).

Remember: Including step 1 in the plan is mandatory.

latest technology

SSO Check your Partners

Test your SAML Service Provider for configuration and implementation errors.

Use our verification service on an ongoing bases and get the SSOChecked Seal.

Single password

Hosted Identity Provider

Use our free public IDP or the white label IDPee for your organization or corporate.

Be sure to get a quote if you are interested in getting one of our products.

write a mail

Contact Us

Interested in our services?

Contact us by sending a mail to info[at]ssocircle[dot]net, or by using our contact form.