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