Catalyst-Plugin-Authentication-Credential-GooglePlus
view release on metacpan or search on metacpan
lib/Catalyst/Plugin/Authentication/Credential/GooglePlus.pm view on Meta::CPAN
Retrieves Google's public certificates, and then retrieves the key from the
cert using L<Crypt::OpenSSL::X509>. Finally, uses the pubkey to decrypt a
Google token using L<JSON::WebToken>.
See https://github.com/errietta/Catalyst-Plugin-Authentication-Credential-GooglePlus-example
for an example.
=cut
sub new {
my ($class, $config, $app, $realm) = @_;
$class = ref $class || $class;
my $self = {
%{ $config },
%{ $realm->{config} },
_app => $app,
_realm => $realm,
};
# Google names it id_token
$self->{token_field} ||= "id_token";
bless $self, $class;
}
=head1 METHODS
=head2 authenticate
Retrieves a JSON web token from either $authinfo, or GET or POST query
parameters. If null, throws exception.
Otherwise, decodes (with L</decode>) the token, and calls find_user on the
given L<Catalyst::Authentication::Realm> object with the data retrieved from
decoding the token.
=head3 ARGUMENTS
=over
=item $c
Catalyst object.
=item $realm
Catalyst::Authentication::Realm (or subclass) object.
=item $authinfo
Optional, authentication info that can contain the token. If not given, the
token is retrieved from GET or POST parameters.
=back
=head3 RETURNS
User found by calling L<Catalyst::Authentication::Realm/find_user> with the
decoded token's information, if any.
=cut
sub authenticate {
my ($self, $c, $realm, $authinfo) = @_;
my $field = $self->{token_field};
if (my $cache = $self->get_cache($c)) {
$self->{cache} ||= $cache;
}
my $id_token = $authinfo->{$field};
$id_token ||= $c->req->method eq 'GET' ?
$c->req->query_params->{$field} : $c->req->body_params->{$field};
unless ($id_token) {
Catalyst::Exception->throw("$field not specified.");
}
my $userinfo = $self->decode($id_token);
my $sub = $userinfo->{sub};
my $openid = $userinfo->{openid_id};
unless ($sub && $openid) {
Catalyst::Exception->throw(
'Could not retrieve sub and openid from token! Is the token
correct? Token was ' . $id_token
);
}
return $realm->find_user($userinfo, $c);
}
=head2 retrieve_certs
Retrieves a pair of JSON-encoded certificates from the given $url (defaults to
Google's public cert url), and returns the decoded JSON object.
If a cache plugin is loaded, the certificate pair is cached; however one of the
certificates is expired, a new pair is fetched from $url.
=head3 ARGUMENTS
=over
=item url
Optional. Location where certificates are located.
This can also be configured as the
'Authentication::Credential::Google::public_cert_url' key in your catalyst
configuration.
Defaults to https://www.googleapis.com/oauth2/v1/certs.
=back
=head3 RETURNS
Decoded JSON object containing certificates.
=cut
sub retrieve_certs {
my ($self, $url) = @_;
my $c = $self->{_app};
my $cached = 0;
my $certs;
my $cache;
$url ||= ( $self->{public_cert_url} || 'https://www.googleapis.com/oauth2/v1/certs' );
if ( $cache = $self->{cache} ) {
if ($certs = $cache->get($self->{cache_key})) {
try {
$certs = decode_json($certs);
} catch {
Catalyst::Exception->throw("Could not decode cached JSON of certs: $_!");
};
foreach my $key (keys %$certs) {
my $cert = $certs->{$key};
my $x509 = Crypt::OpenSSL::X509->new_from_string($cert);
if ($self->is_cert_expired($x509)) {
$cached = 0;
last;
} else {
$cached = 1;
}
}
}
}
unless ($cached) {
my $certs_encoded = get($url);
lib/Catalyst/Plugin/Authentication/Credential/GooglePlus.pm view on Meta::CPAN
or if the pair is expired.
=item $recursive
This will be set to true if this function calls itself to renew an expired
certificate. Used to prevent infinite recursion.
=back
=head3 RETURNS
Public key of certificate.
=cut
sub get_key_from_cert {
my ($self, $kid, $certs, $recursive) = @_;
$certs ||= $self->retrieve_certs;
my $cert = $certs->{$kid};
my $x509;
try {
$x509 = Crypt::OpenSSL::X509->new_from_string($cert);
} catch {
Catalyst::Exception->throw("Could not get public key from provided certificate, is the certificate valid?\nCert was:" . $cert );
};
if ($self->is_cert_expired($x509)) {
unless ($recursive) {
# If we ended up here, we were given
# an old $certs string from the user.
# Let's force getting another.
return $self->get_key_from_cert($kid, undef, 1);
} else {
Catalyst::Exception->throw("Something is very wrong; we were given
an expired cert and got another expired cert trying to get a
new one! \nCert was:" . $cert);
}
}
return $x509->pubkey;
}
=head2 is_cert_expired
Returns if a given L<Crypt::OpenSSL::X509> certificate is expired.
=cut
sub is_cert_expired {
my ($self, $x509) = @_;
my $expiry = str2time($x509->notAfter);
return time > $expiry;
}
=head2 decode
Returns the decoded information contained in a user's token.
=head3 ARGUMENTS
=over
=item $token
Required. The user's token from Google+.
=item $certs
Optional. A pair of public key certs retrieved from Google.
If not given, or if the certificates have expired, a new
pair of certificates is retrieved.
=item $pubkey
Optional. A public key string with which to decode the token.
If not given, the public key will be retrieved from $certs.
=back
=head2 RETURNS
Decoded JSON object from the decrypted token.
=cut
sub decode {
my ($self, $token, $certs, $pubkey) = @_;
unless ($pubkey) {
my $details;
try {
$details = decode_json(
MIME::Base64::decode_base64(
substr( $token, 0, CORE::index($token, '.') )
)
);
} catch {
Catalyst::Exception->throw("Could not decode token, is the token valid? token is " . $token);
};
my $kid = $details->{kid};
unless ($kid) {
Catalyst::Exception->throw("Decoded token has no `kid` member; is token valid? token is " . $token);
}
$pubkey = $self->get_key_from_cert($kid, $certs);
}
my $result;
try {
$result = JSON::WebToken->decode($token, $pubkey);
} catch {
Catalyst::Exception->throw("Could not decode the given token with the public key! Token is " . $token . " and pubkey is " . $pubkey);
};
( run in 0.717 second using v1.01-cache-2.11-cpan-39a47a84364 )