File Coverage

blib/lib/GitHub/Apps/Auth.pm
Criterion Covered Total %
statement 78 95 82.1
branch 12 20 60.0
condition 4 9 44.4
subroutine 19 22 86.3
pod 2 3 66.6
total 115 149 77.1


line stmt bran cond sub pod time code
1             package GitHub::Apps::Auth;
2 5     5   353463 use 5.008001;
  5         40  
3 5     5   28 use strict;
  5         9  
  5         98  
4 5     5   21 use warnings;
  5         10  
  5         340  
5              
6             our $VERSION = "0.03";
7              
8             use Class::Accessor::Lite (
9 5         41 rw => [qw/token expires _prefix _suffix installation_id/],
10             ro => [qw/_furl private_key app_id/],
11 5     5   2519 );
  5         6134  
12              
13 5     5   825 use Carp;
  5         10  
  5         273  
14 5     5   2992 use Crypt::PK::RSA;
  5         95630  
  5         363  
15 5     5   3074 use Crypt::JWT qw/encode_jwt/;
  5         130859  
  5         322  
16 5     5   2476 use Furl;
  5         121891  
  5         244  
17 5     5   3264 use JSON qw/decode_json/;
  5         44566  
  5         33  
18 5     5   2879 use Time::Moment;
  5         5736  
  5         691  
19              
20             use overload
21 0     0   0 "\"\"" => sub { shift->issued_token },
22             "." => sub {
23 2     2   836 my $self = shift;
24 2         5 my $other = shift;
25 2         3 my $reverse = shift;
26              
27 2 50       7 $other = "" unless defined $other;
28              
29 2         6 my $new_self = bless {}, ref $self;
30 2         15 %$new_self = %$self;
31              
32 2 100       12 $reverse ?
33             $new_self->_prefix($other . $new_self->_prefix) :
34             $new_self->_suffix($new_self->_suffix . $other);
35 2         25 return $new_self;
36             },
37 5     5   59 "eq" => sub { shift->issued_token eq shift };
  5     10   11  
  5         50  
  10         6552  
38              
39             sub new {
40 4     4 1 1904431 my ($class, %args) = @_;
41 4 50 33     131 if (!exists $args{private_key} || !$args{private_key}) {
42 0         0 croak "private_key is required.";
43             }
44 4 50 33     88 if (!exists $args{app_id} || !$args{app_id}) {
45 0         0 croak "app_id is required.";
46             }
47 4 50 66     27 if (!$args{installation_id} && !$args{login}) {
48 0         0 croak "must be set installation_id or login.";
49             }
50              
51 4         30 my $pk = Crypt::PK::RSA->new($args{private_key});
52              
53             my $klass = {
54             private_key => $pk,
55             installation_id => $args{installation_id},
56             app_id => $args{app_id},
57 4         1476 expires => 0,
58             _furl => Furl->new,
59             _prefix => "",
60             _suffix => "",
61             };
62 4         347 my $self = bless $klass, $class;
63              
64 4 100       25 if (!$self->installation_id) {
65 1         44 my $installations = $self->installations;
66 1 50       16 if (!exists $installations->{$args{login}}) {
67 0         0 croak $args{login} . " is not found in installations."
68             }
69 1         3 my $installation_id = $installations->{$args{login}};
70 1         4 $self->installation_id($installation_id);
71             }
72              
73 4         184 return $self;
74             }
75              
76             sub installations {
77 0     0 0 0 my $self = shift;
78              
79 0         0 my $header = $self->_generate_request_header();
80              
81 0         0 my $resp = $self->_furl->get(
82             "https://api.github.com/app/installations",
83             $header,
84             );
85 0 0       0 if (!$resp->is_success) {
86 0         0 croak "fail to fetch installations: ". $resp->content;
87             }
88              
89 0         0 my $content = decode_json $resp->content;
90 0         0 my %ids_by_account = map { $_->{account}{login} => $_->{id} } @$content;
  0         0  
91 0         0 return \%ids_by_account;
92             }
93              
94             sub _generate_jwt {
95 4     4   7 my $self = shift;
96              
97 4         12 my $jwt = encode_jwt(
98             payload => {
99             iat => time(),
100             exp => time() + 60,
101             iss => $self->app_id,
102             },
103             alg => "RS256",
104             key => $self->private_key,
105             );
106              
107 4         22937 return $jwt;
108             }
109              
110             sub _generate_request_header {
111 4     4   8 my $self = shift;
112 4         12 my $jwt = $self->_generate_jwt();
113              
114             return [
115 4         19 Authorization => 'Bearer ' . $jwt,
116             Accept => "application/vnd.github.machine-man-preview+json",
117             ];
118             }
119              
120             sub _fetch_access_token {
121 4     4   24 my $self = shift;
122              
123 4         12 my $installation_id = $self->installation_id;
124 4         20 my $header = $self->_generate_request_header();
125 4         20 my $resp = $self->_post_to_access_token($installation_id, $header);
126              
127 4 50       374 if (!$resp->is_success) {
128 0         0 croak "cannot fetch access_token: ". $resp->content;
129             }
130              
131 4         57 my $content = decode_json $resp->content;
132 4         46 my $token = $content->{token};
133 4         17 $self->token($token);
134 4         30 my $expires = $content->{expires_at};
135 4         35 my $tm = Time::Moment->from_string($expires);
136 4         25 $self->expires($tm->epoch);
137              
138 4         24 return $self->_prefix . $token . $self->_suffix;
139             }
140              
141             sub _post_to_access_token {
142 0     0   0 my ($self, $installation_id, $header) = @_;
143              
144 0         0 return $self->_furl->post(
145             "https://api.github.com/app/installations/$installation_id/access_tokens",
146             $header,
147             );
148             }
149              
150             sub _is_expired_token {
151 15     15   24 my $self = shift;
152              
153 15         48 return time() > $self->expires;
154             }
155              
156             sub issued_token {
157 15     15 1 3877 my $self = shift;
158              
159 15 100       36 if ($self->_is_expired_token) {
160 4         89 return $self->_prefix . $self->_fetch_access_token . $self->_suffix;
161             }
162              
163 11         122 return $self->_prefix . $self->token . $self->_suffix;
164             }
165              
166             1;
167             __END__