Merge updates to pull request errietta/login-3622
[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
1c65af41 51 my $email = $authinfo->{email};
52 $email ||= $c->req->method eq 'GET' ? $c->req->query_params->{email} :
53 $c->req->body_params->{email};
54
85990daf 55 my $userinfo = $self->decode($id_token);
c13343b3 56 $userinfo->{email} = $authinfo->{email};
85990daf 57
85990daf 58 my $sub = $userinfo->{sub};
59 my $openid = $userinfo->{openid_id};
60
1c65af41 61 $userinfo->{email} = $email if $email;
62
85990daf 63 if (!$sub || !$openid) {
64 Catalyst::Exception->throw(
65 'Could not retrieve sub and openid from token! Is the token
66 correct?'
67 );
68 }
69
83ed6665 70 return $realm->find_user($userinfo, $c);
85990daf 71}
72
73=head1 METHODS
74
75=head2 retrieve_certs
76
77Retrieves a pair of JSON-encoded certificates from the given URL (defaults to
78Google's public cert url), and returns the decoded JSON object.
79
80=head3 ARGUMENTS
81
82=over
83
84=item url
85
86Optional. Location where certificates are located.
87Defaults to https://www.googleapis.com/oauth2/v1/certs.
88
89=back
90
91=head3 RETURNS
92
93Decoded JSON object containing certificates.
94
95=cut
96
97sub retrieve_certs {
98 my ($self, $url) = @_;
99
e490a3d8 100 my $c = $self->{_app};
101 my $cached = 0;
102 my $certs;
103 my $cache;
104
105 $url ||= ( $c->config->{'Authentication::Credential::Google'}->{public_cert_url} || 'https://www.googleapis.com/oauth2/v1/certs' );
106
107 if ( ($c->registered_plugins('Catalyst::Plugin::Cache')) && ($cache = $c->cache) ) {
108 if ($certs = $cache->get('certs')) {
109 $certs = decode_json($certs);
110
111 foreach my $key (keys %$certs) {
112 my $cert = $certs->{$key};
113 my $x509 = Crypt::OpenSSL::X509->new_from_string($cert);
114
115 if ($self->is_cert_expired($x509)) {
116 $cached = 0;
117 last;
118 } else {
119 $cached = 1;
120 }
121 }
122 }
123 }
124
125 if (!$cached) {
126 my $certs_encoded = get($url);
127
128 if ($cache) {
129 $cache->set('certs', $certs_encoded);
130 }
131
132 $certs = decode_json($certs_encoded);
133 }
134
135 return $certs;
85990daf 136}
137
138=head2 get_key_from_cert
139
140Given a pair of certificates $certs (defaults to L</retrieve_certs>),
141this function returns the public key of the cert identified by $kid.
142
143=head3 ARGUMENTS
144
145=over
146
147=item $kid
148
149Required. Index of the certificate hash $hash where the cert we want is
150located.
151
152=item $certs
153
154Optional. A (hashref) pair of certificates.
155It's retrieved using L</retrieve_certs> if not given,
156or if the pair is expired.
157
158=back
159
160=head3 RETURNS
161
162Public key of certificate.
163
164=cut
165
166sub get_key_from_cert {
167 my ($self, $kid, $certs) = @_;
168
169 $certs ||= $self->retrieve_certs;
170 my $cert = $certs->{$kid};
171 my $x509 = Crypt::OpenSSL::X509->new_from_string($cert);
172
173 if ($self->is_cert_expired($x509)) {
174 # If we ended up here, we were given
175 # an old $certs string from the user.
176 # Let's force getting another.
177 return $self->get_key_from_cert($kid);
178 }
179
180 return $x509->pubkey;
181}
182
183=head2 is_cert_expired
184
185Returns if a given L<Crypt::OpenSSL::X509> certificate is expired.
186
187=cut
188
189sub is_cert_expired {
190 my ($self, $x509) = @_;
191
192 my $expiry = str2time($x509->notAfter);
193
194 return time > $expiry;
195}
196
197=head2 decode
198
199Returns the decoded information contained in a user's token.
200
201=head3 ARGUMENTS
202
203=over
204
205=item $token
206
207Required. The user's token from Google+.
208
209=item $pubkey
210
211Optional. A public key string with which to decode the token.
212If not given, the public key will be retrieved from $certs.
213
214=item $certs
215
216Optional. A pair of public key certs retrieved from Google.
217If not given, or if the certificates have expired, a new
218pair of certificates is retrieved.
219
220=back
221
222=head2 RETURNS
223
224Decoded JSON object from the decrypted token.
225
226=cut
227
228sub decode {
229 my ($self, $token, $certs, $pubkey) = @_;
230
231 if (!$pubkey) {
232 my $details = decode_json(
233 MIME::Base64::decode_base64(
234 substr( $token, 0, CORE::index($token, '.') )
235 )
236 );
237
238 my $kid = $details->{kid};
239 $pubkey = $self->get_key_from_cert($kid, $certs);
240 }
241
242 return JSON::WebToken->decode($token, $pubkey);
243}
244
245=head1 AUTHOR
246
247Errietta Kostala <e.kostala@shadowcat.co.uk>
248
249=head1 LICENSE
250
251This library is free software. You can redistribute it and/or modify
252it under the same terms as Perl itself.
253
254=cut
255
2561;