Rename url to public_cert_url and use app url for realm
[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 $userinfo = $self->decode($id_token);
52
53     my $sub = $userinfo->{sub};
54     my $openid = $userinfo->{openid_id};
55
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
63     return $realm->find_user($userinfo, $c);
64 }
65
66 =head1 METHODS
67
68 =head2 retrieve_certs
69
70 Retrieves a pair of JSON-encoded certificates from the given URL (defaults to
71 Google's public cert url), and returns the decoded JSON object.
72
73 =head3 ARGUMENTS
74
75 =over
76
77 =item url
78
79 Optional. Location where certificates are located.
80 Defaults to https://www.googleapis.com/oauth2/v1/certs.
81
82 =back
83
84 =head3 RETURNS
85
86 Decoded JSON object containing certificates.
87
88 =cut
89
90 sub retrieve_certs {
91     my ($self, $url) = @_;
92
93     $url ||= ( $self->{_app}->config->{'Authentication::Credential::Google'}->{public_cert_url} || 'https://www.googleapis.com/oauth2/v1/certs' );
94     return decode_json(get($url));
95 }
96
97 =head2 get_key_from_cert
98
99 Given a pair of certificates $certs (defaults to L</retrieve_certs>),
100 this function returns the public key of the cert identified by $kid.
101
102 =head3 ARGUMENTS
103
104 =over
105
106 =item $kid
107
108 Required. Index of the certificate hash $hash where the cert we want is
109 located.
110
111 =item $certs
112
113 Optional. A (hashref) pair of certificates.
114 It's retrieved using L</retrieve_certs> if not given,
115 or if the pair is expired.
116
117 =back
118
119 =head3 RETURNS
120
121 Public key of certificate.
122
123 =cut
124
125 sub get_key_from_cert {
126     my ($self, $kid, $certs) = @_;
127
128     $certs ||= $self->retrieve_certs;
129     my $cert = $certs->{$kid};
130     my $x509 = Crypt::OpenSSL::X509->new_from_string($cert);
131
132     if ($self->is_cert_expired($x509)) {
133         # If we ended up here, we were given
134         # an old $certs string from the user.
135         # Let's force getting another.
136         return $self->get_key_from_cert($kid);
137     }
138
139     return $x509->pubkey;
140 }
141
142 =head2 is_cert_expired
143
144 Returns if a given L<Crypt::OpenSSL::X509> certificate is expired.
145
146 =cut
147
148 sub is_cert_expired {
149     my ($self, $x509) = @_;
150
151     my $expiry = str2time($x509->notAfter);
152
153     return time > $expiry;
154 }
155
156 =head2 decode
157
158 Returns the decoded information contained in a user's token.
159
160 =head3 ARGUMENTS
161
162 =over
163
164 =item $token
165
166 Required. The user's token from Google+.
167
168 =item $pubkey
169
170 Optional. A public key string with which to decode the token.
171 If not given, the public key will be retrieved from $certs.
172
173 =item $certs
174
175 Optional. A pair of public key certs retrieved from Google.
176 If not given, or if the certificates have expired, a new
177 pair of certificates is retrieved.
178
179 =back
180
181 =head2 RETURNS
182
183 Decoded JSON object from the decrypted token.
184
185 =cut
186
187 sub decode {
188     my ($self, $token, $certs, $pubkey) = @_;
189
190     if (!$pubkey) {
191         my $details = decode_json(
192             MIME::Base64::decode_base64(
193                 substr( $token, 0, CORE::index($token, '.') )
194             )
195         );
196
197         my $kid = $details->{kid};
198         $pubkey = $self->get_key_from_cert($kid, $certs);
199     }
200
201     return JSON::WebToken->decode($token, $pubkey);
202 }
203
204 =head1 AUTHOR
205
206 Errietta Kostala <e.kostala@shadowcat.co.uk>
207
208 =head1 LICENSE
209
210 This library is free software. You can redistribute it and/or modify
211 it under the same terms as Perl itself.
212
213 =cut
214
215 1;