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>
Related
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>
I just create two routes:
$r->any ( '/api/v:api', [ api => qr/\d+/ ], { api => 1 } )->partial( 1 );
$r->under( '/api/v:api', [ api => qr/\d+/ ], { api => 1 } );
It seems both work same.
What is the difference under hood between them?
UPD
I have noticed that for first case (->any) link to static files are moved to /api/v1. This is noticeable when exception happen. mojo/debug template tries to load static files from /api/v1/... path and not /.... Why?
The under is called before anything else. You can place authentication code inside it.
use strict;
use warnings;
use Mojolicious::Lite;
under sub {
my $self = shift;
my $api_key = $self->req->url->to_abs->username;
my $api_secret = $self->req->url->to_abs->password;
if($api_key ne "Admin" && $api_secret ne "Password123") {
$self->res->headers->www_authenticate('Basic');
$self->render(text => 'Error: API-Credentials are wrong', status => 401);
# No routes will be executed
return undef;
}
# Authentication with success, execute other routes
return 1;
}
# Only executed if the url is http://Admin:Password123#127.0.0.1:3000
any '/' => sub {
my $self = shift;
my $date = Mojo::Date->new;
$self->render(text => $date, status => 200);
};
It's very important that you have to place under above the routines you want to protect.
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.
I am using Mojolicious::Plugin::OAuth2 to build a simple app where you can log in using your google+ credentials, and I am having trouble with the syntax.
My code is very close to the example they give:
use Mojolicious::Lite;
use Mojolicious::Plugin::OAuth2;
plugin 'OAuth2' => {
google => {
key => 'xxxxxx.apps.googleusercontent.com',
secret => 'xxxxxxxx',
},
};
get "/auth" => sub {
my $self = shift;
$self->delay(
sub {
my $delay = shift;
$self->get_token(google => $delay->begin, scope->profile)
},
sub {
my($delay, $token, $tx) = #_;
return $self->render(text => $tx->res->error) unless $token;
$self->session(token => $token);
$self->render(text => $token);
},
);
};
app->start;
The problem area being the scope->profile. Without specifying the scope, I get an error from google saying "400: Error: invalid_request Missing required parameter: scope" but I can't quite get it right and now receive syntax errors.
The documentation says to do it like this $token = $c->get_token($provider_name => \%args); So how do I write that args hash so that it makes sense?
This syntax is what worked for me
$self->get_token('google', scope => 'profile', $delay->begin);
I had to put $delay->begin at the end and google in quotes.
I have an under statement that may generate and error (for instance, authentication error). I use content negotiation all over, and I'd like to return the error inside the under in a proper format. An example code:
under sub {
my $self = shift;
# Authenticated
my $token = $self->param('token') || '';
return 1 if $token eq '123456';
# Not authenticated
$self->respond_to(
json => {
json => { error => 'Invalid authentication token.' },
status => 401
},
text => {
text => 'Unauthorized.',
status => 401
}
);
return undef;
}
I can use render inside under, but respond_to won't work. Probably under work for that. But in that case, what should I do?
In Galileo I have an auth_fail helper which does something like this. While I work out an example, see that code (and the if_author and if_admin unders).
Ok here is an example, the trick turned out to be (at least for this mechanism) before you can redirect to your failure handler, you need to flash the format, which make it available to the next handler.
#!/usr/bin/env perl
use Mojolicious::Lite;
any '/fail' => sub {
my $self = shift;
$self->respond_to(
json => {
json => { error => 'Invalid authentication token.' },
status => 401
},
text => {
text => 'Unauthorized.',
status => 401
}
);
};
under sub {
my $self = shift;
# Authenticated
my $token = $self->param('token') || '';
return 1 if $token eq '123456';
# Not authenticated
$self->flash( format => $self->param('format') );
$self->redirect_to('fail');
return undef;
};
any '/private' => sub {
my $self = shift;
$self->respond_to(
json => {
json => { launch_codes => '9999999' },
},
text => {
text => 'Launch Code: 9999999',
}
);
};
app->start;