Get email and token from g+
Errietta Kostala [Mon, 12 Jan 2015 16:04:13 +0000 (16:04 +0000)]
Makefile.PL
lib/stemmaweb.pm
lib/stemmaweb/Authentication/Credential/Google.pm [new file with mode: 0644]
lib/stemmaweb/Controller/Users.pm
root/src/auth/login.tt
script/maketestdb.pl

index 1052418..c5b870c 100644 (file)
@@ -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';
index 4a8f77a..00c52cc 100644 (file)
@@ -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 (file)
index 0000000..90308ae
--- /dev/null
@@ -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<Crypt::OpenSSL::X509>. Finally, uses the pubkey to decrypt a
+Google token using L<JSON::WebToken>.
+
+=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</retrieve_certs>),
+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</retrieve_certs> 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<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 $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 <e.kostala@shadowcat.co.uk>
+
+=head1 LICENSE
+
+This library is free software. You can redistribute it and/or modify
+it under the same terms as Perl itself.
+
+=cut
+
+1;
index 32f629f..61ccbbc 100644 (file)
@@ -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.
index 9293df9..7e3437e 100644 (file)
@@ -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();
+       }
+
     </script>
+       <script src="https://apis.google.com/js/client:platform.js" async defer></script>
 [% END %]
        <div id="topbanner">
                <h1>Stemmaweb - Sign in</h1>
@@ -47,20 +78,21 @@ $(document).ready(function() {
        <h3><a href="#">Sign in with Google</a></h3>
        <div>
                <p>If you have a Google account, you may use it to sign into Stemmaweb.</p>
-               <form class="openid_form" method="post" action="[% c.uri_for_action('/users/login') | html %]" autocomplete="off">
-                       <input type="hidden" name="realm" value="openid"/>
-                       <input type="hidden" name="openid_identifier" value="https://www.google.com/accounts/o8/id"/>
-                       <input type="submit" class="login_button" id="login_google" value="Sign in with Google"></input>
-               </form>
-       </div>
-
-       <h3><a href="#">Sign in with OpenID</a></h3>
-       <div>
-               <p>If you have an account with an <a href="http://openid.net/get-an-openid/" target="_blank">OpenID provider</a> (e.g. WordPress, Blogger, Flickr, Yahoo), you may use it to sign into Stemmaweb.
-               <form class="openid_form" method="post" action="[% c.uri_for_action('/users/login') | html %]" autocomplete="off">
-                       <input type="hidden" name="realm" value="openid"/>
-                       <input type="text" name="openid_identifier" id="openid_input"/>
-                       <input type="submit" class="login_button" id="login_openid" value="Sign in with OpenID"/>
+               <span id="signinButton">
+                       <span
+                       class="g-signin"
+                       data-callback="googleSignIn"
+                       data-clientid="577442226093-pi2ud795g49ibip78bgfoabhl4kdrguc.apps.googleusercontent.com"
+                       data-cookiepolicy="single_host_origin"
+                       data-requestvisibleactions="http://schema.org/AddAction"
+                       data-scope="https://www.googleapis.com/auth/plus.profile.emails.read"
+                       data-openidrealm="http://sherlock.scsys.co.uk:3000/"
+                       >
+                       </span>
+               </span>
+               <form id="google_form" action="[% c.uri_for_action('/users/login') | html %]" method="post">
+                       <input id='email' name='email' value='' type='hidden' />
+                       <input id='id_token' name='id_token' value='' type='hidden' />
                </form>
        </div>
 
@@ -84,4 +116,4 @@ $(document).ready(function() {
 [% END %]
 </div>
 [% END %]
-[% PROCESS footer.tt %]
\ No newline at end of file
+[% PROCESS footer.tt %]
index a662274..f26659d 100755 (executable)
@@ -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";