Commit e73d4d14 authored by marcus-tun's avatar marcus-tun
Browse files

an initial verision

parent eec8dab2
#!/usr/bin/env perl
use constant {
CREDENTIAL_CREATION_URL => 'https://saml-delegation.data.kit.edu/sd/p.php',
CREDENTIAL_UPLOAD_URL => 'https://saml-delegation.data.kit.edu/sd/upload.py',
DEFAULT_IDP_URL => 'https://idptest.scc.kit.edu/idp/profile/SAML2/SOAP/ECP',
HEADER_ACCEPT=>'text/html; application/vnd.paos+xml' ,
HEADER_PAOS =>'ver="urn:liberty:paos:2003-08";"urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp"' ,
CONFIG_FILE => $ENV{'HOME'}.'/.pluto/config',
COOKIE_FILE => $ENV{'HOME'}.'/.pluto/cookies',
VERSION => '0.1',
IDPFILE => 'file://'.$ENV{'HOME'}.'/.pluto/idplist.txt',
};
######################
# BEGIN MAIN PROGRAM #
######################
use strict;
use Getopt::Long qw(:config bundling);
use Pod::Usage;
use LWP;
use HTTP::Cookies;
use Term::ReadKey;
use Term::ReadLine;
use MIME::Base64 qw( encode_base64 );
# Just for debugging:
use Data::Dumper;
# Handle <Ctrl>+C to reset the terminal to non-bold text
$SIG{INT} = \&resetTerm;
# Declare variables for command line options
my %opts = ();
my %idps = ();
my $verbose = 0;
my $quiet = 0;
my $response='';
my $response_content='';
my $headers='';
# DEFAULTS
my $idpname = 'Test KIT IdP';
my $idplisturl = 'http://saml-delegation.data.kit.edu/idplist.txt';
#my $idpurl = 'https://idptest.scc.kit.edu/idp/profile/SAML2/SOAP/ECP';
my $idpurl = '';
my $idpuser = '';
my $idppass = '';
my $samlfile = '/tmp/samlup_u'.getpwnam($ENV{'USER'});
my $urlfile = '/tmp/samlurl_u'.getpwnam($ENV{'USER'});
my $urlencfile = '/tmp/samlurlenc_u'.getpwnam($ENV{'USER'});
my $url_to_assertion;
print ("\n");
# Create config dir, if it does not exist
ensure_path_exists(CONFIG_FILE);
# Get options
GetOptions(\%opts, 'help|h|?',
'verbose|debug|v|d',
'version|V',
'quiet|q',
'updateidps|U',
'updateidpsFrom|F=s',
'listidps|l',
'idpname|i=s',
'idpurl|I=s',
'idpuser|u=s',
'idppass|p=s',
'samlfile|s=s',
'urlfile|o=s',
'urlencfile|e=s',
) or print ("\n\n") && pod2usage(-verbose=>99) && exit;;
# Parse options
# If the user asked for help, print it and then exit.
if (exists $opts{help}) {
#pod2usage(-verbose=>2) && exit;
pod2usage(-verbose=>99) && exit;
}
# verbose
if (exists $opts{verbose}) {
$verbose = 1;
$quiet = 0;
}
# version
if (exists $opts{version}) {
print "saml-init version '" . main->VERSION . "'\n";
exit;
}
# quiet
if (exists $opts{quiet}) {
$quiet = 1;
}
# Alternate update URL
if (exists $opts{updateidpsFrom}) {
$idplisturl = trim($opts{updateidpsFrom});
}
# Update
if (exists $opts{updateidps}) {
%idps = fetchIdps($idplisturl);
if (!keys %idps) { # MAJOR ERROR! No ECP IdPs fetched!
warn "Error: Unable to download the list of IdPs from '".$idplisturl."'\n" if
(!$quiet);
exit 1;
}
saveIDPfile(IDPFILE);
print "List of IdPs updated from ".$idplisturl." and written to ".IDPFILE."\n";
exit;
}
# load the list of IdPs to list them now or search them later.
%idps = fetchIdps(IDPFILE);
if (!keys %idps) { # MAJOR ERROR! No ECP IdPs fetched!
warn "Error: Unable to find local copy of the list of IdPs." if
(!$quiet);
exit 1;
}
# list
if (exists $opts{listidps}) {
foreach my $key (sort keys %idps) {
print "\e[1m$key\e[0m: \t$idps{$key}\n";
}
exit;
}
# idpname
if (exists $opts{idpname}) {
$idpname = trim($opts{idpname});
}
# idpurl
if (exists $opts{idpurl}) {
$idpurl = trim($opts{idpurl});
}
# idpuser
if (exists $opts{idpuser}) {
$idpuser = trim($opts{idpuser});
}
# idppass
if (exists $opts{idppass}) {
$idppass = trim($opts{idppass});
}
# saml-file
if (exists $opts{samlfile}) {
$samlfile = trim($opts{samlfile});
}
# url-file
if (exists $opts{urlfile}) {
$urlfile = trim($opts{urlfile});
}
# urlenc-file
if (exists $opts{urlencfile}) {
$urlencfile = trim($opts{urlencfile});
}
############################
# <\/><\/> #
# </\> </\> #
# <\/> <\/> #
# </\> </\> #
# <\/> Sanity checks <\/> #
# </\> </\> #
# </\> </\> #
# </\><\/><\/></\> #
# </\></\> #
############################
if (!keys %idps) { # MAJOR ERROR! No ECP IdPs fetched!
warn "Error: Unable to load the list of IdPs." if
(!$quiet);
exit 1;
}
if ($idpurl eq '') {
if ($idpname ne '') {
$idpurl = $idps{$idpname};
#print "assigning idpurl by name: ".$idpurl."\n";
}
if ($idpurl eq '') {
warn("Error: No valid IdP URL specified\n");
exit 1;
}
}
if ($idpuser eq '') {# This will be checked only when no valid session was found
}
if ($idppass eq '') {# This will be checked only when no valid session was found
}
##############################
# Prepare the http session #
##############################
my $ua = LWP::UserAgent->new();
my $cookie_jar = HTTP::Cookies->new(file => COOKIE_FILE,
autosave => 1,
ignore_discard => 1);
$ua->cookie_jar($cookie_jar);
$headers = HTTP::Headers->new();
$response = $ua->get(CREDENTIAL_CREATION_URL);
if ($response->is_success) {
$response_content = $response->decoded_content;
# If we were redirected, we did not obtain our assertion and have to
# retry
if (exists($response->{'_previous'})) {
print "No existing session found => new login at ".$idpname.":\n";
# Prepare the session for login
if ($idpuser eq '') {
my $term = Term::ReadLine->new('readline');
print 'username: ';
$idpuser = <>;
$idpuser = trim($idpuser);
if (length($idpuser) == 0) {
warn "Error: IdP username cannot be empty." if (!$quiet);
}
}
if (length($idppass) == 0) {
# Prompt for the IdP password
print ('password: ');
$idppass = readpassword();
print ("\n");
}
create_valid_session_via_idp($idpurl, $idpuser, $idppass, $ua);
# Reset the cookie to prevent CSRF
my $uri = URI->new(CREDENTIAL_CREATION_URL);
my $randstr = join('',map { ('a'..'z', 0..9)[rand 36] } (1..10));
$cookie_jar->set_cookie(1,'CSRF',$randstr,'/',$uri->host,$uri->port,1,1);
# Retry to access SP
$response = $ua->get(CREDENTIAL_CREATION_URL);
if ($response->is_success) {
$response_content = $response->decoded_content;
# If we were redirected, we did not obtain our assertion and have to
# retry
if (exists($response->{'_previous'})) {
print "Your login did not work\n";
print "You need to log in first\n";
exit 1;
}
}
}
open (FILE, ">", $samlfile) or die $!;
print FILE $response_content;
close (FILE);
print "assertion created successfully\n";
print "\n##### BEGIN SP RESPONSE #####\n";
print "$response_content \n";
print "##### END SP RESPONSE #####\n\n";
} else {
print "Failed! Error code: " . $response->status_line . "\n";
exit 1;
}
exit 0;
##########################################################################
# ###################################################################### #
# # End of Main program # #
# ###################################################################### #
##########################################################################
#########################################################
# subroutine: readpassword read a password from stdin #
# returns: plaintext password string #
#########################################################
sub readpassword {
my $key='';
my $password='';
ReadMode(4); #Disable the control keys
while(ord($key = ReadKey(0)) != 10)
# This will continue until the Enter key is pressed (decimal value of 10)
{
# For all value of ord($key) see http://www.asciitable.com/
if(ord($key) == 127 || ord($key) == 8) {
# DEL/Backspace was pressed
#1. Remove the last char from the password
chop($password);
#2. move the cursor back by one, print a blank character, move the cursor back by one
print "\b \b";
} elsif(ord($key) < 32) {
# Do nothing with these control characters
} else {
$password = $password.$key;
print ("*");
}
}
ReadMode(0); #Reset the terminal once we are done
return $password;
}
########################################################
# subroutine: Create the directory for the config file #
# Parameters: Full path of the file #
########################################################
sub ensure_path_exists {
use File::Basename qw( fileparse );
use File::Path qw( make_path );
use File::Spec;
my $configfile = shift;
my ( $logfile, $dir ) = fileparse $configfile;
if ( !$logfile ) {
$logfile = 'config';
my $full_path = File::Spec->catfile( $configfile, $logfile );
}
if ( !-d $dir ) {
make_path $dir or die "Failed to create path: $dir";
}
}
#########################################################################
# Subroutine: fetchIdps() #
# Parameters: URL at which the idps can be found
# Returns : A hash of IdPs in the form $idps{'idpname'} = 'idpurl' #
# This subroutine fetches the list of Identity Providers from the #
# CILogon server, using the ECP_IDPS_URL defined at the top of this #
# file. It returns a hash where the keys are the "pretty print" names #
# of the IdPs, and the values are the actual URLs of the IdPs. #
#########################################################################
sub fetchIdps
{
my $ecp_idps_url=shift;
my %idps = ();
my $content;
my $ua = LWP::UserAgent->new();
my $response = $ua->get($ecp_idps_url);
if ($response->is_success) {
$content = $response->decoded_content;
} else {
warn $response->status_line;
}
if (defined($content)) {
foreach my $line (split("\n",$content)) {
chomp($line);
my($idpurl,$idpname) = split('\s+',$line,2);
$idps{$idpname} = $idpurl;
}
}
return %idps;
}
sub saveIDPfile {
my $fileURL = shift;
(undef, my $filename) = split ('file://', $fileURL);
open (FILE, ">", $filename) or die $!;
foreach my $key (sort keys %idps) {
print FILE "$idps{$key} $key\n";
}
close(FILE);
}
#########################################################################
# subroutine: trim($str) #
# parameter : $str - a string to trim spaces from. #
# returns : the passed-in string with leading and trailing spaces #
# removed. #
# this subroutine removes leading and trailing spaces from the #
# passed-in string. note that the original string is not modified. #
# rather, a new string without leading/trailing spaces is returned. #
#########################################################################
sub trim
{
my $str = shift;
$str =~ s/^\s+//;
$str =~ s/\s+$//;
return $str;
}
########################################################################
# subroutine: create_valid_session_via_idp #
# returns: Nothing, but creates a valid session with the SP, stored #
# in cooke, stored within ua #
# Parameters: idpurl, idpusername, idppassword, ua session #
########################################################################
sub create_valid_session_via_idp {
my $idpurl=shift;
my $idpuser=shift;
my $idppass=shift;
my $ua=shift;
my $relaystate='';
my $responseConsumerURL='';
my $idpresp='';
my $assertionConsumerServiceURL='';
my $response_content='';
# Prepare the browser session:
$headers = HTTP::Headers->new();
$headers->header(Accept => HEADER_ACCEPT ,
PAOS => HEADER_PAOS
);
$ua->default_headers($headers);
$response = $ua->get(CREDENTIAL_CREATION_URL);
if ($response->is_success) {
$response_content = $response->decoded_content;
# Get <ecp:RelayState> element from the SP's SOAP response
($response_content =~ m#(<ecp:RelayState.*</ecp:RelayState>)#i) && ($relaystate = $1);
if (!$relaystate) {
warn "Error: No <ecp:RelayState> block in response from 'CREDENTIAL_CREATION_URL'." if
(!$quiet);
exit 1;
}
# Extract the xmlns:S from the S:Envelope and put in <ecp:RelayState> block
my $xmlns = '';
($response_content =~ m#<S:Envelope (xmlns:[^>]*)>#) && ($xmlns = $1);
$relaystate =~ s#(xmlns:ecp=[^ ]*)#$1 $xmlns#;
# Get the responseConsumerURL
($response_content=~m#responseConsumerURL=\"([^\"]*)\"#i) && ($responseConsumerURL=$1);
if (!$responseConsumerURL) {
warn "Error: No responseConsumerURL in response from 'CREDENTIAL_CREATION_URL'." if
(!$quiet);
exit 1;
}
# Remove the SOAP Header from the SP's response, use the SOAP Body later
if (!($response_content =~ s#<S:Header>.*</S:Header>##i)) {
warn "Error: No SOAP Header in response from 'CREDENTIAL_CREATION_URL'." if (!$quiet);
exit 1;
}
# Attempt to log in to the IdP with basic authorization
$headers = HTTP::Headers->new();
$headers->authorization_basic($idpuser,$idppass);
$ua->default_headers($headers);
if ($verbose) {
print "\n\nLogging in to IdP '$idpurl' with \n$response_content\n... " ;
}
# send a post to IdP
$response = $ua->post($idpurl,Content=>$response_content);
if ($response->is_success) {
$idpresp = $response->decoded_content;
if ($verbose) {
print "Succeeded!\n";
print "\n\n##### BEGIN IDP RESPONSE #####\n";
print "$idpresp\n";
print "##### END IDP RESPONSE #####\n";
}
} else {
print "Failed! Error code: " . $response->status_line . "\n" if ($verbose);
warn "Error: Unable to log in to IdP '$idpurl'" if (!$quiet);
exit 1;
}
# Find the AssertionConsumerServiceURL from the IdP's response
($idpresp=~m#AssertionConsumerServiceURL=\"([^\"]*)\"#i) &&
($assertionConsumerServiceURL=$1);
if (!$assertionConsumerServiceURL) {
warn "Error: No AssertionConsumerServiceURL in response from '$idpurl'." if
(!$quiet);
exit 1;
}
# Make sure responseConsumerURL and assertionConsumerServiceURL are equal.
# If not, send SOAP fault to the SP and exit.
$headers = HTTP::Headers->new();
$ua->default_headers($headers);
if ($responseConsumerURL ne $assertionConsumerServiceURL) {
warn "Error: responseConsumerURL and assertionConsumerService URL " .
"are not equal.\n" .
"responseConsumerURL = '$responseConsumerURL'\n" .
"assertionConsumerServiceURL = '$assertionConsumerServiceURL'\n" .
"Sending SOAP fault to the Service Provider.\n";
my $soapfault = '<S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/"><S:Body><S:Fault><faultcode>S:Server</faultcode><faultstring>responseConsumerURL from SP and assertionConsumerServiceURL from IdP do not match</faultstring></S:Fault></S:Body></S:Envelope>';
$response = $ua->post($responseConsumerURL,
Content_Type => 'application/vnd.paos+xml',
Content => $soapfault
);
# No need to check for response since we are quitting anyway.
exit 1;
}
# Take the response from the IdP, but replace the <ecp:Response> SOAP header
# with the <ecp:RelayState> SOAP header found earlier. Then send this new
# message to the SP's assertionConsumerServiceURL.
if (!($idpresp =~ s#(<soap11:Header>).*(</soap11:Header>)#$1$relaystate$2#i)) {
warn "Error: Could not find <ecp:Response> SOAP header in the " .
"IdP response." if (!$quiet);
exit 1;
}
print "\n\nContacting '$assertionConsumerServiceURL' with \$idpresp ... " if ($verbose);
$response = $ua->post($assertionConsumerServiceURL,
Content_Type => 'application/vnd.paos+xml',
Content => $idpresp
);
$cookie_jar->save();
print "Done! Cookie created\n" if ($verbose);
# No need to check for response. We only want the (shibboleth) cookie.
}
}
__END__
=head1 NAME
saml-init - Download a SAML assertion via ECP (Enhanced Client Protocol)
=head1 SYNOPSIS
saml-init [options]
=head1 DESCRIPTION
B<This program> enables fetching of a SAML assertion from a
SAML-Delegation SP (e.g. https://saml-delegation.data.kit.edu), as well
as a shortened URL at which the encrypted assertion can be downloaded.
This tool makes use of the SAML ECP (Enhanced Client or Proxy) profile.
This service is also available via the Web-SSO protocol, if you point your
web-browser there.
You need to specify the IdP against which you want to authenticate, as
well as username and password. Please note, that passwords are directly
forwared to the IdP and not stored by the client.
The list of ECP-enabled Identity Providers (IdPs) is maintained on LSDMA
and Cilogon servers. You can use your own IdP by specifying it, either via
command line option or when prompted during interactive program execution.
If you would like to add your IdP to the list maintained on the CILogon
servers, please send email to L<dsit-tools@lists.scc.kit.edu>.
=head1 OPTIONS
=over 8
=item B<-h, --help>
=item B<-v, -d, --verbose, --debug>
=item B<-V, --version>
=item B<-q, --quiet>
=item B<-U, --updateidps>
=item B<-U, --updateidpsFrom>
=item B<-l, --listidps>
=item B<-i, --idpname> <Name of IdP to use>
=item B<-I, --idpurl> <URL of IdP to use>
=item B<-u, --idpuser> <Your username at chosen IdP>
=item B<-s, --samlfile> <name> Where to store your saml-assertion
=item B<-o, --urlencfile> <name> Where to store your encrypted saml-assertion-url
=back
Print out the help message and exit.
This diff is collapsed.
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<html>
<head>
<title>SAML Delegation </title>
</head>
<body>
<h1>Available Endpoints</h1><br/>
<a href="https://saml-delegation.data.kit.edu/sd/ecp.py">ECP</a><br/>
<a href="https://saml-delegation.data.kit.edu/sd/sso.py">Web-SSO</a><br/>
<a href="https://saml-delegation.data.kit.edu/sd/js.py">Web-SSO and Javascript</a><br/>
<a href="https://saml-delegation.data.kit.edu/sd/p.php">PHP Sysinfo</a><br/>
</body></html>
function httpGet(theUrl) { var xmlHttp = null;
xmlHttp = new XMLHttpRequest();
xmlHttp.open( "GET", theUrl, false );
xmlHttp.send( null );
return xmlHttp.responseText;
}
function httpPost(theUrl, content) { var xmlHttp = null;
xmlHttp = new XMLHttpRequest();
xmlHttp.open( "POST", theUrl, false );
xmlHttp.send( content );
return xmlHttp.responseText;
}
function post(path, params, method) {
method = method || "post"; // Set method to post by default if not specified.
// The rest of this code assumes you are not using a library.
// It can be made less wordy if you use one.
var form = document.createElement("form");
form.setAttribute("method", method);
form.setAttribute("action", path);
for(var key in params) {
if(params.hasOwnProperty(key)) {
var hiddenField = document.createElement("input");