Tuesday, December 21, 2010

Integrate your Perl application with Google Apps Marketplace

I spent most of the last week trying to figure out how to take a Perl web app and integrate it with the Google Apps Marketplace. This is where the supposedly 3 million businesses who signed up for Google Apps go for third-party integrations.

You have to sign up as a vendor in order to make your web application available to Google Apps customers. The other requirement is that your app supports OpenID Single Sign-on. This is where the integration turned difficult for me.

I assumed you would use Net::OpenID::Consumer to handle the consumer-side processing. However, after only a little headway, and asking around on StackOverflow as well as the Google Marketplace forums, I was stuck. I could not close the OpenID circuit and continue on to my app.

I eventually solved the problem by switching modules. I changed to the skimpily documented Net::Google::FederatedLogin, and finally got things working.

The code is as follows (substitute example.com below for your actual developer's domain).

First, you have to login your Google Apps Marketplace vendor profile, and add the URL to index.cgi in your application manifest, with the required ${DOMAIN_NAME} variable. ${DOMAIN_NAME} will be replaced by the domain of the user who installs your app. This parameter is integral to the authentication scheme.
...
<Url>http://www.example.com/index.cgi?from=google&domain=${DOMAIN_NAME}</Url>
...
The application manifest is like the installer for your web app. It's detailed here, but is kind of outside of the scope of this post.

Once you've gotten the application manifest done, add the following code to your servers.

index.cgi
use CGI;
use Net::Google::FederatedLogin;

my $q = CGI->new();

my $domain = $q->param('domain');
if (!$domain) {
    print $q->header(), 'Provide domain please.';
    exit 0;
}

my $fl = Net::Google::FederatedLogin->new(
    claimed_id => 
        'https://www.google.com/accounts/o8/site-xrds?hd=' . $domain,
    return_to =>
        'http://www.example.com/return.cgi',
    extensions => [
        {
            ns          => 'ax',
            uri         => 'http://openid.net/srv/ax/1.0',
            attributes  => {
                mode        => 'fetch_request',
                required    => 'email',
                type        => {
                    email => 'http://axschema.org/contact/email'
                }
            }
        }
    ] );

print $q->redirect($fl->get_auth_url());
Note that $domain above is used in the claimed_id parameter and is sent to Google for verification. The extensions parameter informs Google what user data to send back to your site when it redirects to return_to. Which, in this case, is

return.cgi
use CGI;
use Net::Google::FederatedLogin;
use LWP::UserAgent;
use HTTP::Request::Common;
use URI;
use URI::Escape qw(uri_escape);
use Net::OAuth;

# OAuth (to access user's Google data)
# You get these from your vendor profile in Google Apps. Same place
# where you edit the application manifest.
my $CONSUMER_KEY = '??????????????.apps.googleusercontent.com';
my $CONSUMER_SECRET = '??????????????????';

# We want to get some calendar data from the user
my $URL = 
    'https://www.google.com/calendar/feeds/default/allcalendars/full';

my $q = CGI->new();
print $q->header();

# OpenID final step
my $fl = Net::Google::FederatedLogin->new(  
    cgi => $q,
    return_to =>
        'http://www.example.com/return.cgi' );


eval { $fl->verify_auth(); };
if ($@) {
    print 'Error: ' . $@;
}
else {
    my $ext = $fl->get_extension('http://openid.net/srv/ax/1.0');
    get_calendar_oauth($ext->get_parameter('value.email'));
}

# OAuth
sub get_calendar_oauth {
    my $email = shift;

    my $oauth_request =
            Net::OAuth->request('consumer')->new(
              consumer_key => $CONSUMER_KEY,
              consumer_secret => $CONSUMER_SECRET,
              request_url => $URL,
              request_method => 'GET',
              signature_method => 'HMAC-SHA1',
              timestamp => time,
              nonce => nonce(),
              extra_params => {
            'xoauth_requestor_id' => $email
              },
            );
      
    $oauth_request->sign(); 
    my $req = HTTP::Request->new(
        GET => $URL . '?xoauth_requestor_id=' . uri_escape($email) );

    $req->header('Content-type' => 'application/atom+xml');
    $req->header(
        'Authorization' => $oauth_request->to_authorization_header);

    my $ua = LWP::UserAgent->new;
    my $oauth_response = $ua->simple_request($req);
    while($oauth_response->is_redirect) {

      my $url = URI->new($oauth_response->header('Location'));

      $req->uri($url);

      my %query = $url->query_form;
      foreach my $param (keys %query) {
        $oauth_request->{extra_params}->{$param} = $query{$param};
      }

      $url->query(undef); # clear out the query parameters
      $oauth_request->{request_url} = $url;
      $oauth_request->sign; # resign
      $req->header(
        'Authorization' => $oauth_request->to_authorization_header );

      $oauth_response = $ua->simple_request($req);
    }

    print $oauth_response->as_string;

} # get_calendar_oauth

sub nonce {
  my @a = ('A'..'Z', 'a'..'z', 0..9);
  my $nonce = '';
  for(0..31) {
    $nonce .= $a[rand(scalar(@a))];
  }

  $nonce;
}
The final OpenID step is quite minimal, as you can see above. You simply create a new Net::Google::FederatedLogin object and pass it the CGI object plus return_to value. Then you verify, and if there isn't an error, you should be able to access the extension data via the call to get_extension().

Much of the above script is devoted to doing OAuth in order to access the user's Google data, in this case his calendar. If you only need to authenticate a user and not access Google data, you could omit the call to get_calendar_oauth() entirely.

OAuth

When you create your app in the vendor section of Google Apps Marketplace, it will be assigned a Consumer Secret and a Consumer Key. These must be present in the parameters when you instantiate your Net::OAuth object. In the above code, you would set $CONSUMER_KEY and $CONSUMER_SECRET to these values.

The data is returned as Atom/XML. In the above code I do nothing with it except print it out. The code in get_calendar_oauth has been borrowed almost directly from this blog post by Jeremy Smith.

That's basically it. This was intended to be a sparse example covering the two main points for integrating with Google Apps from Perl -- OpenID to grant access to your app via Google credentials, and OAuth for accessing Google data on behalf of the user.

1 comment: