File Coverage

blib/lib/GoogleIDToken/Validator.pm
Criterion Covered Total %
statement 13 15 86.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 18 20 90.0


line stmt bran cond sub pod time code
1             package GoogleIDToken::Validator;
2              
3 1     1   23744 use 5.006;
  1         5  
  1         69  
4 1     1   7 use strict;
  1         2  
  1         43  
5 1     1   6 use warnings FATAL => 'all';
  1         8  
  1         57  
6 1     1   7 use Carp;
  1         2  
  1         94  
7              
8 1     1   439 use MIME::Base64::URLSafe;
  0            
  0            
9             use Crypt::OpenSSL::X509;
10             use Crypt::OpenSSL::RSA;
11             use Date::Parse;
12             use LWP::Simple;
13             use JSON;
14              
15             =head1 NAME
16              
17             GoogleIDToken::Validator - allows you to verify on server side Google Access Token received by mobile application
18              
19             =head1 VERSION
20              
21             Version 0.02
22              
23             =cut
24              
25             our $VERSION = '0.02';
26              
27             =head1 SYNOPSIS
28              
29             Perl implamentation of Google Access Token verification.
30             Details can be found on android developers blog:
31             L
32              
33             This module ONLY:
34             =item * Verifies that this token signed by google certificates.
35             =item * Verifies that this token was received by mobile application with given Client IDs and for given web application Client ID.
36              
37             Nothing more. Nothing connected with authorization on any of Google APIs etc etc
38              
39             use GoogleIDToken::Validator;
40              
41             my $validator = GoogleIDToken::Validator->new(
42             #do_not_cache_certs => 1, # will download google certificates from web every call of verify
43             #google_certs_url => 'https://some.domain.com/certs', # in case they change URL in the future... default is: https://www.googleapis.com/oauth2/v1/certs
44             certs_cache_file => '/tmp/google.crt', # will cache certs in this file for faster verify if you are using CGI
45             web_client_id => '111222333444.apps.googleusercontent.com', # Your Client ID for web applications received in Google APIs console
46             app_client_ids => [ # Array of your Client ID for installed applications received in Google APIs console
47             '777777777777-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com', # for exm. your production keystore ID
48             '888888888888-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com' # and your eclipse debug keystore ID
49             ]
50             );
51              
52             # web_client_id and at least one of app_client_ids are required
53              
54             # get the token from your mobile app somehow...
55             my $token = 'eyJhbG..............very.long.base64.encoded.access.token............';
56              
57             my $payload = $validator->verify($token);
58             if($payload) {
59             # token is OK, lets see what we have got
60             use Data::Dumper;
61             print "payload: ".Dumper($payload);
62             }
63              
64             =cut
65              
66             sub new {
67             my($class, %args) = @_;
68             my $self = bless({}, $class);
69            
70             $self->{google_certs_url} = exists $args{google_certs_url} ? $args{google_certs_url} : 'https://www.googleapis.com/oauth2/v1/certs';
71             $self->{certs_cache_file} = exists $args{certs_cache_file} ? $args{certs_cache_file} : undef;
72             $self->{do_not_cache_certs} = exists $args{do_not_cache_certs} ? $args{do_not_cache_certs} : 0;
73              
74             if($args{web_client_id}) {
75             $self->{web_client_id} = $args{web_client_id};
76             } else {
77             croak "No Web Client ID was specified to check against.";
78             }
79              
80             if($args{app_client_ids}) {
81             $self->{app_client_ids} = $args{app_client_ids};
82             } else {
83             croak "No Application Client IDs were specified to check against.";
84             }
85              
86             $self->{certs} = undef;
87              
88             return $self;
89             }
90              
91             sub verify {
92             my($self, $token) = @_;
93            
94             if($self->certs_expired()) {
95             $self->get_certs();
96             }
97            
98             my($env, $payload, $signature) = split /\./, $token;
99             my $signed = $env . '.' . $payload;
100            
101             $signature = urlsafe_b64decode($signature);
102             $env = decode_json(urlsafe_b64decode($env));
103             $payload = decode_json(urlsafe_b64decode($payload));
104            
105            
106             if(!exists $self->{certs}->{$env->{kid}}) {
107             carp "There are no such certificate that used to sign this token (kid: $env->{kid}).";
108             return undef;
109             }
110             my $rsa = Crypt::OpenSSL::RSA->new_public_key($self->{certs}->{$env->{kid}}->pubkey());
111             $rsa->use_sha256_hash();
112            
113             if(!$rsa->verify($signed, $signature)) {
114             carp "Signature is wrong.";
115             return undef;
116             }
117            
118             if($payload->{aud} ne $self->{web_client_id}) {
119             carp "Web Client ID missmatch. ($payload->{aud}).";
120             return undef;
121             }
122            
123             foreach my $cid (@{$self->{app_client_ids}}) {
124             return $payload if($cid eq $payload->{azp});
125             }
126             carp "App Client ID missmatch. ($payload->{azp})."
127            
128             }
129              
130             sub certs_expired {
131             my $self = shift;
132             return 1 if(!$self->{certs});
133             foreach my $kid (keys %{$self->{certs}}) {
134             return 1 if(str2time($self->{certs}->{$kid}->notAfter()) < time);
135             }
136             return 0;
137             }
138              
139             sub get_certs {
140             my $self = shift;
141             if($self->{do_not_cache_certs}) {
142             $self->get_certs_from_web();
143             } else {
144             if($self->{certs_cache_file} && -e $self->{certs_cache_file}) {
145             $self->get_certs_from_file();
146             }
147             if($self->certs_expired()) {
148             $self->get_certs_from_web();
149             }
150             }
151             }
152              
153             sub get_certs_from_file {
154             my $self = shift;
155             open my $fh, $self->{certs_cache_file} or croak "Can't read certs from cache file($self->{certs_cache_file}): $!";
156             my $json_certs = '';
157             while(<$fh>) { $json_certs .= $_ }
158             if($json_certs) {
159             $self->parse_certs($json_certs);
160             } else {
161             $self->{certs} = undef;
162             }
163             close $fh;
164             }
165              
166             sub get_certs_from_web {
167             my($self) = @_;
168             my $json_certs = get($self->{google_certs_url});
169             if($json_certs) {
170             $self->parse_certs($json_certs);
171             if(!$self->{do_not_cache_certs} && $self->{certs_cache_file}) {
172             open my $fh, ">".$self->{certs_cache_file} or croak "Can't write certs to cache file($self->{certs_cache_file}): $!";
173             print $fh $json_certs;
174             close $fh;
175             }
176            
177             } else {
178             croak "ERROR getting certs from $self->{certs_cache_file}";
179            
180             }
181             }
182              
183             sub parse_certs {
184             my($self, $json_certs) = @_;
185             my $certs = decode_json($json_certs);
186             foreach my $kid (keys %{$certs}) {
187             $self->{certs}->{$kid} = Crypt::OpenSSL::X509->new_from_string($certs->{$kid});
188             }
189             }
190              
191             =head1 AUTHOR
192              
193             Dmitry Mukhin, C<< >>
194              
195             =head1 BUGS
196              
197             Please report any bugs or feature requests to C, or through
198             the web interface at L. I will be notified, and then you'll
199             automatically be notified of progress on your bug as I make changes.
200              
201              
202              
203              
204             =head1 SUPPORT
205              
206             You can find documentation for this module with the perldoc command.
207              
208             perldoc GoogleIDToken::Validator
209              
210              
211             You can also look for information at:
212              
213             =over 4
214              
215             =item * RT: CPAN's request tracker (report bugs here)
216              
217             L
218              
219             =item * AnnoCPAN: Annotated CPAN documentation
220              
221             L
222              
223             =item * CPAN Ratings
224              
225             L
226              
227             =item * Search CPAN
228              
229             L
230              
231             =back
232              
233              
234             =head1 ACKNOWLEDGEMENTS
235              
236              
237             =head1 LICENSE AND COPYRIGHT
238              
239             Copyright 2013 Dmitry Mukhin.
240              
241             This program is free software; you can redistribute it and/or modify it
242             under the terms of the the Artistic License (2.0). You may obtain a
243             copy of the full license at:
244              
245             L
246              
247             Any use, modification, and distribution of the Standard or Modified
248             Versions is governed by this Artistic License. By using, modifying or
249             distributing the Package, you accept this license. Do not use, modify,
250             or distribute the Package, if you do not accept this license.
251              
252             If your Modified Version has been derived from a Modified Version made
253             by someone other than you, you are nevertheless required to ensure that
254             your Modified Version complies with the requirements of this license.
255              
256             This license does not grant you the right to use any trademark, service
257             mark, tradename, or logo of the Copyright Holder.
258              
259             This license includes the non-exclusive, worldwide, free-of-charge
260             patent license to make, have made, use, offer to sell, sell, import and
261             otherwise transfer the Package with respect to any patent claims
262             licensable by the Copyright Holder that are necessarily infringed by the
263             Package. If you institute patent litigation (including a cross-claim or
264             counterclaim) against any party alleging that the Package constitutes
265             direct or contributory patent infringement, then this Artistic License
266             to you shall terminate on the date that such litigation is filed.
267              
268             Disclaimer of Warranty: THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER
269             AND CONTRIBUTORS "AS IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES.
270             THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
271             PURPOSE, OR NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY
272             YOUR LOCAL LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR
273             CONTRIBUTOR WILL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR
274             CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE,
275             EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
276              
277              
278             =cut
279              
280             42; # End of GoogleIDToken::Validator