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>