Commit | Line | Data |
85990daf |
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 | |
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 | |
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 | |
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 | |
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; |