perl 中的 google API 服务帐户流程

Posted

技术标签:

【中文标题】perl 中的 google API 服务帐户流程【英文标题】:google API service account flow in perl 【发布时间】:2013-07-07 17:45:42 【问题描述】:

我需要在 perl 中使用 Google 的服务帐户流程对应用程序进行身份验证和授权。 Google 似乎没有在其文档中将 perl 列为受支持的语言。 有没有人遇到过这个问题?指向任何代码的指针?

【问题讨论】:

【参考方案1】:

搜索 perl Google OAUTH,您会发现许多不同的方法。

有关可用于为您适当配置的 Google Cloud API 项目收集 OAUTH 令牌的快速网络服务器示例,请参阅以下内容:

#!perl

use strict; use warnings; ## required because I can't work out how to get percritic to use my modern config
package goauth;

# 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. 

# 
# Supports multiple users
# similar to that installed as part of the WebService::Google module
# probably originally based on https://gist.github.com/throughnothing/3726907

# OAuth2 for Google. You can find the key (CLIENT ID) and secret (CLIENT SECRET) from the app console here under "APIs & Auth" 
# and "Credentials" in the menu at https://console.developers.google.com/project.

# See also https://developers.google.com/+/quickstart/.

use strict;
use warnings;
use Carp;
use Mojolicious::Lite;
use Data::Dumper;
use Config::JSON;
use Tie::File;
use feature 'say';
use Net::EmptyPort qw(empty_port);

use Crypt::JWT qw(decode_jwt);

my $filename;
if ( $ARGV[0] )

  $filename = $ARGV[0];

else

  $filename = './gapi.json';


if ( -e $filename )

  say "File $filename 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

  say "JSON file '$filename' with OAUTH App Secrets and user tokens not found. Creating new file...";
  setup();
  runserver();


sub setup

  ## 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 'scopes ( space sep list): eg - email profile https://www.googleapis.com/auth/plus.profile.emails.read '
    . "https://www.googleapis.com/auth/calendar https://www.googleapis.com/auth/contacts.readonly https://mail.google.com\n";

  $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 
      = 'email profile https://www.googleapis.com/auth/plus.profile.emails.read '
      . 'https://www.googleapis.com/auth/calendar '
      . 'https://www.googleapis.com/auth/contacts.readonly https://mail.google.com';
  

  my $tokensfile = Config::JSON->create( $filename );
  $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!';

  # Remove comment for Mojolicious::Plugin::JSONConfig compatibility
  tie my @array, 'Tie::File', $filename or croak $!;
  shift @array;
  untie @array;
  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'  = $filename;

  my $config = Config::JSON->new( $ENV 'GOAUTH_TOKENSFILE'  );


  # authorize_url and token_url can be retrieved from OAuth discovery document
  # https://github.com/marcusramberg/Mojolicious-Plugin-OAuth2/issues/52
  plugin "OAuth2" => 
    google => 
      key           => $config->get( 'gapi/client_id' ),                                    # $config->gapiclient_id,
      secret        => $config->get( 'gapi/client_secret' ),                                #$config->gapiclient_secret,
      authorize_url => 'https://accounts.google.com/o/oauth2/v2/auth?response_type=code',
      token_url     => 'https://www.googleapis.com/oauth2/v4/token' ## NB Google credentials.json specifies "https://www.googleapis.com/oauth2/v3/token" 
    
  ;

# 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;
    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' ) );
      my $tokens = $c->get_new_tokens( $c->param( 'code' ) );
      app->log->info( "App got new tokens: " . Dumper $tokens);
      if ( $tokens )
      
        my $user_data;
        if ( $tokens-> id_token  )
        
          # my $jwt = Mojo::JWT->new(claims => $tokens->id_token);
          # carp "Mojo header:".Dumper $jwt->header;

          # my $keys = $c->get_all_google_jwk_keys(); # arrayref
          # my ($header, $data) = decode_jwt( token => $tokens->id_token, decode_header => 1, key => '' ); # exctract kid
          # carp "Decode header :".Dumper $header;

          $user_data = decode_jwt( token => $tokens-> id_token , kid_keys => $c->ua->get( 'https://www.googleapis.com/oauth2/v3/certs' )->res->json, );
          #carp "Decoded user data:" . Dumper $user_data;
        

        #$user_data->email;
        #$user_data->family_name
        #$user_data->given_name
        # $tokensfile->set('tokens/'.$user_data->email, $tokens->access_token);
        $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' ) );
    
    else ## PRESENT USER DEFAULT PAGE TO REQUEST GOOGLE AUTH'D ACCESS TO SERVICES
    
      $c->render( template => 'oauth' );
    
  ;
  app->secrets( ['putyourownsecretcookieseedhereforsecurity' . time] ); ## NB persistence cookies not required beyond server run
  app->start( 'daemon', '-l', "http://*:$port" );
  return 1;


## replacement for STDIN as per https://coderwall.com/p/l9-uvq/reading-from-stdin-the-good-way
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;



=head2 TODO: Improve user interface of the html templates beneath DATA section

=over 1

=item * include Auth with Google button from Google Assets and advertise scopes reqeusted on the oauth.html

=item * More informative details on post-authentication page - perhaps include scopes, filename updated and instructions on revoking

=back

=cut

__DATA__

@@ oauth.html.ep

<%= link_to "Click here to get Google OAUTH2 tokens", $c->oauth2->auth_url("google",
    authorize_query =>  access_type => 'offline',
        scope => $c->config->get('gapi/scopes'), ## scope => "email profile https://www.googleapis.com/auth/plus.profile.emails.read https://www.googleapis.com/auth/calendar https://www.googleapis.com/auth/contacts.readonly",
         )
%>

<br>
<br>

<a href="https://developers.google.com/+/web/api/rest/oauth#authorization-scopes">
Check more about authorization scopes</a>
Once you have a token in your gapi.json you can check the available scopes with curl using <pre>curl https://www.googleapis.com/oauth2/v1/tokeninfo?access_token=<YOUR_ACCESS_TOKEN></pre>

__END__

【讨论】:

以上是关于perl 中的 google API 服务帐户流程的主要内容,如果未能解决你的问题,请参考以下文章

无法使用 Perl 中的 Email::MIME 从 google 群组帐户发送电子邮件/抄送不接收电子邮件

Google Calendar API - 禁止 - 服务帐户错误

Google API 授权(服务帐户)错误:HttpAccessTokenRefreshError:未授权客户端:请求中的未授权客户端或范围

Google Gmail API - 常规或服务帐户

如何通过 Google Drive .NET API v3 使用服务帐户访问 Team Drive

如何使用 Google 服务帐户通过 Activity API 检索 Google Drive 活动?