From: Errietta Kostala Date: Mon, 12 Jan 2015 16:04:13 +0000 (+0000) Subject: Get email and token from g+ X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?p=scpubgit%2Fstemmaweb.git;a=commitdiff_plain;h=85990daf0e04461abd1b789fb283848dd583c134 Get email and token from g+ --- diff --git a/Makefile.PL b/Makefile.PL index 1052418..c5b870c 100644 --- a/Makefile.PL +++ b/Makefile.PL @@ -37,6 +37,14 @@ requires 'CatalystX::Controller::Auth' => '0.22'; requires 'Catalyst::TraitFor::Controller::reCAPTCHA'; requires 'LWP::Protocol::https'; ## +requires 'Date::Parse'; +requires 'LWP::Protocol::https'; +requires 'strictures'; +requires 'Crypt::OpenSSL::X509'; +requires 'Crypt::OpenSSL::RSA'; +requires 'JSON::WebToken'; +requires 'JSON::MaybeXS'; +requires 'MIME::Base64'; requires 'File::Which'; requires 'List::Util'; requires 'Moose'; diff --git a/lib/stemmaweb.pm b/lib/stemmaweb.pm index 4a8f77a..00c52cc 100644 --- a/lib/stemmaweb.pm +++ b/lib/stemmaweb.pm @@ -94,6 +94,15 @@ __PACKAGE__->config( }, auto_create_user => 1, }, + google => { + credential => { + class => '+stemmaweb::Authentication::Credential::Google', + }, + store => { + class => 'Model::KiokuDB', + model_name => 'Directory', + } + }, }, ## Auth with CatalystX::Controller::Auth 'Controller::Users' => { diff --git a/lib/stemmaweb/Authentication/Credential/Google.pm b/lib/stemmaweb/Authentication/Credential/Google.pm new file mode 100644 index 0000000..90308ae --- /dev/null +++ b/lib/stemmaweb/Authentication/Credential/Google.pm @@ -0,0 +1,263 @@ +package stemmaweb::Authentication::Credential::Google; + +use Crypt::OpenSSL::X509; +use JSON::WebToken; +use IO::All; +use JSON::MaybeXS; +use MIME::Base64; +use LWP::Simple qw(get); +use Date::Parse qw(str2time); + +use warnings; +use strict; +use strictures 1; + +=head1 NAME + +stemmaweb::Authentication::Google - JSON Web Token handler for Google tokens. + +=head1 DESCRIPTION + +Retrieves Google's public certificates, and then retrieves the key from the +cert using L. Finally, uses the pubkey to decrypt a +Google token using L. + +=cut + +sub new { + my ($class, $config, $app, $realm) = @_; + $class = ref $class || $class; + + my $self = { + _config => $config, + _app => $app, + _realm => $realm, + }; + + bless $self, $class; +} + +sub authenticate { + my ($self, $c, $realm, $authinfo) =@_; + + my $id_token = $authinfo->{id_token}; + $id_token ||= $c->req->method eq 'GET' ? + $c->req->query_params->{id_token} : $c->req->body_params->{id_token}; + + use Data::Dumper; + $c->log->debug(Dumper $authinfo); + + if (!$id_token) { + Catalyst::Exception->throw("id_token not specified."); + } + + my $userinfo = $self->decode($id_token); + + use Data::Dumper; + $c->log->debug(Dumper $userinfo); + + my $sub = $userinfo->{sub}; + my $openid = $userinfo->{openid_id}; + + $c->log->debug($sub); + $c->log->debug($openid); + + if (!$sub || !$openid) { + Catalyst::Exception->throw( + 'Could not retrieve sub and openid from token! Is the token + correct?' + ); + } + + # Do we have a user with the google id already? + my $user = $realm->find_user({ + id => $sub + }); + + if ($user) { + return $user; + } + + # Do we have a user with the openid? + + $user = $realm->find_user({ + url => $openid + }); + + if (!$user) { + throw ("Could not find a user with that openid or sub!"); + } + + my $new_user = $realm->add_user({ + username => $sub, + password => $user->password, + role => $user->role, + }); + + foreach my $t (@{ $user->traditions }) { + $new_user->add_tradition($t); + } + + warn ($new_user->id); + + warn (scalar @{$user->traditions}); + warn (scalar @{$new_user->traditions}); + + use Data::Dumper; + warn (Dumper($user->id)); + + $realm->delete_user({ username => $user->id }); + + return $new_user; +} + +=head1 METHODS + +=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. + +=head3 ARGUMENTS + +=over + +=item url + +Optional. Location where certificates are located. +Defaults to https://www.googleapis.com/oauth2/v1/certs. + +=back + +=head3 RETURNS + +Decoded JSON object containing certificates. + +=cut + +sub retrieve_certs { + my ($self, $url) = @_; + + $url ||= 'https://www.googleapis.com/oauth2/v1/certs'; + return decode_json(get($url)); +} + +=head2 get_key_from_cert + +Given a pair of certificates $certs (defaults to L), +this function returns the public key of the cert identified by $kid. + +=head3 ARGUMENTS + +=over + +=item $kid + +Required. Index of the certificate hash $hash where the cert we want is +located. + +=item $certs + +Optional. A (hashref) pair of certificates. +It's retrieved using L if not given, +or if the pair is expired. + +=back + +=head3 RETURNS + +Public key of certificate. + +=cut + +sub get_key_from_cert { + my ($self, $kid, $certs) = @_; + + $certs ||= $self->retrieve_certs; + my $cert = $certs->{$kid}; + my $x509 = Crypt::OpenSSL::X509->new_from_string($cert); + + if ($self->is_cert_expired($x509)) { + # 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); + } + + return $x509->pubkey; +} + +=head2 is_cert_expired + +Returns if a given L 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 $pubkey + +Optional. A public key string with which to decode the token. +If not given, the public key will be retrieved from $certs. + +=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. + +=back + +=head2 RETURNS + +Decoded JSON object from the decrypted token. + +=cut + +sub decode { + my ($self, $token, $certs, $pubkey) = @_; + + if (!$pubkey) { + my $details = decode_json( + MIME::Base64::decode_base64( + substr( $token, 0, CORE::index($token, '.') ) + ) + ); + + my $kid = $details->{kid}; + $pubkey = $self->get_key_from_cert($kid, $certs); + } + + return JSON::WebToken->decode($token, $pubkey); +} + +=head1 AUTHOR + +Errietta Kostala + +=head1 LICENSE + +This library is free software. You can redistribute it and/or modify +it under the same terms as Perl itself. + +=cut + +1; diff --git a/lib/stemmaweb/Controller/Users.pm b/lib/stemmaweb/Controller/Users.pm index 32f629f..61ccbbc 100644 --- a/lib/stemmaweb/Controller/Users.pm +++ b/lib/stemmaweb/Controller/Users.pm @@ -2,6 +2,13 @@ package stemmaweb::Controller::Users; use Moose; use namespace::autoclean; +use Google::JWT; + +use JSON::MaybeXS; +use JSON::WebToken; + +use MIME::Base64; + BEGIN {extends 'CatalystX::Controller::Auth'; } with 'Catalyst::TraitFor::Controller::reCAPTCHA'; @@ -75,8 +82,7 @@ before register => sub { ## When submitting, check recaptcha passes, else re-draw form if($c->req->method eq 'POST') { - if(!$c->forward('captcha_check')) { - + if(!$c->forward('captcha_check') || 0 ) { ## Need these two lines to detach, so end can draw the correct template again: my $form = $self->form_handler->new( active => [ $self->login_id_field, 'password', 'confirm_password' ] ); $c->stash( template => $self->register_template, form => $form ); @@ -86,6 +92,16 @@ before register => sub { } }; +before login => sub { + my ($self, $c) = @_; + + if ($c->req->params->{email} && $c->req->params->{id_token}) { + + $c->req->param( realm => 'google'); + + } +}; + =head2 success A stub page returned on login / registration success. diff --git a/root/src/auth/login.tt b/root/src/auth/login.tt index 9293df9..7e3437e 100644 --- a/root/src/auth/login.tt +++ b/root/src/auth/login.tt @@ -18,7 +18,38 @@ $(document).ready(function() { }, 2000 ); } }); + + function googleSignIn(authResult) { + if (authResult['status']['signed_in']) { + document.getElementById('signinButton').setAttribute('style', 'display:none'); + gapi.client.load('plus', 'v1', function apiClientLoaded() { + gapi.client.plus.people.get({ userId: 'me'}).execute(function infoRetrieved(resp) { + var primaryEmail; + for (var i = 0; i < resp.emails.length; i++) { + if (resp.emails[i].type === 'account') { + primaryEmail = resp.emails[i].value; + } + } + + dataRetrieved(authResult, primaryEmail); + }); + }); + } else { + console.log("Error", authResult); + } + } + + function dataRetrieved(login, email) { + console.log(email); + console.log(login.id_token); + + document.getElementById('email').value = email; + document.getElementById('id_token').value = login.id_token; + document.getElementById('google_form').submit(); + } + + [% END %]

Stemmaweb - Sign in

@@ -47,20 +78,21 @@ $(document).ready(function() {

Sign in with Google

If you have a Google account, you may use it to sign into Stemmaweb.

-
- - - -
-
- -

Sign in with OpenID

-
-

If you have an account with an OpenID provider (e.g. WordPress, Blogger, Flickr, Yahoo), you may use it to sign into Stemmaweb. -

- - - + + + + + +
@@ -84,4 +116,4 @@ $(document).ready(function() { [% END %]
[% END %] -[% PROCESS footer.tt %] \ No newline at end of file +[% PROCESS footer.tt %] diff --git a/script/maketestdb.pl b/script/maketestdb.pl index a662274..f26659d 100755 --- a/script/maketestdb.pl +++ b/script/maketestdb.pl @@ -40,13 +40,18 @@ say "Created test database"; my $user = $dir->add_user({ username => 'user@example.org', password => 'UserPass' }); my $admin = $dir->add_user({ username => 'admin@example.org', password => 'AdminPass', role => 'admin' }); -die "Failed to create test users" unless $user && $admin; +my $openid_user = $dir->add_user({ + username => 'https://www.google.com/accounts/o8/id?id=AItOawlFTlpuHGcI67tqahtw7xOod9VNWffB-Qg', + password => 'pass' + }); +die "Failed to create test users" unless $user && $admin && $openid_user; say "Created users"; my $t1 = Text::Tradition->new( input => 'Self', file => 't/data/besoin.xml' ); die "Failed to create test tradition #1" unless $t1; $t1->add_stemma( dotfile => 't/data/besoin_stemweb.dot' ); $user->add_tradition( $t1 ); +$openid_user->add_tradition($t1); $dir->store( $user ); say "Created test user tradition";