The Perl Toolchain Summit 2025 Needs You: You can help 🙏 Learn more

#!perl
package goauth;
our $VERSION = '0.27'; # VERSION
use Carp;
use Mojo::File qw/path/;
use feature 'say';
use Net::EmptyPort qw(empty_port);
use Crypt::JWT qw(decode_jwt);
# ABSTRACT: CLI tool with mini http server for negotiating Google OAuth2 Authorisation access tokens that allow offline access to Google API Services on behalf of the user.
my $filename = $ARGV[0] || 'gapi.json';
my $file = path($filename);
if ($file->stat) ## file exists
{
say qq'File "${\$file->to_abs}" exists';
input_if_not_exists([ 'gapi/client_id', 'gapi/client_secret', 'gapi/scopes' ]);
## this potentially allows mreging with a json file with data external
## to the app or to augment missing scope from file generated from
## earlier versions of goauth from other libs
runserver();
} else {
setup();
runserver();
}
sub setup {
say
qq'Project Credentials config file "${\$file->to_abs}" with OAUTH App Secrets and user tokens not found. Creating new file...';
## TODO: consider allowing the gapi.json to be either seeded or to extend the credentials.json provided by Google
my $oauth = {};
say "Obtain project app client_id and client_secret from http://console.developers.google.com/";
print "client_id: ";
$oauth->{client_id} = _stdin() || croak('client_id is required and has no default');
print "client_secret: ";
$oauth->{client_secret} = _stdin() || croak('client secret is required and has no default');
print
$oauth->{scopes} = _stdin(); ## no croak because empty string is allowed an will evoke defaults
## set default scope if empty string provided
if ($oauth->{scopes} eq '') {
$oauth->{scopes}
}
my $tokensfile = Config::JSON->create($file->to_abs);
$tokensfile->set('gapi/client_id', $oauth->{client_id});
$tokensfile->set('gapi/client_secret', $oauth->{client_secret});
$tokensfile->set('gapi/scopes', $oauth->{scopes});
say 'OAuth details updated!';
return 1;
}
sub input_if_not_exists {
my $fields = shift;
my $config = Config::JSON->new($filename);
for my $i (@$fields) {
if (!defined $config->get($i)) {
print "$i: ";
#chomp( my $val = <STDIN> );
my $val = _stdin();
$config->set($i, $val);
}
}
return 1;
}
sub runserver {
my $port = empty_port(3000);
say
"Starting web server. Before authorization don't forget to allow redirect_uri to http://127.0.0.1 in your Google Console Project";
$ENV{'GOAUTH_TOKENSFILE'} = $file->to_abs;
my $config = Config::JSON->new($ENV{'GOAUTH_TOKENSFILE'});
# authorize_url and token_url can be retrieved from OAuth discovery document
plugin "OAuth2" => {
google => {
key => $config->get('gapi/client_id'), # $config->{gapi}{client_id},
secret => $config->get('gapi/client_secret'), #$config->{gapi}{client_secret},
token_url =>
}
};
# Marked for decomission
# helper get_email => sub {
# my ( $c, $access_token ) = @_;
# my %h = ( 'Authorization' => 'Bearer ' . $access_token );
# $c->ua->get( 'https://www.googleapis.com/auth/plus.profile.emails.read' => form => \%h )->res->json;
# };
helper get_new_tokens => sub {
my ($c, $auth_code) = @_;
my $hash = {};
$hash->{code} = $c->param('code');
$hash->{redirect_uri} = $c->url_for->to_abs->to_string;
$hash->{client_id} = $config->get('gapi/client_id');
$hash->{client_secret} = $config->get('gapi/client_secret');
$hash->{grant_type} = 'authorization_code';
my $tokens = $c->ua->post('https://www.googleapis.com/oauth2/v4/token' => form => $hash)->res->json;
return $tokens;
};
get "/" => sub {
my $c = shift;
$c->{config} = $config;
my $user_data;
my $tokens;
app->log->info("Will store tokens in " . $config->getFilename($config->pathToFile));
if ($c->param('code')) ## postback from google
{
app->log->info("Authorization code was retrieved: " . $c->param('code'));
$tokens = $c->get_new_tokens($c->param('code'));
app->log->info("App got new tokens: "); # . join(',' keys %{$tokens})
if ($tokens) {
if ($tokens->{id_token}) {
$user_data = decode_jwt(
token => $tokens->{id_token},
kid_keys => $c->ua->get('https://www.googleapis.com/oauth2/v3/certs')->res->json,
);
}
$config->addToHash('gapi/tokens/' . $user_data->{email}, 'access_token', $tokens->{access_token});
if ($tokens->{refresh_token}) {
$config->addToHash('gapi/tokens/' . $user_data->{email}, 'refresh_token', $tokens->{refresh_token});
} else ## with access_type=offline set we should receive a refresh token unless user already has an active one.
{
## carp('Google JWT Did not incude a refresh token - when the access token expires services will become inaccessible');
}
}
#$c->render( json => $config->get( 'gapi' ) );
$c->{access_token} = $tokens->{access_token};
$c->{user_email} = $user_data->{email};
$c->render(template => 'oauth_granted');
} else ## PRESENT USER DEFAULT PAGE TO REQUEST GOOGLE AUTH'D ACCESS TO SERVICES
{
$c->render(template => 'oauth');
}
};
#delete $static->extra->{'favicon.ico'}; ## look into https://mojolicious.org/perldoc/Mojolicious/Static with view to repalcing default favicon
app->start('daemon', '-l', "http://*:$port");
return 1;
}
sub _stdin {
my $io;
my $string = q{};
$io = IO::Handle->new();
if ($io->fdopen(fileno(STDIN), 'r')) {
$string = $io->getline();
$io->close();
}
chomp $string;
return $string;
}
# data:image/vnd.microsoft.icon;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABILAAASCwAAAAAAAAAAAAAAAAAAAAAAACMhSAAAAGUBS04uKWNqJXxyeiK+d4Af2XeAINhweCK4YWcmckdJMCEAAP8AGRdQAAAAAAAAAAAAAAAAAERFMwA1NToKY2kmbH6HH9uOmR39lKAc/5WhHP+VoRz/k58c/42YHfx7hCDSX2QoXSgmQQY8PDgAAAAAAERGNwAzMj4KaXAkjIqVHfmWohz/laEc/5WhHP+VoRz/laEc/5WhHP+Wohz/laEc/4eRHvRlayV4HRhMBT8/PQD//wAAZGonboqVHfmWohz/laEc/5WhHP+VoRz/laEc/5WhHP+VoRz/laEc/5WhHP+Wohz/h5Ee819kKlmLmAUATlEzLH+IINuWohz/laEc/5WhHP+VoRz/laEc/5WhHP+VoRz/laEc/5WhHP+VoRz/laEc/5WhHP96gyHLRkc5G2VrJX+Omh3+lqId/ZaiHviVoRz/laEc/5WhHP+Woh/wlqIf8JWhHfyWoh74laEc/5aiHvmWohz9i5Yd+V5kJ2NzeyHClKAc/5aiH+SbpimjlaEc/5WhHP+YoyLSnagumpynK6+ZpCTVm6YoqJWhHP+ZpSaqlqIf5JKeHP9udSOmeYIf4JWhHP+Woh7enqkxeZikI9SWoh7ynKcrk5umKa6ZpCTOmaQkzZumKZGVoRz+maUllZaiH92UoBz/dHwgxnqDHuGVoRz/lqIe3qGrNW6cpyurnagtjZ2oLpSeqTCen6kxl5umKq6gqjN4mKQj2JqlJ5OWoh/dlKAc/3R9H8h1fSHJlKAc/5aiH92cpiuLlqIe+p2nLYeYpCPJm6Yov5qlJsOXoh/tm6YowpumKb+bpiiPlqIf3ZOfHP9vdyOtZ24kjJCbHP+Woh7nnagufpynK5WbpiihlaEd95WhHP+VoRz/laEc/5WhHP+VoRz/maQkpJejHuKNmB39YWcmb1NWLzeCjB/llqIc/5aiHvWWoh7zlaEd/ZWhHP+VoRz/laEc/5WhHP+VoRz/laEc/5WhHfWWohz+focg10xONSUAAIgCaG8mg42ZHf6Wohz/laEc/5WhHP+VoRz/laEc/5WhHP+VoRz/laEc/5WhHP+Wohz/ipUd+mNpKGz//wAAS04xAD9BNhJvdyKmjpkd/paiHP+VoRz/laEc/5WhHP+VoRz/laEc/5WhHP+Wohz/i5Yd+2tyI5IzMz0LRUY2AAAAqQBRVCsAQUMzFGpxJYuEjh7skp0c/5WhHP+Wohz/lqIc/5WhHP+RnBz/gowf5mZtJns5OjYNR0kwAAAAAAAAAAAAAAAAADc3OgAnJUMFVVkqRGtzI6N5giDgfoge936IHvZ4gSDcaXAjmVFVLDkdGVIDMzJEAAAAAAAAAAAA4AcAAMADAACAAQAAgAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAIABAADAAwAA4AcAAA==
=pod
=encoding UTF-8
=head1 NAME
goauth - CLI tool with mini http server for negotiating Google OAuth2 Authorisation access tokens that allow offline access to Google API Services on behalf of the user.
=head1 VERSION
version 0.27
=head2 SUMMARY
Supports multiple users OAuth2 for Google. You can find the key (CLIENT ID) and
secret (CLIENT SECRET) from the app console here under "APIs & Auth" and
Included as a utility within the CPAN L<Webservice::GoogleAPI::Client> Bundle.
=head2 QUICK START
Simply run from the command line
goauth
Optionally you can provide an alternate filename to the default gapi.json as a parameter.
goauth my_differently_named_gapi.json
Once installed as part of the WebService::GoogleAPI::Client bundle, this tool
can be run from the command line to configure a Google Project to access a
Project configuration that allows authenticated and authorised users to grant
permission to access Google API services on their data (Email, Files etc) to the
extent provided by the Access Scopes for the project and the auth tokens.
In order to successfully run C<goauth> for the first time requires the following
Google Project configuration variables:
=over 4
=item * Client ID
=item * Client Secret
=item * List of Scopes to request access to on behalf of users ( must be a subset of those enabled for the project. )
=back
You must also add whatever URL you end up accessing C<goauth> from as a valid
Redirect URI in your Google Cloud Console. You can get there from
the OAuth Client ID that you're using for this project.
You need to add something like
with the port number set to whichever C<goauth> ends up picking. (This is
assuming that you typed C<http://localhost:3001> in your browser when you opened
it, it won't work if you typed in C<127.0.0.1:3001>).
If not already available in the gapi.json file, you will be prompted to supply
these as the file is created.
Once this configuration is complete, the C<goauth> tool will launch a mini HTTP
server to provide an interface to request users to authorise your project
application.
Once you have succesfully created a gapi.json file and have authenticated a user
that is represented in this file then you can start making Google API Requests.
See L<WebService::Google::Client> for more detail.
=head2 gapi.json
The ultimate output of this is the gapi.json file that contains both the Google
Project Specification as well as the authorised user access tokens. The file
describes a set of scopes that must all be configured as available to the
Project through the Google Admin Console. You may have multiple gapi.json files
for the same project containing a different subset of scopes. The gapi.json
file also contains the authorisation tokens granted by users. Multiple users can
be described within a single gapi.json file. If users exist across multiple
gapi.json files for the same project then (I believe) only the most recently
granted set of scopes will be usable.
The user can revoke permissions granted to a project ( Application ) by visiting
This file can be used to access Google API Services using the
WebService::Google:API:Client Google API Client Library.
=head2 References
* probably originally based on https://gist.github.com/throughnothing/3726907
=head1 AUTHOR
Veesh Goldman <veesh@cpan.org>
=head1 COPYRIGHT AND LICENSE
This software is Copyright (c) 2017-2023 by Veesh Goldman and Others.
This is free software, licensed under:
The Apache License, Version 2.0, January 2004
=cut
__DATA__
@@ oauth.html.ep
<h2>goauth Mini HTTP Server to acquire Google Authentication Tokens from a User </h2>
<br>
<%= link_to $c->oauth2->auth_url("google",
authorize_query => { access_type => 'offline'},
scope => $c->{config}->get('gapi/scopes'),
) => begin %>
<img src="data:image/png;base64,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">
<% end %>
<br>
<h2>Scopes</h2>
<ul>
<% for my $j (split /\s/, $c->{config}->get('gapi/scopes') ) { %>
<li>
<%= $j %>
</li>
<% } %>
</ul>
@@ oauth_granted.html.ep
<h2><%= $c->{user_email}; %> is now authenticated and has provided offline access to Google for the specified scopes</h2>
You can read more about authorization scopes in <a href="https://developers.google.com/+/web/api/rest/oauth#authorization-scopes">
the Google Developers Site</a><br/>
With this access token now saved into gapi.json you can query the available scopes etc with curl using the following command:
<br/><br/>
<span style=" background-color:#EEEEEE; font-family:Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New;">
</span>
<br/>
You can check ( and if necessary revoke ) any previously granted permissions for your user account at <a href="https://myaccount.google.com/permissions">https://myaccount.google.com/permissions</a>.
@@ favicon.ico (base64)
AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABILAAASCwAAAAAAAAAAAAAAAAAAAAAAACMhSAAAAGUBS04uKWNqJXxyeiK+d4Af2XeAINhweCK4YWcmckdJMCEAAP8AGRdQAAAAAAAAAAAAAAAAAERFMwA1NToKY2kmbH6HH9uOmR39lKAc/5WhHP+VoRz/k58c/42YHfx7hCDSX2QoXSgmQQY8PDgAAAAAAERGNwAzMj4KaXAkjIqVHfmWohz/laEc/5WhHP+VoRz/laEc/5WhHP+Wohz/laEc/4eRHvRlayV4HRhMBT8/PQD//wAAZGonboqVHfmWohz/laEc/5WhHP+VoRz/laEc/5WhHP+VoRz/laEc/5WhHP+Wohz/h5Ee819kKlmLmAUATlEzLH+IINuWohz/laEc/5WhHP+VoRz/laEc/5WhHP+VoRz/laEc/5WhHP+VoRz/laEc/5WhHP96gyHLRkc5G2VrJX+Omh3+lqId/ZaiHviVoRz/laEc/5WhHP+Woh/wlqIf8JWhHfyWoh74laEc/5aiHvmWohz9i5Yd+V5kJ2NzeyHClKAc/5aiH+SbpimjlaEc/5WhHP+YoyLSnagumpynK6+ZpCTVm6YoqJWhHP+ZpSaqlqIf5JKeHP9udSOmeYIf4JWhHP+Woh7enqkxeZikI9SWoh7ynKcrk5umKa6ZpCTOmaQkzZumKZGVoRz+maUllZaiH92UoBz/dHwgxnqDHuGVoRz/lqIe3qGrNW6cpyurnagtjZ2oLpSeqTCen6kxl5umKq6gqjN4mKQj2JqlJ5OWoh/dlKAc/3R9H8h1fSHJlKAc/5aiH92cpiuLlqIe+p2nLYeYpCPJm6Yov5qlJsOXoh/tm6YowpumKb+bpiiPlqIf3ZOfHP9vdyOtZ24kjJCbHP+Woh7nnagufpynK5WbpiihlaEd95WhHP+VoRz/laEc/5WhHP+VoRz/maQkpJejHuKNmB39YWcmb1NWLzeCjB/llqIc/5aiHvWWoh7zlaEd/ZWhHP+VoRz/laEc/5WhHP+VoRz/laEc/5WhHfWWohz+focg10xONSUAAIgCaG8mg42ZHf6Wohz/laEc/5WhHP+VoRz/laEc/5WhHP+VoRz/laEc/5WhHP+Wohz/ipUd+mNpKGz//wAAS04xAD9BNhJvdyKmjpkd/paiHP+VoRz/laEc/5WhHP+VoRz/laEc/5WhHP+Wohz/i5Yd+2tyI5IzMz0LRUY2AAAAqQBRVCsAQUMzFGpxJYuEjh7skp0c/5WhHP+Wohz/lqIc/5WhHP+RnBz/gowf5mZtJns5OjYNR0kwAAAAAAAAAAAAAAAAADc3OgAnJUMFVVkqRGtzI6N5giDgfoge936IHvZ4gSDcaXAjmVFVLDkdGVIDMzJEAAAAAAAAAAAA4AcAAMADAACAAQAAgAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAIABAADAAwAA4AcAAA==
__END__