Lookup openid users to convert via Email if OpenID lookup fails
[scpubgit/stemmaweb.git] / lib / stemmaweb / Authentication / Credential / Google.pm
1 package stemmaweb::Authentication::Credential::Google;
2
3 use Crypt::OpenSSL::X509;
4 use JSON::WebToken;
5 use IO::All;
6 use JSON::MaybeXS;
7 use MIME::Base64;
8 use LWP::Simple qw(get);
9 use Date::Parse qw(str2time);
10
11 use warnings;
12 use strict;
13 use strictures 1;
14
15 =head1 NAME
16
17 stemmaweb::Authentication::Google - JSON Web Token handler for Google tokens.
18
19 =head1 DESCRIPTION
20
21 Retrieves Google's public certificates, and then retrieves the key from the
22 cert using L<Crypt::OpenSSL::X509>. Finally, uses the pubkey to decrypt a
23 Google token using L<JSON::WebToken>.
24
25 =cut
26
27 sub 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
40 sub 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
47     if (!$id_token) {
48         Catalyst::Exception->throw("id_token not specified.");
49     }
50
51     my $email = $authinfo->{email};
52     $email ||= $c->req->method eq 'GET' ? $c->req->query_params->{email} :
53     $c->req->body_params->{email};
54
55     my $userinfo = $self->decode($id_token);
56     $userinfo->{email} = $authinfo->{email};
57
58     my $sub = $userinfo->{sub};
59     my $openid = $userinfo->{openid_id};
60
61     $userinfo->{email} = $email if $email;
62
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
70     return $realm->find_user($userinfo, $c);
71 }
72
73 =head1 METHODS
74
75 =head2 retrieve_certs
76
77 Retrieves a pair of JSON-encoded certificates from the given URL (defaults to
78 Google's public cert url), and returns the decoded JSON object.
79
80 =head3 ARGUMENTS
81
82 =over
83
84 =item url
85
86 Optional. Location where certificates are located.
87 Defaults to https://www.googleapis.com/oauth2/v1/certs.
88
89 =back
90
91 =head3 RETURNS
92
93 Decoded JSON object containing certificates.
94
95 =cut
96
97 sub retrieve_certs {
98     my ($self, $url) = @_;
99
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;
136 }
137
138 =head2 get_key_from_cert
139
140 Given a pair of certificates $certs (defaults to L</retrieve_certs>),
141 this function returns the public key of the cert identified by $kid.
142
143 =head3 ARGUMENTS
144
145 =over
146
147 =item $kid
148
149 Required. Index of the certificate hash $hash where the cert we want is
150 located.
151
152 =item $certs
153
154 Optional. A (hashref) pair of certificates.
155 It's retrieved using L</retrieve_certs> if not given,
156 or if the pair is expired.
157
158 =back
159
160 =head3 RETURNS
161
162 Public key of certificate.
163
164 =cut
165
166 sub 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
185 Returns if a given L<Crypt::OpenSSL::X509> certificate is expired.
186
187 =cut
188
189 sub 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
199 Returns the decoded information contained in a user's token.
200
201 =head3 ARGUMENTS
202
203 =over
204
205 =item $token
206
207 Required. The user's token from Google+.
208
209 =item $pubkey
210
211 Optional. A public key string with which to decode the token.
212 If not given, the public key will be retrieved from $certs.
213
214 =item $certs
215
216 Optional. A pair of public key certs retrieved from Google.
217 If not given, or if the certificates have expired, a new
218 pair of certificates is retrieved.
219
220 =back
221
222 =head2 RETURNS
223
224 Decoded JSON object from the decrypted token.
225
226 =cut
227
228 sub 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
247 Errietta Kostala <e.kostala@shadowcat.co.uk>
248
249 =head1 LICENSE
250
251 This library is free software. You can redistribute it and/or modify
252 it under the same terms as Perl itself.
253
254 =cut
255
256 1;