In less than a day, a whole series of old Google API’s are being switched off.
If you are running any of those API’s, then whilst this is bad news, you’ve had several years to upgrade (for example, the Provisioning API features have now been replaced by the Admin SDK). However, one of the API’s not mentioned on the linked page is the old ClientLogin API, which will also stop working on April 20.
ClientLogin involves directly sending a username and password to Google, and then receiving a login token back. There are two main categories of people who use the ClientLogin API which this post is targeted at:
- People who haven’t figured out OAuth yet.
- People using client libraries for the Google Data API’s, which are not deprecated, but often use this login.
If your use case involves users actually entering their username and password, then this post wont help (but this page will), but if you have an installed script which runs administrative functions on your domain, then read on, as it’s quite easy to retrofit your scripts with an OAuth2 service account.
Setting up a service account
Service accounts are designed for server-side use (ie, no browser), and replace using ClientLogin to authenticate as a super-admin for the domain.
First, log in to the developer console (console.developers.google.com), and create a new project, then jump to the credentials menu. Create and download a p12 private key.
Now under “Manage this Domain”, locate the advanced security options, and add a delegation for your new service account for one or more scopes. This allows the service account to access to a given API and act on behalf of users. As an example, the next section uses the Email Settings API, which has this scope:
https://apps-apis.google.com/a/feeds/emailsettings/2.0/
And it’s added like so:
Example: How to retro-fit a PHP app with OAuth 2
Assuming you already have code which sends requests to an API, all we need to change is the Authorize: header. A ClientLogin request looks like this:
Authorize: GoogleLogin auth=<ClientLogin token>
But an OAuth2 request looks like this:
Authorize: Bearer <OAuth token>
You can generate a new token for yourself at the OAuth 2.0 Playground from Google to make sure your old API’s work with the new tokens (they will- any issues are probably permission or user problems).
So how do you generate one of these tokens for a service account? The best way is to use client libraries for this. In PHP, this is the google-api-php-client.
This library also has support for newer API’s (not older Google Data API’s), which you can use to replace your implementation where applicable. For this example, we’ll just be getting an access token.
First up, we need to do some structuring. I’ve wrapped old signature code into an abstract EmailSignatureUpdater class so that I can write two versions: The old ClientLoginEmailSignatureUpdater, and the new OAuthEmailSignatureUpdater, differing only by login method and Authorize: header.
So here is some ClientLogin code:
/**
* Functions dependent on ClientLogin, which will stop working after 2015-04-20.
*/
class ClientLoginEmailSignatureUpdater extends EmailSignatureUpdater {
/**
* @param array $conf Configuration for the Google login.
*/
protected function __construct(array $conf) {
/* Getting account */
$account = array(
'accountType' => 'GOOGLE',
'Email' => $conf['user']."@".$conf['domain'],
'Passwd' => $conf['password'],
'service' => 'apps');
/* Log in */
$login = $this -> login($account);
if(!isset($login['Auth'])) {
if(isset($login['Error'])) {
throw new Exception("Google returned ".$login['Error']);
} else {
throw new Exception("Google login failed");
}
}
$this -> token = $login['Auth'];
}
/**
* Perform Google ClientLogin authentication
*
* @param array:string $account Account details (see top for components)
* @return string Response, including login token.
*/
private function login($account){
/* Log in to google apps */
$tk_ch = curl_init();
curl_setopt($tk_ch, CURLOPT_URL, "https://www.google.com/accounts/ClientLogin");
curl_setopt($tk_ch, CURLOPT_POST, true);
curl_setopt($tk_ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($tk_ch, CURLOPT_POSTFIELDS, $account);
$responseTxt = curl_exec($tk_ch);
curl_close($tk_ch);
/* Parse response (very hack-y but works) */
$responseLines = split('=', $responseTxt);
$lastkey = false;
for($i = 0; $i < count($responseLines); $i++) {
$line = $responseLines[$i];
if($i == 0) {
/* First line is just a key */
$lastkey = $line;
} else {
$vals = split("\n", $line);
if($i != count($responseLines) - 1) {
/* Split into value and next key */
$nextkey = $vals[count($vals) - 1];
unset($vals[count($vals) - 1]);
} else {
$nextkey = false;
}
$response[$lastkey] = implode("\n", $vals);
$lastkey = $nextkey;
}
}
return $response;
}
protected function authorize() {
return 'Authorization: GoogleLogin auth='.trim($this -> token);
}
}
And the part you’re probably searching for: how to get an access token out of the google-api-php-client:
/**
* New OAuth2 authentication using google-api-php-client to get us logged in instead.
*/
class OAuthEmailSignatureUpdater extends EmailSignatureUpdater {
protected function __construct(array $conf) {
$this -> token = $this -> login($conf);
}
private function login(array $conf) {
require_once(dirname(__FILE__) . "/vendor/google-api-php-client/src/Google/autoload.php");
// Start client
$client = new Google_Client();
$client -> setApplicationName("Test Application");
// Load key
$key = file_get_contents($conf['key_file']);
if(!$key) {
throw new Exception("Key could not be loaded from file " .$conf['key_file']);
}
// Set up auth with scopes
$gauth = new Google_Auth_AssertionCredentials(
$conf['service_account'],
array('https://apps-apis.google.com/a/feeds/emailsettings/2.0/'),
$key);
// sub- pretend to be a user
// Should probably be the same as the user you logged in as with ClientLogin
if(isset($conf['sub'])) {
$gauth -> sub = $conf['sub'];
}
$client -> setAssertionCredentials($gauth);
if($client -> getAuth() -> isAccessTokenExpired()) {
$client -> getAuth() -> refreshTokenWithAssertion($gauth);
}
$client -> setClientId($conf['client_id']);
/* Save token for later use */
$token = $client -> getAccessToken();
if (!$token) {
throw new Exception("Google apps login failed!");
}
$arr = json_decode($token);
if(!isset($arr -> access_token)) {
throw new Exception("Google apps login did not return access token!");
}
return $arr -> access_token;
}
protected function authorize() {
return 'Authorization: Bearer ' . $this -> token;
}
}
In the superclass, we provide a getUpdater() function to dish out an old or new updater based on the configuration. In future releases, you could just throw an Exception instead of logging in with ClientLogin, as you know it will fail!
/**
* Contains email signature updater methods, with no login method
*/
abstract class EmailSignatureUpdater {
/**
* @var string The login token.
*/
protected $token;
/**
* @param array $conf The configuration to use.
*/
protected abstract function __construct(array $conf);
/**
* Return authorization header
*/
protected abstract function authorize();
// A series of methods which make raw HTTP requests
// to https://apps-apis.google.com/a/feeds/emailsettings/2.0/ were
// located here, and are not relevant to this post.
/**
* Based on the configuration, return a signature updater which logs in using the correct API.
*
* @param array $conf
*/
static function getUpdater(array $conf) {
if(isset($conf['user']) && isset($conf['domain']) && isset($conf['password'])) {
return new ClientLoginEmailSignatureUpdater($conf);
} else if(isset($conf['service_account']) && isset($conf['client_id']) && isset($conf['key_file'])) {
return new OAuthEmailSignatureUpdater($conf);
}
throw new Exception("Configuration insufficient for loading email signatures. See config.php.example.");
}
}
To utilise this, you need to change HTTP methods to care less about where their headers come from. I was using cURL for this, but on any library, you need to set the header using the new authorize() function before you send off the request:
curl_setopt($ch, CURLOPT_HTTPHEADER, array($this -> authorize(), ..., ...);
The idea is that users can simply alter their configuration to pick up the newly supported OAuth, allowing you to shift it from a software problem to an administrative/configuration problem:
/* Google apps domain config */
/* Old style ClientLogin usage (deprecated) */
//$config['google']['user'] = "admin";
//$config['google']['domain'] = "example.com";
//$config['google']['password'] = "...";
/* New login details for OAuth */
$config['google']['service_account'] = '...@developer.gserviceaccount.com';
$config['google']['client_id'] = '....apps.googleusercontent.com';
$config['google']['key_file'] = dirname(__FILE__) . '/google-key.p12';
$config['google']['sub'] = "admin@example.com"; // optional
Within your application, you then construct a new any-authentication object based on the configuration, and call its old methods as normal:
$updater = EmailSignatureUpdater::getUpdater($config['google']);
$updater -> doThing('param1', 'param2', etc);
Good luck!