Google-Auth
view release on metacpan or search on metacpan
lib/Google/Auth/ServiceAccountCredentials.pm view on Meta::CPAN
# Copyright 2026 Google LLC and contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
package Google::Auth::ServiceAccountCredentials;
use strict;
use warnings;
use Moo;
extends 'Google::Auth::Credentials';
use JSON::PP;
use MIME::Base64 qw(encode_base64url);
use LWP::UserAgent;
use Google::Auth;
use Google::Auth::Exceptions;
use Google::Auth::RetryHelper;
use Log::Any qw($log);
has json_key => (
is => 'ro',
required => 0,
);
has private_key_id => (
is => 'ro',
required => 0,
);
has private_key => (
is => 'ro',
required => 0,
);
has client_email => (
is => 'ro',
required => 0,
);
has client_id => (
is => 'ro',
required => 0,
);
has auth_uri => (
is => 'ro',
required => 0,
);
has token_uri => (
is => 'ro',
required => 0,
default => sub {
$ENV{GOOGLE_AUTH_TOKEN_URI} // 'https://oauth2.googleapis.com/token';
},
);
has auth_provider_x509_cert_url => (
is => 'ro',
required => 0,
);
has client_x509_cert_url => (
is => 'ro',
required => 0,
);
has scope => (
is => 'ro',
required => 0,
);
has ua => (
is => 'ro',
default => sub {
my $ua = LWP::UserAgent->new(timeout => 10);
$ua->env_proxy;
return $ua;
},
);
around BUILDARGS => sub {
my ($orig, $class, @args) = @_;
my $args = $class->$orig(@args);
if (my $json = $args->{json_key}) {
$args->{project_id} //= $json->{project_id};
$args->{private_key_id} //= $json->{private_key_id};
$args->{private_key} //= $json->{private_key};
$args->{client_email} //= $json->{client_email};
$args->{client_id} //= $json->{client_id};
$args->{auth_uri} //= $json->{auth_uri};
$args->{token_uri} //= $json->{token_uri};
$args->{auth_provider_x509_cert_url} //=
$json->{auth_provider_x509_cert_url};
$args->{client_x509_cert_url} //= $json->{client_x509_cert_url};
}
return $args;
};
sub _encode_base64url {
my ($data) = @_;
my $s = MIME::Base64::encode_base64url($data);
$s =~ s/=+$//;
return $s;
}
sub fetch_access_token {
my ($self, %options) = @_;
my $private_key = $self->private_key;
my $client_email = $self->client_email;
my $token_uri = $self->token_uri;
$self->_validate_url($token_uri, 'token_uri');
if (!defined $private_key || !defined $client_email) {
$log->errorf(
'Missing private_key or client_email for ServiceAccountCredentials token exchange'
);
Google::Auth::Error->throw(
'Missing private_key or client_email to sign and fetch token');
}
my $now = time();
my $header = {
alg => 'RS256',
typ => 'JWT',
};
$header->{kid} = $self->private_key_id if defined $self->private_key_id;
my $scope = $self->scope;
if (ref($scope) eq 'ARRAY') {
$scope = join(' ', @$scope);
}
my $payload = {
iss => $client_email,
sub => $client_email,
aud => $token_uri,
exp => $now + 3600,
iat => $now,
};
$payload->{scope} = $scope if defined $scope;
my $header_b64 = _encode_base64url(encode_json($header));
my $payload_b64 = _encode_base64url(encode_json($payload));
my $message = $header_b64 . '.' . $payload_b64;
$log->tracef('Signing JWT assertion for service account %s (key ID: %s)...',
$client_email, $self->private_key_id // 'N/A');
my $signature_raw = Google::Auth::rsa_sign_sha256($private_key, $message);
if (!defined $signature_raw) {
$log->errorf('Failed to sign JWT assertion for service account %s',
$client_email);
Google::Auth::Error->throw(
'Failed to sign JWT assertion using private key');
}
my $signature_b64 = _encode_base64url($signature_raw);
my $assertion = $message . '.' . $signature_b64;
my $ua = $self->ua;
my $post_body = {
grant_type => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
assertion => $assertion,
};
$log->infof('Exchanging signed JWT assertion for access token at %s...',
$token_uri);
my $response = Google::Auth::RetryHelper->execute_with_retry(
sub {
my $res = $ua->post(
$token_uri,
'Content-Type' => 'application/x-www-form-urlencoded',
'Content' => $post_body
);
if (!$res->is_success) {
$log->warnf('Service account token request failed at %s: status %s',
$token_uri, $res->code);
Google::Auth::Error->throw('HTTP request failed with status ' .
$res->code . ': ' . $res->decoded_content);
}
return $res;
},
%options
);
my $res_data = decode_json($response->decoded_content);
my $token = $res_data->{access_token};
my $expires = $res_data->{expires_in} // 3600;
$self->access_token($token);
$self->expires_at($now + $expires);
return $token;
}
1;
( run in 0.622 second using v1.01-cache-2.11-cpan-8dfa8b56332 )