connect to google spreadsheet - perl

i have a web app created using perl, and i have a table on a spreadsheet document, i want to connect my app to the spreadsheet, i tried the documentation but i can't get the token
here is what i did:
sub authenticate {
my $oauth2 = Net::Google::DataAPI::Auth::OAuth2->new(
client_id => 'my client id',
client_secret => 'my secret code',
scope => ['http://spreadsheets.google.com/feeds/'],
);
my $url = $oauth2->authorize_url(access_type => 'offline', approval_prompt => 'force');
use Data::Dumper;
Core::Global::Logger::debug(Dumper($url));
#you will need to put code here and receive token
print "OAuth URL, get code: \n$url\n";
use Term::Prompt;
my $code = prompt('x', 'my code', '', '');
my $token = $oauth2->get_access_token($code) or die;
#save token for future use
my $session = $token->session_freeze;
store( $session, 'google_spreadsheet.session' );
}

I was not able to make Net::Google::Spreadsheets::V4 work with a service account and using Net::Google::DataAPI::Auth::OAuth2 to get the access token. But you should be able to get the access token from WWW::Google::Cloud::Auth::ServiceAccount instead, and then use that token to authorize requests to the google drive rest api. The following worked for me:
use feature qw(say);
use strict;
use warnings;
use WWW::Google::Cloud::Auth::ServiceAccount;
use LWP::UserAgent;
use URI;
use HTTP::Request;
use Mojolicious::Lite;
use Data::Dumper;
use JSON;
use experimental qw(declared_refs refaliasing signatures);
{
my #scopes = ('https://www.googleapis.com/auth/spreadsheets',
'https://www.googleapis.com/auth/drive');
my $auth = WWW::Google::Cloud::Auth::ServiceAccount->new(
credentials_path => 'credentials.json',
scope => join ' ', #scopes
);
get '/' => sub {
my $c = shift;
my $token = $auth->get_token();
my ($return_code, $files) = get_all_spreadsheets($token);
$c->render(template => 'index', return_code => $return_code);
};
app->start;
}
sub get_all_spreadsheets($token) {
my $url = 'https://www.googleapis.com/drive/v3/files';
my $query = 'mimeType="application/vnd.google-apps.spreadsheet"';
my $params = {
"q" => $query,
"pageSize" => 1000,
"supportsAllDrives" => 1,
"includeItemsFromAllDrives" => 1,
"fields" => "kind,nextPageToken,"
. "files(id,name,createdTime,modifiedTime)",
};
my $more_pages = 1;
my $page_token = '';
my $status_line;
my #files;
while ($more_pages) {
$params->{pageToken} = $page_token if $page_token;
my $result = send_google_drive_get_request($url, $params, $token);
$status_line = $result->status_line;
if (!$result->is_success) {
return $status_line;
}
my $hash = decode_json($result->content);
push #files, $hash->{files};
if (exists $hash->{nextPageToken}) {
$page_token = $hash->{nextPageToken};
}
else {
$more_pages = 0;
}
}
return $status_line, \#files;
}
sub send_google_drive_get_request( $url, $params, $token ) {
my $uri = URI->new( $url );
$uri->query_form($params);
my $str = $uri->as_string();
my #headers = get_headers($token);
my $req = HTTP::Request->new(
'GET',
$uri->as_string(),
\#headers,
);
my $ua = LWP::UserAgent->new();
my $res = $ua->request($req);
return $res;
}
sub get_headers($token) {
return 'Accept-Encoding' => 'gzip, deflate',
'Accept' => '*/*',
'Connection' => 'keep-alive',
"Authorization" => "Bearer $token";
}
__DATA__
## index.html.ep
<!DOCTYPE html>
<html>
<head><title>Testing access to google sheets...</title></head>
<body>
<h1>Return code = <%= $return_code %></h1>
</body>
</html>
Edit:
To get the value of a cell for a given sheet with a given id, you can use the following url: https://sheets.googleapis.com/v4/spreadsheets/%s/values/%s where the first %s is replaced by the id of the sheet and the second %s represents the cell range to extract. Here is an example:
use feature qw(say);
use strict;
use warnings;
use WWW::Google::Cloud::Auth::ServiceAccount;
use LWP::UserAgent;
use URI;
use URI::Encode;
use HTTP::Request;
use Mojolicious::Lite;
use Data::Dumper;
use JSON;
use experimental qw(declared_refs refaliasing signatures);
{
my #scopes = ('https://www.googleapis.com/auth/spreadsheets',
'https://www.googleapis.com/auth/drive');
my $auth = WWW::Google::Cloud::Auth::ServiceAccount->new(
credentials_path => 'credentials.json',
scope => join " ", #scopes
);
get '/' => sub {
my $c = shift;
my $token = $auth->get_token();
my $sheet_id = '1FPyDuIPPzwUeLNpLbdI-RzfouKcm-2duOJ9Jio-Z-Qw';
my $sheet_cell = 'B1';
my ($return_code, $cell_value) =
read_spreadsheet_cell($token, $sheet_id, $sheet_cell);
app->log->debug(app->dumper( { cell_value => $cell_value } ));
$c->render(template => 'index', return_code => $return_code);
};
app->start;
}
sub read_spreadsheet_cell($token, $id, $cell) {
my $encoder = URI::Encode->new();
my $value_range = $encoder->encode(sprintf "'Sheet1'!%s", $cell);
my $url = sprintf 'https://sheets.googleapis.com/v4/spreadsheets/%s/values/%s',
$id, $value_range;
my $result = send_google_drive_get_request($url, $token);
my $status_line = $result->status_line;
if (!$result->is_success) {
return $status_line;
}
my $result_hash = decode_json( $result->content );
#app->log->debug(app->dumper( $result_hash ));
return $status_line, $result_hash->{values}[0][0];
}
sub send_google_drive_get_request( $url, $token ) {
my #headers = get_headers($token);
my $req = HTTP::Request->new('GET', $url, \#headers);
my $ua = LWP::UserAgent->new();
my $res = $ua->request($req);
return $res;
}
sub get_headers($token) {
return
'User-Agent' => 'Mozilla/8.0',
'Accept-Encoding' => 'gzip, deflate',
'Accept' => '*/*',
'Connection' => 'keep-alive',
"Authorization" => "Bearer $token";
}
__DATA__
## index.html.ep
<!DOCTYPE html>
<html>
<head><title>Testing access to google sheets...</title></head>
<body>
<h1>Return code = <%= $return_code %></h1>
</body>
</html>

Related

How do I get refresh tokens with Mojolicious::Plugin::OAuth2

I am a happy user of Mojolicious::Plugin::OAuth2, but there is a but: I can get an access token without a problem, but I have no idea on how to get a refresh one. The documentation is a bit terse and I could not find examples in the wild.
Currently I do this:
plugin OAuth2 => {
providers => {
google => {
key => 'somekey',
secret => 'somesecret',
redirect => 'http://localhost:3000/login/google',
access_type => 'offline',
scope => join ' ', qw|some scopes|,
}
}
};
get '/' => sub {
my $c = shift;
$c->render(template => 'login');
};
get '/done' => sub {
my $c = shift;
$c->render(text => 'done: ' . $c->session('token'));
};
get '/login/google' => sub {
my $c = shift;
my $otx = $c->render_later->tx;
my $args = { redirect_uri => 'http://localhost:3000/login/google' };
$c->oauth2->get_token_p(google => $args)
->then(sub {
my $otx = $otx;
return unless my $res = shift;
$c->session(token => $res->{access_token});
1;
})
->then(sub {
my $tx = shift;
my $ua = $c->app->ua;
my $url = 'https://www.googleapis.com/userinfo/v2/me';
my $tx = $ua->build_tx(GET => $url);
$tx->req->headers->authorization('Bearer ' . $c->session('token'));
return $ua->start_p($tx);
})
->then(sub {
my $tx = shift;
my $otx = $otx;
my $data = $tx->res->json;
$c->app->log->info($tx->res->body);
$c->app->log->info(dumper $tx->res->json);
$c->redirect_to('/done');
})
->catch(sub {
my $err = shift;
$c->log->info($err);
$c->render(text => $err);
});
};
(sorry for the dump) which is pretty much the standard flow for Mojolicious::Plugin::OAuth2.
The response from Google however does not contain any refresh token as far as I can see, nor can I figure out how to ask for one - inserting $c->oauth2->get_refresh_token_p($provider_name => \%args); somewhere in the middle gives me a bad request response.
So, how should I do this so it works ok?
If you set access_type => 'offline' when creating the OAuth2 plugin instance (as you did in your example), get_access_token_p() will return the refresh token (in addition to the access_token) as explained here. You should store the refresh token at a safe place. Then you can use it at a later time to refresh the access token (for example, if an api call returns an access token expired error). In that case you can call get_refresh_token_p() with the refresh token that you have already stored as a parameter.
Here, is an example:
use feature qw(say);
use strict;
use warnings;
use experimental qw(declared_refs refaliasing signatures);
use JSON;
use Mojolicious::Lite -signatures;
{
my #scopes = ('https://www.googleapis.com/auth/userinfo.email');
my $cred = read_credentials('credentials.json');
plugin OAuth2 => {
providers => {
google => {
# "offline": instructs the Google authorization server to also return a
# refresh token the first time the application exchanges an
# authorization code for tokens
access_type => 'offline',
key => $cred->{client_id},
secret => $cred->{client_secret},
}
}
};
# Note that this /login/google end point callback is called in two different cases:
# - the first case is when the user accesses the login page,
# - the second case is when the google authorization server redirects back
# to the redirect_uri parameter. In this case the "code" query parameter is
# set.
# The OAuth2 plugin can differentiate between these two cases, such that
# get_token_p() will behave correctly for each case..
#
get '/login/google' => sub {
my $c = shift;
my $app = $c->app;
my $args = {
redirect => 1, # tell get_token_p() to redirect to the current route
scope => (join ' ', #scopes),
};
$c->oauth2->get_token_p(google => $args)->then(
# This callback is for the second response from google aut. server,
# (the first response is handled by get_token_p() internally)
sub {
my $res = shift; # The response from the authorization server
$c->session(token => $res->{access_token});
$c->session(refresh_token => $res->{refresh_token});
#------------------------------------------
# This should log the refresh token
#------------------------------------------
$c->log->info('Refresh token: ' . $res->{refresh_token});
#------------------------------------------
my $ua = $app->ua;
my $url = 'https://www.googleapis.com/userinfo/v2/me';
my $tx = $ua->build_tx(GET => $url);
$tx->req->headers->authorization('Bearer ' . $c->session('token'));
return $ua->start_p($tx);
}
)->then(
sub {
my $tx = shift;
my $data = $tx->res->json;
#$app->log->info($app->dumper($data));
$c->session(user_email => $data->{email});
$c->redirect_to('/done');
}
)->catch(
sub {
my $err = shift;
$c->log->info($err);
$c->render(text => $err);
}
);
};
get '/google/refreshtoken' => sub {
my $c = shift;
my $app = $c->app;
my $refresh_token = $c->session('refresh_token');
if ($refresh_token) {
my $args = {
refresh_token => $refresh_token,
};
$c->oauth2->get_refresh_token_p(google => $args)->then(
sub {
my $res = shift;
# update stored access token
$c->session(token => $res->{access_token});
$c->render(template => 'refreshed');
}
);
}
else {
$c->render(text => "No refresh token stored. Please login first");
}
};
get '/' => sub {
my $c = shift;
$c->render(template => 'index');
};
get '/done' => sub {
my $c = shift;
$c->render(template => 'done');
};
app->start;
}
sub read_credentials( $fn ) {
open ( my $fh, '<', $fn ) or die "Could not open file '$fn': $!";
my $str = do { local $/; <$fh> };
close $fh;
my $cred = decode_json( $str );
return $cred->{installed};
}
__DATA__
## index.html.ep
<!DOCTYPE html>
<html>
<head><title>Testing mojolicious oauth2 refresh token...</title></head>
<body>
<h1>Please access route /login/google to start...</h1>
</body>
</html>
## done.html.ep
<!DOCTYPE html>
<html>
<head><title>Done testing mojolicious oauth2</title></head>
<body>
<h1>Done testing. User email: <%= $c->session('user_email') %></h1>
</body>
</html>
## refreshed.html.ep
<!DOCTYPE html>
<html>
<head><title>Refreshed token</title></head>
<body>
<h1>Refreshed token</h1>
</body>
</html>

Perl rest client declaration causes failure of user agent call with custom headers with another end point

I have 2 subroutines called in a single perl program .
First one (get_secrets) I am using the perl REST client directly with custom header and second one (app_restart) I am using LWP user agent and make and HTTP call .
my second subroutine fails when the $client header declaration is available in the first subroutine , as soon as i remove the that subroutine or comment the lines those lines app_restart subroutine works fine .
use REST::Client;
use Data::Dumper;
use JSON; #use strict;
use MIME::Base64 qw( decode_base64 );
use POSIX 'strftime';
use Date::Parse;
use DateTime;
use Date::Calc qw(:all);
use LWP::UserAgent;
#use IO::Socket::SSL 'debug4';
use Data::Dumper qw(Dumper);
use Getopt::Long;
sub toList {
my $data = shift;
my $key = shift;
if ( ref( $data->{$key} ) eq 'ARRAY' ) {
$data->{$key};
}
elsif ( ref( $data->{$key} ) eq 'HASH' ) {
[ $data->{$key} ];
}
else {
[];
}
}
$ENV{PERL_LWP_SSL_VERIFY_HOSTNAME} = 0;
$endpoint = $ENV{'ENDPOINT'};
$token = `cat /var/run/secrets/kubernetes.io/serviceaccount/token`;
$namespace = $ENV{'NAMESPACE'};
$apikey = $ENV{'APIKEY'};
$instance = $ENV{'INSTANCE'};
$appid = $ENV{'APPID'};
$storeid = "checker";
sub get_secrets {
my $client = REST::Client->new();
$client->setHost("https://${endpoint}");
#$client->addHeader('Authorization', "Bearer ${token}");
$client->addHeader( 'Accept', "application/json" );
$client->GET("/api/v1/namespaces/${namespace}/secrets?labelSelector=true");
}
get_secrets();
$ENV{PERL_NET_HTTPS_SSL_SOCKET_CLASS} = "Net::SSL";
my $ua = LWP::UserAgent->new( 'send_te' => '0' );
$ua->ssl_opts(
SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE,
SSL_hostname => '',
verify_hostname => 0
);
sub app_restart {
$ENV{PERL_NET_HTTPS_SSL_SOCKET_CLASS} = "Net::SSL";
$ua = LWP::UserAgent->new( 'send_te' => '0' );
$ua->ssl_opts(
SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE,
verify_hostname => 0
print "$instance\n";
print "$apikey\n";
print "$cstoreid\n";
print "$appid\n";
my $r = HTTP::Request->new(
'PUT' =>
"https://api.service.intranet.com/rest/application/$appid/instance/$instance/action?action=restart&config=$storeid&deploy=0",
[
'Accept' => '*/*',
'Authorization' => "Realm $apikey",
'Host' => 'api.service.intranet.com:443',
'User-Agent' => 'curl/7.55.1',
],
);
my $res = $ua->request( $r, );
#$response = $res->decoded_content;
$json = JSON->new->allow_nonref;
$response_decoded = $json->decode( $res->decoded_content );
$actionID = $response_decoded->{'action_id'};
print "$actionID\n";
}
app_restart();

How can I use NTLM or Kerberos authentication with Mojo::UserAgent

I am trying to get Mojo::UserAgent to authenticate via NTLM. Rougly like this:
use Mojo::UserAgent;
use Mojo::URL;
use Data::Dump qw/dump/;
use Path::Tiny;
use Authen::NTLM;
$\ = "\n"; $|++;
my $ntlm = Authen::NTLM-> new(host => "some.hidden.pl", user => 'foo',
domain => "bar", password => "baz", version => 2);
my $xml = path($ARGV[0])->slurp;
my $ua = Mojo::UserAgent->new;
my $url = Mojo::URL->new('https://some.hidden.pl/ews/exchange.asmx');
$url->userinfo(sprintf('%s\%s:%s', qw/bar foo baz/));
my $tx = $ua->get($url);
my $tx = $ua->build_tx(GET => $url);
$challenge = $ntlm->challenge;
$tx->req->headers->header('Authorization' => 'NTLM ' . $challenge);
$ua->start($tx);
$challenge = [ split /,\s*/, $tx->res->headers->header('www-authenticate') ]->[0] =~ s/NTLM //r;
$challenge = $ntlm->challenge($challenge);
my $tx = $ua->build_tx(GET => $url);
$tx->req->headers->header('Authorization' => 'NTLM ' . $challenge);
$ua->start($tx);
$tx = $ua->build_tx(POST => $url, {'Content-Type' => 'text/xml'}, $xml );
$tx->req->headers->content_type('text/xml');
$tx->req->headers->header('Authorization' => 'NTLM ' . $challenge);
$ua->start($tx);
print dump $tx->res;
but I keep getting a 401 at the second response from the server.
What am I getting wrong? And would it be easier to use Kerberos authentication (if so, how)?
thanks
I just published a new module that should be pretty helpful in this respect Mojolicious::Plugin::SPNEGO. It is pretty simple to use:
use Mojolicious::Lite;
my $SERVER = 'my-ad-server.example.com';
app->secrets(['My secret passphrase here']);
plugin 'SPNEGO', ad_server => $SERVER;
get '/' => sub {
my $c = shift;
if (not $c->session('user')){
$c->ntlm_auth({
auth_success_cb => sub {
my $c = shift;
my $user = shift;
my $ldap = shift; # bound Net::LDAP::SPNEGO connection
$c->session('user',$user->{samaccountname});
$c->session('name',$user->{displayname});
my $groups = $ldap->get_ad_groups($user->{samaccountname});
$c->session('groups',[ sort keys %$groups]);
return 1;
}
}) or return;
}
} => 'index';
app->start;
__DATA__
## index.html.ep
<!DOCTYPE html>
<html>
<head>
<title>NTLM Auth Test</title>
</head>
<body>
<h1>Hello <%= session 'name' %></h1>
<div>Your account '<%= session 'user' %>' belongs to the following groups:</div>
<ul>
% for my $group (#{session 'groups' }) {
<li>'<%= $group %>'</li>
% }
</ul>
</body>
</html>
The module is based on the also newly released Net::LDAP::SPNEGO module which provides the basic buildingblocks for the SPNEGO dialog.

how to get session id from cookie jar in perl?

My question is very simple.. It is how to get session id from cookie jar ... I have tried below code :-
use warnings;
use HTTP::Cookies;
use HTTP::Request::Common;
use LWP::UserAgent;
$ua = new LWP::UserAgent;
if ( !$ua ) {
print "Can not get the page :UserAgent fialed \n";
return 0;
}
my $cookies = new HTTP::Cookies( file => './cookies.dat', autosave => 1 );
$ua->cookie_jar($cookies);
# push does all magic to exrtact cookies and add to header for further reqs. useragent should be newer
push #{ $ua->requests_redirectable }, 'POST';
$result = $ua->request(
POST "URL",
{ Username => 'admin',
Password => 'admin',
Submit => 'Submit',
}
);
my $session_id = $cookies->extract_cookies($result);
print $session_id->content;
print "\n\n";
$resp = $result->content;
#print "Result is \n\n\n $resp \n";
$anotherURI = URL;
$requestObject = HTTP::Request::Common::GET $anotherURI;
$result = $ua->request($requestObject);
$resp = $result->content;
#print $resp."\n";
I am not getting where the session id is stored and how to fetch it ?
Note:- URL contains the URL of the page.
I wrote HTTP::CookieMonster to make this kind of thing a bit easier. If you don't know which cookie you're looking for, you can do something like this:
use strict;
use warnings;
use HTTP::CookieMonster;
use WWW::Mechanize;
my $mech = WWW::Mechanize->new;
my $monster = HTTP::CookieMonster->new( $mech->cookie_jar );
my $url = 'http://www.nytimes.com';
$mech->get( $url );
my #all_cookies = $monster->all_cookies;
foreach my $cookie ( #all_cookies ) {
printf( "key: %s value: %s\n", $cookie->key, $cookie->val);
}
If you already know the cookie's key, you can something like:
my $cookie = $monster->get_cookie('RMID');
my $session_id = $cookie->val;
Have a look at HTTP::Cookies->scan.
Something like this should do the trick (should add a constraint on the domain at least):
my $session_id;
$cookie_jar->scan(
sub {
my ($key, $val, $path, $domain, $port,
$path_spec, $secure, $expires, $discard, $hash
) = #_;
if ( $key eq "session_id" ) {
$session_id = $val;
}
}
);

Twitter OAuth 1.0 authentication with signature in perl

I'm getting this problem with my app in perl for oauth authentication:
401 Unauthorized Failed to validate oauth signature and token
Here is my code:
sub Twitter {
my $IN = new CGI;
my $qs = build_query({
oauth_callback => $callback_url,
oauth_consumer_key => $consumer_key,
oauth_nonce => time,
oauth_signature_method => "HMAC-SHA1",
oauth_timestamp => time,
oauth_version => "1.0"
});
# Create Signature
my $signing_key = $IN->escape($consumer_secret)."&";
my $base_signature = "POST&".$IN->escape($request_token_url)."&".$qs;
use Digest::HMAC_SHA1;
my $hmac = Digest::HMAC_SHA1->new($signing_key);
$hmac->add($base_signature);
$qs .= "&oauth_signature=".$IN->escape($hmac->b64digest);
# Fetch the page
use LWP;
my $ua = LWP::UserAgent->new;
my $req = HTTP::Request->new(POST => $request_token_url);
$req->content_type('application/x-www-form-urlencoded');
$req->content($qs);
my $res = $ua->request($req);
# Check the outcome of the response
unless ($res->is_success) {
print $IN->header.$res->status_line, "\n";
print $res->content;
exit;
}
print $IN->header.$res->content;
}
sub build_query {
my $input = shift;
use URI;
my $uri = URI->new;
$uri->query_form($input);
return $uri->query;
}
I have obviously deleted my callback url and key information.
I figured it out. I was encoding the signature wrong, I had to sort my query strings, and the call back URL is not needed in this instance. Here is my working code:
sub Twitter {
my $IN = new CGI;
my $params = {
oauth_consumer_key => $consumer_key,
oauth_nonce => time,
oauth_signature_method => "HMAC-SHA1",
oauth_timestamp => time,
oauth_version => "1.0"
};
my $qs = build_sorted_query($params);
my $signing_key = $IN->escape($consumer_secret)."&";
my $signature_base = "POST&".$IN->escape($request_token_url)."&".$IN->escape($qs);
use Digest::HMAC_SHA1;
use MIME::Base64;
my $hmac = Digest::HMAC_SHA1->new($signing_key);
$hmac->add($signature_base);
$params->{oauth_signature} = $IN->escape(encode_base64($hmac->digest));
$qs = build_sorted_query($params);
use LWP;
my $ua = LWP::UserAgent->new;
my $req = HTTP::Request->new(POST => $request_token_url);
$req->content_type('application/x-www-form-urlencoded');
$req->content($qs);
my $res = $ua->request($req);
# Check the outcome of the response
unless ($res->is_success) {
print $IN->header.$res->status_line, "\n";
print $res->content;
exit;
}
print $IN->header.$res->content;
return;
}
sub build_sorted_query {
my $input = shift;
my $qs;
foreach (sort keys %$input) {
$qs .= $_."=".$input->{$_}."&";
}
return substr ($qs, 0, -1);
}
Thanks for looking!