File Coverage

blib/lib/Amon2/Auth/Site/LINE.pm
Criterion Covered Total %
statement 21 98 21.4
branch 0 16 0.0
condition 0 21 0.0
subroutine 7 16 43.7
pod 0 9 0.0
total 28 160 17.5


line stmt bran cond sub pod time code
1             package Amon2::Auth::Site::LINE;
2              
3 1     1   676 use strict;
  1         2  
  1         24  
4 1     1   4 use warnings;
  1         2  
  1         19  
5 1     1   499 use utf8;
  1         12  
  1         4  
6 1     1   513 use URI;
  1         3895  
  1         24  
7 1     1   583 use JSON;
  1         6782  
  1         4  
8 1     1   567 use Mouse;
  1         23753  
  1         4  
9 1     1   944 use LWP::UserAgent;
  1         32632  
  1         1234  
10            
11             our $VERSION = '0.03';
12              
13 0     0 0   sub moniker { 'line' }
14              
15             has client_id => (
16             is => 'ro',
17             isa => 'Str',
18             required => 1,
19             );
20            
21             has client_secret => (
22             is => 'ro',
23             isa => 'Str',
24             required => 1,
25             );
26            
27             has redirect_uri => (
28             is => 'ro',
29             isa => 'Str',
30             );
31              
32             has state => (
33             is => 'ro',
34             isa => 'Str',
35             );
36              
37             has scope => (
38             is => 'ro',
39             isa => 'ArrayRef',
40             default => sub { [qw(profile)] },
41             );
42              
43             has nonce => (
44             is => 'ro',
45             isa => 'Str',
46             );
47              
48             has prompt => (
49             is => 'ro',
50             isa => 'Str',
51             );
52              
53             has max_age => (
54             is => 'ro',
55             isa => 'Int',
56             );
57              
58             has ui_locales => (
59             is => 'ro',
60             isa => 'Str',
61             );
62              
63             has bot_prompt => (
64             is => 'ro',
65             isa => 'Str',
66             );
67              
68             has user_info => (
69             is => 'rw',
70             isa => 'Bool',
71             default => 1,
72             );
73              
74             has state_session_key => (
75             is => 'ro',
76             isa => 'Str',
77             default => 'line_login_state',
78             );
79              
80             has nonce_session_key => (
81             is => 'ro',
82             isa => 'Str',
83             default => 'line_login_nonce',
84             );
85              
86             has authorize_url => (
87             is => 'ro',
88             isa => 'Str',
89             default => 'https://access.line.me/oauth2/v2.1/authorize',
90             );
91              
92             has access_token_url => (
93             is => 'ro',
94             isa => 'Str',
95             default => 'https://api.line.me/oauth2/v2.1/token',
96             );
97              
98             has verify_url => (
99             is => 'ro',
100             isa => 'Str',
101             default => 'https://api.line.me/oauth2/v2.1/verify',
102             );
103              
104             has profile_url => (
105             is => 'ro',
106             isa => 'Str',
107             default => 'https://api.line.me/v2/profile',
108             );
109              
110             has ua => (
111             is => 'ro',
112             isa => 'LWP::UserAgent',
113             lazy => 1,
114             default => sub {
115             LWP::UserAgent->new(agent => "Amon2::Auth::Site::LINE/$VERSION");
116             },
117             );
118              
119             sub auth_uri {
120 0     0 0   my($self, $c, $callback_uri) = @_;
121              
122             # required parameters
123 0   0       my $redirect_uri = $self->redirect_uri // $callback_uri;
124             my %params = (
125             response_type => 'code',
126             client_id => $self->client_id,
127 0           scope => join(' ', @{$self->scope}),
  0            
128             redirect_uri => $redirect_uri,
129             state => $self->get_state($c),
130             );
131              
132             # optional parameters
133 0           $params{nonce} = $self->get_nonce($c);
134              
135 0           for my $key (qw(prompt max_age ui_locales bot_prompt)) {
136 0           my $value = $self->$key;
137 0 0         if (defined $value) {
138 0           $params{$key} = $value;
139             }
140             }
141              
142 0           my $auth_uri = URI->new($self->authorize_url);
143 0           $auth_uri->query_form(%params);
144              
145 0           return $auth_uri->as_string;
146             }
147              
148             sub callback {
149 0     0 0   my($self, $c, $callback) = @_;
150              
151             # state mismatch
152 0 0         if ($c->req->param('state') ne $self->get_state($c)) {
153 0           return $callback->{on_error}->('state parameter mismatch');
154             }
155              
156             # access denied
157 0 0         if ($c->req->param('error')) {
158 0           return $callback->{on_error}->($c->req->param('error_description'));
159             }
160            
161 0           my @args = ();
162              
163 0           my %api_response = ();
164              
165             # getting an access token
166 0           my $token_data;
167             {
168 0   0       my $redirect_uri = $self->redirect_uri // do { # it should be me
  0            
169 0           my $current_uri = $c->req->uri;
170 0           $current_uri->query(undef);
171 0           $current_uri->as_string;
172             };
173 0           my $res = $self->ua->post($self->access_token_url => +{
174             grant_type => 'authorization_code',
175             code => $c->req->param('code'),
176             redirect_uri => $redirect_uri,
177             client_id => $self->client_id,
178             client_secret => $self->client_secret,
179             });
180 0 0         unless ($res->is_success) {
181 0           warn $res->decoded_content;
182 0           return $callback->{on_error}->($res->status_line);
183             }
184              
185 0           $token_data = decode_json($res->content);
186 0           %api_response = (%api_response, %$token_data);
187             }
188              
189             # verify access token
190 0           my $verify_data;
191             {
192 0           my $uri = URI->new($self->verify_url);
  0            
193 0           $uri->query_form(access_token => $token_data->{access_token});
194              
195 0           my $res = $self->ua->get($uri->as_string);
196 0 0         unless ($res->is_success) {
197 0           warn $res->decoded_content;
198 0           return $callback->{on_error}->($res->status_line);
199             }
200              
201 0           $verify_data = decode_json($res->content);
202 0 0         if ($verify_data->{client_id} ne $self->client_id) {
203 0           return $callback->{on_error}->('client_id mismatch');
204             }
205              
206 0           push @args, $token_data->{access_token};
207 0           %api_response = (%api_response, %$verify_data);
208             }
209              
210             # get user profile
211 0 0 0       if ($self->user_info && $verify_data->{scope} =~ /\bprofile\b/) {
212 0           my $uri = URI->new($self->profile_url);
213             my $res = $self->ua->get(
214             $uri->as_string,
215             Authorization => 'Bearer ' . $token_data->{access_token},
216 0           );
217 0 0         $res->is_success or do {
218 0           warn $res->decoded_content;
219 0           return $callback->{on_error}->($res->decoded_content);
220             };
221 0           my $user = decode_json($res->content);
222 0           %api_response = (%api_response, %$user);
223             }
224 0           push @args, \%api_response;
225              
226 0           $self->clear_state($c);
227 0           $self->clear_nonce($c);
228              
229 0           $callback->{on_finished}->(@args);
230             }
231              
232             sub get_state {
233 0     0 0   my($self, $c) = @_;
234 0   0       my $state = $self->state // $c->session->get($self->state_session_key) // do {
      0        
235 0           require String::Random;
236 0           String::Random->new->randregex('[a-zA-Z0-9]{16}');
237             };
238 0           $self->set_state($c, $state);
239 0           return $state;
240             }
241              
242             sub set_state {
243 0     0 0   my($self, $c, $state) = @_;
244 0           return $c->session->set($self->state_session_key => $state);
245             }
246              
247             sub clear_state {
248 0     0 0   my($self, $c) = @_;
249 0           return $c->session->remove($self->state_session_key);
250             }
251              
252             sub get_nonce {
253 0     0 0   my($self, $c) = @_;
254 0   0       my $nonce = $self->nonce // $c->session->get($self->nonce_session_key) // do {
      0        
255 0           require String::Random;
256 0           String::Random->new->randregex('[a-zA-Z0-9]{16}');
257             };
258 0           $self->set_nonce($c, $nonce);
259 0           return $nonce;
260             }
261              
262             sub set_nonce {
263 0     0 0   my($self, $c, $nonce) = @_;
264 0           return $c->session->set($self->nonce_session_key => $nonce);
265             }
266              
267             sub clear_nonce {
268 0     0 0   my($self, $c) = @_;
269 0           return $c->session->remove($self->nonce_session_key);
270             }
271            
272             1;
273             __END__