File Coverage

blib/lib/GitHub/Apps/Auth.pm
Criterion Covered Total %
statement 84 102 82.3
branch 11 20 55.0
condition 4 9 44.4
subroutine 27 31 87.1
pod 2 3 66.6
total 128 165 77.5


line stmt bran cond sub pod time code
1             package GitHub::Apps::Auth;
2 5     5   366775 use 5.008001;
  5         32  
3 5     5   28 use strict;
  5         8  
  5         98  
4 5     5   22 use warnings;
  5         9  
  5         344  
5              
6             our $VERSION = "0.04";
7              
8             use Class::Accessor::Lite (
9 5         39 rw => [qw/token expires installation_id/],
10             ro => [qw/_furl private_key app_id/],
11 5     5   2488 );
  5         5918  
12              
13 5     5   734 use Carp;
  5         11  
  5         259  
14 5     5   2947 use Crypt::PK::RSA;
  5         90996  
  5         246  
15 5     5   3042 use Crypt::JWT qw/encode_jwt/;
  5         129091  
  5         350  
16 5     5   2397 use Furl;
  5         120698  
  5         174  
17 5     5   3310 use JSON qw/decode_json/;
  5         40406  
  5         86  
18 5     5   2669 use Time::Moment;
  5         5796  
  5         869  
19              
20             sub _lazy(&) {
21 1     1   9 return GitHub::Apps::Auth::Lazy->new($_[0]);
22             }
23              
24             use overload
25 15     15   2309 "\"\"" => sub { shift->issued_token },
26             "." => sub {
27 1     1   313 my ($self, $other, $reverse) = @_;
28             return $reverse ?
29 5     5   14 _lazy { "$other" . "$self" } :
30 1 50   0   10 _lazy { "$self" . "$other" };
  0         0  
31 5     5   38 };
  5         11  
  5         47  
32              
33             sub new {
34 4     4 1 2034124 my ($class, %args) = @_;
35 4 50 33     61 if (!exists $args{private_key} || !$args{private_key}) {
36 0         0 croak "private_key is required.";
37             }
38 4 50 33     43 if (!exists $args{app_id} || !$args{app_id}) {
39 0         0 croak "app_id is required.";
40             }
41 4 50 66     27 if (!$args{installation_id} && !$args{login}) {
42 0         0 croak "must be set installation_id or login.";
43             }
44              
45 4         30 my $pk = Crypt::PK::RSA->new($args{private_key});
46              
47             my $klass = {
48             private_key => $pk,
49             installation_id => $args{installation_id},
50             app_id => $args{app_id},
51 4         1527 expires => 0,
52             _furl => Furl->new,
53             };
54 4         343 my $self = bless $klass, $class;
55              
56 4 100       29 if (!$self->installation_id) {
57 1         44 my $installations = $self->installations;
58 1 50       14 if (!exists $installations->{$args{login}}) {
59 0         0 croak $args{login} . " is not found in installations."
60             }
61 1         3 my $installation_id = $installations->{$args{login}};
62 1         4 $self->installation_id($installation_id);
63             }
64              
65 4         155 return $self;
66             }
67              
68             sub installations {
69 0     0 0 0 my $self = shift;
70              
71 0         0 my $header = $self->_generate_request_header();
72              
73 0         0 my $resp = $self->_furl->get(
74             "https://api.github.com/app/installations",
75             $header,
76             );
77 0 0       0 if (!$resp->is_success) {
78 0         0 croak "fail to fetch installations: ". $resp->content;
79             }
80              
81 0         0 my $content = decode_json $resp->content;
82 0         0 my %ids_by_account = map { $_->{account}{login} => $_->{id} } @$content;
  0         0  
83 0         0 return \%ids_by_account;
84             }
85              
86             sub _generate_jwt {
87 6     6   13 my $self = shift;
88              
89 6         18 my $jwt = encode_jwt(
90             payload => {
91             iat => time(),
92             exp => time() + 60,
93             iss => $self->app_id,
94             },
95             alg => "RS256",
96             key => $self->private_key,
97             );
98              
99 6         35129 return $jwt;
100             }
101              
102             sub _generate_request_header {
103 6     6   12 my $self = shift;
104 6         19 my $jwt = $self->_generate_jwt();
105              
106             return [
107 6         31 Authorization => 'Bearer ' . $jwt,
108             Accept => "application/vnd.github.machine-man-preview+json",
109             ];
110             }
111              
112             sub _fetch_access_token {
113 6     6   12 my $self = shift;
114              
115 6         34 my $installation_id = $self->installation_id;
116 6         36 my $header = $self->_generate_request_header();
117 6         31 my $resp = $self->_post_to_access_token($installation_id, $header);
118              
119 6 50       553 if (!$resp->is_success) {
120 0         0 croak "cannot fetch access_token: ". $resp->content;
121             }
122              
123 6         88 my $content = decode_json $resp->content;
124 6         74 my $token = $content->{token};
125 6         25 $self->token($token);
126 6         41 my $expires = $content->{expires_at};
127 6         55 my $tm = Time::Moment->from_string($expires);
128 6         40 $self->expires($tm->epoch);
129              
130 6         96 return $token;
131             }
132              
133             sub _post_to_access_token {
134 0     0   0 my ($self, $installation_id, $header) = @_;
135              
136 0         0 return $self->_furl->post(
137             "https://api.github.com/app/installations/$installation_id/access_tokens",
138             $header,
139             );
140             }
141              
142             sub _is_expired_token {
143 20     20   31 my $self = shift;
144              
145 20         63 return time() > $self->expires;
146             }
147              
148             sub issued_token {
149 20     20 1 2924 my $self = shift;
150              
151 20 100       51 if ($self->_is_expired_token) {
152 6         123 return $self->_fetch_access_token;
153             }
154              
155 14         210 return $self->token;
156             }
157              
158             package
159             GitHub::Apps::Auth::Lazy;
160              
161              
162             sub _lazy(&) {
163 2     2   5 return GitHub::Apps::Auth::Lazy->new($_[0]);
164             }
165              
166             use overload
167 15     15   3834 '""' => sub { shift->{sub}->() . "" },
168             "." => sub {
169 2     2   6 my ($self, $other, $reverse) = @_;
170             return $reverse ?
171 0     0   0 _lazy { "$other" . "$self" } :
172 2 50   10   12 _lazy { "$self" . "$other" };
  10         26  
173 5     5   4450 };
  5         11  
  5         43  
174              
175             sub new {
176 3     3   7 my ($class, $sub) = @_;
177 3         38 return bless { sub => $sub }, $class;
178             }
179              
180             1;
181             __END__