Last active
February 25, 2021 04:20
-
-
Save irctrakz/5390285 to your computer and use it in GitHub Desktop.
Perl script to programmatically pull oAuth v2.0 data via SSO provider (PingIdentity) for Box.com
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#! /usr/bin/perl | |
# This prototype written to pull Box oAuth 2.0 authentication data via SSO Provider. | |
# | |
# If behind authenticating (NTLM) proxy there are dependencies: | |
# 1. Curl (/usr/bin/curl) to be available; NTLM to PingIdentity. | |
# 2. Transparent proxy available (cntlm/tsocks); NTLM via proxy (tested with TMG). | |
# | |
# There are some architectural assumptions: | |
# 1. Internet client -> Public forms based Auth -> PingIdentity -> Box | |
# 2. Intranet client -> NTLM Endpoint -> PingIdentity -> Box (All through proxy). | |
use strict; | |
use warnings; | |
use JSON; # Parse Box response data | |
use Socket; # Provide user data | |
use URI::Escape; # HTTP Escape | |
use Term::ReadKey; # Accept user input | |
use LWP::UserAgent; # Standard HTTP library | |
use HTTP::Request::Common; # Build HTTP request object | |
$|=1; | |
$ENV{'PERL_NET_HTTPS_SSL_SOCKET_CLASS'} = "Net::SSL"; | |
$ENV{'PERL_LWP_SSL_VERIFY_HOSTNAME'} = 0; | |
# Authentication information for PingIdentity (SSO Provider). | |
my $corp_name = q/username/; | |
my $corp_domain = q/DOMAIN/; | |
my $emailsuffix = q/@domain.com/; | |
# Box.com authentication info: | |
my $box_client_id = "12345678901234567890abcdefghijkl"; | |
my $box_client_secret = "12345678901234567890abcdefghijkl"; | |
# These are likely static box auth urls. | |
my $box_auth_url = "https://api.box.com/oauth2"; | |
my $box_sso_url = "https://sso.services.box.net/sp/ACS.saml2"; | |
# Company specific: Login form URL, external IP, and SSO SAML URL. | |
my %COMPANY_FORM = ( "company_login_url" => "https://login.domain.com", # External forms based auth. | |
"company_login_ip" => "10.1.2.3", # Internal v External (this should be internal IP). | |
"company_sso_url" => "https://sso.domain.com/idp/SSO.saml2" # See company_web function. | |
); | |
my $cookie_location = "/tmp/box_auth_cookies"; # This must be read/write | |
# | |
# Nothing below should require modification; unless the form's in use for end-user auth are modified substantially from PingIdentity templates. | |
# If this doesn't work, the company_web function can be modified to suit the specific use-case. | |
# | |
my $browser = LWP::UserAgent->new(keep_alive=>1,'cookie_jar' => {file => $cookie_location, autosave => 1, ignore_discard => 1}, requests_redirectable => []); | |
sub company_web { | |
$_[0]->content =~ /id=\"__VIEWSTATE\" value=\"(.*?)\" \/\>/;my $viewstate = $1; | |
$_[0]->content =~ /id=\"__EVENTVALIDATION\" value=\"(.*?)\" \/\>/;my $eventvalidation = $1; | |
my @SBVAL = split('\?', $_[1]); | |
my $web_form = HTTP::Request->new(POST => $COMPANY_FORM{'company_login_url'}."/LoginFormAuth.aspx?".$SBVAL[1]); | |
$web_form->header('Content-Type' => 'application/x-www-form-urlencoded'); | |
$web_form->content("__LASTFOCUS=&__EVENTTARGET=&__EVENTARGUMENT=&__VIEWSTATE=".uri_escape($viewstate). | |
"&__EVENTVALIDATION=".uri_escape($eventvalidation)."&ctl00%24LeftSection%24ddlDomain=".$corp_domain. | |
"&ctl00%24LeftSection%24txtUserName=".$corp_name."&ctl00%24LeftSection%24txtPassword=".uri_escape(getPassword())."&ctl00%24LeftSection%24btnLogin="); | |
my $web_response = $browser->request($web_form); | |
if ($web_response->status_line eq "302 Found"){return $web_response->headers->{'location'};}else{die "Web login failure: Bad password / incorrect field extract? (".$web_response->status_line.")\n";} | |
} | |
sub box_login { | |
my %BROWSER_PARAMS; | |
$BROWSER_PARAMS{'response_type'} = "code"; | |
$BROWSER_PARAMS{'state'} = "authenticated"; | |
my $get_response = $browser->get($box_auth_url."/authorize?response_type=".$BROWSER_PARAMS{"response_type"}."&client_id=".$box_client_id."&state=".$BROWSER_PARAMS{"state"}); | |
my $search_id = "Error"; | |
if ($get_response->content =~ /var request_token = \'(.*?)\';/){ | |
$BROWSER_PARAMS{"request_token"} = $1; | |
my $request = HTTP::Request->new(POST => $box_auth_url."/authorize?response_type=".$BROWSER_PARAMS{"response_type"}."&client_id=".$box_client_id."&state=".$BROWSER_PARAMS{"state"}); | |
$request->header('Content-Type' => 'application/x-www-form-urlencoded'); | |
$request->content("login=".uri_escape($corp_name.$emailsuffix)."&password=&_pw_sql=&remember_login=on&__login=1&dologin=1&client_id=".$box_client_id."&response_type=".uri_escape($BROWSER_PARAMS{"response_type"}). | |
"&scope=%5B%22root_readwrite%22%5D&state=authenticated®_step=&submit1=1&folder=&skip_framework_login=1&login_or_register_mode=login&new_login_or_register_mode=&request_token=".$BROWSER_PARAMS{"request_token"}); | |
my $post_response = $browser->request($request); | |
if ($post_response->status_line eq "302 Found"){ | |
$BROWSER_PARAMS{'location'} = $post_response->headers->{'location'}; | |
return \%BROWSER_PARAMS; | |
}else{ | |
die $post_response->status_line." : Box Login Error (invalid username / box id / secret?)\n"; | |
} | |
}; | |
die $get_response->status_line."\n\nCONNECTION FAIL: Are you perhaps behind an authenticating proxy?\nCheck out: http://cntlm.sourceforge.net/ and http://tsocks.sourceforge.net/\n\n"; | |
} | |
sub sso_auth { | |
my %AUTH_DATA; | |
my $request = HTTP::Request->new(POST => $_[0]); | |
$request->header('Content-Type' => 'application/x-www-form-urlencoded'); | |
my $response = $browser->request($request); | |
if (!($response->is_success)){print $response->status_line."\n".$response->content;return $response->status_line;}else{ | |
$response->content =~ /name=\"SAMLRequest\" value=\"(.*?)\"\/>/;my $saml_request = $1; | |
$response->content =~ /name=\"RelayState\" value=\"(.*?)\"\/>/;my $relay_state = $1; | |
if ($saml_request){ | |
$AUTH_DATA{'relaystate'} = $relay_state; | |
$response = $browser->post($COMPANY_FORM{'company_sso_url'}, {'SAMLRequest' => $saml_request, 'RelayState' => $relay_state}); | |
$AUTH_DATA{'location'} = $response->headers->{'location'}; | |
if ($response->status_line eq "302 Found"){return \%AUTH_DATA;} | |
die $response->status_line."\n".$response->content."\nERROR\n"; | |
}else{ | |
die "SAMLRequest ERRR\n"; | |
} | |
} | |
} | |
sub getPassword { | |
print "Password for ".$corp_domain."\\".$corp_name.": "; | |
ReadMode 'cbreak'; | |
my $com_pwrd = ReadLine(0); | |
ReadMode 'normal'; | |
print "\r\e[K"; # Clear the terminal line. | |
$com_pwrd =~ s/\s+$//; | |
return $com_pwrd; | |
} | |
sub do_auth { | |
if($_[1] == 1){ | |
# This is an ugly hack using Curl around Perl's poor LWP NTLM support against PingIdentity. | |
#print $_[0]."\n"; #Debug: View PingIdentity auth URL | |
my $com_fqname = $corp_domain."\\".$corp_name.":".getPassword(); | |
if (qx/curl -silent -kbj --ntlm --user "$com_fqname" --url "$_[0]"/ =~ /Object moved to <a href=\"(.*?)\">here<\/a>/){return $1;}else{die "Curl error: Bad password?\n";} | |
}else{ | |
my $inter_response = $browser->request(HTTP::Request->new(GET => $_[0])); | |
if($inter_response->is_success){ | |
return company_web($inter_response, $_[0]); | |
}else{ | |
if ($inter_response->status_line eq "302 Found"){ | |
if($inter_response->headers->{'location'} =~ /^https/){return $inter_response->headers->{'location'};}else{ | |
return company_web($browser->request(HTTP::Request->new(GET => $COMPANY_FORM{'company_login_url'}.$inter_response->headers->{'location'})), $COMPANY_FORM{'company_login_url'}.$inter_response->headers->{'location'}); | |
} | |
}else{ | |
die "Web form login error (".$inter_response->status_line.")\n"; | |
} | |
} | |
} | |
} | |
sub get_keys { | |
$browser->default_header('WWW-Authenticate' => ''); | |
my $response = $browser->request(HTTP::Request->new(GET => $_[0])); | |
if ($response->is_success){ | |
if ($response->content =~ /name=\"SAMLResponse\" value=\"(.*?)\"\/>/){ | |
$response = $browser->post($box_sso_url, { 'SAMLResponse' => $1, 'RelayState' => $_[1] }); | |
if ($response->is_success){ | |
my @AUTH_DATA; | |
$response->content =~ /method=\"post\" action=\"(.*?)\">/;$AUTH_DATA[0] = $1; | |
$response->content =~ /type=\"hidden\" name=\"opentoken\" value=\"(.*?)\"\/>/;my $post_opentoken = $1; | |
my $redir_request = HTTP::Request->new(POST => $AUTH_DATA[0]); | |
$redir_request->header('Content-Type' => 'application/x-www-form-urlencoded'); | |
$redir_request->content("opentoken=".$post_opentoken); | |
my $box_auth_response = $browser->request($redir_request); | |
if ($box_auth_response->content =~ /name=\"ic\" value=\"(.*?)\" \/>/){ | |
$AUTH_DATA[1] = $1; | |
return \@AUTH_DATA; | |
}else{ | |
die "Error in response: no ic field\n"; | |
} | |
}else{ | |
die $response->status."\nRedirection Error. Check SSO/Box integration\n\n."; #https://support.box.com/requests/231990 | |
} | |
} | |
}else{ | |
print "Response: ".$response->status_line; | |
print "\nContent: ".$response->content."\n"; | |
if ($response->status_line eq "302 Found"){print "SAML 302?\n";return;} | |
} | |
} | |
sub json_authToken { | |
my $submit_data = $_[0]; | |
my $request = HTTP::Request->new(POST => @$submit_data[0]); | |
$request->header('Content-Type' => 'application/x-www-form-urlencoded'); | |
$request->content("client_id=".$box_client_id."&response_type=".uri_escape($_[1]->{'response_type'})."&scope=root_readwrite&state=authenticated&doconsent=doconsent&ic=". | |
@$submit_data[1]."&consent_accept=Accept&request_token=".$_[1]->{'request_token'}); | |
my $response = $browser->request($request); | |
if ($response->status_line eq "302 Found"){ | |
$response->headers->{'location'} =~ /code=(.*?)$/; | |
#print "Auth code: ".$1."\n"; | |
my $json_request = HTTP::Request->new(POST => $box_auth_url."/token"); | |
$json_request->header('Content-Type' => 'application/x-www-form-urlencoded'); | |
$json_request->content("grant_type=".uri_escape("authorization_code")."&code=".$1."&client_id=".$box_client_id."&client_secret=".$box_client_secret); | |
my $box_json_response = $browser->request($json_request); | |
if ($box_json_response->is_success){return from_json($box_json_response->content);}else{die $box_json_response->status_line."\n".$box_json_response->content."\n\nJSON pull error\n";} | |
}else{ | |
die $response->content."\nApproval error\n"; | |
} | |
return; | |
} | |
sub isInternal{ | |
if($COMPANY_FORM{'company_login_url'} =~ /^https:\/\/(.*?)$/){ | |
my $hostip = gethostbyname($1); | |
if (inet_ntoa($hostip) =~ /^$COMPANY_FORM{'company_login_ip'}$/){return 0;}else{return 1;} | |
} | |
} | |
print "\nBox Authentication Data:\n"; | |
print "App ID:\t\t".$box_client_id."\n"; | |
print "App Secret:\t".$box_client_secret."\n"; | |
my $box_auth_data = box_login(); | |
my $sso_auth_data = sso_auth($box_auth_data->{'location'}); | |
my $company_auth = do_auth($sso_auth_data->{'location'}, isInternal()); | |
my $approve_app = get_keys($company_auth, $sso_auth_data->{'relaystate'}); | |
my $tokens = json_authToken($approve_app, $box_auth_data); | |
print "Token Expires:\t".(time()+scalar($tokens->{'expires_in'}))."\n"; | |
print "Access Token:\t".$tokens->{'access_token'}."\n"; | |
print "Refresh Token:\t".$tokens->{'refresh_token'}."\n\n"; | |
#Cleanup cookie auth data | |
$browser = "";unlink $cookie_location; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment