use 5.008001;
use strict;
our $VERSION = "0.02";
rw => [qw/token expires _prefix _suffix/],
ro => [qw/_furl private_key app_id installation_id/],
);
use Carp;
use Crypt::JWT qw/encode_jwt/;
use Furl;
use JSON qw/decode_json/;
"\"\"" => sub { shift->issued_token },
"." => sub {
my $self = shift;
my $other = shift;
my $reverse = shift;
$other = "" unless defined $other;
my $new_self = bless {}, ref $self;
%$new_self = %$self;
$reverse ?
$new_self->_prefix($other . $new_self->_prefix) :
$new_self->_suffix($new_self->_suffix . $other);
return $new_self;
},
"eq" => sub { shift->issued_token eq shift };
sub new {
my ($class, %args) = @_;
if (!exists $args{private_key} || !$args{private_key}) {
croak "private_key is required.";
}
if (!exists $args{app_id} || !$args{app_id}) {
croak "app_id is required.";
}
if (!exists $args{installation_id} || !$args{installation_id}) {
croak "installation_id is required.";
}
my $pk = Crypt::PK::RSA->new($args{private_key});
my $self = {
private_key => $pk,
installation_id => $args{installation_id},
app_id => $args{app_id},
expires => 0,
_furl => Furl->new,
_prefix => "",
_suffix => "",
};
return bless $self, $class;
}
sub _generate_jwt {
my $self = shift;
my $jwt = encode_jwt(
payload => {
iat => time(),
exp => time() + 60,
iss => $self->app_id,
},
alg => "RS256",
key => $self->private_key,
);
return $jwt;
}
sub _generate_request_header {
my $self = shift;
my $jwt = $self->_generate_jwt();
return [
Authorization => 'Bearer ' . $jwt,
Accept => "application/vnd.github.machine-man-preview+json",
];
}
sub _fetch_access_token {
my $self = shift;
my $installation_id = $self->installation_id;
my $header = $self->_generate_request_header();
my $resp = $self->_post_to_access_token($installation_id, $header);
if (!$resp->is_success) {
croak "cannot fetch access_token: ". $resp->content;
}
my $content = decode_json $resp->content;
my $token = $content->{token};
$self->token($token);
my $expires = $content->{expires_at};
my $tm = Time::Moment->from_string($expires);
$self->expires($tm->epoch);
return $self->_prefix . $token . $self->_suffix;
}
sub _post_to_access_token {
my ($self, $installation_id, $header) = @_;
return $self->_furl->post(
"https://api.github.com/app/installations/$installation_id/access_tokens",
$header,
);
}
sub _is_expired_token {
my $self = shift;
return time() > $self->expires;
}
sub issued_token {
my $self = shift;
if ($self->_is_expired_token) {
return $self->_prefix . $self->_fetch_access_token . $self->_suffix;
}
return $self->_prefix . $self->token . $self->_suffix;
}
1;
__END__
=encoding utf-8
=head1 NAME
GitHub::Apps::Auth - The fetcher that get a token for GitHub Apps
=head1 SYNOPSIS
use GitHub::Apps::Auth;
my $auth = GitHub::Apps::Auth->new(
private_key => "<filename>", # when read private key from file
private_key => \$pk, # when read private key from variable
app_id => <app_id>,
installation_id => <installation_id>
);
# This method returns the cached token inside an object.
# However, refresh expired token automatically.
my $token = $auth->issued_token;
# If you want to use with Pithub
use Pithub;
# GitHub::Apps::Auth object behaves like a string.
# This object calls the `issued_token` method
# each time it evaluates as a string.
my $ph = Pithub->new(token => $auth, ...);
=head1 DESCRIPTION
GitHub::Apps::Auth is the fetcher for getting a GitHub token of GitHub Apps.
This module provides a way to get a token that need to be updated regularly for GitHub API.
=head1 CONSTRUCTOR
=head2 new
my $auth = GitHub::Apps::Auth->new(
private_key => "<filename>",
app_id => <app_id>,
installation_id => <installation_id>
);
Constructs an instance of C<GitHub::Apps::Auth> from credentials.
=head3 parameters
=head4 private_key
B<Required: true>
This parameter is a private key of the GitHub Apps.
This must be a filename or string in the pem format. You can get a private key from Settings page of GitHub Apps. See L<Generating a private key|https://developer.github.com/apps/building-github-apps/authenticating-with-github-apps/#generating-a-private-key>.
=head4 app_id
B<Required: true>
This parameter is the App ID of your GitHub Apps. Use the C<App ID> in the About section of your GitHub Apps page.
=head4 installation_id
B<Required: true>
A C<installation_id> is an identifier of installation Organizations or repositories in GitHub Apps. This value is can be obtained from a webhook that is fired during installation. Also can be obtained from webhook's C<Recent Deliveries> of GitHub apps settings.
=head1 METHODS
=head2 issued_token
my $token = $auth->issued_token;
C<issued_token> returns a API token in string. This token is cached while valid.
When calling this method with condition that expired token, this method refreshes a token automatically.
=head2 token
This method returns an API token. Unlike C<issued_token>, this method not refresh an expired token.
=head2 expires
This returns the token expiration date in the epoch.
=head1 OPERATOR OVERLOADS
C<GitHub::Apps::Auth> is overloaded so that C<issued_token> is called when evaluated as a string. So probably be usable in GitHub client that use raw string API token. Ex L<Pithub>.
=head1 SEE ALSO
=head1 LICENSE
Copyright (C) mackee.
This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself.
=head1 AUTHOR
mackee E<lt>macopy123@gmail.comE<gt>
=cut