Skip to content

Instantly share code, notes, and snippets.

@irctrakz
Last active February 25, 2021 04:20
Show Gist options
  • Save irctrakz/5390285 to your computer and use it in GitHub Desktop.
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
#! /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&reg_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