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