From 6aa2dd9de20b31de36372ada2cd1e0af947215fb Mon Sep 17 00:00:00 2001 From: Ichinose Shogo Date: Sat, 25 Oct 2014 22:15:24 +0900 Subject: [PATCH 1/4] support OAuth2 with JSON Web Token --- Makefile.PL | 1 + .../Google/DataAPI/Auth/OAuth2/SignedJWT.pm | 90 +++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 lib/Net/Google/DataAPI/Auth/OAuth2/SignedJWT.pm diff --git a/Makefile.PL b/Makefile.PL index f8a54a5..61b5380 100644 --- a/Makefile.PL +++ b/Makefile.PL @@ -19,6 +19,7 @@ requires_any_moose( moose => '0.56', mouse => '0.51', ); +requires 'JSON::WebToken'; tests_recursive; author_tests 'xt'; diff --git a/lib/Net/Google/DataAPI/Auth/OAuth2/SignedJWT.pm b/lib/Net/Google/DataAPI/Auth/OAuth2/SignedJWT.pm new file mode 100644 index 0000000..e2051e2 --- /dev/null +++ b/lib/Net/Google/DataAPI/Auth/OAuth2/SignedJWT.pm @@ -0,0 +1,90 @@ +package Net::Google::DataAPI::Auth::OAuth2::SignedJWT; +use Any::Moose; +with 'Net::Google::DataAPI::Role::Auth'; +our $VERSION = '0.01'; + +use LWP::UserAgent; +use HTTP::Request::Common; +use JSON; +use JSON::WebToken; +use constant OAUTH2_TOKEN_ENDPOINT => "https://accounts.google.com/o/oauth2/token"; +use constant OAUTH2_CLAIM_AUDIENCE => "https://accounts.google.com/o/oauth2/token"; +use constant JWT_GRANT_TYPE => "urn:ietf:params:oauth:grant-type:jwt-bearer"; +use constant JWT_ALGORITHIM => "RS256"; +use constant JWT_TYP => "JWT"; +use constant OAUTH2_TOKEN_LIFETIME_SECS => 3600; + +has private_key => ( + is => 'ro', + isa => 'Str', + required => 1, +); + +has service_account => ( + is => 'ro', + isa => 'Str', + required => 1, +); + +has scope => ( + is => 'ro', + isa => 'ArrayRef[Str]', + required => 1, + auto_deref => 1, + default => sub {[ + 'https://www.googleapis.com/auth/userinfo.profile', + 'https://www.googleapis.com/auth/userinfo.email' + ]}, +); + +has token_type => ( + is => 'rw', + isa => 'Str', +); + +has access_token => ( + is => 'rw', + isa => 'Str', +); + +sub get_access_token { + my ($self) = @_; + + my $jwt_params = { + iss => $self->service_account, + scope => join(' ', $self->scope), + aud => OAUTH2_CLAIM_AUDIENCE, + exp => time() + OAUTH2_TOKEN_LIFETIME_SECS, + iat => time() + }; + + my $jwt = JSON::WebToken::encode_jwt($jwt_params, $self->private_key, JWT_ALGORITHIM, { + typ => JWT_TYP + }); + + my $jwt_ua = LWP::UserAgent->new; + my $jwt_response = $jwt_ua->request(POST OAUTH2_TOKEN_ENDPOINT, { + grant_type => JWT_GRANT_TYPE, + assertion => $jwt + }); + + my $json_response = JSON->new->utf8->decode($jwt_response->decoded_content); + $self->token_type($json_response->{token_type}); + $self->access_token($json_response->{access_token}); +} + +sub sign_request { + my ($self, $req) = @_; + $req->header(Authorization => join(' ', + $self->token_type, + $self->access_token, + ) + ); + return $req; +} + +__PACKAGE__->meta->make_immutable; + +no Any::Moose; + +1; From cb8bebfc28bc4782fadefa3db8b5f972897e57fb Mon Sep 17 00:00:00 2001 From: Ichinose Shogo Date: Sat, 25 Oct 2014 23:03:21 +0900 Subject: [PATCH 2/4] add test for SignedJWT --- t/04_auth/05_signed_jwt.t | 74 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 t/04_auth/05_signed_jwt.t diff --git a/t/04_auth/05_signed_jwt.t b/t/04_auth/05_signed_jwt.t new file mode 100644 index 0000000..f0a302b --- /dev/null +++ b/t/04_auth/05_signed_jwt.t @@ -0,0 +1,74 @@ +use strict; +use warnings; +use Test::More; +use Test::MockModule; +use Test::Exception; +use LWP::UserAgent; +use JSON::WebToken; +use URI; +use JSON; +BEGIN { + use_ok 'Net::Google::DataAPI::Auth::OAuth2::SignedJWT'; +} +{ + throws_ok sub { + Net::Google::DataAPI::Auth::OAuth2::SignedJWT->new( + private_key => 'PRIVATE KEY', + ) + }, qr/Attribute \(service_account\) is required/; +} +{ + throws_ok sub { + Net::Google::DataAPI::Auth::OAuth2::SignedJWT->new( + service_account => 'example@developer.gserviceaccount.com', + ) + }, qr/Attribute \(private_key\) is required/; +} +{ + my $jwt = Test::MockModule->new('JSON::WebToken'); + $jwt->mock(encode_jwt => sub { + my ($param, $private_key, $algorithm, $type) = @_; + is $param->{iss}, 'example@developer.gserviceaccount.com'; + is $param->{scope}, 'https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email'; + is $param->{aud}, 'https://accounts.google.com/o/oauth2/token'; + like $param->{exp}, qr/[0-9]+/; + like $param->{iat}, qr/[0-9]+/; + is $private_key, 'PRIVATE KEY'; + is $algorithm, 'RS256'; + is_deeply $type, { typ => 'JWT' }; + return 'my-json-web-token'; + }); + + my $ua = Test::MockModule->new('LWP::UserAgent'); + $ua->mock(request => sub { + my ($self, $req) = @_; + is $req->method, 'POST'; + my $q = {URI->new('?'.$req->content)->query_form}; + is_deeply $q, { + grant_type => 'urn:ietf:params:oauth:grant-type:jwt-bearer', + assertion => 'my-json-web-token', + }; + + my $res = HTTP::Response->new(200); + $res->header('Content-Type' => 'text/json'); + my $json = to_json({ + access_token => 'my_access_token', + token_type => 'Bearer', + }); + $res->content($json); + return $res; + } + ); + ok my $oauth2 = Net::Google::DataAPI::Auth::OAuth2::SignedJWT->new( + private_key => 'PRIVATE KEY', + service_account => 'example@developer.gserviceaccount.com', + ); + + $oauth2->get_access_token; + is $oauth2->access_token, 'my_access_token'; + my $req = HTTP::Request->new('get' => 'http://foo.bar.com'); + ok $oauth2->sign_request($req); + is $req->header('Authorization'), 'Bearer my_access_token'; +} + +done_testing; From 1b3f9e31d0991aca5333f44b07b2c459bf745edc Mon Sep 17 00:00:00 2001 From: Ichinose Shogo Date: Sat, 25 Oct 2014 23:03:41 +0900 Subject: [PATCH 3/4] add POD for SignedJWT --- .../Google/DataAPI/Auth/OAuth2/SignedJWT.pm | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/lib/Net/Google/DataAPI/Auth/OAuth2/SignedJWT.pm b/lib/Net/Google/DataAPI/Auth/OAuth2/SignedJWT.pm index e2051e2..b997aba 100644 --- a/lib/Net/Google/DataAPI/Auth/OAuth2/SignedJWT.pm +++ b/lib/Net/Google/DataAPI/Auth/OAuth2/SignedJWT.pm @@ -47,6 +47,8 @@ has access_token => ( isa => 'Str', ); +# https://developers.google.com/accounts/docs/OAuth2ServiceAccount +# https://github.com/comewalk/google-api-perl-client/blob/master/lib/Google/API/OAuth2/SignedJWT.pm sub get_access_token { my ($self) = @_; @@ -71,6 +73,7 @@ sub get_access_token { my $json_response = JSON->new->utf8->decode($jwt_response->decoded_content); $self->token_type($json_response->{token_type}); $self->access_token($json_response->{access_token}); + return $self->access_token; } sub sign_request { @@ -88,3 +91,70 @@ __PACKAGE__->meta->make_immutable; no Any::Moose; 1; +__END__ + +=head1 NAME + +Net::Google::DataAPI::Auth::OAuth2::SignedJWT - OAuth2 support for Server to Server Applications + +=head1 SYNOPSIS + + use Net::Google::DataAPI::Auth::OAuth2::SignedJWT; + + my $oauth2 = Net::Google::DataAPI::Auth::OAuth2->new( + private_key => <<'__KEY__', + -----BEGIN PRIVATE KEY----- + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + -----END PRIVATE KEY----- + __KEY__ + service_account => 'xxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx@developer.gserviceaccount.com', + scope => ['http://spreadsheets.google.com/feeds/'], + ); + + $oauth2->get_access_token oe die; + + # after retrieving token, you can use $oauth2 with Net::Google::DataAPI items: + + my $client = Net::Google::Spreadsheets->new(auth => $oauth2); + +=head1 DESCRIPTION + +Net::Google::DataAPI::Auth::OAuth2::SignedJWT interacts with google OAuth 2.0 service +and adds Authorization header to given request. + +=head1 ATTRIBUTES + +You can make Net::Google::DataAPI::Auth::OAuth2::SignedJWT instance with those arguments below: + +=over 2 + +=item * private_key + +private key of Crypt::OpenSSL::RSA. You can get it at L. + +=item * service_account + +E-mail address for service account. + +=item * scope + +URL identifying the service(s) to be accessed. You can see the list of the urls to use at L + +=back + +See L for details. + +=head1 AUTHOR + +Ichinose Shogo Eshogo82148@gmail.comE + +=head1 SEE ALSO + +L + +=head1 LICENSE + +This library is free software; you can redistribute it and/or modify +it under the same terms as Perl itself. + +=cut From a5696d9c5563c36c1dd12d8ca6ee6ffd9822f0fa Mon Sep 17 00:00:00 2001 From: Ichinose Shogo Date: Sat, 25 Oct 2014 23:10:13 +0900 Subject: [PATCH 4/4] fix typo --- lib/Net/Google/DataAPI/Auth/OAuth2/SignedJWT.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Net/Google/DataAPI/Auth/OAuth2/SignedJWT.pm b/lib/Net/Google/DataAPI/Auth/OAuth2/SignedJWT.pm index b997aba..4d07fa6 100644 --- a/lib/Net/Google/DataAPI/Auth/OAuth2/SignedJWT.pm +++ b/lib/Net/Google/DataAPI/Auth/OAuth2/SignedJWT.pm @@ -111,7 +111,7 @@ Net::Google::DataAPI::Auth::OAuth2::SignedJWT - OAuth2 support for Server to Ser scope => ['http://spreadsheets.google.com/feeds/'], ); - $oauth2->get_access_token oe die; + $oauth2->get_access_token or die; # after retrieving token, you can use $oauth2 with Net::Google::DataAPI items: