Cache certs
[scpubgit/stemmaweb.git] / lib / stemmaweb / Authentication / Credential / Google.pm
CommitLineData
85990daf 1package stemmaweb::Authentication::Credential::Google;
2
3use Crypt::OpenSSL::X509;
4use JSON::WebToken;
5use IO::All;
6use JSON::MaybeXS;
7use MIME::Base64;
8use LWP::Simple qw(get);
9use Date::Parse qw(str2time);
10
11use warnings;
12use strict;
13use strictures 1;
14
15=head1 NAME
16
17stemmaweb::Authentication::Google - JSON Web Token handler for Google tokens.
18
19=head1 DESCRIPTION
20
21Retrieves Google's public certificates, and then retrieves the key from the
22cert using L<Crypt::OpenSSL::X509>. Finally, uses the pubkey to decrypt a
23Google token using L<JSON::WebToken>.
24
25=cut
26
27sub new {
28 my ($class, $config, $app, $realm) = @_;
29 $class = ref $class || $class;
30
31 my $self = {
32 _config => $config,
33 _app => $app,
34 _realm => $realm,
35 };
36
37 bless $self, $class;
38}
39
40sub authenticate {
41 my ($self, $c, $realm, $authinfo) =@_;
42
43 my $id_token = $authinfo->{id_token};
44 $id_token ||= $c->req->method eq 'GET' ?
45 $c->req->query_params->{id_token} : $c->req->body_params->{id_token};
46
85990daf 47 if (!$id_token) {
48 Catalyst::Exception->throw("id_token not specified.");
49 }
50
51 my $userinfo = $self->decode($id_token);
52
85990daf 53 my $sub = $userinfo->{sub};
54 my $openid = $userinfo->{openid_id};
55
85990daf 56 if (!$sub || !$openid) {
57 Catalyst::Exception->throw(
58 'Could not retrieve sub and openid from token! Is the token
59 correct?'
60 );
61 }
62
83ed6665 63 return $realm->find_user($userinfo, $c);
85990daf 64}
65
66=head1 METHODS
67
68=head2 retrieve_certs
69
70Retrieves a pair of JSON-encoded certificates from the given URL (defaults to
71Google's public cert url), and returns the decoded JSON object.
72
73=head3 ARGUMENTS
74
75=over
76
77=item url
78
79Optional. Location where certificates are located.
80Defaults to https://www.googleapis.com/oauth2/v1/certs.
81
82=back
83
84=head3 RETURNS
85
86Decoded JSON object containing certificates.
87
88=cut
89
90sub retrieve_certs {
91 my ($self, $url) = @_;
92
e490a3d8 93 my $c = $self->{_app};
94 my $cached = 0;
95 my $certs;
96 my $cache;
97
98 $url ||= ( $c->config->{'Authentication::Credential::Google'}->{public_cert_url} || 'https://www.googleapis.com/oauth2/v1/certs' );
99
100 if ( ($c->registered_plugins('Catalyst::Plugin::Cache')) && ($cache = $c->cache) ) {
101 if ($certs = $cache->get('certs')) {
102 $certs = decode_json($certs);
103
104 foreach my $key (keys %$certs) {
105 my $cert = $certs->{$key};
106 my $x509 = Crypt::OpenSSL::X509->new_from_string($cert);
107
108 if ($self->is_cert_expired($x509)) {
109 $cached = 0;
110 last;
111 } else {
112 $cached = 1;
113 }
114 }
115 }
116 }
117
118 if (!$cached) {
119 my $certs_encoded = get($url);
120
121 if ($cache) {
122 $cache->set('certs', $certs_encoded);
123 }
124
125 $certs = decode_json($certs_encoded);
126 }
127
128 return $certs;
85990daf 129}
130
131=head2 get_key_from_cert
132
133Given a pair of certificates $certs (defaults to L</retrieve_certs>),
134this function returns the public key of the cert identified by $kid.
135
136=head3 ARGUMENTS
137
138=over
139
140=item $kid
141
142Required. Index of the certificate hash $hash where the cert we want is
143located.
144
145=item $certs
146
147Optional. A (hashref) pair of certificates.
148It's retrieved using L</retrieve_certs> if not given,
149or if the pair is expired.
150
151=back
152
153=head3 RETURNS
154
155Public key of certificate.
156
157=cut
158
159sub get_key_from_cert {
160 my ($self, $kid, $certs) = @_;
161
162 $certs ||= $self->retrieve_certs;
163 my $cert = $certs->{$kid};
164 my $x509 = Crypt::OpenSSL::X509->new_from_string($cert);
165
166 if ($self->is_cert_expired($x509)) {
167 # If we ended up here, we were given
168 # an old $certs string from the user.
169 # Let's force getting another.
170 return $self->get_key_from_cert($kid);
171 }
172
173 return $x509->pubkey;
174}
175
176=head2 is_cert_expired
177
178Returns if a given L<Crypt::OpenSSL::X509> certificate is expired.
179
180=cut
181
182sub is_cert_expired {
183 my ($self, $x509) = @_;
184
185 my $expiry = str2time($x509->notAfter);
186
187 return time > $expiry;
188}
189
190=head2 decode
191
192Returns the decoded information contained in a user's token.
193
194=head3 ARGUMENTS
195
196=over
197
198=item $token
199
200Required. The user's token from Google+.
201
202=item $pubkey
203
204Optional. A public key string with which to decode the token.
205If not given, the public key will be retrieved from $certs.
206
207=item $certs
208
209Optional. A pair of public key certs retrieved from Google.
210If not given, or if the certificates have expired, a new
211pair of certificates is retrieved.
212
213=back
214
215=head2 RETURNS
216
217Decoded JSON object from the decrypted token.
218
219=cut
220
221sub decode {
222 my ($self, $token, $certs, $pubkey) = @_;
223
224 if (!$pubkey) {
225 my $details = decode_json(
226 MIME::Base64::decode_base64(
227 substr( $token, 0, CORE::index($token, '.') )
228 )
229 );
230
231 my $kid = $details->{kid};
232 $pubkey = $self->get_key_from_cert($kid, $certs);
233 }
234
235 return JSON::WebToken->decode($token, $pubkey);
236}
237
238=head1 AUTHOR
239
240Errietta Kostala <e.kostala@shadowcat.co.uk>
241
242=head1 LICENSE
243
244This library is free software. You can redistribute it and/or modify
245it under the same terms as Perl itself.
246
247=cut
248
2491;