diff --git a/bin/probe_saml_idp.sh b/bin/probe_saml_idp.sh new file mode 100755 index 0000000..53e0916 --- /dev/null +++ b/bin/probe_saml_idp.sh @@ -0,0 +1,474 @@ +#!/bin/bash + +####################################################################### +# Copyright 2016 Internet2 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +####################################################################### + +script_version="0.2" +user_agent_string="SAML IdP Probe ${script_version}" + +####################################################################### +# help message +####################################################################### + +display_help () { +/bin/cat <<- HELP_MSG + ${user_agent_string} + + Given a single identifier, assumed to be an IdP entityID, probe + all browser-facing SSO endpoints in IdP metadata. + + Usage: ${0##*/} [-hvq] [-a] [-t CONNECT_TIME [-m MAX_TIME]] [-r MAX_REDIRS] ID + + Options: + -h Display this message + -v Write verbose messages to stdout + -q Run quietly (i.e., write no messages to stdout) + -a Probe all SAML endpoints, including SAML1 endpoint(s) + -t Allowed time (in secs) to connect to the host + -m Maximum time (in secs) of a complete probe + -r Maximum number of HTTP redirects followed + + Option -h is mutually exclusive of all other options. Options + -q and -v are mutually exclusive of each other. Options -u and -f + are mutually exclusive of each other as well. + + The argument of the -t option is the TCP connect time, that is, + the maximum time (in secs) allotted to obtain a TCP connection. + Note that the TCP connect time includes the time it takes to do + a DNS name lookup. Since the latter is unconstrained, it may + consume all available TCP connect time. Thus the TCP connect + time should be kept small (on the order of a few seconds) since + larger values will slow this script considerably. + + The argument of the -m option is the maximum total time (in secs) + allotted to each probe. A reasonable value is a few seconds + beyond the TCP connect time. Any value less than the TCP connect + time causes the script to immediately fail. + + CONFIGURATION + + The script reads a file of config parameters. The script loads the + config file from the following file location: + + $config_file_default + + As a result of reading the config file, the following config + parameters are initialized: + + MDQ_BASE_URL + SAML2_SP_ENTITY_ID + SAML2_SP_ACS_URL + SAML2_SP_ACS_BINDING + SAML1_SP_ENTITY_ID + SAML1_SP_ACS_URL + SAML1_SP_ACS_BINDING + + The MDQ_BASE_URL is the base URL of a Metadata Query Server + (i.e., a server that conforms to the Metadata Query Protocol). + The base URL is used to construct an MDQ request URL, which the + script uses to request entity metadata just-in-time. + + The three SAML2_SP parameters define a SAML2 SP, that is, an SP + with one or more SAML2 browser-facing endpoints in metadata. The + SAML AuthnRequest transmitted to the IdP contains the values of + these parameters. Note: An IdP reacts differently to requests + from different SPs. Changing the values of these parameters may + produce different probe results. + + Similarly, the three SAML1_SP parameters define a SAML1 SP, that + is, an SP with a SAML1 browser-facing endpoint in metadata. (Any + given SP may support both SAML2 and SAML1, in which case these + parameters may be identical to the SAML2_SP parameters.) The + script probes SAML1 endpoints only if the -a option is given on + the command line. Be careful with this option, however, since + not all IdPs have SAML1 browser-facing endpoints in metadata. +HELP_MSG +} + +####################################################################### +# Bootstrap +####################################################################### + +script_bin=${0%/*} # equivalent to dirname $0 +script_name=${0##*/} # equivalent to basename $0 + +# determine the source lib directory +if [ -z "$LIB_DIR" ]; then + echo "ERROR: $script_name requires env var LIB_DIR" >&2 + exit 2 +fi +if [ ! -d "$LIB_DIR" ]; then + echo "ERROR: $script_name: LIB_DIR does not exist: $LIB_DIR" >&2 + exit 2 +fi + +# library filenames (always list command_paths first) +LIB_FILENAMES="command_paths.sh +compatible_mktemp.sh +http_tools.sh +md_tools.sh +saml_tools.sh +config_tools.sh" + +# source lib files +for lib_filename in $LIB_FILENAMES; do + lib_file="$LIB_DIR/$lib_filename" + if [ ! -f "$lib_file" ]; then + echo "ERROR: $script_name: lib file does not exist: $lib_file" >&2 + exit 2 + fi + source "$lib_file" + status_code=$? + if [ $status_code -ne 0 ]; then + echo "ERROR: $script_name failed to source lib file ($status_code) $lib_file" >&2 + exit 2 + fi +done + +# basic curl defaults +connect_timeout_default=2; max_redirs_default=7 + +# default config file +config_file_default="${script_bin}/.config_saml_idp_probe.sh" + +####################################################################### +# Process command-line options and arguments +####################################################################### + +help_mode=false; quiet_mode=false; verbose_mode=false +saml1_disabled=true +local_opts=; connect_timeout=; max_time=; max_redirs= +curl_opts= +while getopts ":hqvat:m:r:" opt; do + case $opt in + h) + help_mode=true + ;; + q) + quiet_mode=true + verbose_mode=false + #local_opts="$local_opts -$opt" + ;; + a) + saml1_disabled=false + ;; + v) + quiet_mode=false + verbose_mode=true + local_opts="$local_opts -$opt" + ;; + t) + connect_timeout="$OPTARG" + curl_opts="$curl_opts -t $OPTARG" + ;; + m) + max_time="$OPTARG" + curl_opts="$curl_opts -m $OPTARG" + ;; + r) + max_redirs="$OPTARG" + curl_opts="$curl_opts -r $OPTARG" + ;; + \?) + echo "ERROR: $script_name: Unrecognized option: -$OPTARG" >&2 + exit 2 + ;; + :) + echo "ERROR: $script_name: Option -$OPTARG requires an argument" >&2 + exit 2 + ;; + esac +done + +if $help_mode; then + display_help + exit 0 +fi + +# redirect stdout and stderr to the bit bucket +$quiet_mode && exec 1>/dev/null +$quiet_mode && exec 2>/dev/null + +# check consistency of timeout options +if [ -n "$max_time" -a -z "$connect_timeout" ]; then + echo "ERROR: $script_name: the -m option requires the presence of the -t option" >&2 + exit 2 +fi + +# set default connect timeout if necessary +if [ -z "$connect_timeout" ]; then + connect_timeout=$connect_timeout_default + curl_opts="$curl_opts -t $connect_timeout" +else + if [ "$connect_timeout" -le 0 ] ; then + echo "ERROR: $script_name: connect timeout ($connect_timeout) must be a positive integer" >&2 + exit 2 + fi +fi + +# compute max time if necessary +if [ -z "$max_time" ]; then + max_time=$(( connect_timeout + 2 )) + curl_opts="$curl_opts -m $max_time" +else + if [ "$max_time" -le "$connect_timeout" ]; then + echo "ERROR: $script_name: max time ($max_time) must be greater than the connect timeout ($connect_timeout)" >&2 + exit 2 + fi +fi + +# check maximum number of redirects +if [ -z "$max_redirs" ]; then + max_redirs=$max_redirs_default + curl_opts="$curl_opts -r $max_redirs" +fi + +if $verbose_mode; then + printf "$script_name using connect timeout: %d secs\n" $connect_timeout + printf "$script_name using max time: %d secs\n" $max_time + printf "$script_name using max redirects: %d\n" $max_redirs +fi + +config_file="$config_file_default" +$verbose_mode && echo "$script_name using config file $config_file" + +# determine the entityID +shift $(( OPTIND - 1 )) +if [ $# -ne 1 ]; then + echo "ERROR: $script_name: wrong number of arguments: $# (1 required)" >&2 + exit 2 +fi +if [ -z "$1" ] ; then + echo "ERROR: $script_name: empty string" >&2 + exit 2 +fi +entityID="$1" +$verbose_mode && echo "$script_name using entityID $entityID" + +##################################################################### +# Initialization +##################################################################### + +if $verbose_mode; then + printf "$script_name using source lib directory: %s\n" "$LIB_DIR" + for lib_filename in $LIB_FILENAMES; do + lib_file="$LIB_DIR/$lib_filename" + printf "$script_name sourcing lib file: %s\n" "$lib_file" + done +fi + +# determine temporary directory +if [ -n "$TMP_DIR" ] && [ -d "$TMP_DIR" ]; then + $verbose_mode && printf "$script_name using existing temporary dir: %s\n" "$TMP_DIR" + # use existing temporary directory (remove trailing slash) + tmp_dir="${TMP_DIR%%/}/probe_saml_idp_$$" +elif [ -n "$TMPDIR" ] && [ -d "$TMPDIR" ]; then + $verbose_mode && printf "$script_name using system temporary dir: %s\n" "$TMPDIR" + # use system temporary directory (remove trailing slash) + tmp_dir="${TMPDIR%%/}/probe_saml_idp_$$" +else + # create temporary directory + new_dir="$( make_temp_file -d )" + if [ ! -d "$new_dir" ] ; then + printf "ERROR: $script_name unable to create temporary dir\n" >&2 + exit 2 + fi + $verbose_mode && printf "$script_name using new temporary dir: %s\n" "$new_dir" + # use new temporary directory (remove trailing slash) + tmp_dir="${new_dir%%/}/probe_saml_idp_$$" +fi + +# every run of this script gets its own subdir +if [ -d "$tmp_dir" ]; then + echo "ERROR: $script_name: directory already exists: $tmp_dir" >&2 + exit 2 +fi + +# create temporary directory if necessary +$verbose_mode && printf "$script_name creating temporary subdir: %s\n" "$tmp_dir" +/bin/mkdir "$tmp_dir" +status_code=$? +if [ $status_code -ne 0 ]; then + echo "ERROR: $script_name failed to create tmp dir ($status_code) $tmp_dir" >&2 + exit 2 +fi + +# load config file +$verbose_mode && echo "$script_name loading config file $config_file" +load_config $local_opts "$config_file" +status_code=$? +if [ $status_code -ne 0 ]; then + echo "ERROR: $script_name failed to load $config_file" >&2 + exit 2 +fi + +# validate config parameters +if [ -z "$MDQ_BASE_URL" ]; then + echo "ERROR: $script_name requires config param MDQ_BASE_URL" >&2 + exit 2 +fi +if [ -z "$SAML2_SP_ENTITY_ID" ]; then + echo "ERROR: $script_name requires config param SAML2_SP_ENTITY_ID" >&2 + exit 2 +fi +if [ -z "$SAML2_SP_ACS_URL" ]; then + echo "ERROR: $script_name requires config param SAML2_SP_ACS_URL" >&2 + exit 2 +fi +if [ -z "$SAML2_SP_ACS_BINDING" ]; then + echo "ERROR: $script_name requires config param SAML2_SP_ACS_BINDING" >&2 + exit 2 +fi +if [ -z "$SAML1_SP_ENTITY_ID" ]; then + echo "ERROR: $script_name requires config param SAML1_SP_ENTITY_ID" >&2 + exit 2 +fi +if [ -z "$SAML1_SP_ACS_URL" ]; then + echo "ERROR: $script_name requires config param SAML1_SP_ACS_URL" >&2 + exit 2 +fi +if [ -z "$SAML1_SP_ACS_BINDING" ]; then + echo "ERROR: $script_name requires config param SAML1_SP_ACS_BINDING" >&2 + exit 2 +fi + +##################################################################### +# Main processing +##################################################################### + +# get entity metadata +entityDescriptor=$( getEntityFromServer -T "$tmp_dir" -u "$MDQ_BASE_URL" $entityID ) +exit_status=$? +if [ "$exit_status" -ne 0 ]; then + echo "ERROR: $script_name: unable to obtain metadata for entityID: $entityID" >&2 + exit 3 +fi + +# extract the registrar ID from the entity descriptor +registrarID=$( echo "$entityDescriptor" \ + | $_GREP -F -m 1 ' registrationAuthority=' \ + | $_SED -e 's/^.* registrationAuthority="\([^"]*\)".*$/\1/' +) + +# compute all SSO endpoints +endpoints=$( echo "$entityDescriptor" \ + | $_GREP -E '<(md:)?SingleSignOnService ' +) + +# iterate over the SAML2 browser-facing SSO endpoints +http_bindings="Redirect POST POST-SimpleSign" +for http_binding in $http_bindings; do + + # compute the SAML2 SSO endpoint + endpoint=$( echo "$endpoints" \ + | $_GREP -F -m 1 ' Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-'$http_binding'"' + ) + [ -z "$endpoint" ] && continue + + # compute the endpoint location and binding + location=$( echo "$endpoint" \ + | $_SED -e 's/^.* Location="\([^"]*\)".*$/\1/' + ) + binding=$( echo "$endpoint" \ + | $_SED -e 's/^.* Binding="\([^"]*\)".*$/\1/' + ) + $verbose_mode && printf "$script_name probing endpoint with Location=\"%s\" and Binding=\"%s\"\n" "$location" "$binding" + + # construct the SAML message + saml_message=$( construct_SAML2_AuthnRequest $location ) + exit_status=$? + if [ "$exit_status" -ne 0 ]; then + echo "ERROR: $script_name: construct_SAML2_AuthnRequest failed ($exit_status)" >&2 + exit 3 + fi + + # create temporary subdirectory if necessary + tmp_subdir="$tmp_dir/${http_binding}_SSO" + if [ ! -d "$tmp_subdir" ]; then + /bin/mkdir "$tmp_subdir" + exit_status=$? + if [ $exit_status -ne 0 ]; then + echo "ERROR: $script_name failed to create tmp dir ($exit_status) $tmp_subdir" >&2 + exit 2 + fi + fi + + # probe the endpoint + output=$( probe_saml2_idp_endpoint \ + -t $connect_timeout -m $max_time -r $max_redirs \ + -V "$tmp_subdir/curl_trace.txt" \ + -o "$tmp_subdir/idp_http_response.html" \ + -T "$tmp_subdir" \ + $location $binding "$saml_message" + ) + exit_status=$? + if [ "$exit_status" -ne 0 ]; then + echo "ERROR: $script_name: probe_saml2_idp_endpoint failed ($exit_status)" >&2 + exit 3 + fi + + echo "$output $entityID $registrarID" + +done + +$saml1_disabled && exit 0 + +# compute the Shibboleth SSO endpoint +endpoint=$( echo "$endpoints" \ + | $_GREP -F -m 1 ' Binding="urn:mace:shibboleth:1.0:profiles:AuthnRequest"' +) +if [ -z "$endpoint" ]; then + $verbose_mode && echo "$script_name: Shibboleth SSO endpoint not found" + exit 0 +fi + +# compute the endpoint location and binding +location=$( echo "$endpoint" \ + | $_SED -e 's/^.* Location="\([^"]*\)".*$/\1/' +) +binding=$( echo "$endpoint" \ + | $_SED -e 's/^.* Binding="\([^"]*\)".*$/\1/' +) +$verbose_mode && printf "$script_name probing endpoint with Location=\"%s\" and Binding=\"%s\"\n" "$location" "$binding" + +# create temporary subdirectory if necessary +tmp_subdir="$tmp_dir/Shibboleth_SSO" +if [ ! -d "$tmp_subdir" ]; then + /bin/mkdir "$tmp_subdir" + exit_status=$? + if [ $exit_status -ne 0 ]; then + echo "ERROR: $script_name failed to create tmp dir ($exit_status) $tmp_subdir" >&2 + exit 2 + fi +fi + +# probe the endpoint +output=$( probe_shibboleth_sso_endpoint \ + -t $connect_timeout -m $max_time -r $max_redirs \ + -V "$tmp_subdir/curl_trace.txt" \ + -o "$tmp_subdir/idp_http_response.html" \ + -T "$tmp_subdir" \ + $location $binding +) +exit_status=$? +if [ "$exit_status" -ne 0 ]; then + echo "ERROR: $script_name: probe_shibboleth_sso_endpoint failed ($exit_status)" >&2 + exit 3 +fi + +echo "$output $entityID $registrarID" + +exit 0 diff --git a/install.sh b/install.sh index 54079d2..e102745 100755 --- a/install.sh +++ b/install.sh @@ -94,6 +94,7 @@ while read script_file; do done <